diff --git a/docs/userguide b/docs/userguide index af669e9b..11772b69 100644 --- a/docs/userguide +++ b/docs/userguide @@ -1209,23 +1209,41 @@ Available modifiers are Mod1-Mod5, Shift, Control (see +xmodmap(1)+). === Mouse button commands Specifies a command to run when a button was pressed on i3bar to override the -default behavior. Currently only the mouse wheel buttons are supported. This is -useful for disabling the scroll wheel action or running scripts that implement -custom behavior for these buttons. +default behavior. This is useful, e.g., for disabling the scroll wheel action +or running scripts that implement custom behavior for these buttons. + +A button is always named +button+, where 1 to 5 are default buttons as follows and higher +numbers can be special buttons on devices offering more buttons: + +button1:: + Left mouse button. +button2:: + Middle mouse button. +button3:: + Right mouse button. +button4:: + Scroll wheel up. +button5:: + Scroll wheel down. + +Please note that the old +wheel_up_cmd+ and +wheel_down_cmd+ commands are deprecated +and will be removed in a future release. We strongly recommend using the more general ++bindsym+ with +button4+ and +button5+ instead. *Syntax*: ---------------------- -wheel_up_cmd -wheel_down_cmd ---------------------- +---------------------------- +bindsym button +---------------------------- *Example*: ---------------------- +--------------------------------------------------------- bar { - wheel_up_cmd nop - wheel_down_cmd exec ~/.i3/scripts/custom_wheel_down + # disable clicking on workspace buttons + bindsym button1 nop + # execute custom script when scrolling downwards + bindsym button5 exec ~/.i3/scripts/custom_wheel_down } ---------------------- +--------------------------------------------------------- === Bar ID diff --git a/i3bar/include/config.h b/i3bar/include/config.h index aeb9f0fd..d0291917 100644 --- a/i3bar/include/config.h +++ b/i3bar/include/config.h @@ -22,10 +22,16 @@ typedef enum { M_DOCK = 0, M_HIDE = 1, M_INVISIBLE = 2 } bar_display_mode_t; +typedef struct binding_t { + int input_code; + char *command; + + TAILQ_ENTRY(binding_t) bindings; +} binding_t; + typedef struct config_t { int modifier; - char *wheel_up_cmd; - char *wheel_down_cmd; + TAILQ_HEAD(bindings_head, binding_t) bindings; position_t position; int verbose; struct xcb_color_strings_t colors; diff --git a/i3bar/src/config.c b/i3bar/src/config.c index b708895a..a59dd5c1 100644 --- a/i3bar/src/config.c +++ b/i3bar/src/config.c @@ -20,6 +20,7 @@ #include "common.h" static char *cur_key; +static bool parsing_bindings; /* * Parse a key. @@ -34,6 +35,14 @@ static int config_map_key_cb(void *params_, const unsigned char *keyVal, size_t strncpy(cur_key, (const char *)keyVal, keyLen); cur_key[keyLen] = '\0'; + if (strcmp(cur_key, "bindings") == 0) + parsing_bindings = true; + + return 1; +} + +static int config_end_array_cb(void *params_) { + parsing_bindings = false; return 1; } @@ -63,6 +72,27 @@ static int config_string_cb(void *params_, const unsigned char *val, size_t _len if (!strcmp(cur_key, "id") || !strcmp(cur_key, "socket_path")) return 1; + if (parsing_bindings) { + if (strcmp(cur_key, "command") == 0) { + binding_t *binding = TAILQ_LAST(&(config.bindings), bindings_head); + if (binding == NULL) { + ELOG("There is no binding to put the current command onto. This is a bug in i3.\n"); + return 0; + } + + if (binding->command != NULL) { + ELOG("The binding for input_code = %d already has a command. This is a bug in i3.\n", binding->input_code); + return 0; + } + + sasprintf(&(binding->command), "%.*s", len, val); + return 1; + } + + ELOG("Unknown key \"%s\" while parsing bar bindings.\n", cur_key); + return 0; + } + if (!strcmp(cur_key, "mode")) { DLOG("mode = %.*s, len = %d\n", len, val, len); config.hide_on_modifier = (len == 4 && !strncmp((const char *)val, "dock", strlen("dock")) ? M_DOCK @@ -112,17 +142,25 @@ static int config_string_cb(void *params_, const unsigned char *val, size_t _len return 1; } + /* This key was sent in <= 4.10.2. We keep it around to avoid breakage for + * users updating from that version and restarting i3bar before i3. */ if (!strcmp(cur_key, "wheel_up_cmd")) { DLOG("wheel_up_cmd = %.*s\n", len, val); - FREE(config.wheel_up_cmd); - sasprintf(&config.wheel_up_cmd, "%.*s", len, val); + binding_t *binding = scalloc(sizeof(binding_t)); + binding->input_code = 4; + sasprintf(&(binding->command), "%.*s", len, val); + TAILQ_INSERT_TAIL(&(config.bindings), binding, bindings); return 1; } + /* This key was sent in <= 4.10.2. We keep it around to avoid breakage for + * users updating from that version and restarting i3bar before i3. */ if (!strcmp(cur_key, "wheel_down_cmd")) { DLOG("wheel_down_cmd = %.*s\n", len, val); - FREE(config.wheel_down_cmd); - sasprintf(&config.wheel_down_cmd, "%.*s", len, val); + binding_t *binding = scalloc(sizeof(binding_t)); + binding->input_code = 5; + sasprintf(&(binding->command), "%.*s", len, val); + TAILQ_INSERT_TAIL(&(config.bindings), binding, bindings); return 1; } @@ -232,11 +270,34 @@ static int config_boolean_cb(void *params_, int val) { return 0; } +/* + * Parse an integer value + * + */ +static int config_integer_cb(void *params_, long long val) { + if (parsing_bindings) { + if (strcmp(cur_key, "input_code") == 0) { + binding_t *binding = scalloc(sizeof(binding_t)); + binding->input_code = val; + TAILQ_INSERT_TAIL(&(config.bindings), binding, bindings); + + return 1; + } + + ELOG("Unknown key \"%s\" while parsing bar bindings.\n", cur_key); + return 0; + } + + return 0; +} + /* A datastructure to pass all these callbacks to yajl */ static yajl_callbacks outputs_callbacks = { .yajl_null = config_null_cb, .yajl_boolean = config_boolean_cb, + .yajl_integer = config_integer_cb, .yajl_string = config_string_cb, + .yajl_end_array = config_end_array_cb, .yajl_map_key = config_map_key_cb, }; @@ -249,6 +310,8 @@ void parse_config_json(char *json) { yajl_status state; handle = yajl_alloc(&outputs_callbacks, NULL, NULL); + TAILQ_INIT(&(config.bindings)); + state = yajl_parse(handle, (const unsigned char *)json, strlen(json)); /* FIXME: Proper error handling for JSON parsing */ diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c index b59288a0..365ea366 100644 --- a/i3bar/src/xcb.c +++ b/i3bar/src/xcb.c @@ -468,20 +468,23 @@ void handle_button(xcb_button_press_event_t *event) { return; } + /* If a custom command was specified for this mouse button, it overrides + * the default behavior. */ + binding_t *binding; + TAILQ_FOREACH(binding, &(config.bindings), bindings) { + if (binding->input_code != event->detail) + continue; + + i3_send_msg(I3_IPC_MESSAGE_TYPE_COMMAND, binding->command); + return; + } + switch (event->detail) { case 4: /* Mouse wheel up. We select the previous ws, if any. * If there is no more workspace, don’t even send the workspace * command, otherwise (with workspace auto_back_and_forth) we’d end * up on the wrong workspace. */ - - /* If `wheel_up_cmd [COMMAND]` was specified, it should override - * the default behavior */ - if (config.wheel_up_cmd) { - i3_send_msg(I3_IPC_MESSAGE_TYPE_COMMAND, config.wheel_up_cmd); - return; - } - if (cur_ws == TAILQ_FIRST(walk->workspaces)) return; @@ -492,14 +495,6 @@ void handle_button(xcb_button_press_event_t *event) { * If there is no more workspace, don’t even send the workspace * command, otherwise (with workspace auto_back_and_forth) we’d end * up on the wrong workspace. */ - - /* if `wheel_down_cmd [COMMAND]` was specified, it should override - * the default behavior */ - if (config.wheel_down_cmd) { - i3_send_msg(I3_IPC_MESSAGE_TYPE_COMMAND, config.wheel_down_cmd); - return; - } - if (cur_ws == TAILQ_LAST(walk->workspaces, ws_head)) return; diff --git a/include/config.h b/include/config.h index 33cbaba0..9e881cc3 100644 --- a/include/config.h +++ b/include/config.h @@ -281,13 +281,7 @@ struct Barconfig { M_MOD5 = 7 } modifier; - /** Command that should be run when mouse wheel up button is pressed over - * i3bar to override the default behavior. */ - char *wheel_up_cmd; - - /** Command that should be run when mouse wheel down button is pressed over - * i3bar to override the default behavior. */ - char *wheel_down_cmd; + TAILQ_HEAD(bar_bindings_head, Barbinding) bar_bindings; /** Bar position (bottom by default). */ enum { P_BOTTOM = 0, @@ -353,6 +347,21 @@ struct Barconfig { TAILQ_ENTRY(Barconfig) configs; }; +/** + * Defines a mouse command to be executed instead of the default behavior when + * clicking on the non-statusline part of i3bar. + * + */ +struct Barbinding { + /** The button to be used (e.g., 1 for "button1"). */ + int input_code; + + /** The command which is to be executed for this button. */ + char *command; + + TAILQ_ENTRY(Barbinding) bindings; +}; + /** * Finds the configuration file to use (either the one specified by * override_configpath), the user’s one or the system default) and calls diff --git a/include/config_directives.h b/include/config_directives.h index 1a7a3932..a7da4914 100644 --- a/include/config_directives.h +++ b/include/config_directives.h @@ -80,6 +80,7 @@ CFGFUN(bar_verbose, const char *verbose); CFGFUN(bar_modifier, const char *modifier); CFGFUN(bar_wheel_up_cmd, const char *command); CFGFUN(bar_wheel_down_cmd, const char *command); +CFGFUN(bar_bindsym, const char *button, const char *command); CFGFUN(bar_position, const char *position); CFGFUN(bar_i3bar_command, const char *i3bar_command); CFGFUN(bar_color, const char *colorclass, const char *border, const char *background, const char *text); @@ -90,4 +91,5 @@ CFGFUN(bar_status_command, const char *command); CFGFUN(bar_binding_mode_indicator, const char *value); CFGFUN(bar_workspace_buttons, const char *value); CFGFUN(bar_strip_workspace_numbers, const char *value); +CFGFUN(bar_start); CFGFUN(bar_finish); diff --git a/parser-specs/config.spec b/parser-specs/config.spec index b52fafc2..e0d422c8 100644 --- a/parser-specs/config.spec +++ b/parser-specs/config.spec @@ -393,7 +393,7 @@ state BARBRACE: end -> '{' - -> BAR + -> call cfg_bar_start(); BAR state BAR: end -> @@ -409,6 +409,7 @@ state BAR: 'modifier' -> BAR_MODIFIER 'wheel_up_cmd' -> BAR_WHEEL_UP_CMD 'wheel_down_cmd' -> BAR_WHEEL_DOWN_CMD + 'bindsym' -> BAR_BINDSYM 'position' -> BAR_POSITION 'output' -> BAR_OUTPUT 'tray_output' -> BAR_TRAY_OUTPUT @@ -463,6 +464,14 @@ state BAR_WHEEL_DOWN_CMD: command = string -> call cfg_bar_wheel_down_cmd($command); BAR +state BAR_BINDSYM: + button = word + -> BAR_BINDSYM_COMMAND + +state BAR_BINDSYM_COMMAND: + command = string + -> call cfg_bar_bindsym($button, $command); BAR + state BAR_POSITION: position = 'top', 'bottom' -> call cfg_bar_position($position); BAR diff --git a/src/config_directives.c b/src/config_directives.c index 37bd0121..cf72a4db 100644 --- a/src/config_directives.c +++ b/src/config_directives.c @@ -530,14 +530,44 @@ CFGFUN(bar_modifier, const char *modifier) { current_bar.modifier = M_SHIFT; } +static void bar_configure_binding(const char *button, const char *command) { + if (strncasecmp(button, "button", strlen("button")) != 0) { + ELOG("Bindings for a bar can only be mouse bindings, not \"%s\", ignoring.\n", button); + return; + } + + int input_code = atoi(button + strlen("button")); + if (input_code < 1) { + ELOG("Button \"%s\" does not seem to be in format 'buttonX'.\n", button); + return; + } + + struct Barbinding *current; + TAILQ_FOREACH(current, &(current_bar.bar_bindings), bindings) { + if (current->input_code == input_code) { + ELOG("command for button %s was already specified, ignoring.\n", button); + return; + } + } + + struct Barbinding *new_binding = scalloc(sizeof(struct Barbinding)); + new_binding->input_code = input_code; + new_binding->command = sstrdup(command); + TAILQ_INSERT_TAIL(&(current_bar.bar_bindings), new_binding, bindings); +} + CFGFUN(bar_wheel_up_cmd, const char *command) { - FREE(current_bar.wheel_up_cmd); - current_bar.wheel_up_cmd = sstrdup(command); + ELOG("'wheel_up_cmd' is deprecated. Please us 'bindsym button4 %s' instead.\n", command); + bar_configure_binding("button4", command); } CFGFUN(bar_wheel_down_cmd, const char *command) { - FREE(current_bar.wheel_down_cmd); - current_bar.wheel_down_cmd = sstrdup(command); + ELOG("'wheel_down_cmd' is deprecated. Please us 'bindsym button5 %s' instead.\n", command); + bar_configure_binding("button5", command); +} + +CFGFUN(bar_bindsym, const char *button, const char *command) { + bar_configure_binding(button, command); } CFGFUN(bar_position, const char *position) { @@ -611,6 +641,10 @@ CFGFUN(bar_strip_workspace_numbers, const char *value) { current_bar.strip_workspace_numbers = eval_boolstr(value); } +CFGFUN(bar_start) { + TAILQ_INIT(&(current_bar.bar_bindings)); +} + CFGFUN(bar_finish) { DLOG("\t new bar configuration finished, saving.\n"); /* Generate a unique ID for this bar if not already configured */ diff --git a/src/ipc.c b/src/ipc.c index ae213b3a..be600978 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -469,6 +469,28 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) { y(map_close); } +static void dump_bar_bindings(yajl_gen gen, Barconfig *config) { + if (TAILQ_EMPTY(&(config->bar_bindings))) + return; + + ystr("bindings"); + y(array_open); + + struct Barbinding *current; + TAILQ_FOREACH(current, &(config->bar_bindings), bindings) { + y(map_open); + + ystr("input_code"); + y(integer, current->input_code); + ystr("command"); + ystr(current->command); + + y(map_close); + } + + y(array_close); +} + static void dump_bar_config(yajl_gen gen, Barconfig *config) { y(map_open); @@ -549,15 +571,7 @@ static void dump_bar_config(yajl_gen gen, Barconfig *config) { break; } - if (config->wheel_up_cmd) { - ystr("wheel_up_cmd"); - ystr(config->wheel_up_cmd); - } - - if (config->wheel_down_cmd) { - ystr("wheel_down_cmd"); - ystr(config->wheel_down_cmd); - } + dump_bar_bindings(gen, config); ystr("position"); if (config->position == P_BOTTOM) diff --git a/testcases/t/201-config-parser.t b/testcases/t/201-config-parser.t index de25e7ff..faf2f0db 100644 --- a/testcases/t/201-config-parser.t +++ b/testcases/t/201-config-parser.t @@ -688,8 +688,9 @@ bar { EOT $expected = <<'EOT'; +cfg_bar_start() cfg_bar_output(LVDS-1) -ERROR: CONFIG: Expected one of these tokens: , '#', 'set', 'i3bar_command', 'status_command', 'socket_path', 'mode', 'hidden_state', 'id', 'modifier', 'wheel_up_cmd', 'wheel_down_cmd', 'position', 'output', 'tray_output', 'font', 'separator_symbol', 'binding_mode_indicator', 'workspace_buttons', 'strip_workspace_numbers', 'verbose', 'colors', '}' +ERROR: CONFIG: Expected one of these tokens: , '#', 'set', 'i3bar_command', 'status_command', 'socket_path', 'mode', 'hidden_state', 'id', 'modifier', 'wheel_up_cmd', 'wheel_down_cmd', 'bindsym', 'position', 'output', 'tray_output', 'font', 'separator_symbol', 'binding_mode_indicator', 'workspace_buttons', 'strip_workspace_numbers', 'verbose', 'colors', '}' ERROR: CONFIG: (in file ) ERROR: CONFIG: Line 1: bar { ERROR: CONFIG: Line 2: output LVDS-1 diff --git a/testcases/t/525-i3bar-mouse-bindings.t b/testcases/t/525-i3bar-mouse-bindings.t new file mode 100644 index 00000000..eabcad7a --- /dev/null +++ b/testcases/t/525-i3bar-mouse-bindings.t @@ -0,0 +1,104 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • http://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • http://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • http://build.i3wm.org/docs/ipc.html +# (or docs/ipc) +# +# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf +# (unless you are already familiar with Perl) +# +# Ensures that mouse bindings on the i3bar work correctly. +# Ticket: #1695 +use i3test i3_autostart => 0; + +my ($cv, $timer); +sub reset_test { + $cv = AE::cv; + $timer = AE::timer(1, 0, sub { $cv->send(0); }); +} + +my $config = < /dev/null); + skip 'xdotool is required for this test', 1 if $?; + + my $pid = launch_with_config($config); + my $i3 = i3(get_socket_path()); + $i3->connect()->recv; + my $ws = fresh_workspace; + + reset_test; + $i3->subscribe({ + window => sub { + my ($event) = @_; + if ($event->{change} eq 'focus') { + $cv->send($event->{container}); + } + }, + })->recv; + + my $left = open_window; + my $right = open_window; + sync_with_i3; + my $con = $cv->recv; + is($con->{window}, $right->{id}, 'focus is initially on the right container'); + reset_test; + + qx(xdotool mousemove 3 3 click 1); + sync_with_i3; + $con = $cv->recv; + is($con->{window}, $left->{id}, 'button 1 moves focus left'); + reset_test; + + qx(xdotool mousemove 3 3 click 2); + sync_with_i3; + $con = $cv->recv; + is($con->{window}, $right->{id}, 'button 2 moves focus right'); + reset_test; + + qx(xdotool mousemove 3 3 click 3); + sync_with_i3; + $con = $cv->recv; + is($con->{window}, $left->{id}, 'button 3 moves focus left'); + reset_test; + + qx(xdotool mousemove 3 3 click 4); + sync_with_i3; + $con = $cv->recv; + is($con->{window}, $right->{id}, 'button 4 moves focus right'); + reset_test; + + qx(xdotool mousemove 3 3 click 5); + sync_with_i3; + $con = $cv->recv; + is($con->{window}, $left->{id}, 'button 5 moves focus left'); + reset_test; + + exit_gracefully($pid); + +} + +done_testing;