Merge pull request #1697 from Airblader/feature-1695

Extend mouse commands on i3bar
This commit is contained in:
Michael Stapelberg 2015-06-18 20:50:56 +02:00
commit 696d844ffa
11 changed files with 310 additions and 55 deletions

View File

@ -1209,23 +1209,41 @@ Available modifiers are Mod1-Mod5, Shift, Control (see +xmodmap(1)+).
=== Mouse button commands === Mouse button commands
Specifies a command to run when a button was pressed on i3bar to override the 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 default behavior. This is useful, e.g., for disabling the scroll wheel action
useful for disabling the scroll wheel action or running scripts that implement or running scripts that implement custom behavior for these buttons.
custom behavior for these buttons.
A button is always named +button<n>+, 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*: *Syntax*:
--------------------- ----------------------------
wheel_up_cmd <command> bindsym button<n> <command>
wheel_down_cmd <command> ----------------------------
---------------------
*Example*: *Example*:
--------------------- ---------------------------------------------------------
bar { bar {
wheel_up_cmd nop # disable clicking on workspace buttons
wheel_down_cmd exec ~/.i3/scripts/custom_wheel_down bindsym button1 nop
# execute custom script when scrolling downwards
bindsym button5 exec ~/.i3/scripts/custom_wheel_down
} }
--------------------- ---------------------------------------------------------
=== Bar ID === Bar ID

View File

@ -22,10 +22,16 @@ typedef enum { M_DOCK = 0,
M_HIDE = 1, M_HIDE = 1,
M_INVISIBLE = 2 } bar_display_mode_t; 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 { typedef struct config_t {
int modifier; int modifier;
char *wheel_up_cmd; TAILQ_HEAD(bindings_head, binding_t) bindings;
char *wheel_down_cmd;
position_t position; position_t position;
int verbose; int verbose;
struct xcb_color_strings_t colors; struct xcb_color_strings_t colors;

View File

@ -20,6 +20,7 @@
#include "common.h" #include "common.h"
static char *cur_key; static char *cur_key;
static bool parsing_bindings;
/* /*
* Parse a key. * 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); strncpy(cur_key, (const char *)keyVal, keyLen);
cur_key[keyLen] = '\0'; 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; 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")) if (!strcmp(cur_key, "id") || !strcmp(cur_key, "socket_path"))
return 1; 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")) { if (!strcmp(cur_key, "mode")) {
DLOG("mode = %.*s, len = %d\n", len, val, len); DLOG("mode = %.*s, len = %d\n", len, val, len);
config.hide_on_modifier = (len == 4 && !strncmp((const char *)val, "dock", strlen("dock")) ? M_DOCK 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; 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")) { if (!strcmp(cur_key, "wheel_up_cmd")) {
DLOG("wheel_up_cmd = %.*s\n", len, val); DLOG("wheel_up_cmd = %.*s\n", len, val);
FREE(config.wheel_up_cmd); binding_t *binding = scalloc(sizeof(binding_t));
sasprintf(&config.wheel_up_cmd, "%.*s", len, val); binding->input_code = 4;
sasprintf(&(binding->command), "%.*s", len, val);
TAILQ_INSERT_TAIL(&(config.bindings), binding, bindings);
return 1; 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")) { if (!strcmp(cur_key, "wheel_down_cmd")) {
DLOG("wheel_down_cmd = %.*s\n", len, val); DLOG("wheel_down_cmd = %.*s\n", len, val);
FREE(config.wheel_down_cmd); binding_t *binding = scalloc(sizeof(binding_t));
sasprintf(&config.wheel_down_cmd, "%.*s", len, val); binding->input_code = 5;
sasprintf(&(binding->command), "%.*s", len, val);
TAILQ_INSERT_TAIL(&(config.bindings), binding, bindings);
return 1; return 1;
} }
@ -232,11 +270,34 @@ static int config_boolean_cb(void *params_, int val) {
return 0; 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 */ /* A datastructure to pass all these callbacks to yajl */
static yajl_callbacks outputs_callbacks = { static yajl_callbacks outputs_callbacks = {
.yajl_null = config_null_cb, .yajl_null = config_null_cb,
.yajl_boolean = config_boolean_cb, .yajl_boolean = config_boolean_cb,
.yajl_integer = config_integer_cb,
.yajl_string = config_string_cb, .yajl_string = config_string_cb,
.yajl_end_array = config_end_array_cb,
.yajl_map_key = config_map_key_cb, .yajl_map_key = config_map_key_cb,
}; };
@ -249,6 +310,8 @@ void parse_config_json(char *json) {
yajl_status state; yajl_status state;
handle = yajl_alloc(&outputs_callbacks, NULL, NULL); handle = yajl_alloc(&outputs_callbacks, NULL, NULL);
TAILQ_INIT(&(config.bindings));
state = yajl_parse(handle, (const unsigned char *)json, strlen(json)); state = yajl_parse(handle, (const unsigned char *)json, strlen(json));
/* FIXME: Proper error handling for JSON parsing */ /* FIXME: Proper error handling for JSON parsing */

View File

@ -468,20 +468,23 @@ void handle_button(xcb_button_press_event_t *event) {
return; 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) { switch (event->detail) {
case 4: case 4:
/* Mouse wheel up. We select the previous ws, if any. /* Mouse wheel up. We select the previous ws, if any.
* If there is no more workspace, dont even send the workspace * If there is no more workspace, dont even send the workspace
* command, otherwise (with workspace auto_back_and_forth) wed end * command, otherwise (with workspace auto_back_and_forth) wed end
* up on the wrong workspace. */ * 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)) if (cur_ws == TAILQ_FIRST(walk->workspaces))
return; return;
@ -492,14 +495,6 @@ void handle_button(xcb_button_press_event_t *event) {
* If there is no more workspace, dont even send the workspace * If there is no more workspace, dont even send the workspace
* command, otherwise (with workspace auto_back_and_forth) wed end * command, otherwise (with workspace auto_back_and_forth) wed end
* up on the wrong workspace. */ * 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)) if (cur_ws == TAILQ_LAST(walk->workspaces, ws_head))
return; return;

View File

@ -281,13 +281,7 @@ struct Barconfig {
M_MOD5 = 7 M_MOD5 = 7
} modifier; } modifier;
/** Command that should be run when mouse wheel up button is pressed over TAILQ_HEAD(bar_bindings_head, Barbinding) bar_bindings;
* 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;
/** Bar position (bottom by default). */ /** Bar position (bottom by default). */
enum { P_BOTTOM = 0, enum { P_BOTTOM = 0,
@ -353,6 +347,21 @@ struct Barconfig {
TAILQ_ENTRY(Barconfig) configs; 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 * Finds the configuration file to use (either the one specified by
* override_configpath), the users one or the system default) and calls * override_configpath), the users one or the system default) and calls

View File

@ -80,6 +80,7 @@ CFGFUN(bar_verbose, const char *verbose);
CFGFUN(bar_modifier, const char *modifier); CFGFUN(bar_modifier, const char *modifier);
CFGFUN(bar_wheel_up_cmd, const char *command); CFGFUN(bar_wheel_up_cmd, const char *command);
CFGFUN(bar_wheel_down_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_position, const char *position);
CFGFUN(bar_i3bar_command, const char *i3bar_command); CFGFUN(bar_i3bar_command, const char *i3bar_command);
CFGFUN(bar_color, const char *colorclass, const char *border, const char *background, const char *text); 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_binding_mode_indicator, const char *value);
CFGFUN(bar_workspace_buttons, const char *value); CFGFUN(bar_workspace_buttons, const char *value);
CFGFUN(bar_strip_workspace_numbers, const char *value); CFGFUN(bar_strip_workspace_numbers, const char *value);
CFGFUN(bar_start);
CFGFUN(bar_finish); CFGFUN(bar_finish);

View File

@ -393,7 +393,7 @@ state BARBRACE:
end end
-> ->
'{' '{'
-> BAR -> call cfg_bar_start(); BAR
state BAR: state BAR:
end -> end ->
@ -409,6 +409,7 @@ state BAR:
'modifier' -> BAR_MODIFIER 'modifier' -> BAR_MODIFIER
'wheel_up_cmd' -> BAR_WHEEL_UP_CMD 'wheel_up_cmd' -> BAR_WHEEL_UP_CMD
'wheel_down_cmd' -> BAR_WHEEL_DOWN_CMD 'wheel_down_cmd' -> BAR_WHEEL_DOWN_CMD
'bindsym' -> BAR_BINDSYM
'position' -> BAR_POSITION 'position' -> BAR_POSITION
'output' -> BAR_OUTPUT 'output' -> BAR_OUTPUT
'tray_output' -> BAR_TRAY_OUTPUT 'tray_output' -> BAR_TRAY_OUTPUT
@ -463,6 +464,14 @@ state BAR_WHEEL_DOWN_CMD:
command = string command = string
-> call cfg_bar_wheel_down_cmd($command); BAR -> 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: state BAR_POSITION:
position = 'top', 'bottom' position = 'top', 'bottom'
-> call cfg_bar_position($position); BAR -> call cfg_bar_position($position); BAR

View File

@ -530,14 +530,44 @@ CFGFUN(bar_modifier, const char *modifier) {
current_bar.modifier = M_SHIFT; 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) { CFGFUN(bar_wheel_up_cmd, const char *command) {
FREE(current_bar.wheel_up_cmd); ELOG("'wheel_up_cmd' is deprecated. Please us 'bindsym button4 %s' instead.\n", command);
current_bar.wheel_up_cmd = sstrdup(command); bar_configure_binding("button4", command);
} }
CFGFUN(bar_wheel_down_cmd, const char *command) { CFGFUN(bar_wheel_down_cmd, const char *command) {
FREE(current_bar.wheel_down_cmd); ELOG("'wheel_down_cmd' is deprecated. Please us 'bindsym button5 %s' instead.\n", command);
current_bar.wheel_down_cmd = sstrdup(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) { 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); current_bar.strip_workspace_numbers = eval_boolstr(value);
} }
CFGFUN(bar_start) {
TAILQ_INIT(&(current_bar.bar_bindings));
}
CFGFUN(bar_finish) { CFGFUN(bar_finish) {
DLOG("\t new bar configuration finished, saving.\n"); DLOG("\t new bar configuration finished, saving.\n");
/* Generate a unique ID for this bar if not already configured */ /* Generate a unique ID for this bar if not already configured */

View File

@ -469,6 +469,28 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) {
y(map_close); 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) { static void dump_bar_config(yajl_gen gen, Barconfig *config) {
y(map_open); y(map_open);
@ -549,15 +571,7 @@ static void dump_bar_config(yajl_gen gen, Barconfig *config) {
break; break;
} }
if (config->wheel_up_cmd) { dump_bar_bindings(gen, config);
ystr("wheel_up_cmd");
ystr(config->wheel_up_cmd);
}
if (config->wheel_down_cmd) {
ystr("wheel_down_cmd");
ystr(config->wheel_down_cmd);
}
ystr("position"); ystr("position");
if (config->position == P_BOTTOM) if (config->position == P_BOTTOM)

View File

@ -688,8 +688,9 @@ bar {
EOT EOT
$expected = <<'EOT'; $expected = <<'EOT';
cfg_bar_start()
cfg_bar_output(LVDS-1) cfg_bar_output(LVDS-1)
ERROR: CONFIG: Expected one of these tokens: <end>, '#', '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: <end>, '#', '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 <stdin>) ERROR: CONFIG: (in file <stdin>)
ERROR: CONFIG: Line 1: bar { ERROR: CONFIG: Line 1: bar {
ERROR: CONFIG: Line 2: output LVDS-1 ERROR: CONFIG: Line 2: output LVDS-1

View File

@ -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 = <<EOT;
# i3 config file (v4)
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
focus_follows_mouse no
bar {
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
position top
bindsym button1 focus left
bindsym button2 focus right
bindsym button3 focus left
bindsym button4 focus right
bindsym button5 focus left
}
EOT
SKIP: {
qx(command -v xdotool 2> /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;