From 31b9d24c2b7eb4e6f652731fc5f9e9cb32a4ce40 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Thu, 16 Feb 2012 23:27:11 +0000 Subject: [PATCH] Implement the i3bar JSON protocol (with fallback to plain text) If the first line of the input starts with {"version":, then the input is considered to be JSON, otherwise it is interpreted as plain text. Only the "full_text" and "color" parts of a block are currently understood by i3bar. --- i3bar/include/common.h | 25 ++++++- i3bar/src/child.c | 144 +++++++++++++++++++++++++++++++++++++---- i3bar/src/xcb.c | 45 +++++++++---- 3 files changed, 188 insertions(+), 26 deletions(-) diff --git a/i3bar/include/common.h b/i3bar/include/common.h index bce31a4d..212b9dd1 100644 --- a/i3bar/include/common.h +++ b/i3bar/include/common.h @@ -9,6 +9,9 @@ #define COMMON_H_ #include +#include +#include +#include "queue.h" typedef struct rect_t rect; @@ -23,7 +26,27 @@ struct rect_t { int h; }; -#include "queue.h" +/* This data structure represents one JSON dictionary, multiple of these make + * up one status line. */ +struct status_block { + char *full_text; + + char *color; + + /* full_text, but converted to UCS-2. This variable is only temporarily + * used in refresh_statusline(). */ + xcb_char2b_t *ucs2_full_text; + size_t glyph_count_full_text; + + /* The amount of pixels necessary to render this block. This variable is + * only temporarily used in refresh_statusline(). */ + uint32_t width; + + TAILQ_ENTRY(status_block) blocks; +}; + +TAILQ_HEAD(statusline_head, status_block) statusline_head; + #include "child.h" #include "ipc.h" #include "outputs.h" diff --git a/i3bar/src/child.c b/i3bar/src/child.c index e294fb9a..41bb8832 100644 --- a/i3bar/src/child.c +++ b/i3bar/src/child.c @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include "common.h" @@ -28,7 +30,25 @@ pid_t child_pid; ev_io *stdin_io; ev_child *child_sig; +/* JSON parser for stdin */ +bool first_line = true; +bool plaintext = false; +yajl_callbacks callbacks; +yajl_handle parser; + +typedef struct parser_ctx { + /* A copy of the last JSON map key. */ + char *last_map_key; + + /* The current block. Will be filled, then copied and put into the list of + * blocks. */ + struct status_block block; +} parser_ctx; + +parser_ctx parser_context; + /* The buffer statusline points to */ +struct statusline_head statusline_head = TAILQ_HEAD_INITIALIZER(statusline_head); char *statusline_buffer = NULL; /* @@ -50,6 +70,70 @@ void cleanup() { } } +/* + * The start of a new array is the start of a new status line, so we clear all + * previous entries. + * + */ +static int stdin_start_array(void *context) { + struct status_block *first; + while (!TAILQ_EMPTY(&statusline_head)) { + first = TAILQ_FIRST(&statusline_head); + FREE(first->full_text); + FREE(first->color); + TAILQ_REMOVE(&statusline_head, first, blocks); + free(first); + } + return 1; +} + +/* + * The start of a map is the start of a single block of the status line. + * + */ +static int stdin_start_map(void *context) { + parser_ctx *ctx = context; + memset(&(ctx->block), '\0', sizeof(struct status_block)); + return 1; +} + +static int stdin_map_key(void *context, const unsigned char *key, size_t len) { + parser_ctx *ctx = context; + FREE(ctx->last_map_key); + sasprintf(&(ctx->last_map_key), "%.*s", len, key); + return 1; +} + +static int stdin_string(void *context, const unsigned char *val, size_t len) { + parser_ctx *ctx = context; + if (strcasecmp(ctx->last_map_key, "full_text") == 0) { + sasprintf(&(ctx->block.full_text), "%.*s", len, val); + } + if (strcasecmp(ctx->last_map_key, "color") == 0) { + sasprintf(&(ctx->block.color), "%.*s", len, val); + } + return 1; +} + +static int stdin_end_map(void *context) { + parser_ctx *ctx = context; + struct status_block *new_block = smalloc(sizeof(struct status_block)); + memcpy(new_block, &(ctx->block), sizeof(struct status_block)); + TAILQ_INSERT_TAIL(&statusline_head, new_block, blocks); + return 1; +} + +static int stdin_end_array(void *context) { + DLOG("dumping statusline:\n"); + struct status_block *current; + TAILQ_FOREACH(current, &statusline_head, blocks) { + DLOG("full_text = %s\n", current->full_text); + DLOG("color = %s\n", current->color); + } + DLOG("end of dump\n"); + return 1; +} + /* * Callbalk for stdin. We read a line from stdin and store the result * in statusline @@ -60,25 +144,19 @@ void stdin_io_cb(struct ev_loop *loop, ev_io *watcher, int revents) { int n = 0; int rec = 0; int buffer_len = STDIN_CHUNK_SIZE; - char *buffer = smalloc(buffer_len); + unsigned char *buffer = smalloc(buffer_len); buffer[0] = '\0'; while(1) { n = read(fd, buffer + rec, buffer_len - rec); if (n == -1) { if (errno == EAGAIN) { - /* remove trailing newline and finish up */ - buffer[rec-1] = '\0'; + /* finish up */ break; } ELOG("read() failed!: %s\n", strerror(errno)); exit(EXIT_FAILURE); } if (n == 0) { - if (rec != 0) { - /* remove trailing newline and finish up */ - buffer[rec-1] = '\0'; - } - /* end of file, kill the watcher */ ELOG("stdin: received EOF\n"); cleanup(); @@ -96,13 +174,41 @@ void stdin_io_cb(struct ev_loop *loop, ev_io *watcher, int revents) { FREE(buffer); return; } - FREE(statusline_buffer); - statusline = statusline_buffer = buffer; - for (n = 0; buffer[n] != '\0'; ++n) { - if (buffer[n] == '\n') - statusline = &buffer[n + 1]; + + unsigned char *json_input = buffer; + if (first_line) { + DLOG("Detecting input type based on buffer *%.*s*\n", rec, buffer); + /* Detect whether this is JSON or plain text. */ + plaintext = (strncasecmp((char*)buffer, "{\"version\":", strlen("{\"version\":")) != 0); + if (plaintext) { + /* In case of plaintext, we just add a single block and change its + * full_text pointer later. */ + struct status_block *new_block = scalloc(sizeof(struct status_block)); + TAILQ_INSERT_TAIL(&statusline_head, new_block, blocks); + } else { + /* At the moment, we don’t care for the version. This might change + * in the future, but for now, we just discard it. */ + while (*json_input != '\n' && *json_input != '\0') { + json_input++; + rec--; + } + } + first_line = false; + } + if (!plaintext) { + yajl_status status = yajl_parse(parser, json_input, rec); + if (status != yajl_status_ok) { + fprintf(stderr, "[i3bar] Could not parse JSON input: %.*s\n", + rec, json_input); + } + free(buffer); + } else { + struct status_block *first = TAILQ_FIRST(&statusline_head); + /* Remove the trailing newline and terminate the string at the same + * time. */ + buffer[rec-1] = '\0'; + first->full_text = (char*)buffer; } - DLOG("%s\n", statusline); draw_bars(); } @@ -126,6 +232,16 @@ void child_sig_cb(struct ev_loop *loop, ev_child *watcher, int revents) { * */ void start_child(char *command) { + /* Allocate a yajl parser which will be used to parse stdin. */ + memset(&callbacks, '\0', sizeof(yajl_callbacks)); + callbacks.yajl_map_key = stdin_map_key; + callbacks.yajl_string = stdin_string; + callbacks.yajl_start_array = stdin_start_array; + callbacks.yajl_end_array = stdin_end_array; + callbacks.yajl_start_map = stdin_start_map; + callbacks.yajl_end_map = stdin_end_map; + parser = yajl_alloc(&callbacks, NULL, &parser_context); + child_pid = 0; if (command != NULL) { int fd[2]; diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c index f75f23de..a3312ca3 100644 --- a/i3bar/src/xcb.c +++ b/i3bar/src/xcb.c @@ -108,28 +108,51 @@ int _xcb_request_failed(xcb_void_cookie_t cookie, char *err_msg, int line) { * */ void refresh_statusline() { - size_t glyph_count; + struct status_block *block; - if (statusline == NULL) { - return; + uint32_t old_statusline_width = statusline_width; + statusline_width = 0; + + /* Convert all blocks from UTF-8 to UCS-2 and predict the text width (in + * pixels). */ + TAILQ_FOREACH(block, &statusline_head, blocks) { + block->ucs2_full_text = (xcb_char2b_t*)convert_utf8_to_ucs2(block->full_text, &(block->glyph_count_full_text)); + block->width = predict_text_width((char*)block->ucs2_full_text, block->glyph_count_full_text, true); + /* If this is not the last block, add some pixels for a separator. */ + if (TAILQ_NEXT(block, blocks) != NULL) + block->width += 9; + statusline_width += block->width; } - xcb_char2b_t *text = (xcb_char2b_t*)convert_utf8_to_ucs2(statusline, &glyph_count); - uint32_t old_statusline_width = statusline_width; - statusline_width = predict_text_width((char*)text, glyph_count, true); /* If the statusline is bigger than our screen we need to make sure that * the pixmap provides enough space, so re-allocate if the width grew */ if (statusline_width > xcb_screen->width_in_pixels && statusline_width > old_statusline_width) realloc_sl_buffer(); + /* Clear the statusline pixmap. */ xcb_rectangle_t rect = { 0, 0, xcb_screen->width_in_pixels, font.height }; xcb_poly_fill_rectangle(xcb_connection, statusline_pm, statusline_clear, 1, &rect); - set_font_colors(statusline_ctx, colors.bar_fg, colors.bar_bg); - draw_text((char*)text, glyph_count, true, statusline_pm, statusline_ctx, - 0, 0, xcb_screen->width_in_pixels); - FREE(text); + /* Draw the text of each block. */ + uint32_t x = 0; + TAILQ_FOREACH(block, &statusline_head, blocks) { + uint32_t colorpixel = (block->color ? get_colorpixel(block->color) : colors.bar_fg); + set_font_colors(statusline_ctx, colorpixel, colors.bar_bg); + draw_text((char*)block->ucs2_full_text, block->glyph_count_full_text, + true, statusline_pm, statusline_ctx, x, 0, block->width); + x += block->width; + + if (TAILQ_NEXT(block, blocks) != NULL) { + /* This is not the last block, draw a separator. */ + set_font_colors(statusline_ctx, get_colorpixel("#666666"), colors.bar_bg); + xcb_poly_line(xcb_connection, XCB_COORD_MODE_ORIGIN, statusline_pm, + statusline_ctx, 2, + (xcb_point_t[]){ { x - 5, 2 }, { x - 5, font.height - 2 } }); + } + + FREE(block->ucs2_full_text); + } } /* @@ -1371,7 +1394,7 @@ void draw_bars() { 1, &rect); - if (statusline != NULL) { + if (!TAILQ_EMPTY(&statusline_head)) { DLOG("Printing statusline!\n"); /* Luckily we already prepared a seperate pixmap containing the rendered