diff options
-rw-r--r-- | CMakeLists.txt | 55 | ||||
-rw-r--r-- | LICENSE | 2 | ||||
-rw-r--r-- | README.adoc | 3 | ||||
-rw-r--r-- | ddc-ci.c | 2 | ||||
-rw-r--r-- | fancontrol-ng.c | 10 | ||||
-rw-r--r-- | gdm-switch-user.c | 2 | ||||
-rw-r--r-- | genpass.c | 151 | ||||
-rw-r--r-- | iexec.c | 113 | ||||
-rw-r--r-- | input-switch.c | 88 | ||||
m--------- | liberty | 0 | ||||
-rw-r--r-- | paswitch.c | 2 | ||||
-rwxr-xr-x | wmstatus-weather.pl | 3 | ||||
-rw-r--r-- | wmstatus.c | 1051 | ||||
-rw-r--r-- | wmstatus.conf.example | 63 |
14 files changed, 1209 insertions, 336 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e18b66..233ebde 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ include_directories ( link_directories ( ${x_LIBRARY_DIRS} ${pulse_LIBRARY_DIRS} ${dbus_LIBRARY_DIRS}) -option (WITH_GDM "Compile with GDM support" ${gdm_FOUND}) +option (WITH_GDM "Compile with GDM utilities" ${gdm_FOUND}) # Generate a configuration file configure_file (${PROJECT_SOURCE_DIR}/config.h.in @@ -33,7 +33,7 @@ configure_file (${PROJECT_SOURCE_DIR}/config.h.in include_directories (${PROJECT_BINARY_DIR}) # Build -set (targets wmstatus paswitch siprandom big-brother) +set (targets wmstatus paswitch siprandom genpass) if ("${CMAKE_SYSTEM_NAME}" STREQUAL Linux) # These use Linux i2c APIs, but can be made to work on macOS list (APPEND targets brightness input-switch) @@ -47,7 +47,7 @@ elseif (APPLE) add_definitions (-D_DARWIN_C_SOURCE) endif () -foreach (name ${targets}) +foreach (name big-brother ${targets}) add_executable (${name} ${name}.c) endforeach () @@ -58,9 +58,10 @@ target_link_libraries (wmstatus add_threads (wmstatus) if (WITH_GDM) - include_directories (${gdm_INCLUDE_DIRS}) - link_directories (${gdm_LIBRARY_DIRS}) + list (APPEND targets gdm-switch-user) add_executable (gdm-switch-user gdm-switch-user.c) + target_include_directories (gdm-switch-user PUBLIC ${gdm_INCLUDE_DIRS}) + target_link_directories (gdm-switch-user PUBLIC ${gdm_LIBRARY_DIRS}) target_link_libraries (gdm-switch-user ${gdm_LIBRARIES}) endif () @@ -71,27 +72,43 @@ include (GNUInstallDirs) set (SYSTEMD_UNITDIR /lib/systemd/system CACHE PATH "Base directory for systemd unit files") -configure_file (${PROJECT_SOURCE_DIR}/fancontrol-ng.service.in - ${PROJECT_BINARY_DIR}/fancontrol-ng.service @ONLY) -install (FILES fancontrol-ng.conf.example - DESTINATION ${CMAKE_INSTALL_DATADIR}/fancontrol-ng) +if ("${CMAKE_SYSTEM_NAME}" STREQUAL Linux) + configure_file (${PROJECT_SOURCE_DIR}/fancontrol-ng.service.in + ${PROJECT_BINARY_DIR}/fancontrol-ng.service @ONLY) + install (FILES fancontrol-ng.conf.example + DESTINATION ${CMAKE_INSTALL_DATADIR}/fancontrol-ng) -configure_file (${PROJECT_SOURCE_DIR}/priod.service.in - ${PROJECT_BINARY_DIR}/priod.service @ONLY) -install (FILES priod.conf.example - DESTINATION ${CMAKE_INSTALL_DATADIR}/priod) + configure_file (${PROJECT_SOURCE_DIR}/priod.service.in + ${PROJECT_BINARY_DIR}/priod.service @ONLY) + install (FILES priod.conf.example + DESTINATION ${CMAKE_INSTALL_DATADIR}/priod) -# System-wide unit files should be installed under /lib and not /usr/lib -install (FILES - ${PROJECT_BINARY_DIR}/fancontrol-ng.service - ${PROJECT_BINARY_DIR}/priod.service - DESTINATION "${SYSTEMD_UNITDIR}") + # System-wide unit files should be installed under /lib and not /usr/lib + install (FILES + ${PROJECT_BINARY_DIR}/fancontrol-ng.service + ${PROJECT_BINARY_DIR}/priod.service + DESTINATION "${SYSTEMD_UNITDIR}") +endif () if (WITH_GDM) install (TARGETS gdm-switch-user DESTINATION ${CMAKE_INSTALL_BINDIR}) endif () -list (REMOVE_ITEM targets big-brother) +# These should be accessible by users, but need to touch system devices. +# Use the setuid bit, for simplicity. +set (SETUID "SETUID" CACHE STRING "Set this empty on permission issues") +foreach (target brightness input-switch) + if (${target} IN_LIST targets) + list (REMOVE_ITEM targets ${target}) + install (TARGETS ${target} DESTINATION ${CMAKE_INSTALL_BINDIR} + PERMISSIONS + OWNER_WRITE OWNER_READ OWNER_EXECUTE + GROUP_READ GROUP_EXECUTE + WORLD_READ WORLD_EXECUTE + ${SETUID}) + endif () +endforeach () + install (TARGETS ${targets} DESTINATION ${CMAKE_INSTALL_BINDIR}) install (PROGRAMS shellify DESTINATION ${CMAKE_INSTALL_BINDIR}) install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) @@ -1,4 +1,4 @@ -Copyright (c) 2015 - 2021, Přemysl Eric Janouch <p@janouch.name> +Copyright (c) 2015 - 2025, Přemysl Eric Janouch <p@janouch.name> Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. diff --git a/README.adoc b/README.adoc index 0bd0de4..9f2b5f4 100644 --- a/README.adoc +++ b/README.adoc @@ -37,7 +37,8 @@ Regular releases are sporadic. git master should be stable enough. Building -------- Build dependencies: CMake, pkg-config, liberty (included) + -Runtime dependencies: libpulse, libx11, dbus-1, libgdm (optional) +Runtime dependencies: libpulse, libx11, dbus-1 + +Optional runtime dependencies: libgdm (gdm-switch-user) $ git clone --recursive https://git.janouch.name/p/desktop-tools.git $ mkdir desktop-tools/build @@ -46,7 +46,7 @@ log_message_custom (void *user_data, const char *quote, const char *fmt, static void wait_ms (long ms) { - struct timespec ts = { 0, ms * 1000 * 1000 }; + struct timespec ts = { ms / 1000, (ms % 1000) * 1000 * 1000 }; nanosleep (&ts, NULL); } diff --git a/fancontrol-ng.c b/fancontrol-ng.c index ec06dfb..e87dce1 100644 --- a/fancontrol-ng.c +++ b/fancontrol-ng.c @@ -124,7 +124,7 @@ config_validate_nonnegative (const struct config_item *item, struct error **e) return error_set (e, "must be non-negative"); } -static struct config_schema g_config_device[] = +static const struct config_schema g_config_device[] = { { .name = "name", .comment = "Device identifier", @@ -137,7 +137,7 @@ static struct config_schema g_config_device[] = {} }; -static struct config_schema g_config_pwm[] = +static const struct config_schema g_config_pwm[] = { { .name = "temp", .comment = "Path to temperature sensor output", @@ -415,7 +415,7 @@ device_create (struct app_context *ctx, const char *path, // There is no room for errors in the configuration, everything must be valid. // Thus the reset to defaults on invalid values is effectively disabled here. static bool -apply_schema (struct config_schema *schema, struct config_item *object, +apply_schema (const struct config_schema *schema, struct config_item *object, struct error **e) { struct error *warning = NULL, *error = NULL; @@ -445,7 +445,7 @@ static bool check_device_configuration (struct config_item *subtree, struct error **e) { // Check regular fields in the device object - for (struct config_schema *s = g_config_device; s->name; s++) + for (const struct config_schema *s = g_config_device; s->name; s++) if (!apply_schema (s, subtree, e)) return false; @@ -465,7 +465,7 @@ check_device_configuration (struct config_item *subtree, struct error **e) while ((pwm = str_map_iter_next (&iter))) { const char *subpath = iter.link->key; - for (struct config_schema *s = g_config_pwm; s->name; s++) + for (const struct config_schema *s = g_config_pwm; s->name; s++) if (!apply_schema (s, pwm, &error)) { error_set (e, "PWM `%s': %s", subpath, error->message); diff --git a/gdm-switch-user.c b/gdm-switch-user.c index 5718c31..55d8ba3 100644 --- a/gdm-switch-user.c +++ b/gdm-switch-user.c @@ -1,5 +1,5 @@ // Public domain -#include <gdm-user-switching.h> +#include <gdm/gdm-user-switching.h> int main (int argc, char *argv[]) diff --git a/genpass.c b/genpass.c new file mode 100644 index 0000000..623eecb --- /dev/null +++ b/genpass.c @@ -0,0 +1,151 @@ +/* + * genpass.c: password generator + * + * Copyright (c) 2025, Přemysl Eric Janouch <p@janouch.name> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +#include "config.h" +#undef PROGRAM_NAME +#define PROGRAM_NAME "genpass" +#include "liberty/liberty.c" + +static struct str +parse_group (const char *group) +{ + bool present[0x100] = {}; + for (size_t i = 0; group[i]; i++) + { + unsigned char c = group[i]; + if (!i || c != '-' || !group[i + 1]) + present[c] = true; + else if (group[i + 1] < group[i - 1]) + exit_fatal ("character ranges must be increasing"); + else + for (c = group[i - 1]; ++c <= group[i + 1]; ) + present[c] = true; + } + + struct str alphabet = str_make (); + for (size_t i = 1; i < N_ELEMENTS (present); i++) + if (present[i]) + str_append_c (&alphabet, i); + if (!alphabet.len) + exit_fatal ("empty group"); + return alphabet; +} + +static void +parse_program_arguments (int argc, char **argv, + unsigned long *length, struct strv *groups, struct str *alphabet) +{ + static const struct opt opts[] = + { + { 'l', "length", "CHARACTERS", 0, "set password length" }, + { 'd', "debug", NULL, 0, "run in debug mode" }, + { 'h', "help", NULL, 0, "display this help and exit" }, + { 'V', "version", NULL, 0, "output version information and exit" }, + { 0, NULL, NULL, 0, NULL } + }; + + struct opt_handler oh = + opt_handler_make (argc, argv, opts, "GROUP...", "Password generator."); + + int c; + while ((c = opt_handler_get (&oh)) != -1) + switch (c) + { + case 'l': + if (!xstrtoul (length, optarg, 10) || *length <= 0) + print_fatal ("invalid length argument"); + break; + case 'd': + g_debug_mode = true; + break; + case 'h': + opt_handler_usage (&oh, stdout); + exit (EXIT_SUCCESS); + case 'V': + printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); + exit (EXIT_SUCCESS); + default: + print_error ("wrong options"); + opt_handler_usage (&oh, stderr); + exit (EXIT_FAILURE); + } + + argc -= optind; + argv += optind; + + for (int i = 0; i < argc; i++) + { + struct str alphabet = parse_group (argv[i]); + strv_append_owned (groups, str_steal (&alphabet)); + } + + bool present[0x100] = {}; + for (size_t i = 0; i < groups->len; i++) + for (size_t k = 0; groups->vector[i][k]; k++) + { + unsigned char c = groups->vector[i][k]; + if (present[c]) + exit_fatal ("groups are not disjunct"); + present[c] = true; + } + for (size_t i = 1; i < N_ELEMENTS (present); i++) + if (present[i]) + str_append_c (alphabet, i); + + if (groups->len > *length) + exit_fatal ("the requested length is less than the number of groups"); + if (!groups->len) + { + opt_handler_usage (&oh, stderr); + exit (EXIT_FAILURE); + } + + opt_handler_free (&oh); +} + +int +main (int argc, char *argv[]) +{ + unsigned long length = 8; + struct strv groups = strv_make (); + struct str alphabet = str_make (); + parse_program_arguments (argc, argv, &length, &groups, &alphabet); + + unsigned seed = 0; + if (!random_bytes (&seed, sizeof seed, NULL)) + exit_fatal ("failed to initialize random numbers"); + srand (seed); + + // Select from a joined alphabet, but make sure all groups are represented. + struct str candidate = str_make (); + while (true) + { +restart: + for (size_t i = length; i--; ) + str_append_c (&candidate, alphabet.str[rand () % alphabet.len]); + for (size_t i = 0; i < groups.len; i++) + if (!strpbrk (candidate.str, groups.vector[i])) + { + str_reset (&candidate); + goto restart; + } + + printf ("%s\n", candidate.str); + return 0; + } +} @@ -1,7 +1,7 @@ /* * iexec.c: run a program and restart on file change * - * Copyright (c) 2017, Přemysl Eric Janouch <p@janouch.name> + * Copyright (c) 2017 - 2023, Přemysl Eric Janouch <p@janouch.name> * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. @@ -24,34 +24,54 @@ // This can also work on BSD if someone puts in the effort to support kqueue #include <sys/inotify.h> -static pid_t g_child; -static bool g_restarting = false; -static int g_inotify_fd, g_inotify_wd; +static struct +{ + pid_t child; ///< Watched child or 0 + bool exits; ///< Don't restart child when it exits + bool respawn; ///< Respawn child ASAP + bool killing; ///< Waiting for child to die + int inotify_fd, inotify_wd; +} +g; +// Note that this program doesn't queue up file-based restarts static void -handle_file_change (const char *base) +handle_inotify_event (const struct inotify_event *e, const char *base) { - char buf[4096]; ssize_t len; const struct inotify_event *e; - while ((len = read (g_inotify_fd, buf, sizeof buf)) > 0) - for (char *ptr = buf; ptr < buf + len; ptr += sizeof *e + e->len) - { - e = (const struct inotify_event *) buf; - if (e->wd != g_inotify_wd || strcmp (e->name, base)) - continue; + if (e->wd != g.inotify_wd || strcmp (e->name, base)) + return; + if (g.child) + { print_debug ("file changed, killing child"); - g_restarting = true; - if (kill (g_child, SIGINT)) + if (kill (g.child, SIGINT)) print_error ("kill: %s", strerror (errno)); + g.killing = true; } + else + { + print_debug ("file changed, respawning"); + g.respawn = true; + } +} + +static void +handle_file_change (const char *base) +{ + char buf[4096]; + ssize_t len = 0; + struct inotify_event *e = NULL; + while ((len = read (g.inotify_fd, buf, sizeof buf)) > 0) + for (char *ptr = buf; ptr < buf + len; ptr += sizeof *e + e->len) + handle_inotify_event ((e = (struct inotify_event *) buf), base); } static void spawn (char *argv[]) { - if ((g_child = fork ()) == -1) + if ((g.child = fork ()) == -1) exit_fatal ("fork: %s", strerror (errno)); - else if (g_child) + else if (g.child) return; // A linker can create spurious CLOSE_WRITEs, wait until it's executable @@ -64,23 +84,22 @@ spawn (char *argv[]) } static bool -check_child_death (char *argv[]) +check_child_death (void) { - if (waitpid (g_child, NULL, WNOHANG) != g_child) + int status = 0; + if (waitpid (g.child, &status, WNOHANG) != g.child) return true; - if (!g_restarting) + g.child = 0; + if (!g.killing) { print_debug ("child died on its own, not respawning"); - return false; - } - else - { - print_debug ("child died on request, respawning"); - spawn (argv); - g_restarting = false; - return true; + return g.exits; } + + g.killing = false; + print_debug ("child died on request, respawning"); + return g.respawn = true; } static void @@ -93,8 +112,11 @@ sigchld_handler (int signum) int main (int argc, char *argv[]) { + const char *target = NULL; static const struct opt opts[] = { + { 'f', "file", "PATH", 0, "watch this path rather than the program" }, + { 'e', "exits", NULL, 0, "allow the program to exit on its own" }, { 'd', "debug", NULL, 0, "run in debug mode" }, { 'h', "help", NULL, 0, "display this help and exit" }, { 'V', "version", NULL, 0, "output version information and exit" }, @@ -111,6 +133,12 @@ main (int argc, char *argv[]) while ((c = opt_handler_get (&oh)) != -1) switch (c) { + case 'f': + target = optarg; + break; + case 'e': + g.exits = true; + break; case 'd': g_debug_mode = true; break; @@ -136,6 +164,9 @@ main (int argc, char *argv[]) argc -= optind; argv += optind; + if (!target) + target = argv[0]; + (void) signal (SIGPIPE, SIG_IGN); struct sigaction sa = { .sa_handler = sigchld_handler }; sigemptyset (&sa.sa_mask); @@ -148,27 +179,33 @@ main (int argc, char *argv[]) if (sigprocmask (SIG_BLOCK, &chld, &orig)) exit_fatal ("sigprocmask: %s", strerror (errno)); - char *path = xstrdup (argv[0]); + char *path = NULL; + char *dir = dirname ((path = xstrdup (target))); - if ((g_inotify_fd = inotify_init1 (IN_NONBLOCK)) < 0) + if ((g.inotify_fd = inotify_init1 (IN_NONBLOCK)) < 0) exit_fatal ("inotify_init1: %s", strerror (errno)); - if ((g_inotify_wd = inotify_add_watch (g_inotify_fd, - dirname (path), IN_MOVED_TO | IN_CLOSE_WRITE)) < 0) + if ((g.inotify_wd = inotify_add_watch (g.inotify_fd, + dir, IN_MOVED_TO | IN_CLOSE_WRITE)) < 0) exit_fatal ("inotify_add_watch: %s", strerror (errno)); free (path); - char *base = basename ((path = xstrdup (argv[0]))); - spawn (argv); - + char *base = basename ((path = xstrdup (target))); + g.respawn = true; do { - fd_set r; FD_SET (g_inotify_fd, &r); - (void) pselect (g_inotify_fd + 1, &r, NULL, NULL, NULL, &orig); + if (g.respawn) + { + spawn (argv); + g.respawn = false; + } + + fd_set r; FD_SET (g.inotify_fd, &r); + (void) pselect (g.inotify_fd + 1, &r, NULL, NULL, NULL, &orig); handle_file_change (base); } - while (check_child_death (argv)); + while (check_child_death ()); free (path); - close (g_inotify_fd); + xclose (g.inotify_fd); return EXIT_SUCCESS; } diff --git a/input-switch.c b/input-switch.c index da93670..5788f4a 100644 --- a/input-switch.c +++ b/input-switch.c @@ -1,7 +1,7 @@ /* * input-switch.c: switches display input via DDC/CI * - * Copyright (c) 2017, Přemysl Eric Janouch <p@janouch.name> + * Copyright (c) 2017 - 2022, Přemysl Eric Janouch <p@janouch.name> * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. @@ -28,11 +28,60 @@ #include "ddc-ci.c" #include <dirent.h> +// This list is from the MCCS 2.2a specification +struct +{ + int code; ///< Input code + const char *name; ///< Input name + int index; ///< Input index +} +g_inputs[] = +{ + { 0x01, "VGA", 1, }, // Analog video (R/G/B) 1 + { 0x02, "VGA", 2, }, // Analog video (R/G/B) 2 + { 0x03, "DVI", 1, }, // Digital video (TMDS) 1 DVI 1 + { 0x04, "DVI", 2, }, // Digital video (TMDS) 2 DVI 2 + { 0x05, "composite", 1, }, // Composite video 1 + { 0x06, "composite", 2, }, // Composite video 2 + { 0x07, "S-Video", 1, }, // S-video 1 + { 0x08, "S-Video", 2, }, // S-video 2 + { 0x09, "tuner", 1, }, // Tuner 1 + { 0x0A, "tuner", 2, }, // Tuner 2 + { 0x0B, "tuner", 3, }, // Tuner 3 + { 0x0C, "component", 1, }, // Component video (YPbPr/YCbCr) 1 + { 0x0D, "component", 2, }, // Component video (YPbPr/YCbCr) 2 + { 0x0E, "component", 3, }, // Component video (YPbPr/YCbCr) 3 + { 0x0F, "DP", 1, }, // DisplayPort 1 + { 0x10, "DP", 2, }, // DisplayPort 2 + { 0x11, "HDMI", 1, }, // Digital Video (TMDS) 3 HDMI 1 + { 0x12, "HDMI", 2, }, // Digital Video (TMDS) 4 HDMI 2 + { 0x15, "bnq-tb", 1, }, // Thunderbolt on BenQ PD3220U (no spec) +}; + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - typedef bool (*ActionFunc) (int fd, int param, struct error **); static bool +get_input_source (int fd, int input, struct error **e) +{ + struct vcp_feature_readout readout = {}; + if (!vcp_get_feature (fd, VCP_INPUT_SOURCE, &readout, e)) + return false; + + (void) input; + for (size_t i = 0; i < N_ELEMENTS (g_inputs); i++) + if (g_inputs[i].code == readout.cur) + { + printf ("input is %s %d\n", g_inputs[i].name, g_inputs[i].index); + return true; + } + + printf ("input is %d\n", readout.cur); + return true; +} + +static bool set_input_source (int fd, int input, struct error **e) { struct vcp_feature_readout readout = {}; @@ -114,36 +163,6 @@ i2c (ActionFunc action, int param) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// This list is from the MCCS 2.2a specification -struct -{ - int code; ///< Input code - const char *name; ///< Input name - int index; ///< Input index -} -g_inputs[] = -{ - { 0x01, "vga", 1, }, // Analog video (R/G/B) 1 - { 0x02, "vga", 2, }, // Analog video (R/G/B) 2 - { 0x03, "dvi", 1, }, // Digital video (TMDS) 1 DVI 1 - { 0x04, "dvi", 2, }, // Digital video (TMDS) 2 DVI 2 - { 0x05, "composite", 1, }, // Composite video 1 - { 0x06, "composite", 2, }, // Composite video 2 - { 0x07, "s-video", 1, }, // S-video 1 - { 0x08, "s-video", 2, }, // S-video 2 - { 0x09, "tuner", 1, }, // Tuner 1 - { 0x0A, "tuner", 2, }, // Tuner 2 - { 0x0B, "tuner", 3, }, // Tuner 3 - { 0x0C, "component", 1, }, // Component video (YPbPr/YCbCr) 1 - { 0x0D, "component", 2, }, // Component video (YPbPr/YCbCr) 2 - { 0x0E, "component", 3, }, // Component video (YPbPr/YCbCr) 3 - { 0x0F, "dp", 1, }, // DisplayPort 1 - { 0x10, "dp", 2, }, // DisplayPort 2 - { 0x11, "hdmi", 1, }, // Digital Video (TMDS) 3 HDMI 1 - { 0x12, "hdmi", 2, }, // Digital Video (TMDS) 4 HDMI 2 - { 0x15, "bnq-tb", 1, }, // Thunderbolt on BenQ PD3220U (no spec) -}; - int main (int argc, char *argv[]) { @@ -151,9 +170,14 @@ main (int argc, char *argv[]) if (argc <= 1) { - printf ("Usage: %s <input> [<index>]\n", argv[0]); + printf ("Usage: %s {? | INPUT [INDEX]}\n", argv[0]); exit (EXIT_FAILURE); } + if (!strcmp (argv[1], "?")) + { + i2c (get_input_source, -1); + exit (EXIT_SUCCESS); + } unsigned long input_source = 0; if (xstrtoul (&input_source, argv[1], 10)) diff --git a/liberty b/liberty -Subproject 782a9a5977bd5f2101e8808b94d659fe52e2490 +Subproject 75fc6f1c374796f9e794297c3893089009b8772 @@ -290,7 +290,7 @@ on_sink_info (pa_context *context, const pa_sink_info *info, int eol, sink->ports_len++; struct port *port = sink->ports = - xcalloc (sizeof *sink->ports, sink->ports_len); + xcalloc (sink->ports_len, sizeof *sink->ports); for (struct pa_sink_port_info **iter = info->ports; *iter; iter++) { port->name = xstrdup ((*iter)->name); diff --git a/wmstatus-weather.pl b/wmstatus-weather.pl index d5252f2..2d60bc4 100755 --- a/wmstatus-weather.pl +++ b/wmstatus-weather.pl @@ -20,7 +20,8 @@ my %legends; sub retrieve_legends { # HTTP/Tiny supports TLS, but with non-core IO::Socket::SSL, so use cURL open(my $sock, '-|', 'curl', '-sSA', $agent, - "$base/weathericon/2.0/legends.txt") or return $!; + 'https://raw.githubusercontent.com/' . + 'metno/weathericons/main/weather/legend.csv') or return $!; while (local $_ = <$sock>) { $legends{$1} = $2 if /^(.+?),(.+?),/ } close($sock); } @@ -1,7 +1,7 @@ /* * wmstatus.c: simple PulseAudio-enabled status setter for dwm and i3/sway * - * Copyright (c) 2015 - 2021, Přemysl Eric Janouch <p@janouch.name> + * Copyright (c) 2015 - 2024, Přemysl Eric Janouch <p@janouch.name> * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. @@ -20,7 +20,10 @@ #define LIBERTY_WANT_ASYNC #define LIBERTY_WANT_PROTO_MPD -#define _GNU_SOURCE // openat +// openat, dirfd +#define _XOPEN_SOURCE 700 +#define _ATFILE_SOURCE +#define _GNU_SOURCE #include "config.h" #undef PROGRAM_NAME @@ -67,6 +70,20 @@ log_message_custom (void *user_data, const char *quote, const char *fmt, fputs ("\n", stream); } +static void +shell_quote (const char *str, struct str *output) +{ + // See SUSv3 Shell and Utilities, 2.2.3 Double-Quotes + str_append_c (output, '"'); + for (const char *p = str; *p; p++) + { + if (strchr ("`$\"\\", *p)) + str_append_c (output, '\\'); + str_append_c (output, *p); + } + str_append_c (output, '"'); +} + // --- NUT --------------------------------------------------------------------- // More or less copied and pasted from the MPD client. This code doesn't even @@ -779,41 +796,197 @@ backend_i3_new (void) // --- Configuration ----------------------------------------------------------- -static struct simple_config_item g_config_table[] = +static const struct config_schema g_config_general[] = { - { "mpd_address", "localhost", "MPD host or socket" }, - { "mpd_service", "6600", "MPD service name or port" }, - { "mpd_password", NULL, "MPD password" }, + { .name = "command", + .comment = "Command to run for more info", + .type = CONFIG_ITEM_STRING }, + { .name = "sleep_timer", + .comment = "Idle seconds to suspend after", + .type = CONFIG_ITEM_INTEGER }, + {} +}; - { "nut_enabled", "off", "NUT UPS status reading enabled" }, - { "nut_load_thld", "50", "NUT threshold for load display" }, +static const struct config_schema g_config_mpd[] = +{ + { .name = "address", + .comment = "MPD host or socket", + .type = CONFIG_ITEM_STRING, + .default_ = "\"localhost\"" }, + { .name = "service", + .comment = "MPD service name or port", + .type = CONFIG_ITEM_STRING, + .default_ = "\"6600\"" }, + { .name = "password", + .comment = "MPD password", + .type = CONFIG_ITEM_STRING }, + {} +}; + +static const struct config_schema g_config_nut[] = +{ + { .name = "enabled", + .comment = "NUT UPS status reading enabled", + .type = CONFIG_ITEM_BOOLEAN, + .default_ = "off" }, + { .name = "load_thld", + .comment = "NUT threshold for load display", + .type = CONFIG_ITEM_INTEGER, + .default_ = "50" }, // This is just a hack because my UPS doesn't report that value; a more // proper way of providing this information would be by making use of the // enhanced configuration format and allowing arbitrary per-UPS overrides - { "nut_load_power", NULL, "ups.realpower.nominal override" }, - - { "command", NULL, "command to run for more info" }, - { "sleep_timer", NULL, "idle seconds to suspend after" }, - { NULL, NULL, NULL } + { .name = "load_power", + .comment = "ups.realpower.nominal fallback", + .type = CONFIG_ITEM_INTEGER }, + {} }; +static void +app_load_config_general (struct config_item *subtree, void *user_data) +{ + config_schema_apply_to_object (g_config_general, subtree, user_data); +} + +static void +app_load_config_mpd (struct config_item *subtree, void *user_data) +{ + config_schema_apply_to_object (g_config_mpd, subtree, user_data); +} + +static void +app_load_config_nut (struct config_item *subtree, void *user_data) +{ + config_schema_apply_to_object (g_config_nut, subtree, user_data); +} + +static struct config +app_make_config (void) +{ + struct config config = config_make (); + 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, "nut", app_load_config_nut, NULL); + + // Bootstrap configuration, so that we can access schema items at all + config_load (&config, config_item_object ()); + return config; +} + +static const char * +get_config_string (struct config_item *root, const char *key) +{ + struct config_item *item = config_item_get (root, key, NULL); + hard_assert (item); + if (item->type == CONFIG_ITEM_NULL) + return NULL; + hard_assert (config_item_type_is_string (item->type)); + return item->value.string.str; +} + +static const int64_t * +get_config_integer (struct config_item *root, const char *key) +{ + struct config_item *item = config_item_get (root, key, NULL); + hard_assert (item); + if (item->type == CONFIG_ITEM_NULL) + return NULL; + hard_assert (item->type == CONFIG_ITEM_INTEGER); + return &item->value.integer; +} + +static const bool * +get_config_boolean (struct config_item *root, const char *key) +{ + struct config_item *item = config_item_get (root, key, NULL); + hard_assert (item); + if (item->type == CONFIG_ITEM_NULL) + return NULL; + hard_assert (item->type == CONFIG_ITEM_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': + 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 ------------------------------------------------------------- struct app_context { - struct str_map config; ///< Program configuration + struct config config; ///< Program configuration struct backend *backend; ///< WM backend Display *dpy; ///< X display handle struct poller_fd x_event; ///< X11 event - const char *prefix; ///< User-defined prefix struct poller poller; ///< Poller struct poller_timer time_changed; ///< Time change timer struct poller_timer make_context; ///< Start PulseAudio communication struct poller_timer refresh_rest; ///< Refresh unpollable information + // IPC: + + int ipc_fd; ///< The IPC datagram socket (file) + struct poller_fd ipc_event; ///< IPC event + // Sleep timer: int xsync_base_event_code; ///< XSync base event code @@ -834,6 +1007,7 @@ struct app_context // Hotkeys: + struct binding *bindings; ///< Global bindings int xkb_base_event_code; ///< Xkb base event code char *layout; ///< Keyboard layout @@ -849,7 +1023,7 @@ struct app_context struct mpd_client mpd_client; ///< MPD client char *mpd_song; ///< MPD current song - char *mpd_status; ///< MPD status (overrides song) + bool mpd_stopped; ///< MPD stopped (overrides song) // NUT: @@ -920,8 +1094,7 @@ app_context_init (struct app_context *self) { memset (self, 0, sizeof *self); - self->config = str_map_make (free); - simple_config_load_defaults (&self->config, g_config_table); + self->config = app_make_config (); if (!(self->dpy = XkbOpenDisplay (NULL, &self->xkb_base_event_code, NULL, NULL, NULL, NULL))) @@ -930,10 +1103,13 @@ app_context_init (struct app_context *self) poller_init (&self->poller); self->api = poller_pa_new (&self->poller); + self->ipc_fd = -1; + self->ipc_event = poller_fd_make (&self->poller, self->ipc_fd); + self->command_current = strv_make (); self->command_pid = -1; self->command_fd = -1; - self->command_event = poller_fd_make (&self->poller, -1); + self->command_event = poller_fd_make (&self->poller, self->command_fd); self->command_buffer = str_make (); set_cloexec (ConnectionNumber (self->dpy)); @@ -963,8 +1139,8 @@ app_context_init (struct app_context *self) static void app_context_free (struct app_context *self) { - str_map_free (&self->config); - if (self->backend) self->backend->destroy (self->backend); + config_free (&self->config); + if (self->backend) self->backend->destroy (self->backend); poller_fd_reset (&self->x_event); cstr_set (&self->layout, NULL); @@ -973,6 +1149,12 @@ app_context_free (struct app_context *self) if (self->context) pa_context_unref (self->context); if (self->dpy) XCloseDisplay (self->dpy); + if (self->ipc_fd != -1) + { + poller_fd_reset (&self->ipc_event); + xclose (self->ipc_fd); + } + strv_free (&self->command_current); if (self->command_pid != -1) (void) kill (self->command_pid, SIGTERM); @@ -989,7 +1171,6 @@ app_context_free (struct app_context *self) mpd_client_free (&self->mpd_client); cstr_set (&self->mpd_song, NULL); - cstr_set (&self->mpd_status, NULL); nut_client_free (&self->nut_client); str_map_free (&self->nut_ups_info); @@ -1112,6 +1293,15 @@ try_power_supply (int dir, struct error **e) return NULL; } + bool offline = !read_number (dir, "online", &error); + if (error) + { + error_free (error); + error = NULL; + } + else if (offline) + return NULL; + bool is_relevant = !strcmp (type, "Battery") || !strcmp (type, "USB") || @@ -1226,9 +1416,7 @@ make_noise_status (struct app_context *ctx) static void refresh_status (struct app_context *ctx) { - if (ctx->prefix) ctx->backend->add (ctx->backend, ctx->prefix); - - if (ctx->mpd_status) ctx->backend->add (ctx->backend, ctx->mpd_status); + if (ctx->mpd_stopped) ctx->backend->add (ctx->backend, "MPD stopped"); else if (ctx->mpd_song) ctx->backend->add (ctx->backend, ctx->mpd_song); if (ctx->noise_end_time) @@ -1415,7 +1603,8 @@ static void on_command_start (void *user_data) { struct app_context *ctx = user_data; - char *command = str_map_find (&ctx->config, "command"); + const char *command = + get_config_string (ctx->config.root, "general.command"); if (!command) return; @@ -1435,7 +1624,7 @@ on_command_start (void *user_data) posix_spawn_file_actions_addclose (&actions, output_pipe[PIPE_WRITE]); pid_t pid = -1; - char *argv[] = { "sh", "-c", command, NULL }; + char *argv[] = { "sh", "-c", (char *) command, NULL }; int result = posix_spawnp (&pid, argv[0], &actions, NULL, argv, environ); posix_spawn_file_actions_destroy (&actions); @@ -1495,9 +1684,8 @@ mpd_on_info_response (const struct mpd_response *response, struct str_map map; mpd_vector_to_map (data, &map); - cstr_set (&ctx->mpd_status, NULL); - struct str s = str_make (); + ctx->mpd_stopped = false; const char *value; if ((value = str_map_find (&map, "state"))) @@ -1505,7 +1693,7 @@ mpd_on_info_response (const struct mpd_response *response, // Unicode approximates since in proportional fonts ASCII looks ugly // and I don't want to depend on a particular font with player chars if (!strcmp (value, "stop")) - ctx->mpd_status = xstrdup ("MPD stopped"); + ctx->mpd_stopped = true; else if (!strcmp (value, "pause")) str_append (&s, "▯▯ " /* "|| " */); else @@ -1585,7 +1773,7 @@ mpd_on_connected (void *user_data) struct app_context *ctx = user_data; struct mpd_client *c = &ctx->mpd_client; - const char *password = str_map_find (&ctx->config, "mpd_password"); + const char *password = get_config_string (ctx->config.root, "mpd.password"); if (password) { mpd_client_send_command (c, "password", password, NULL); @@ -1602,6 +1790,10 @@ mpd_on_failure (void *user_data) struct app_context *ctx = user_data; print_error ("connection to MPD failed"); mpd_queue_reconnect (ctx); + + cstr_set (&ctx->mpd_song, NULL); + ctx->mpd_stopped = false; + refresh_status (ctx); } static void @@ -1628,9 +1820,10 @@ on_mpd_reconnect (void *user_data) c->on_io_hook = mpd_on_io_hook; struct error *e = NULL; + struct config_item *root = ctx->config.root; if (!mpd_client_connect (&ctx->mpd_client, - str_map_find (&ctx->config, "mpd_address"), - str_map_find (&ctx->config, "mpd_service"), &e)) + get_config_string (root, "mpd.address"), + get_config_string (root, "mpd.service"), &e)) { print_error ("%s: %s", "cannot connect to MPD", e->message); error_free (e); @@ -1688,6 +1881,7 @@ nut_process_ups (struct app_context *ctx, struct strv *ups_list, const char *charge = str_map_find (dict, "battery.charge"); const char *runtime = str_map_find (dict, "battery.runtime"); const char *load = str_map_find (dict, "ups.load"); + const char *power = str_map_find (dict, "ups.realpower.nominal"); if (!soft_assert (status && charge && runtime)) return; @@ -1717,24 +1911,25 @@ nut_process_ups (struct app_context *ctx, struct strv *ups_list, strv_append_owned (&items, interval_string (runtime_sec)); // Only show load if it's higher than the threshold so as to not distract - const char *threshold = str_map_find (&ctx->config, "nut_load_thld"); - unsigned long load_n, threshold_n; + struct config_item *root = ctx->config.root; + const int64_t *threshold = get_config_integer (root, "nut.load_thld"); + const int64_t *fallback = get_config_integer (root, "nut.load_power"); + unsigned long load_n, power_n; if (load - && xstrtoul (&load_n, load, 10) - && xstrtoul (&threshold_n, threshold, 10) - && load_n >= threshold_n) + && xstrtoul (&load_n, load, 10) + && load_n >= (unsigned long) *threshold) { struct str item = str_make (); str_append_printf (&item, "load %s%%", load); - const char *power = str_map_find (dict, "ups.realpower.nominal"); - // Override if NUT cannot tell it correctly for whatever reason - if (!power) power = str_map_find (&ctx->config, "nut_load_power"); - - // Approximation of how much electricity the perpihery actually uses - unsigned long power_n; + // Approximation of how much electricity the perpihery actually uses. + // Use fallback if NUT cannot tell it correctly for whatever reason. if (power && xstrtoul (&power_n, power, 10)) - str_append_printf (&item, " (~%luW)", power_n * load_n / 100); + str_append_printf (&item, + " (~%luW)", power_n * load_n / 100); + else if (fallback && *fallback >= 0) + str_append_printf (&item, + " (~%luW)", (unsigned long) *fallback * load_n / 100); strv_append_owned (&items, str_steal (&item)); } @@ -1877,12 +2072,7 @@ static void on_nut_reconnect (void *user_data) { struct app_context *ctx = user_data; - - bool want_nut = false; - if (!set_boolean_if_valid (&want_nut, - str_map_find (&ctx->config, "nut_enabled"))) - print_error ("invalid configuration value for `%s'", "nut_enabled"); - if (!want_nut) + if (!*get_config_boolean (ctx->config.root, "nut.enabled")) return; struct nut_client *c = &ctx->nut_client; @@ -2048,15 +2238,33 @@ on_noise_timer (void *user_data) } 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_iterator = 0; - if (!ctx->noise_end_time && (arg < 0 || !noise_start (ctx))) + if (!ctx->noise_end_time && (arg <= 0 || !noise_start (ctx))) return; - // The granularity of noise playback is whole minutes - ctx->noise_end_time += arg * 60; + time_t now = time (NULL); + int diff = difftime (ctx->noise_end_time, now); + + // The granularity of noise playback setting is whole hours. + enum { SECOND = 1, MINUTE = 60, HOUR = 3600 }; + if (arg > 0) + // Add a minute to enable stepping up from 0:59 to 2:00. + diff = (diff + arg * HOUR + MINUTE) / HOUR * HOUR; + else if (arg++ < 0) + // Remove a second to enable stepping down from 2:00 to 1:00. + diff = (diff + arg * HOUR - SECOND) / HOUR * HOUR; + + ctx->noise_end_time = now + diff; on_noise_timer (ctx); } @@ -2198,27 +2406,35 @@ spawn (char *argv[]) posix_spawn_file_actions_destroy (&actions); } -#define MPD_SIMPLE(name, ...) \ - static void \ - on_mpd_ ## name (struct app_context *ctx, int arg) \ - { \ - (void) arg; \ - 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 +action_exec (struct app_context *ctx, const struct strv *args) +{ + (void) ctx; + spawn (args->vector); +} -// XXX: pause without argument is deprecated, we can watch play state -// if we want to have the toggle pause/play functionality -MPD_SIMPLE (play, "pause", NULL) -MPD_SIMPLE (stop, "stop", NULL) -MPD_SIMPLE (prev, "previous", NULL) -MPD_SIMPLE (next, "next", NULL) -MPD_SIMPLE (forward, "seekcur", "+10", NULL) -MPD_SIMPLE (backward, "seekcur", "-10", NULL) +static void +action_mpd (struct app_context *ctx, const struct strv *args) +{ + struct mpd_client *c = &ctx->mpd_client; + if (c->state != MPD_CONNECTED) + return; + mpd_client_send_commandv (c, args->vector); + mpd_client_add_task (c, NULL, NULL); + mpd_client_idle (c, 0); +} + +static void +action_mpd_play_toggle (struct app_context *ctx, const struct strv *args) +{ + (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 on_volume_finish (pa_context *context, int success, void *userdata) @@ -2231,9 +2447,9 @@ on_volume_finish (pa_context *context, int success, void *userdata) } 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) return; @@ -2243,9 +2459,9 @@ on_volume_mic_mute (struct app_context *ctx, int arg) } 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) return; @@ -2262,9 +2478,9 @@ on_volume_switch (struct app_context *ctx, int arg) } 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) return; @@ -2274,14 +2490,20 @@ on_volume_mute (struct app_context *ctx, int arg) } 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) return; + long arg = strtol (args->vector[0], NULL, 10); pa_cvolume volume = ctx->sink_volume; 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 pa_cvolume_dec (&volume, (pa_volume_t) -arg * PA_VOLUME_NORM / 100); pa_operation_unref (pa_context_set_sink_volume_by_name (ctx->context, @@ -2289,51 +2511,6 @@ on_volume_set (struct app_context *ctx, int arg) } 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 go_insomniac (struct app_context *ctx) { static const char *what = "sleep:idle"; @@ -2378,9 +2555,9 @@ go_insomniac (struct app_context *ctx) } 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); // Get rid of the lock if we hold one, establish it otherwise @@ -2396,102 +2573,70 @@ on_insomnia (struct app_context *ctx, int arg) } 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; + } + + long group = strtol (args->vector[0], NULL, 10); + if (group < XkbGroup1Index || group > XkbGroup4Index) + print_warning ("invalid XKB group index: %s", args->vector[0]); + else + XkbLockGroup (ctx->dpy, XkbUseCoreKbd, group); } -struct +static const struct action { - unsigned mod; - KeySym keysym; - void (*handler) (struct app_context *ctx, int arg); - int arg; + const char *name; + void (*handler) (struct app_context *ctx, const struct strv *args); } -g_keys[] = +g_handlers[] = { - // This key should be labeled L on normal Qwert[yz] layouts - { Mod4Mask, XK_n, on_lock, 0 }, - - // 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 - - // MPD - { Mod4Mask, XK_Up, on_mpd_play, 0 }, - { Mod4Mask, XK_Down, on_mpd_stop, 0 }, - { Mod4Mask, XK_Left, on_mpd_prev, 0 }, - { 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, 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 }, + { "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 }, +}; - // 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 }, +struct binding +{ + LIST_HEADER (struct binding) - // Noise playback - { ControlMask, XF86XK_AudioRaiseVolume, on_noise_adjust, 60 }, - { ControlMask, XF86XK_AudioLowerVolume, on_noise_adjust, -60 }, + unsigned mods; ///< Modifiers + KeyCode keycode; ///< Key code + struct action handler; ///< Handling procedure + struct strv args; ///< Arguments to the handler }; +static struct action +action_by_name (const char *name) +{ + for (size_t i = 0; i < N_ELEMENTS (g_handlers); i++) + if (!strcmp (g_handlers[i].name, name)) + return g_handlers[i]; + return (struct action) {}; +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void on_x_keypress (struct app_context *ctx, XEvent *e) { XKeyEvent *ev = &e->xkey; - unsigned unconsumed_mods; - KeySym keysym; - if (!XkbLookupKeySym (ctx->dpy, - (KeyCode) ev->keycode, ev->state, &unconsumed_mods, &keysym)) - return; - 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); + LIST_FOR_EACH (struct binding, iter, ctx->bindings) + if (iter->keycode == ev->keycode + && iter->mods == ev->state + && iter->handler.handler) + iter->handler.handler (ctx, &iter->args); } static void @@ -2546,33 +2691,143 @@ on_x_ready (const struct pollfd *pfd, void *user_data) } } -static void -init_xlib_events (struct app_context *ctx) +static bool +parse_key_modifier (const char *modifier, unsigned *mods) { - unsigned long n; - const char *sleep_timer = str_map_find (&ctx->config, "sleep_timer"); - if (sleep_timer && ctx->idle_counter) + static const struct { - if (!xstrtoul (&n, sleep_timer, 10) || !n || n > INT_MAX / 1000) - exit_fatal ("invalid value for the sleep timer"); - XSyncIntToValue (&ctx->idle_timeout, n * 1000); - set_idle_alarm (ctx, &ctx->idle_alarm_inactive, - XSyncPositiveComparison, ctx->idle_timeout); + 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 = action_by_name (args.vector[0]); + 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)); - 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); + struct str_map *keys = + &config_item_get (ctx->config.root, "keys", NULL)->value.object; + struct str_map_iter iter = str_map_iter_make (keys); - XSelectInput (ctx->dpy, root, KeyPressMask); + 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 +init_xlib_events (struct app_context *ctx) +{ + const int64_t *sleep_timer = + get_config_integer (ctx->config.root, "general.sleep_timer"); + if (sleep_timer && ctx->idle_counter) + { + if (*sleep_timer <= 0 || *sleep_timer > INT_MAX / 1000) + exit_fatal ("invalid value for the sleep timer"); + XSyncIntToValue (&ctx->idle_timeout, *sleep_timer * 1000); + set_idle_alarm (ctx, &ctx->idle_alarm_inactive, + XSyncPositiveComparison, ctx->idle_timeout); + } + + init_bindings (ctx); XSync (ctx->dpy, False); ctx->x_event.dispatcher = on_x_ready; @@ -2586,8 +2841,323 @@ init_xlib_events (struct app_context *ctx) XkbAllStateComponentsMask, XkbGroupStateMask); } +// --- IPC --------------------------------------------------------------------- + +#define IPC_SOCKET "ipc.socket" + +static void +on_ipc_message (struct app_context *ctx, const char *message, size_t len) +{ + struct action handler = action_by_name (message); + if (!handler.handler) + { + print_error ("ipc: %s: %s", "unknown action", message); + return; + } + + struct strv args = strv_make (); + const char *p = memchr (message, 0, len); + while (p) + { + strv_append (&args, ++p); + p = memchr (p, 0, len - (p - message)); + } + + handler.handler (ctx, &args); + strv_free (&args); +} + +static void +on_ipc_ready (const struct pollfd *pfd, void *user_data) +{ + struct app_context *ctx = user_data; + char buf[65536] = {}; + + while (true) + { + ssize_t len = read (pfd->fd, buf, sizeof buf - 1 /* NUL-terminated */); + if (len >= 0) + { + buf[len] = 0; + on_ipc_message (ctx, buf, len); + } + else if (errno == EAGAIN) + return; + else if (errno != EINTR) + print_warning ("ipc: %s: %s", "read", strerror (errno)); + + } +} + +static void +app_setup_ipc (struct app_context *ctx) +{ + int fd = socket (AF_UNIX, SOCK_DGRAM, 0); + if (fd == -1) + { + print_error ("ipc: %s: %s", "socket", strerror (errno)); + return; + } + + set_cloexec (fd); + char *path = resolve_relative_runtime_filename (IPC_SOCKET); + + // This is unfortunately the only way to prevent EADDRINUSE. + unlink (path); + + struct sockaddr_un sa = { .sun_family = AF_UNIX }; + strncpy (sa.sun_path, path, sizeof sa.sun_path - 1); + if (bind (fd, (struct sockaddr *) &sa, sizeof sa)) + { + print_error ("ipc: %s: %s", path, strerror (errno)); + xclose (fd); + goto out; + } + + set_blocking (fd, false); + ctx->ipc_fd = fd; + ctx->ipc_event = poller_fd_make (&ctx->poller, fd); + ctx->ipc_event.dispatcher = on_ipc_ready; + ctx->ipc_event.user_data = ctx; + poller_fd_set (&ctx->ipc_event, POLLIN); +out: + free (path); +} + +static int +ipc_send (int argc, char *argv[]) +{ + int fd = socket (AF_UNIX, SOCK_DGRAM, 0); + if (fd == -1) + print_fatal ("ipc: %s: %s", "socket", strerror (errno)); + + struct str message = str_make (); + for (int i = 0; i < argc; i++) + { + if (i > 0) + str_append_c (&message, 0); + str_append (&message, argv[i]); + } + + char *path = resolve_relative_runtime_filename (IPC_SOCKET); + struct sockaddr_un sa = { .sun_family = AF_UNIX }; + strncpy (sa.sun_path, path, sizeof sa.sun_path - 1); + + int result = EXIT_FAILURE; + ssize_t sent = sendto (fd, message.str, message.len, 0, + (struct sockaddr *) &sa, sizeof sa); + if (sent < 0) + print_error ("ipc: %s: %s", path, strerror (errno)); + else if (sent != (ssize_t) message.len) + print_error ("ipc: %s: %s", path, "incomplete message sent"); + else + result = 0; + + free (path); + str_free (&message); + return result; +} + +// --- Configuration ----------------------------------------------------------- + +static void +app_load_configuration (struct app_context *ctx) +{ + char *filename = resolve_filename + (PROGRAM_NAME ".conf", resolve_relative_config_filename); + if (!filename) + return; + + struct error *e = NULL; + struct config_item *root = config_read_from_file (filename, &e); + free (filename); + + if (e) + exit_fatal ("error loading configuration: %s", e->message); + + if (root) + { + config_load (&ctx->config, root); + config_schema_call_changed (ctx->config.root); + } +} + +static void +app_save_configuration (struct app_context *ctx, const char *path_hint) +{ + static const char *prolog = + "# " PROGRAM_NAME " " PROGRAM_VERSION " configuration file\n\n"; + + struct str data = str_make (); + str_append (&data, prolog); + config_item_write (ctx->config.root, true, &data); + + struct error *e = NULL; + char *filename = write_configuration_file (path_hint, &data, &e); + str_free (&data); + + if (!filename) + { + print_error ("%s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + print_status ("configuration written to `%s'", filename); + free (filename); +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +static bool +sway_command_argument_needs_quoting (const char *word) +{ + while (*word) + if (!isalnum_ascii (*word++)) + return true; + return false; +} + +static void +sway_append_command_argument (struct str *out, const char *word) +{ + if (out->len) + str_append_c (out, ' '); + + if (!sway_command_argument_needs_quoting (word)) + { + str_append (out, word); + return; + } + + str_append_c (out, '\''); + for (const char *p = word; *p; p++) + { + if (*p == '\'' || *p == '\\') + str_append_c (out, '\\'); + str_append_c (out, *p); + } + str_append_c (out, '\''); +} + +static char * +sway_shell_command_for_action (const struct strv *args) +{ + // The i3/Sway quoting is properly fucked up, + // and its exec command forwards to `sh -c`. + struct str shell_command = str_make (); + if (strcmp (args->vector[0], "exec")) + { + // argv[0] would need realpath() applied on it. + shell_quote (PROGRAM_NAME, &shell_command); + str_append (&shell_command, " -- "); + shell_quote (args->vector[0], &shell_command); + str_append_c (&shell_command, ' '); + } + for (size_t i = 1; i < args->len; i++) + { + shell_quote (args->vector[i], &shell_command); + str_append_c (&shell_command, ' '); + } + if (shell_command.len) + shell_command.str[--shell_command.len] = 0; + return str_steal (&shell_command); +} + +static const char * +sway_bindsym (const char *combination, const char *action) +{ + const char *error = NULL; + struct strv keys = strv_make (); + struct strv args = strv_make (); + if (!parse_binding (combination, &keys)) + { + error = "parsing key combination failed"; + goto out; + } + if (!parse_binding (action, &args) || !args.len) + { + error = "parsing the binding failed"; + goto out; + } + + struct action handler = action_by_name (args.vector[0]); + if (!handler.name) + { + error = "unknown action"; + goto out; + } + + // This command name may not be quoted. + // Note that i3-msg doesn't accept bindsym at all, only swaymsg does. + struct str sway_command = str_make (); + sway_append_command_argument (&sway_command, "bindsym"); + char *recombined = strv_join (&keys, "+"); + sway_append_command_argument (&sway_command, recombined); + free (recombined); + + if (handler.handler == action_xkb_lock_group) + { + // This should also switch the XWayland layout, + // though it has been observed to not take effect immediately. + sway_append_command_argument (&sway_command, "input"); + sway_append_command_argument (&sway_command, "type:keyboard"); + sway_append_command_argument (&sway_command, "xkb_switch_layout"); + for (size_t i = 1; i < args.len; i++) + sway_append_command_argument (&sway_command, args.vector[i]); + } + else + { + sway_append_command_argument (&sway_command, "exec"); + char *shell_command = sway_shell_command_for_action (&args); + sway_append_command_argument (&sway_command, shell_command); + free (shell_command); + } + + struct strv argv = strv_make (); + strv_append (&argv, "swaymsg"); + strv_append_owned (&argv, str_steal (&sway_command)); + + posix_spawn_file_actions_t actions; + posix_spawn_file_actions_init (&actions); + posix_spawnp (NULL, argv.vector[0], &actions, NULL, argv.vector, environ); + posix_spawn_file_actions_destroy (&actions); + + strv_free (&argv); +out: + strv_free (&keys); + strv_free (&args); + return error; +} + +static void +sway_forward_bindings (void) +{ + // app_context_init() has side-effects. + struct app_context ctx = { .config = app_make_config () }; + app_load_configuration (&ctx); + + 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 = sway_bindsym (combination, action->value.string.str); + } + if (err) + print_warning ("configuration: key `%s': %s", combination, err); + } +} + +// --- Signals ----------------------------------------------------------------- + static int g_signal_pipe[2]; ///< A pipe used to signal... signals static struct poller_fd g_signal_event; ///< Signal pipe is readable @@ -2650,6 +3220,8 @@ setup_signal_handlers (struct app_context *ctx) poller_fd_set (&g_signal_event, POLLIN); } +// --- Initialisation, event handling ------------------------------------------ + static void poller_timer_init_and_set (struct poller_timer *self, struct poller *poller, poller_timer_fn cb, void *user_data) @@ -2671,15 +3243,16 @@ main (int argc, char *argv[]) { 'd', "debug", NULL, 0, "run in debug mode" }, { 'h', "help", NULL, 0, "display this help and exit" }, { 'V', "version", NULL, 0, "output version information and exit" }, - { '3', "i3bar", NULL, 0, "print output for i3bar/sway-bar instead" }, + { '3', "i3bar", NULL, 0, "print output for i3-bar/swaybar instead" }, + { 's', "bind-sway", NULL, 0, "import bindings over swaymsg" }, { 'w', "write-default-cfg", "FILENAME", OPT_OPTIONAL_ARG | OPT_LONG_ONLY, "write a default configuration file and exit" }, { 0, NULL, NULL, 0, NULL } }; - struct opt_handler oh = - opt_handler_make (argc, argv, opts, NULL, "Set root window name."); + struct opt_handler oh = opt_handler_make (argc, argv, opts, "[ACTION...]", + "Set root window name."); bool i3bar = false; int c; @@ -2698,9 +3271,16 @@ main (int argc, char *argv[]) case '3': i3bar = true; break; + case 's': + sway_forward_bindings (); + exit (EXIT_SUCCESS); case 'w': - call_simple_config_write_default (optarg, g_config_table); + { + // app_context_init() has side-effects. + struct app_context ctx = { .config = app_make_config () }; + app_save_configuration (&ctx, optarg); exit (EXIT_SUCCESS); + } default: print_error ("wrong options"); opt_handler_usage (&oh, stderr); @@ -2711,16 +3291,15 @@ main (int argc, char *argv[]) argv += optind; opt_handler_free (&oh); + if (argc > 0) + return ipc_send (argc, argv); struct app_context ctx; app_context_init (&ctx); - ctx.prefix = argc > 1 ? argv[1] : NULL; + app_load_configuration (&ctx); + app_setup_ipc (&ctx); setup_signal_handlers (&ctx); - struct error *e = NULL; - if (!simple_config_update_from_file (&ctx.config, &e)) - exit_fatal ("%s", e->message); - poller_timer_init_and_set (&ctx.time_changed, &ctx.poller, on_time_changed, &ctx); poller_timer_init_and_set (&ctx.make_context, &ctx.poller, diff --git a/wmstatus.conf.example b/wmstatus.conf.example new file mode 100644 index 0000000..8d6a7fb --- /dev/null +++ b/wmstatus.conf.example @@ -0,0 +1,63 @@ +# 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 0" + "Mod4 F2" = "xkb-lock-group 1" + "Mod4 F3" = "xkb-lock-group 2" + "Mod4 F4" = "xkb-lock-group 3" + + "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" + + # Turns on or off Pioneer integrated amplifiers + "Mod4 Control Delete" = "exec elksmart-comm --nec A538" +} |