From c55abca11555d735bf15b14d4f7eaeb61ee6c034 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sun, 10 Jul 2011 14:33:19 +0200 Subject: [PATCH] add i3-nagbar. tells you about config file errors (for example) --- common.mk | 2 + i3-nagbar/Makefile | 28 +++ i3-nagbar/atoms.xmacro | 6 + i3-nagbar/i3-nagbar.h | 26 +++ i3-nagbar/main.c | 392 +++++++++++++++++++++++++++++++++++++++++ i3-nagbar/xcb.c | 132 ++++++++++++++ include/config.h | 13 ++ include/i3.h | 1 + include/log.h | 12 +- include/util.h | 17 ++ man/Makefile | 3 +- man/i3-nagbar.man | 34 ++++ src/cfgparse.y | 141 +++++++++++---- src/cmdparse.y | 1 + src/log.c | 24 +++ src/main.c | 27 +-- src/util.c | 50 ++++++ 17 files changed, 864 insertions(+), 45 deletions(-) create mode 100644 i3-nagbar/Makefile create mode 100644 i3-nagbar/atoms.xmacro create mode 100644 i3-nagbar/i3-nagbar.h create mode 100644 i3-nagbar/main.c create mode 100644 i3-nagbar/xcb.c create mode 100644 man/i3-nagbar.man diff --git a/common.mk b/common.mk index fed147b8..41946d41 100644 --- a/common.mk +++ b/common.mk @@ -8,6 +8,7 @@ SYSCONFDIR=/etc else SYSCONFDIR=$(PREFIX)/etc endif +TERM_EMU=xterm # The escaping is absurd, but we need to escape for shell, sed, make, define GIT_VERSION:="$(shell git describe --tags --always) ($(shell git log --pretty=format:%cd --date=short -n1), branch $(shell [ -f .git/HEAD ] && sed 's/ref: refs\/heads\/\(.*\)/\\\\\\"\1\\\\\\"/g' .git/HEAD || echo 'unknown'))" VERSION:=$(shell git describe --tags --abbrev=0) @@ -46,6 +47,7 @@ CFLAGS += $(call cflags_for_lib, yajl) CFLAGS += $(call cflags_for_lib, libev) CFLAGS += -DI3_VERSION=\"${GIT_VERSION}\" CFLAGS += -DSYSCONFDIR=\"${SYSCONFDIR}\" +CFLAGS += -DTERM_EMU=\"$(TERM_EMU)\" LDFLAGS += -lm LDFLAGS += $(call ldflags_for_lib, xcb-event, xcb-event) diff --git a/i3-nagbar/Makefile b/i3-nagbar/Makefile new file mode 100644 index 00000000..8d9283bc --- /dev/null +++ b/i3-nagbar/Makefile @@ -0,0 +1,28 @@ +# Default value so one can compile i3-nagbar standalone +TOPDIR=.. + +include $(TOPDIR)/common.mk + +# Depend on the object files of all source-files in src/*.c and on all header files +FILES=$(patsubst %.c,%.o,$(wildcard *.c)) +HEADERS=$(wildcard *.h) + +# Depend on the specific file (.c for each .o) and on all headers +%.o: %.c ${HEADERS} + echo "CC $<" + $(CC) $(CFLAGS) -c -o $@ $< + +all: ${FILES} + echo "LINK i3-nagbar" + $(CC) -o i3-nagbar ${FILES} $(LDFLAGS) + +install: all + echo "INSTALL" + $(INSTALL) -d -m 0755 $(DESTDIR)$(PREFIX)/bin + $(INSTALL) -m 0755 i3-nagbar $(DESTDIR)$(PREFIX)/bin/ + +clean: + rm -f *.o + +distclean: clean + rm -f i3-nagbar diff --git a/i3-nagbar/atoms.xmacro b/i3-nagbar/atoms.xmacro new file mode 100644 index 00000000..333ba2d6 --- /dev/null +++ b/i3-nagbar/atoms.xmacro @@ -0,0 +1,6 @@ +xmacro(_NET_WM_WINDOW_TYPE) +xmacro(_NET_WM_WINDOW_TYPE_DOCK) +xmacro(_NET_WM_STRUT_PARTIAL) +xmacro(I3_SOCKET_PATH) +xmacro(ATOM) +xmacro(CARDINAL) diff --git a/i3-nagbar/i3-nagbar.h b/i3-nagbar/i3-nagbar.h new file mode 100644 index 00000000..2fbe3cbb --- /dev/null +++ b/i3-nagbar/i3-nagbar.h @@ -0,0 +1,26 @@ +#ifndef _I3_NAGBAR +#define _I3_NAGBAR + +#include + +#define die(...) errx(EXIT_FAILURE, __VA_ARGS__); +#define FREE(pointer) do { \ + if (pointer != NULL) { \ + free(pointer); \ + pointer = NULL; \ + } \ +} \ +while (0) + +#define xmacro(atom) xcb_atom_t A_ ## atom; +#include "atoms.xmacro" +#undef xmacro + +extern xcb_window_t root; + +uint32_t get_colorpixel(xcb_connection_t *conn, char *hex); +xcb_window_t open_input_window(xcb_connection_t *conn, uint32_t width, uint32_t height); +int get_font_id(xcb_connection_t *conn, char *pattern, int *font_height); +void xcb_change_gc_single(xcb_connection_t *conn, xcb_gcontext_t gc, uint32_t mask, uint32_t value); + +#endif diff --git a/i3-nagbar/main.c b/i3-nagbar/main.c new file mode 100644 index 00000000..73d6f63b --- /dev/null +++ b/i3-nagbar/main.c @@ -0,0 +1,392 @@ +/* + * vim:ts=4:sw=4:expandtab + * + * i3 - an improved dynamic tiling window manager + * + * © 2009-2011 Michael Stapelberg and contributors + * + * See file LICENSE for license information. + * + * i3-nagbar is a utility which displays a nag message. + * + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "i3-nagbar.h" + +typedef struct { + char *label; + char *action; + int16_t x; + uint16_t width; +} button_t; + +static xcb_window_t win; +static xcb_pixmap_t pixmap; +static xcb_gcontext_t pixmap_gc; +static xcb_rectangle_t rect = { 0, 0, 600, 20 }; +static char *glyphs_ucs[512]; +static int input_position; +static int font_height; +static char *prompt = "You have an error in your i3 config file!"; +static button_t *buttons; +static int buttoncnt; +xcb_window_t root; + +/* + * Starts the given application by passing it through a shell. We use double fork + * to avoid zombie processes. As the started application’s parent exits (immediately), + * the application is reparented to init (process-id 1), which correctly handles + * childs, so we don’t have to do it :-). + * + * The shell is determined by looking for the SHELL environment variable. If it + * does not exist, /bin/sh is used. + * + */ +static void start_application(const char *command) { + printf("executing: %s\n", command); + if (fork() == 0) { + /* Child process */ + setsid(); + if (fork() == 0) { + /* Stores the path of the shell */ + static const char *shell = NULL; + + if (shell == NULL) + if ((shell = getenv("SHELL")) == NULL) + shell = "/bin/sh"; + + /* This is the child */ + execl(shell, shell, "-c", command, (void*)NULL); + /* not reached */ + } + exit(0); + } + wait(0); +} + +static button_t *get_button_at(int16_t x, int16_t y) { + for (int c = 0; c < buttoncnt; c++) + if (x >= (buttons[c].x) && x <= (buttons[c].x + buttons[c].width)) + return &buttons[c]; + + return NULL; +} + +static void handle_button_press(xcb_connection_t *conn, xcb_button_press_event_t *event) { + printf("button pressed on x = %d, y = %d\n", + event->event_x, event->event_y); + /* TODO: set a flag for the button, re-render */ +} + +/* + * Called when the user releases the mouse button. Checks whether the + * coordinates are over a button and executes the appropriate action. + * + */ +static void handle_button_release(xcb_connection_t *conn, xcb_button_release_event_t *event) { + printf("button released on x = %d, y = %d\n", + event->event_x, event->event_y); + /* If the user hits the close button, we exit(0) */ + if (event->event_x >= (rect.width - 32)) + exit(0); + button_t *button = get_button_at(event->event_x, event->event_y); + if (!button) + return; + start_application(button->action); + + /* TODO: unset flag, re-render */ +} + +/* + * Handles expose events (redraws of the window) and rendering in general. Will + * be called from the code with event == NULL or from X with event != NULL. + * + */ +static int handle_expose(xcb_connection_t *conn, xcb_expose_event_t *event) { + printf("expose!\n"); + + /* re-draw the background */ + xcb_change_gc_single(conn, pixmap_gc, XCB_GC_FOREGROUND, get_colorpixel(conn, "#900000")); + xcb_poly_fill_rectangle(conn, pixmap, pixmap_gc, 1, &rect); + + /* restore font color */ + uint32_t values[3]; + values[0] = get_colorpixel(conn, "#FFFFFF"); + values[1] = get_colorpixel(conn, "#900000"); + xcb_change_gc(conn, pixmap_gc, XCB_GC_FOREGROUND | XCB_GC_BACKGROUND, values); + xcb_image_text_8(conn, strlen(prompt), pixmap, pixmap_gc, 4 + 4/* X */, + font_height + 2 + 4 /* Y = baseline of font */, prompt); + + /* render close button */ + int line_width = 4; + int w = 20; + int y = rect.width; + values[0] = get_colorpixel(conn, "#680a0a"); + values[1] = line_width; + xcb_change_gc(conn, pixmap_gc, XCB_GC_FOREGROUND | XCB_GC_LINE_WIDTH, values); + + xcb_rectangle_t close = { y - w - (2 * line_width), 0, w + (2 * line_width), rect.height }; + xcb_poly_fill_rectangle(conn, pixmap, pixmap_gc, 1, &close); + + xcb_change_gc_single(conn, pixmap_gc, XCB_GC_FOREGROUND, get_colorpixel(conn, "#d92424")); + xcb_point_t points[] = { + { y - w - (2 * line_width), line_width / 2 }, + { y - (line_width / 2), line_width / 2 }, + { y - (line_width / 2), (rect.height - (line_width / 2)) - 2 }, + { y - w - (2 * line_width), (rect.height - (line_width / 2)) - 2 }, + { y - w - (2 * line_width), line_width / 2 } + }; + xcb_poly_line(conn, XCB_COORD_MODE_ORIGIN, pixmap, pixmap_gc, 5, points); + + values[0] = get_colorpixel(conn, "#ffffff"); + values[1] = get_colorpixel(conn, "#680a0a"); + values[2] = 1; + xcb_change_gc(conn, pixmap_gc, XCB_GC_FOREGROUND | XCB_GC_BACKGROUND | XCB_GC_LINE_WIDTH, values); + xcb_image_text_8(conn, strlen("x"), pixmap, pixmap_gc, y - w - line_width + (w / 2) - 4/* X */, + font_height + 2 + 4 - 1/* Y = baseline of font */, "X"); + y -= w; + + y -= 20; + + /* render custom buttons */ + line_width = 1; + for (int c = 0; c < buttoncnt; c++) { + /* TODO: make w = text extents of the label */ + w = 90; + y -= 30; + xcb_change_gc_single(conn, pixmap_gc, XCB_GC_FOREGROUND, get_colorpixel(conn, "#680a0a")); + close = (xcb_rectangle_t){ y - w - (2 * line_width), 2, w + (2 * line_width), rect.height - 6 }; + xcb_poly_fill_rectangle(conn, pixmap, pixmap_gc, 1, &close); + + xcb_change_gc_single(conn, pixmap_gc, XCB_GC_FOREGROUND, get_colorpixel(conn, "#d92424")); + buttons[c].x = y - w - (2 * line_width); + buttons[c].width = w; + xcb_point_t points2[] = { + { y - w - (2 * line_width), (line_width / 2) + 2 }, + { y - (line_width / 2), (line_width / 2) + 2 }, + { y - (line_width / 2), (rect.height - 4 - (line_width / 2)) }, + { y - w - (2 * line_width), (rect.height - 4 - (line_width / 2)) }, + { y - w - (2 * line_width), (line_width / 2) + 2 } + }; + xcb_poly_line(conn, XCB_COORD_MODE_ORIGIN, pixmap, pixmap_gc, 5, points2); + + values[0] = get_colorpixel(conn, "#ffffff"); + values[1] = get_colorpixel(conn, "#680a0a"); + xcb_change_gc(conn, pixmap_gc, XCB_GC_FOREGROUND | XCB_GC_BACKGROUND, values); + xcb_image_text_8(conn, strlen(buttons[c].label), pixmap, pixmap_gc, y - w - line_width + 6/* X */, + font_height + 2 + 3/* Y = baseline of font */, buttons[c].label); + + y -= w; + } + + /* border line at the bottom */ + line_width = 2; + values[0] = get_colorpixel(conn, "#470909"); + values[1] = line_width; + xcb_change_gc(conn, pixmap_gc, XCB_GC_FOREGROUND | XCB_GC_LINE_WIDTH, values); + xcb_point_t bottom[] = { + { 0, rect.height - 0 }, + { rect.width, rect.height - 0 } + }; + xcb_poly_line(conn, XCB_COORD_MODE_ORIGIN, pixmap, pixmap_gc, 2, bottom); + + + /* Copy the contents of the pixmap to the real window */ + xcb_copy_area(conn, pixmap, win, pixmap_gc, 0, 0, 0, 0, rect.width, rect.height); + xcb_flush(conn); + + return 1; +} + +int main(int argc, char *argv[]) { + char *pattern = "-misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1"; + int o, option_index = 0; + + static struct option long_options[] = { + {"version", no_argument, 0, 'v'}, + {"font", required_argument, 0, 'f'}, + {"button", required_argument, 0, 'b'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + char *options_string = "b:f:vh"; + + while ((o = getopt_long(argc, argv, options_string, long_options, &option_index)) != -1) { + switch (o) { + case 'v': + printf("i3-nagbar " I3_VERSION); + return 0; + case 'f': + FREE(pattern); + pattern = strdup(optarg); + break; + case 'h': + printf("i3-nagbar " I3_VERSION); + printf("i3-nagbar [-s ] [-p ] [-l ] [-P ] [-f ] [-v]\n"); + return 0; + case 'b': + buttons = realloc(buttons, sizeof(button_t) * (buttoncnt + 1)); + buttons[buttoncnt].label = optarg; + buttons[buttoncnt].action = argv[optind]; + printf("button with label *%s* and action *%s*\n", + buttons[buttoncnt].label, + buttons[buttoncnt].action); + buttoncnt++; + printf("now %d buttons\n", buttoncnt); + if (optind < argc) + optind++; + break; + } + } + + int screens; + xcb_connection_t *conn = xcb_connect(NULL, &screens); + if (xcb_connection_has_error(conn)) + die("Cannot open display\n"); + + /* Place requests for the atoms we need as soon as possible */ + #define xmacro(atom) \ + xcb_intern_atom_cookie_t atom ## _cookie = xcb_intern_atom(conn, 0, strlen(#atom), #atom); + #include "atoms.xmacro" + #undef xmacro + + xcb_screen_t *root_screen = xcb_aux_get_screen(conn, screens); + root = root_screen->root; + + uint32_t font_id = get_font_id(conn, pattern, &font_height); + + /* Open an input window */ + win = open_input_window(conn, 500, font_height + 8 + 8 /* 8px padding */); + + /* Setup NetWM atoms */ + #define xmacro(name) \ + do { \ + xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(conn, name ## _cookie, NULL); \ + if (!reply) \ + die("Could not get atom " # name "\n"); \ + \ + A_ ## name = reply->atom; \ + free(reply); \ + } while (0); + #include "atoms.xmacro" + #undef xmacro + + /* Set dock mode */ + xcb_void_cookie_t dock_cookie = xcb_change_property(conn, + XCB_PROP_MODE_REPLACE, + win, + A__NET_WM_WINDOW_TYPE, + A_ATOM, + 32, + 1, + (unsigned char*) &A__NET_WM_WINDOW_TYPE_DOCK); + + /* Reserve some space at the top of the screen */ + struct { + uint32_t left; + uint32_t right; + uint32_t top; + uint32_t bottom; + uint32_t left_start_y; + uint32_t left_end_y; + uint32_t right_start_y; + uint32_t right_end_y; + uint32_t top_start_x; + uint32_t top_end_x; + uint32_t bottom_start_x; + uint32_t bottom_end_x; + } __attribute__((__packed__)) strut_partial = {0,}; + + strut_partial.top = font_height + 6; + strut_partial.top_start_x = 0; + strut_partial.top_end_x = 800; + + xcb_void_cookie_t strut_cookie = xcb_change_property(conn, + XCB_PROP_MODE_REPLACE, + win, + A__NET_WM_STRUT_PARTIAL, + A_CARDINAL, + 32, + 12, + &strut_partial); + + /* Create pixmap */ + pixmap = xcb_generate_id(conn); + pixmap_gc = xcb_generate_id(conn); + xcb_create_pixmap(conn, root_screen->root_depth, pixmap, win, 500, font_height + 8); + xcb_create_gc(conn, pixmap_gc, pixmap, 0, 0); + + /* Create graphics context */ + xcb_change_gc_single(conn, pixmap_gc, XCB_GC_FONT, font_id); + + /* Grab the keyboard to get all input */ + xcb_flush(conn); + + xcb_generic_event_t *event; + while ((event = xcb_wait_for_event(conn)) != NULL) { + if (event->response_type == 0) { + fprintf(stderr, "X11 Error received! sequence %x\n", event->sequence); + continue; + } + + /* Strip off the highest bit (set if the event is generated) */ + int type = (event->response_type & 0x7F); + + switch (type) { + case XCB_EXPOSE: + handle_expose(conn, (xcb_expose_event_t*)event); + break; + + case XCB_BUTTON_PRESS: + handle_button_press(conn, (xcb_button_press_event_t*)event); + break; + + case XCB_BUTTON_RELEASE: + handle_button_release(conn, (xcb_button_release_event_t*)event); + break; + + case XCB_CONFIGURE_NOTIFY: { + xcb_configure_notify_event_t *configure_notify = (xcb_configure_notify_event_t*)event; + rect = (xcb_rectangle_t){ + configure_notify->x, + configure_notify->y, + configure_notify->width, + configure_notify->height + }; + + /* Recreate the pixmap / gc */ + xcb_free_pixmap(conn, pixmap); + xcb_free_gc(conn, pixmap_gc); + + xcb_create_pixmap(conn, root_screen->root_depth, pixmap, win, rect.width, rect.height); + xcb_create_gc(conn, pixmap_gc, pixmap, 0, 0); + + /* Create graphics context */ + xcb_change_gc_single(conn, pixmap_gc, XCB_GC_FONT, font_id); + break; + } + } + + free(event); + } + + return 0; +} diff --git a/i3-nagbar/xcb.c b/i3-nagbar/xcb.c new file mode 100644 index 00000000..ed1bfd89 --- /dev/null +++ b/i3-nagbar/xcb.c @@ -0,0 +1,132 @@ +/* + * vim:ts=8:expandtab + * + * i3 - an improved dynamic tiling window manager + * + * © 2009 Michael Stapelberg and contributors + * + * See file LICENSE for license information. + * + */ +#include +#include +#include +#include + +#include +#include + +#include + +#include "i3-nagbar.h" + +/* + * Convenience-wrapper around xcb_change_gc which saves us declaring a variable + * + */ +void xcb_change_gc_single(xcb_connection_t *conn, xcb_gcontext_t gc, uint32_t mask, uint32_t value) { + xcb_change_gc(conn, gc, mask, &value); +} + +/* + * Returns the colorpixel to use for the given hex color (think of HTML). + * + * The hex_color has to start with #, for example #FF00FF. + * + * NOTE that get_colorpixel() does _NOT_ check the given color code for validity. + * This has to be done by the caller. + * + */ +uint32_t get_colorpixel(xcb_connection_t *conn, char *hex) { + char strgroups[3][3] = {{hex[1], hex[2], '\0'}, + {hex[3], hex[4], '\0'}, + {hex[5], hex[6], '\0'}}; + uint32_t rgb16[3] = {(strtol(strgroups[0], NULL, 16)), + (strtol(strgroups[1], NULL, 16)), + (strtol(strgroups[2], NULL, 16))}; + + return (rgb16[0] << 16) + (rgb16[1] << 8) + rgb16[2]; +} + +/* + * Opens the window we use for input/output and maps it + * + */ +xcb_window_t open_input_window(xcb_connection_t *conn, uint32_t width, uint32_t height) { + xcb_window_t win = xcb_generate_id(conn); + //xcb_cursor_t cursor_id = xcb_generate_id(conn); + +#if 0 + /* Use the default cursor (left pointer) */ + if (cursor > -1) { + i3Font *cursor_font = load_font(conn, "cursor"); + xcb_create_glyph_cursor(conn, cursor_id, cursor_font->id, cursor_font->id, + XCB_CURSOR_LEFT_PTR, XCB_CURSOR_LEFT_PTR + 1, + 0, 0, 0, 65535, 65535, 65535); + } +#endif + + uint32_t mask = 0; + uint32_t values[3]; + + mask |= XCB_CW_BACK_PIXEL; + values[0] = 0; + + mask |= XCB_CW_EVENT_MASK; + values[1] = XCB_EVENT_MASK_EXPOSURE | + XCB_EVENT_MASK_STRUCTURE_NOTIFY | + XCB_EVENT_MASK_BUTTON_PRESS | + XCB_EVENT_MASK_BUTTON_RELEASE; + + xcb_create_window(conn, + XCB_COPY_FROM_PARENT, + win, /* the window id */ + root, /* parent == root */ + 50, 50, width, height, /* dimensions */ + 0, /* border = 0, we draw our own */ + XCB_WINDOW_CLASS_INPUT_OUTPUT, + XCB_WINDOW_CLASS_COPY_FROM_PARENT, /* copy visual from parent */ + mask, + values); + +#if 0 + if (cursor > -1) + xcb_change_window_attributes(conn, result, XCB_CW_CURSOR, &cursor_id); +#endif + + /* Map the window (= make it visible) */ + xcb_map_window(conn, win); + + return win; +} + +/* + * Returns the ID of the font matching the given pattern and stores the height + * of the font (in pixels) in *font_height. die()s if no font matches. + * + */ +int get_font_id(xcb_connection_t *conn, char *pattern, int *font_height) { + xcb_void_cookie_t font_cookie; + xcb_list_fonts_with_info_cookie_t info_cookie; + + /* Send all our requests first */ + int result; + result = xcb_generate_id(conn); + font_cookie = xcb_open_font_checked(conn, result, strlen(pattern), pattern); + info_cookie = xcb_list_fonts_with_info(conn, 1, strlen(pattern), pattern); + + xcb_generic_error_t *error = xcb_request_check(conn, font_cookie); + if (error != NULL) { + fprintf(stderr, "ERROR: Could not open font: %d\n", error->error_code); + exit(1); + } + + /* Get information (height/name) for this font */ + xcb_list_fonts_with_info_reply_t *reply = xcb_list_fonts_with_info_reply(conn, info_cookie, NULL); + if (reply == NULL) + die("Could not load font \"%s\"\n", pattern); + + *font_height = reply->font_ascent + reply->font_descent; + + return result; +} diff --git a/include/config.h b/include/config.h index 9ba5e0f9..1021a612 100644 --- a/include/config.h +++ b/include/config.h @@ -32,6 +32,8 @@ extern SLIST_HEAD(modes_head, Mode) modes; * */ struct context { + bool has_errors; + int line_number; char *line_copy; const char *filename; @@ -190,6 +192,17 @@ void switch_mode(const char *new_mode); */ Binding *get_binding(uint16_t modifiers, xcb_keycode_t keycode); +/** + * Kills the configerror i3-nagbar process, if any. + * + * Called when reloading/restarting. + * + * If wait_for_it is set (restarting), this function will waitpid(), otherwise, + * ev is assumed to handle it (reloading). + * + */ +void kill_configerror_nagbar(bool wait_for_it); + /* prototype for src/cfgparse.y */ void parse_file(const char *f); diff --git a/include/i3.h b/include/i3.h index 7eb48ecc..73b61178 100644 --- a/include/i3.h +++ b/include/i3.h @@ -32,5 +32,6 @@ extern SLIST_HEAD(stack_wins_head, Stack_Window) stack_wins; extern uint8_t root_depth; extern bool xcursor_supported, xkb_supported; extern xcb_window_t root; +extern struct ev_loop *main_loop; #endif diff --git a/include/log.h b/include/log.h index 9b284f0a..c1e10b06 100644 --- a/include/log.h +++ b/include/log.h @@ -1,9 +1,9 @@ /* - * vim:ts=8:expandtab + * vim:ts=4:sw=4:expandtab * * i3 - an improved dynamic tiling window manager * - * © 2009-2010 Michael Stapelberg and contributors + * © 2009-2011 Michael Stapelberg and contributors * * See file LICENSE for license information. * @@ -21,6 +21,14 @@ #define DLOG(fmt, ...) debuglog(LOGLEVEL, "%s:%s:%d - " fmt, __FILE__, __FUNCTION__, __LINE__, ##__VA_ARGS__) extern char *loglevels[]; +extern char *errorfilename; + +/** + * Initializes logging by creating an error logfile in /tmp (or + * XDG_RUNTIME_DIR, see get_process_filename()). + * + */ +void init_logging(); /** * Enables the given loglevel. diff --git a/include/util.h b/include/util.h index df0e3065..276ea5b9 100644 --- a/include/util.h +++ b/include/util.h @@ -102,6 +102,23 @@ char *sstrdup(const char *str); */ void start_application(const char *command); +/** + * exec()s an i3 utility, for example the config file migration script or + * i3-nagbar. This function first searches $PATH for the given utility named, + * then falls back to the dirname() of the i3 executable path and then falls + * back to the dirname() of the target of /proc/self/exe (on linux). + * + * This function should be called after fork()ing. + * + * The first argument of the given argv vector will be overwritten with the + * executable name, so pass NULL. + * + * If the utility cannot be found in any of these locations, it exits with + * return code 2. + * + */ +void exec_i3_utility(char *name, char *argv[]); + /** * Checks a generic cookie for errors and quits with the given message if * there was an error. diff --git a/man/Makefile b/man/Makefile index 151b9abc..cea07ed5 100644 --- a/man/Makefile +++ b/man/Makefile @@ -2,8 +2,9 @@ all: a2x -f manpage --asciidoc-opts="-f asciidoc.conf" i3.man a2x -f manpage --asciidoc-opts="-f asciidoc.conf" i3-msg.man a2x -f manpage --asciidoc-opts="-f asciidoc.conf" i3-input.man + a2x -f manpage --asciidoc-opts="-f asciidoc.conf" i3-nagbar.man clean: - for file in "i3 i3-msg i3-input"; \ + for file in "i3 i3-msg i3-input i3-nagbar"; \ do \ rm -f $${file}.1 $${file}.html $${file}.xml; \ done diff --git a/man/i3-nagbar.man b/man/i3-nagbar.man new file mode 100644 index 00000000..3dd37bb7 --- /dev/null +++ b/man/i3-nagbar.man @@ -0,0 +1,34 @@ +i3-nagbar(1) +============ +Michael Stapelberg +v4.0, July 2011 + +== NAME + +i3-nagbar - displays an error bar on top of your screen + +== SYNOPSIS + +i3-nagbar -m 'message' -b 'label' 'action' + +== DESCRIPTION + +i3-nagbar is used by i3 to tell you about errors in your configuration file +(for example). While these errors are logged to the logfile (if any), the past +has proven that users are either not aware of their logfile or do not check it +after modifying the configuration file. + +== EXAMPLE + +------------------------------------------------ +i3-nagbar -m 'You have an error in your i3 config file!' \ +-b 'edit config' 'xterm $EDITOR ~/.i3/config' +------------------------------------------------ + +== SEE ALSO + +i3(1) + +== AUTHOR + +Michael Stapelberg and contributors diff --git a/src/cfgparse.y b/src/cfgparse.y index 83d95fc5..22747108 100644 --- a/src/cfgparse.y +++ b/src/cfgparse.y @@ -9,10 +9,11 @@ #include #include #include -#include #include "all.h" +static pid_t configerror_pid = -1; + static Match current_match; typedef struct yy_buffer_state *YY_BUFFER_STATE; @@ -30,17 +31,18 @@ static struct context *context; //int yydebug = 1; void yyerror(const char *error_message) { + context->has_errors = true; + ELOG("\n"); ELOG("CONFIG: %s\n", error_message); ELOG("CONFIG: in file \"%s\", line %d:\n", context->filename, context->line_number); ELOG("CONFIG: %s\n", context->line_copy); - ELOG("CONFIG: "); + char buffer[context->last_column+1]; + buffer[context->last_column] = '\0'; for (int c = 1; c <= context->last_column; c++) - if (c >= context->first_column) - printf("^"); - else printf(" "); - printf("\n"); + buffer[c-1] = (c >= context->first_column ? '^' : ' '); + ELOG("CONFIG: %s\n", buffer); ELOG("\n"); } @@ -146,32 +148,11 @@ static char *migrate_config(char *input, off_t size) { close(readpipe[0]); dup2(readpipe[1], 1); - /* start the migration script, search PATH first */ - char *migratepath = "i3-migrate-config-to-v4.pl"; - execlp(migratepath, migratepath, NULL); - - /* if the script is not in path, maybe the user installed to a strange - * location and runs the i3 binary with an absolute path. We use - * argv[0]’s dirname */ - char *pathbuf = strdup(start_argv[0]); - char *dir = dirname(pathbuf); - asprintf(&migratepath, "%s/%s", dir, "i3-migrate-config-to-v4.pl"); - execlp(migratepath, migratepath, NULL); - -#if defined(__linux__) - /* on linux, we have one more fall-back: dirname(/proc/self/exe) */ - char buffer[BUFSIZ]; - if (readlink("/proc/self/exe", buffer, BUFSIZ) == -1) { - warn("could not read /proc/self/exe"); - exit(1); - } - dir = dirname(buffer); - asprintf(&migratepath, "%s/%s", dir, "i3-migrate-config-to-v4.pl"); - execlp(migratepath, migratepath, NULL); -#endif - - warn("Could not start i3-migrate-config-to-v4.pl"); - exit(2); + static char *argv[] = { + NULL, /* will be replaced by the executable path */ + NULL + }; + exec_i3_utility("i3-migrate-config-to-v4.pl", argv); } /* parent */ @@ -237,6 +218,98 @@ static char *migrate_config(char *input, off_t size) { return converted; } +/* + * Handler which will be called when we get a SIGCHLD for the nagbar, meaning + * it exited (or could not be started, depending on the exit code). + * + */ +static void nagbar_exited(EV_P_ ev_child *watcher, int revents) { + ev_child_stop(EV_A_ watcher); + if (!WIFEXITED(watcher->rstatus)) { + fprintf(stderr, "ERROR: i3-nagbar did not exit normally.\n"); + return; + } + + int exitcode = WEXITSTATUS(watcher->rstatus); + printf("i3-nagbar process exited with status %d\n", exitcode); + if (exitcode == 2) { + fprintf(stderr, "ERROR: i3-nagbar could not be found. Is it correctly installed on your system?\n"); + } + + configerror_pid = -1; +} + +/* + * Starts an i3-nagbar process which alerts the user that his configuration + * file contains one or more errors. Also offers two buttons: One to launch an + * $EDITOR on the config file and another one to launch a $PAGER on the error + * logfile. + * + */ +static void start_configerror_nagbar(const char *config_path) { + fprintf(stderr, "Would start i3-nagscreen now\n"); + configerror_pid = fork(); + if (configerror_pid == -1) { + warn("Could not fork()"); + return; + } + + /* child */ + if (configerror_pid == 0) { + char *editaction, + *pageraction; + if (asprintf(&editaction, TERM_EMU " -e $EDITOR \"%s\"", config_path) == -1) + exit(1); + if (asprintf(&pageraction, TERM_EMU " -e $PAGER \"%s\"", errorfilename) == -1) + exit(1); + char *argv[] = { + NULL, /* will be replaced by the executable path */ + "-m", + "You have an error in your i3 config file!", + "-b", + "edit config", + editaction, + (errorfilename ? "-b" : NULL), + "show errors", + pageraction, + NULL + }; + exec_i3_utility("i3-nagbar", argv); + } + + /* parent */ + /* install a child watcher */ + ev_child *child = smalloc(sizeof(ev_child)); + ev_child_init(child, &nagbar_exited, configerror_pid, 0); + ev_child_start(main_loop, child); +} + +/* + * Kills the configerror i3-nagbar process, if any. + * + * Called when reloading/restarting. + * + * If wait_for_it is set (restarting), this function will waitpid(), otherwise, + * ev is assumed to handle it (reloading). + * + */ +void kill_configerror_nagbar(bool wait_for_it) { + if (configerror_pid == -1) + return; + + if (kill(configerror_pid, SIGTERM) == -1) + warn("kill(configerror_nagbar) failed"); + + if (!wait_for_it) + return; + + /* When restarting, we don’t enter the ev main loop anymore and after the + * exec(), our old pid is no longer watched. So, ev won’t handle SIGCHLD + * for us and we would end up with a process. Therefore we + * waitpid() here. */ + waitpid(configerror_pid, NULL, 0); +} + void parse_file(const char *f) { SLIST_HEAD(variables_head, Variable) variables = SLIST_HEAD_INITIALIZER(&variables); int fd, ret, read_bytes = 0; @@ -390,6 +463,10 @@ void parse_file(const char *f) { exit(1); } + if (context->has_errors) { + start_configerror_nagbar(f); + } + FREE(context->line_copy); free(context); free(new); diff --git a/src/cmdparse.y b/src/cmdparse.y index 95dc27b6..0b80b6b3 100644 --- a/src/cmdparse.y +++ b/src/cmdparse.y @@ -380,6 +380,7 @@ reload: TOK_RELOAD { printf("reloading\n"); + kill_configerror_nagbar(false); load_configuration(conn, NULL, true); x_set_i3_atoms(); /* Send an IPC event just in case the ws names have changed */ diff --git a/src/log.c b/src/log.c index 0371e9be..99c2d4d3 100644 --- a/src/log.c +++ b/src/log.c @@ -14,6 +14,7 @@ #include #include #include +#include #include "util.h" #include "log.h" @@ -23,6 +24,23 @@ static uint64_t loglevel = 0; static bool verbose = true; +static FILE *errorfile; +char *errorfilename; + +/* + * Initializes logging by creating an error logfile in /tmp (or + * XDG_RUNTIME_DIR, see get_process_filename()). + * + */ +void init_logging() { + errorfilename = get_process_filename("errorlog"); + if (errorfilename == NULL) { + ELOG("Could not initialize errorlog\n"); + return; + } + + errorfile = fopen(errorfilename, "w"); +} /* * Set verbosity of i3. If verbose is set to true, informative messages will @@ -101,6 +119,12 @@ void errorlog(char *fmt, ...) { va_start(args, fmt); vlog(fmt, args); va_end(args); + + /* also log to the error logfile, if opened */ + va_start(args, fmt); + vfprintf(errorfile, fmt, args); + fflush(errorfile); + va_end(args); } /* diff --git a/src/main.c b/src/main.c index b4ed4a1a..a2764cc1 100644 --- a/src/main.c +++ b/src/main.c @@ -19,6 +19,8 @@ xcb_connection_t *conn; xcb_window_t root; uint8_t root_depth; +struct ev_loop *main_loop; + xcb_key_symbols_t *keysyms; /* Those are our connections to X11 for use with libXcursor and XKB */ @@ -178,6 +180,8 @@ int main(int argc, char *argv[]) { if (!isatty(fileno(stdout))) setbuf(stdout, NULL); + init_logging(); + start_argv = argv; while ((opt = getopt_long(argc, argv, "c:CvaL:hld:V", long_options, &option_index)) != -1) { @@ -254,6 +258,13 @@ int main(int argc, char *argv[]) { if (xcb_connection_has_error(conn)) errx(EXIT_FAILURE, "Cannot open display\n"); + /* Initialize the libev event loop. This needs to be done before loading + * the config file because the parser will install an ev_child watcher + * for the nagbar when config errors are found. */ + main_loop = EV_DEFAULT; + if (main_loop == NULL) + die("Could not initialize libev. Bad LIBEV_FLAGS?\n"); + xcb_screen_t *root_screen = xcb_aux_get_screen(conn, screens); root = root_screen->root; root_depth = root_screen->root_depth; @@ -395,10 +406,6 @@ int main(int argc, char *argv[]) { tree_render(); - struct ev_loop *loop = ev_loop_new(0); - if (loop == NULL) - die("Could not initialize libev. Bad LIBEV_FLAGS?\n"); - /* Create the UNIX domain socket for IPC */ int ipc_socket = ipc_create_socket(config.ipc_socket_path); if (ipc_socket == -1) { @@ -407,7 +414,7 @@ int main(int argc, char *argv[]) { free(config.ipc_socket_path); struct ev_io *ipc_io = scalloc(sizeof(struct ev_io)); ev_io_init(ipc_io, ipc_new_client, ipc_socket, EV_READ); - ev_io_start(loop, ipc_io); + ev_io_start(main_loop, ipc_io); } /* Set up i3 specific atoms like I3_SOCKET_PATH and I3_CONFIG_PATH */ @@ -419,22 +426,22 @@ int main(int argc, char *argv[]) { struct ev_prepare *xcb_prepare = scalloc(sizeof(struct ev_prepare)); ev_io_init(xcb_watcher, xcb_got_event, xcb_get_file_descriptor(conn), EV_READ); - ev_io_start(loop, xcb_watcher); + ev_io_start(main_loop, xcb_watcher); if (xkb_supported) { ev_io_init(xkb, xkb_got_event, ConnectionNumber(xkbdpy), EV_READ); - ev_io_start(loop, xkb); + ev_io_start(main_loop, xkb); /* Flush the buffer so that libev can properly get new events */ XFlush(xkbdpy); } ev_check_init(xcb_check, xcb_check_cb); - ev_check_start(loop, xcb_check); + ev_check_start(main_loop, xcb_check); ev_prepare_init(xcb_prepare, xcb_prepare_cb); - ev_prepare_start(loop, xcb_prepare); + ev_prepare_start(main_loop, xcb_prepare); xcb_flush(conn); @@ -456,5 +463,5 @@ int main(int argc, char *argv[]) { } } - ev_loop(loop, 0); + ev_loop(main_loop, 0); } diff --git a/src/util.c b/src/util.c index f95ccaf3..cc93df21 100644 --- a/src/util.c +++ b/src/util.c @@ -19,6 +19,7 @@ #include #include #include +#include #include "all.h" @@ -118,6 +119,53 @@ void start_application(const char *command) { wait(0); } +/* + * exec()s an i3 utility, for example the config file migration script or + * i3-nagbar. This function first searches $PATH for the given utility named, + * then falls back to the dirname() of the i3 executable path and then falls + * back to the dirname() of the target of /proc/self/exe (on linux). + * + * This function should be called after fork()ing. + * + * The first argument of the given argv vector will be overwritten with the + * executable name, so pass NULL. + * + * If the utility cannot be found in any of these locations, it exits with + * return code 2. + * + */ +void exec_i3_utility(char *name, char *argv[]) { + /* start the migration script, search PATH first */ + char *migratepath = name; + argv[0] = migratepath; + execvp(migratepath, argv); + + /* if the script is not in path, maybe the user installed to a strange + * location and runs the i3 binary with an absolute path. We use + * argv[0]’s dirname */ + char *pathbuf = strdup(start_argv[0]); + char *dir = dirname(pathbuf); + asprintf(&migratepath, "%s/%s", dir, name); + argv[0] = migratepath; + execvp(migratepath, argv); + +#if defined(__linux__) + /* on linux, we have one more fall-back: dirname(/proc/self/exe) */ + char buffer[BUFSIZ]; + if (readlink("/proc/self/exe", buffer, BUFSIZ) == -1) { + warn("could not read /proc/self/exe"); + exit(1); + } + dir = dirname(buffer); + asprintf(&migratepath, "%s/%s", dir, name); + argv[0] = migratepath; + execvp(migratepath, argv); +#endif + + warn("Could not start %s", name); + exit(2); +} + /* * Checks a generic cookie for errors and quits with the given message if there * was an error. @@ -358,6 +406,8 @@ char *store_restart_layout() { void i3_restart(bool forget_layout) { char *restart_filename = forget_layout ? NULL : store_restart_layout(); + kill_configerror_nagbar(true); + restore_geometry(); ipc_shutdown();