wmstatus: make bindings configurable

This commit is contained in:
Přemysl Eric Janouch 2024-08-06 18:22:24 +02:00
parent 4a22708f52
commit 9ccdc3430c
Signed by: p
GPG Key ID: A0420B94F92B9493
2 changed files with 338 additions and 172 deletions

View File

@ -852,6 +852,7 @@ app_make_config (void)
{ {
struct config config = config_make (); struct config config = config_make ();
config_register_module (&config, "general", app_load_config_general, NULL); config_register_module (&config, "general", app_load_config_general, NULL);
config_register_module (&config, "keys", NULL, NULL);
config_register_module (&config, "mpd", app_load_config_mpd, NULL); config_register_module (&config, "mpd", app_load_config_mpd, NULL);
config_register_module (&config, "nut", app_load_config_nut, NULL); config_register_module (&config, "nut", app_load_config_nut, NULL);
@ -893,6 +894,65 @@ get_config_boolean (struct config_item *root, const char *key)
return &item->value.boolean; return &item->value.boolean;
} }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// This is essentially simplified shell command language syntax,
// without comments or double quotes, and line feeds are whitespace.
static bool
parse_binding (const char *line, struct strv *out)
{
enum { STA, DEF, ESC, WOR, QUO, STATES };
enum { TAKE = 1 << 3, PUSH = 1 << 4, STOP = 1 << 5, ERROR = 1 << 6 };
enum { TWOR = TAKE | WOR };
// We never transition back to the start state, so it can stay as a no-op
static char table[STATES][7] =
{
// state NUL SP, TAB, LF ' \ default
/* STA */ { STOP, DEF, QUO, ESC, TWOR },
/* DEF */ { STOP, 0, QUO, ESC, TWOR },
/* ESC */ { ERROR, TWOR, TWOR, TWOR, TWOR },
/* WOR */ { STOP | PUSH, DEF | PUSH, QUO, ESC, TAKE },
/* QUO */ { ERROR, TAKE, WOR, TAKE, TAKE },
};
strv_reset (out);
struct str token = str_make ();
int state = STA, edge = 0, ch = 0;
while (true)
{
switch ((ch = (unsigned char) *line++))
{
case 0: edge = table[state][0]; break;
case '\t':
case '\n': edge = table[state][4]; break;
case ' ': edge = table[state][1]; break;
case '\'': edge = table[state][2]; break;
case '\\': edge = table[state][3]; break;
default: edge = table[state][4]; break;
}
if (edge & TAKE)
str_append_c (&token, ch);
if (edge & PUSH)
{
strv_append_owned (out, str_steal (&token));
token = str_make ();
}
if (edge & STOP)
{
str_free (&token);
return true;
}
if (edge & ERROR)
{
str_free (&token);
return false;
}
if (edge &= 7)
state = edge;
}
}
// --- Application ------------------------------------------------------------- // --- Application -------------------------------------------------------------
struct app_context struct app_context
@ -929,6 +989,7 @@ struct app_context
// Hotkeys: // Hotkeys:
struct binding *bindings; ///< Global bindings
int xkb_base_event_code; ///< Xkb base event code int xkb_base_event_code; ///< Xkb base event code
char *layout; ///< Keyboard layout char *layout; ///< Keyboard layout
@ -2152,8 +2213,15 @@ on_noise_timer (void *user_data)
} }
static void static void
on_noise_adjust (struct app_context *ctx, int arg) action_noise_adjust (struct app_context *ctx, const struct strv *args)
{ {
if (args->len != 1)
{
print_error ("usage: noise-adjust +/-HOURS");
return;
}
long arg = strtol (args->vector[0], NULL, 10);
ctx->noise_fadeout_samples = 0; ctx->noise_fadeout_samples = 0;
ctx->noise_fadeout_iterator = 0; ctx->noise_fadeout_iterator = 0;
if (!ctx->noise_end_time && (arg < 0 || !noise_start (ctx))) if (!ctx->noise_end_time && (arg < 0 || !noise_start (ctx)))
@ -2313,32 +2381,34 @@ spawn (char *argv[])
posix_spawn_file_actions_destroy (&actions); posix_spawn_file_actions_destroy (&actions);
} }
#define MPD_SIMPLE(name, ...) \ static void
static void \ action_exec (struct app_context *ctx, const struct strv *args)
on_mpd_ ## name (struct app_context *ctx, int arg) \ {
{ \ (void) ctx;
(void) arg; \ spawn (args->vector);
struct mpd_client *c = &ctx->mpd_client; \
if (c->state != MPD_CONNECTED) \
return; \
mpd_client_send_command (c, __VA_ARGS__); \
mpd_client_add_task (c, NULL, NULL); \
mpd_client_idle (c, 0); \
} }
static void
MPD_SIMPLE (play, "play", NULL) action_mpd (struct app_context *ctx, const struct strv *args)
MPD_SIMPLE (toggle, "pause", NULL) {
MPD_SIMPLE (stop, "stop", NULL) struct mpd_client *c = &ctx->mpd_client;
MPD_SIMPLE (prev, "previous", NULL) if (c->state != MPD_CONNECTED)
MPD_SIMPLE (next, "next", NULL) return;
MPD_SIMPLE (forward, "seekcur", "+10", NULL) mpd_client_send_commandv (c, args->vector);
MPD_SIMPLE (backward, "seekcur", "-10", NULL) mpd_client_add_task (c, NULL, NULL);
mpd_client_idle (c, 0);
}
static void static void
on_mpd_play_toggle (struct app_context *ctx, int arg) action_mpd_play_toggle (struct app_context *ctx, const struct strv *args)
{ {
(ctx->mpd_stopped ? on_mpd_play : on_mpd_toggle) (ctx, arg); (void) args;
struct mpd_client *c = &ctx->mpd_client;
if (c->state != MPD_CONNECTED)
return;
mpd_client_send_command (c, ctx->mpd_stopped ? "play" : "pause", NULL);
mpd_client_add_task (c, NULL, NULL);
mpd_client_idle (c, 0);
} }
static void static void
@ -2352,9 +2422,9 @@ on_volume_finish (pa_context *context, int success, void *userdata)
} }
static void static void
on_volume_mic_mute (struct app_context *ctx, int arg) action_audio_mic_mute (struct app_context *ctx, const struct strv *args)
{ {
(void) arg; (void) args;
if (!ctx->context) if (!ctx->context)
return; return;
@ -2364,9 +2434,9 @@ on_volume_mic_mute (struct app_context *ctx, int arg)
} }
static void static void
on_volume_switch (struct app_context *ctx, int arg) action_audio_switch (struct app_context *ctx, const struct strv *args)
{ {
(void) arg; (void) args;
if (!ctx->context || !ctx->sink_port_active || !ctx->sink_ports.len) if (!ctx->context || !ctx->sink_port_active || !ctx->sink_ports.len)
return; return;
@ -2383,9 +2453,9 @@ on_volume_switch (struct app_context *ctx, int arg)
} }
static void static void
on_volume_mute (struct app_context *ctx, int arg) action_audio_mute (struct app_context *ctx, const struct strv *args)
{ {
(void) arg; (void) args;
if (!ctx->context) if (!ctx->context)
return; return;
@ -2395,65 +2465,26 @@ on_volume_mute (struct app_context *ctx, int arg)
} }
static void static void
on_volume_set (struct app_context *ctx, int arg) action_audio_volume (struct app_context *ctx, const struct strv *args)
{ {
if (args->len != 1)
{
print_error ("usage: audio-volume +/-PERCENT");
return;
}
if (!ctx->context) if (!ctx->context)
return; return;
long arg = strtol (args->vector[0], NULL, 10);
pa_cvolume volume = ctx->sink_volume; pa_cvolume volume = ctx->sink_volume;
if (arg > 0) if (arg > 0)
pa_cvolume_inc (&volume, (pa_volume_t) arg * PA_VOLUME_NORM / 100); pa_cvolume_inc (&volume, (pa_volume_t) +arg * PA_VOLUME_NORM / 100);
else else
pa_cvolume_dec (&volume, (pa_volume_t) -arg * PA_VOLUME_NORM / 100); pa_cvolume_dec (&volume, (pa_volume_t) -arg * PA_VOLUME_NORM / 100);
pa_operation_unref (pa_context_set_sink_volume_by_name (ctx->context, pa_operation_unref (pa_context_set_sink_volume_by_name (ctx->context,
DEFAULT_SINK, &volume, on_volume_finish, ctx)); DEFAULT_SINK, &volume, on_volume_finish, ctx));
} }
static void
on_lock (struct app_context *ctx, int arg)
{
(void) ctx;
(void) arg;
// One of these will work
char *argv_gdm[] = { "gdm-switch-user", NULL };
spawn (argv_gdm);
char *argv_ldm[] = { "dm-tool", "lock", NULL };
spawn (argv_ldm);
}
static void
on_input_switch (struct app_context *ctx, int arg)
{
(void) ctx;
char *values[] = { "vga", "dvi", "hdmi", "dp" },
*numbers[] = { "1", "2" };
char *argv[] = { "input-switch",
values[arg & 0xf], numbers[arg >> 4], NULL };
spawn (argv);
}
static void
on_brightness (struct app_context *ctx, int arg)
{
(void) ctx;
char *value = xstrdup_printf ("%d", arg);
char *argv[] = { "brightness", value, NULL };
spawn (argv);
free (value);
}
static void
on_standby (struct app_context *ctx, int arg)
{
(void) ctx;
(void) arg;
// We need to wait a little while until user releases the key
spawn ((char *[]) { "sh", "-c", "sleep 1; xset dpms force standby", NULL });
}
static void static void
go_insomniac (struct app_context *ctx) go_insomniac (struct app_context *ctx)
{ {
@ -2499,9 +2530,9 @@ go_insomniac (struct app_context *ctx)
} }
static void static void
on_insomnia (struct app_context *ctx, int arg) action_insomnia (struct app_context *ctx, const struct strv *args)
{ {
(void) arg; (void) args;
cstr_set (&ctx->insomnia_info, NULL); cstr_set (&ctx->insomnia_info, NULL);
// Get rid of the lock if we hold one, establish it otherwise // Get rid of the lock if we hold one, establish it otherwise
@ -2517,84 +2548,48 @@ on_insomnia (struct app_context *ctx, int arg)
} }
static void static void
on_lock_group (struct app_context *ctx, int arg) action_xkb_lock_group (struct app_context *ctx, const struct strv *args)
{ {
XkbLockGroup (ctx->dpy, XkbUseCoreKbd, arg); if (args->len != 1)
{
print_error ("usage: xkb-lock-group GROUP");
return;
} }
struct long group = strtol (args->vector[0], NULL, 10) - 1;
{ if (group < XkbGroup1Index || group > XkbGroup4Index)
unsigned mod; print_warning ("invalid XKB group index: %s", args->vector[0]);
KeySym keysym; else
void (*handler) (struct app_context *ctx, int arg); XkbLockGroup (ctx->dpy, XkbUseCoreKbd, group);
int arg;
} }
g_keys[] =
static const struct action
{ {
// This key should be labeled L on normal Qwert[yz] layouts const char *name;
{ Mod4Mask, XK_n, on_lock, 0 }, void (*handler) (struct app_context *ctx, const struct strv *args);
}
g_handlers[] =
{
{ "exec", action_exec },
{ "mpd", action_mpd },
{ "mpd-play-toggle", action_mpd_play_toggle },
{ "xkb-lock-group", action_xkb_lock_group },
{ "insomnia", action_insomnia },
{ "audio-switch", action_audio_switch },
{ "audio-mute", action_audio_mute },
{ "audio-mic-mute", action_audio_mic_mute },
{ "audio-volume", action_audio_volume },
{ "noise-adjust", action_noise_adjust },
};
// xmodmap | grep -e Alt_R -e Meta_R -e ISO_Level3_Shift -e Mode_switch struct binding
// can be used to figure out which modifier is AltGr {
LIST_HEADER (struct binding)
// MPD unsigned mods; ///< Modifiers
{ Mod4Mask, XK_Up, on_mpd_play_toggle, 0 }, KeyCode keycode; ///< Key code
{ Mod4Mask, XK_Down, on_mpd_stop, 0 }, struct action handler; ///< Handling procedure
{ Mod4Mask, XK_Left, on_mpd_prev, 0 }, struct strv args; ///< Arguments to the handler
{ Mod4Mask, XK_Right, on_mpd_next, 0 },
{ Mod4Mask | ShiftMask, XK_Left, on_mpd_backward, 0 },
{ Mod4Mask | ShiftMask, XK_Right, on_mpd_forward, 0 },
{ 0, XF86XK_AudioPlay, on_mpd_play_toggle, 0 },
{ 0, XF86XK_AudioPrev, on_mpd_prev, 0 },
{ 0, XF86XK_AudioNext, on_mpd_next, 0 },
// Keyboard groups
{ Mod4Mask, XK_F1, on_lock_group, 0 },
{ Mod4Mask, XK_F2, on_lock_group, 1 },
{ Mod4Mask, XK_F3, on_lock_group, 2 },
{ Mod4Mask, XK_F4, on_lock_group, 3 },
#define CSMask (ControlMask | ShiftMask)
// Display input sources
{ Mod4Mask | ControlMask, XK_F1, on_input_switch, 0 },
{ Mod4Mask | CSMask, XK_F1, on_input_switch, 16 | 0 },
{ Mod4Mask | ControlMask, XK_F2, on_input_switch, 1 },
{ Mod4Mask | CSMask, XK_F2, on_input_switch, 16 | 1 },
{ Mod4Mask | ControlMask, XK_F3, on_input_switch, 2 },
{ Mod4Mask | CSMask, XK_F3, on_input_switch, 16 | 2 },
{ Mod4Mask | ControlMask, XK_F4, on_input_switch, 3 },
{ Mod4Mask | CSMask, XK_F4, on_input_switch, 16 | 3 },
// Brightness
{ Mod4Mask, XK_Home, on_brightness, 10 },
{ Mod4Mask, XK_End, on_brightness, -10 },
{ 0, XF86XK_MonBrightnessUp, on_brightness, 10 },
{ 0, XF86XK_MonBrightnessDown, on_brightness, -10 },
{ Mod4Mask, XK_F5, on_standby, 0 },
{ Mod4Mask | ShiftMask, XK_F5, on_insomnia, 0 },
{ Mod4Mask, XK_Pause, on_standby, 0 },
{ Mod4Mask | ShiftMask, XK_Pause, on_insomnia, 0 },
// Volume
{ Mod4Mask, XK_Insert, on_volume_switch, 0 },
{ Mod4Mask, XK_Delete, on_volume_mute, 0 },
{ Mod4Mask | ShiftMask, XK_Delete, on_volume_mic_mute, 0 },
{ Mod4Mask, XK_Page_Up, on_volume_set, 5 },
{ Mod4Mask | ShiftMask, XK_Page_Up, on_volume_set, 1 },
{ Mod4Mask, XK_Page_Down, on_volume_set, -5 },
{ Mod4Mask | ShiftMask, XK_Page_Down, on_volume_set, -1 },
{ 0, XF86XK_AudioRaiseVolume, on_volume_set, 5 },
{ ShiftMask, XF86XK_AudioRaiseVolume, on_volume_set, 1 },
{ 0, XF86XK_AudioLowerVolume, on_volume_set, -5 },
{ ShiftMask, XF86XK_AudioLowerVolume, on_volume_set, -1 },
{ 0, XF86XK_AudioMute, on_volume_mute, 0 },
{ 0, XF86XK_AudioMicMute, on_volume_mic_mute, 0 },
// Noise playback
{ ControlMask, XF86XK_AudioRaiseVolume, on_noise_adjust, 1 },
{ ControlMask, XF86XK_AudioLowerVolume, on_noise_adjust, -1 },
}; };
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -2603,16 +2598,11 @@ static void
on_x_keypress (struct app_context *ctx, XEvent *e) on_x_keypress (struct app_context *ctx, XEvent *e)
{ {
XKeyEvent *ev = &e->xkey; XKeyEvent *ev = &e->xkey;
unsigned unconsumed_mods; LIST_FOR_EACH (struct binding, iter, ctx->bindings)
KeySym keysym; if (iter->keycode == ev->keycode
if (!XkbLookupKeySym (ctx->dpy, && iter->mods == ev->state
(KeyCode) ev->keycode, ev->state, &unconsumed_mods, &keysym)) && iter->handler.handler)
return; iter->handler.handler (ctx, &iter->args);
for (size_t i = 0; i < N_ELEMENTS (g_keys); i++)
if (g_keys[i].keysym == keysym
&& g_keys[i].mod == ev->state
&& g_keys[i].handler)
g_keys[i].handler (ctx, g_keys[i].arg);
} }
static void static void
@ -2667,6 +2657,134 @@ on_x_ready (const struct pollfd *pfd, void *user_data)
} }
} }
static bool
parse_key_modifier (const char *modifier, unsigned *mods)
{
static const struct
{
const char *name;
unsigned mask;
}
modifiers[] =
{
{"Shift", ShiftMask},
{"Lock", LockMask},
{"Control", ControlMask},
{"Mod1", Mod1Mask},
{"Mod2", Mod2Mask},
{"Mod3", Mod3Mask},
{"Mod4", Mod4Mask},
{"Mod5", Mod5Mask},
};
for (size_t k = 0; k < N_ELEMENTS (modifiers); k++)
if (!strcasecmp_ascii (modifiers[k].name, modifier))
{
*mods |= modifiers[k].mask;
return true;
}
return false;
}
static bool
parse_key_vector (const struct strv *keys, unsigned *mods, KeySym *keysym)
{
for (size_t i = 0; i < keys->len; i++)
{
if (parse_key_modifier (keys->vector[i], mods))
continue;
if (*keysym)
return false;
*keysym = XStringToKeysym (keys->vector[i]);
}
return *keysym != 0;
}
static bool
parse_key_combination (const char *combination, unsigned *mods, KeySym *keysym)
{
struct strv keys = strv_make ();
bool result = parse_binding (combination, &keys)
&& parse_key_vector (&keys, mods, keysym);
strv_free (&keys);
return result;
}
static const char *
init_grab (struct app_context *ctx, const char *combination, const char *action)
{
unsigned mods = 0;
KeySym keysym = 0;
if (!parse_key_combination (combination, &mods, &keysym))
return "parsing key combination failed";
KeyCode keycode = XKeysymToKeycode (ctx->dpy, keysym);
if (!keycode)
return "no keycode found";
struct strv args = strv_make ();
if (!parse_binding (action, &args) || !args.len)
{
strv_free (&args);
return "parsing the binding failed";
}
struct action handler = {};
for (size_t i = 0; i < N_ELEMENTS (g_handlers); i++)
if (!strcmp (g_handlers[i].name, args.vector[0]))
{
handler = g_handlers[i];
break;
}
free (strv_steal (&args, 0));
if (!handler.name)
{
strv_free (&args);
return "unknown action";
}
XGrabKey (ctx->dpy, keycode, mods, DefaultRootWindow (ctx->dpy),
False /* ? */, GrabModeAsync, GrabModeAsync);
struct binding *binding = xcalloc (1, sizeof *binding);
binding->mods = mods;
binding->keycode = keycode;
binding->handler = handler;
binding->args = args;
LIST_PREPEND (ctx->bindings, binding);
return NULL;
}
static void
init_bindings (struct app_context *ctx)
{
unsigned ignored_locks =
LockMask | XkbKeysymToModifiers (ctx->dpy, XK_Num_Lock);
hard_assert (XkbSetIgnoreLockMods
(ctx->dpy, XkbUseCoreKbd, ignored_locks, ignored_locks, 0, 0));
struct str_map *keys =
&config_item_get (ctx->config.root, "keys", NULL)->value.object;
struct str_map_iter iter = str_map_iter_make (keys);
struct config_item *action;
while ((action = str_map_iter_next (&iter)))
{
const char *combination = iter.link->key, *err = NULL;
if (action->type != CONFIG_ITEM_NULL)
{
if (action->type != CONFIG_ITEM_STRING)
err = "expected a string";
else
err = init_grab (ctx, combination, action->value.string.str);
}
if (err)
print_warning ("configuration: key `%s': %s", combination, err);
}
XSelectInput (ctx->dpy, DefaultRootWindow (ctx->dpy), KeyPressMask);
}
static void static void
init_xlib_events (struct app_context *ctx) init_xlib_events (struct app_context *ctx)
{ {
@ -2681,19 +2799,7 @@ init_xlib_events (struct app_context *ctx)
XSyncPositiveComparison, ctx->idle_timeout); XSyncPositiveComparison, ctx->idle_timeout);
} }
unsigned ignored_locks = init_bindings (ctx);
LockMask | XkbKeysymToModifiers (ctx->dpy, XK_Num_Lock);
hard_assert (XkbSetIgnoreLockMods
(ctx->dpy, XkbUseCoreKbd, ignored_locks, ignored_locks, 0, 0));
KeyCode code;
Window root = DefaultRootWindow (ctx->dpy);
for (size_t i = 0; i < N_ELEMENTS (g_keys); i++)
if ((code = XKeysymToKeycode (ctx->dpy, g_keys[i].keysym)))
XGrabKey (ctx->dpy, code, g_keys[i].mod, root,
False /* ? */, GrabModeAsync, GrabModeAsync);
XSelectInput (ctx->dpy, root, KeyPressMask);
XSync (ctx->dpy, False); XSync (ctx->dpy, False);
ctx->x_event.dispatcher = on_x_ready; ctx->x_event.dispatcher = on_x_ready;

60
wmstatus.conf.example Normal file
View File

@ -0,0 +1,60 @@
# vim: set ft=libertyconf:
keys = {
# This key should be labeled L on normal Qwert[yz] layouts
"Mod4 n" = "exec dm-tool lock" # gdm-switch-user
# xmodmap grep -e Alt_R -e Meta_R -e ISO_Level3_Shift -e Mode_switch
# can be used to figure out which modifier is AltGr
"Mod4 Up" = "mpd-play-toggle"
"Mod4 Down" = "mpd stop"
"Mod4 Left" = "mpd previous"
"Mod4 Right" = "mpd next"
"Mod4 Shift Left" = "mpd seekcur -10"
"Mod4 Shift Right" = "mpd seekcur +10"
"XF86AudioPlay" = "mpd-play-toggle"
"XF86AudioPrev" = "mpd previous"
"XF86AudioNext" = "mpd next"
"Mod4 F1" = "xkb-lock-group 1"
"Mod4 F2" = "xkb-lock-group 2"
"Mod4 F3" = "xkb-lock-group 3"
"Mod4 F4" = "xkb-lock-group 4"
"Mod4 Control F1" = "exec input-switch vga 1"
"Mod4 Control Shift F1" = "exec input-switch vga 2"
"Mod4 Control F2" = "exec input-switch dvi 1"
"Mod4 Control Shift F2" = "exec input-switch dvi 2"
"Mod4 Control F3" = "exec input-switch hdmi 1"
"Mod4 Control Shift F3" = "exec input-switch hdmi 2"
"Mod4 Control F4" = "exec input-switch dp 1"
"Mod4 Control Shift F4" = "exec input-switch dp 2"
"Mod4 Home" = "exec brightness +10"
"Mod4 End" = "exec brightness -10"
"XF86MonBrightnessUp" = "exec brightness +10"
"XF86MonBrightnessDown" = "exec brightness -10"
# We need to wait a little while until user releases the key
"Mod4 F5" = "exec sh -c 'sleep 1; xset dpms force standby'"
"Mod4 Shift F5" = "insomnia"
"Mod4 Pause" = "exec sh -c 'sleep 1; xset dpms force standby'"
"Mod4 Shift Pause" = "insomnia"
"Mod4 Insert" = "audio-switch"
"Mod4 Delete" = "audio-mute"
"Mod4 Shift Delete" = "audio-mic-mute"
"Mod4 Page_Up" = "audio-volume +5"
"Mod4 Shift Page_Up" = "audio-volume +1"
"Mod4 Page_Down" = "audio-volume -5"
"Mod4 Shift Page_Down" = "audio-volume -1"
" XF86AudioRaiseVolume" = "audio-volume +5"
"Shift XF86AudioRaiseVolume" = "audio-volume +1"
" XF86AudioLowerVolume" = "audio-volume -5"
"Shift XF86AudioLowerVolume" = "audio-volume -1"
" XF86AudioMute" = "audio-mute"
" XF86AudioMicMute" = "audio-mic-mute"
"Control XF86AudioRaiseVolume" = "noise-adjust +1"
"Control XF86AudioLowerVolume" = "noise-adjust -1"
}