Support keyboard led indicator on wayland as well

This commit is contained in:
dec05eba
2025-11-08 03:28:18 +01:00
parent 0995e86e89
commit d1f8db3760
12 changed files with 282 additions and 47 deletions

2
TODO
View File

@@ -245,4 +245,4 @@ Show the currently recorded capture in the ui, to preview if everything looks ok
Show a question mark beside options. When hovering the question mark show a tooltip that explains the options.
Make led indicator work on wayland (set led with /sys/... blabla with root access). Maybe add a command in gsr-global-hotkeys for that, set_led true/false <led name>. That should loop all input devices and set their led status for the led name.
Remove all mgl::Clock usage in Overlay. We only need to get the time once per update in Overlay::handle_events. Also get time in other places outside handle_events.

View File

@@ -1,12 +1,12 @@
#pragma once
#include <X11/Xlib.h>
#include <sys/types.h>
#include <mglpp/system/Clock.hpp>
namespace gsr {
class LedIndicator {
public:
LedIndicator(Display *dpy);
LedIndicator();
LedIndicator(const LedIndicator&) = delete;
LedIndicator& operator=(const LedIndicator&) = delete;
~LedIndicator();
@@ -15,10 +15,10 @@ namespace gsr {
void blink();
void update();
private:
bool run_gsr_global_hotkeys_set_leds(bool enabled);
void update_led(bool new_state);
private:
Display *dpy = nullptr;
Atom scroll_lock_atom = None;
pid_t gsr_global_hotkeys_pid = -1;
bool led_indicator_on = false;
bool led_enabled = false;
bool perform_blink = false;

View File

@@ -94,6 +94,7 @@ executable(
'tools/gsr-global-hotkeys/hotplug.c',
'tools/gsr-global-hotkeys/keyboard_event.c',
'tools/gsr-global-hotkeys/keys.c',
'tools/gsr-global-hotkeys/leds.c',
'tools/gsr-global-hotkeys/main.c'
],
c_args : '-fstack-protector-all',

View File

@@ -231,8 +231,8 @@ namespace gsr {
{"record.record_options.overclock", &config.record_config.record_options.overclock},
{"record.record_options.record_cursor", &config.record_config.record_options.record_cursor},
{"record.record_options.restore_portal_session", &config.record_config.record_options.restore_portal_session},
{"record.record_options.show_notifications", &config.replay_config.record_options.show_notifications},
{"record.record_options.use_led_indicator", &config.replay_config.record_options.use_led_indicator},
{"record.record_options.show_notifications", &config.record_config.record_options.show_notifications},
{"record.record_options.use_led_indicator", &config.record_config.record_options.use_led_indicator},
{"record.save_video_in_game_folder", &config.record_config.save_video_in_game_folder},
{"record.save_directory", &config.record_config.save_directory},
{"record.container", &config.record_config.container},

View File

@@ -1,15 +1,21 @@
#include "../include/LedIndicator.hpp"
#include <X11/XKBlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
namespace gsr {
LedIndicator::LedIndicator(Display *dpy) : dpy(dpy) {
scroll_lock_atom = XInternAtom(dpy, "Scroll Lock", False);
XkbSetNamedIndicator(dpy, scroll_lock_atom, True, False, False, NULL);
LedIndicator::LedIndicator() {
run_gsr_global_hotkeys_set_leds(false);
}
LedIndicator::~LedIndicator() {
XkbSetNamedIndicator(dpy, scroll_lock_atom, True, False, False, NULL);
run_gsr_global_hotkeys_set_leds(false);
if(gsr_global_hotkeys_pid > 0) {
int status;
waitpid(gsr_global_hotkeys_pid, &status, 0);
}
}
void LedIndicator::set_led(bool enabled) {
@@ -22,12 +28,48 @@ namespace gsr {
blink_timer.restart();
}
bool LedIndicator::run_gsr_global_hotkeys_set_leds(bool enabled) {
if(gsr_global_hotkeys_pid > 0) {
int status;
if(waitpid(gsr_global_hotkeys_pid, &status, WNOHANG) == 0) {
// Still running
return false;
}
gsr_global_hotkeys_pid = -1;
}
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
const char *user_homepath = getenv("HOME");
if(!user_homepath)
user_homepath = "/tmp";
gsr_global_hotkeys_pid = vfork();
if(gsr_global_hotkeys_pid == -1) {
fprintf(stderr, "Error: LedIndicator::run_gsr_global_hotkeys_set_leds: failed to fork\n");
return false;
} else if(gsr_global_hotkeys_pid == 0) { // Child
if(inside_flatpak) {
const char *args[] = { "flatpak-spawn", "--host", "/var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/bin/kms-server-proxy", "launch-gsr-global-hotkeys", user_homepath, "--set-led", "Scroll Lock", enabled ? "on" : "off", nullptr };
execvp(args[0], (char* const*)args);
} else {
const char *args[] = { "gsr-global-hotkeys", "--set-led", "Scroll Lock", enabled ? "on" : "off", nullptr };
execvp(args[0], (char* const*)args);
}
perror("gsr-global-hotkeys");
_exit(127);
return true;
} else { // Parent
return true;
}
}
void LedIndicator::update_led(bool new_state) {
if(new_state == led_indicator_on)
return;
if(run_gsr_global_hotkeys_set_leds(new_state))
led_indicator_on = new_state;
XkbSetNamedIndicator(dpy, scroll_lock_atom, True, new_state, False, NULL);
}
void LedIndicator::update() {

View File

@@ -522,8 +522,8 @@ namespace gsr {
}
}
if(x11_dpy)
led_indicator = std::make_unique<LedIndicator>(x11_dpy);
// TODO: Only do this if led indicator is enabled (at startup or when changing recording/screenshot settings to enabled it)
led_indicator = std::make_unique<LedIndicator>();
}
Overlay::~Overlay() {

View File

@@ -229,7 +229,7 @@ namespace gsr {
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_led_indicator() {
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, gsr_info->system_info.display_server == DisplayServer::X11 ? "Blink scroll lock led when taking a screenshot" : "Blink scroll lock led when taking a screenshot (not supported by Wayland)");
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Blink scroll lock led when taking a screenshot");
checkbox->set_checked(true);
led_indicator_checkbox_ptr = checkbox.get();
return checkbox;

View File

@@ -832,10 +832,7 @@ namespace gsr {
std::unique_ptr<CheckBox> SettingsPage::create_led_indicator(const char *type) {
char label_str[256];
if(gsr_info->system_info.display_server == DisplayServer::X11)
snprintf(label_str, sizeof(label_str), "Show %s status with scroll lock led", type);
else
snprintf(label_str, sizeof(label_str), "Show %s status with scroll lock led (not supported by Wayland)", type);
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, label_str);
checkbox->set_checked(false);

View File

@@ -237,6 +237,17 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
fprintf(stderr, "Error: failed to write event data to virtual keyboard for exclusively grabbed device\n");
}
if(event.type == EV_LED) {
write(fd, &event, sizeof(event));
const struct input_event syn_event = {
.type = EV_SYN,
.code = 0,
.value = 0
};
write(fd, &syn_event, sizeof(syn_event));
}
if(!extra_data->is_possibly_non_keyboard_device)
return;
@@ -389,7 +400,7 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
if(keyboard_event_has_event_with_dev_input_fd(self, dev_input_id))
return false;
const int fd = open(dev_input_filepath, O_RDONLY);
const int fd = open(dev_input_filepath, O_RDWR);
if(fd == -1)
return false;
@@ -553,10 +564,12 @@ static int setup_virtual_keyboard_input(const char *name) {
if(is_keyboard_key(i) || is_mouse_button(i))
success &= (ioctl(fd, UI_SET_KEYBIT, i) != -1);
}
for(int i = 0; i < REL_MAX; ++i) {
success &= (ioctl(fd, UI_SET_RELBIT, i) != -1);
}
// for(int i = 0; i < LED_MAX; ++i) {
// for(int i = 0; i <= LED_CHARGING; ++i) {
// success &= (ioctl(fd, UI_SET_LEDBIT, i) != -1);
// }
@@ -635,6 +648,22 @@ bool keyboard_event_init(keyboard_event *self, bool exclusive_grab, keyboard_gra
return false;
}
self->event_polls[self->num_event_polls] = (struct pollfd) {
.fd = self->timer_fd,
.events = POLLIN,
.revents = 0
};
self->event_extra_data[self->num_event_polls] = (event_extra_data) {
.dev_input_id = -1,
.grabbed = false,
.key_states = NULL,
.key_presses_grabbed = NULL,
.num_keys_pressed = 0
};
++self->num_event_polls;
/* 0.5 seconds */
const struct itimerspec timer_value = {
.it_value = (struct timespec) {
@@ -653,22 +682,6 @@ bool keyboard_event_init(keyboard_event *self, bool exclusive_grab, keyboard_gra
return false;
}
self->event_polls[self->num_event_polls] = (struct pollfd) {
.fd = self->timer_fd,
.events = POLLIN,
.revents = 0
};
self->event_extra_data[self->num_event_polls] = (event_extra_data) {
.dev_input_id = -1,
.grabbed = false,
.key_states = NULL,
.key_presses_grabbed = NULL,
.num_keys_pressed = 0
};
++self->num_event_polls;
self->uinput_written_time_seconds = clock_get_monotonic_seconds();
if(hotplug_event_init(&self->hotplug_ev)) {
@@ -717,11 +730,6 @@ void keyboard_event_deinit(keyboard_event *self) {
self->uinput_fd = -1;
}
if(self->timer_fd > 0) {
close(self->timer_fd);
self->timer_fd = -1;
}
for(int i = 0; i < self->num_event_polls; ++i) {
if(self->event_polls[i].fd > 0) {
ioctl(self->event_polls[i].fd, EVIOCGRAB, 0);

View File

@@ -0,0 +1,139 @@
#include "leds.h"
/* C stdlib */
#include <string.h>
#include <stdio.h>
#include <limits.h>
#include <stddef.h>
/* POSIX */
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
/* LINUX */
#include <linux/input.h>
static int get_max_brightness(const char *sys_class_path, const char *filename) {
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/%s/max_brightness", sys_class_path, filename);
const int fd = open(path, O_RDONLY);
if(fd == -1) {
fprintf(stderr, "Warning: get_max_brightness open error: %s\n", strerror(errno));
return false;
}
bool success = false;
int max_brightness = 0;
char buffer[32];
const ssize_t num_bytes_read = read(fd, buffer, sizeof(buffer));
if(num_bytes_read > 0) {
buffer[num_bytes_read] = '\0';
success = sscanf(buffer, "%d", &max_brightness) == 1;
}
close(fd);
return success;
}
static bool string_starts_with(const char *str, const char *sub) {
const int str_len = strlen(str);
const int sub_len = strlen(sub);
return str_len >= sub_len && memcmp(str, sub, sub_len) == 0;
}
static bool string_ends_with(const char *str, const char *sub) {
const int str_len = strlen(str);
const int sub_len = strlen(sub);
return str_len >= sub_len && memcmp(str + str_len - sub_len, sub, sub_len) == 0;
}
static int sys_class_led_path_get_event_number(const char *sys_class_path, const char *filename) {
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/%s/device", sys_class_path, filename);
DIR *dir = opendir(path);
if(!dir)
return -1;
int event_number = -1;
struct dirent *entry;
while((entry = readdir(dir)) != NULL) {
int v = -1;
if(sscanf(entry->d_name, "event%d", &v) == 1 && v >= 0) {
event_number = v;
break;
}
}
closedir(dir);
return event_number;
}
/*
We have to do this retardation instead of setting /sys/class/leds brightness since it doesn't work with /dev/uinput
and we cant loop all /dev/input devices and open and write to them either since closing a /dev/input is very slow on linux.
So we instead check which devices have the led before opening it.
*/
static bool set_device_leds(const char *led_name_path, bool enabled) {
DIR *dir = opendir("/sys/class/leds");
if(!dir)
return false;
char dev_input_filepath[1024];
struct dirent *entry;
while((entry = readdir(dir)) != NULL) {
if(entry->d_name[0] == '.')
continue;
if(!string_starts_with(entry->d_name, "input") || !string_ends_with(entry->d_name, led_name_path))
continue;
const int event_number = sys_class_led_path_get_event_number("/sys/class/leds", entry->d_name);
if(event_number == -1)
continue;
snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/event%d", event_number);
const int device_fd = open(dev_input_filepath, O_WRONLY);
if(device_fd == -1)
continue;
int brightness = 0;
if(enabled) {
brightness = get_max_brightness("/sys/class/leds", entry->d_name);
if(brightness < 0)
brightness = 1;
}
struct input_event led_data = {
.type = EV_LED,
.code = LED_SCROLLL,
.value = brightness
};
write(device_fd, &led_data, sizeof(led_data));
struct input_event syn_data = {
.type = EV_SYN,
.code = 0,
.value = 0
};
write(device_fd, &syn_data, sizeof(syn_data));
close(device_fd);
}
closedir(dir);
return true;
}
bool set_leds(const char *led_name, bool enabled) {
if(strcmp(led_name, "Scroll Lock") == 0) {
return set_device_leds("::scrolllock", enabled);
} else {
fprintf(stderr, "Error: invalid led: \"%s\", expected \"Scroll Lock\"\n", led_name);
return false;
}
}

View File

@@ -0,0 +1,9 @@
#ifndef LEDS_H
#define LEDS_H
/* C stdlib */
#include <stdbool.h>
bool set_leds(const char *led_name, bool enabled);
#endif /* LEDS_H */

View File

@@ -1,19 +1,26 @@
#include "keyboard_event.h"
#include "leds.h"
/* C stdlib */
#include <stdio.h>
#include <string.h>
#include <locale.h>
#include <stdbool.h>
/* POSIX */
#include <unistd.h>
static void usage(void) {
fprintf(stderr, "usage: gsr-global-hotkeys [--all|--virtual]\n");
fprintf(stderr, "usage: gsr-global-hotkeys [--all|--virtual|--no-grab|--set-led] [Scroll Lock] [on|off]\n");
fprintf(stderr, "OPTIONS:\n");
fprintf(stderr, " --all Grab all devices.\n");
fprintf(stderr, " --virtual Grab all virtual devices only.\n");
fprintf(stderr, " --no-grab Don't grab devices, only listen to them.\n");
fprintf(stderr, " --set-led Turn device led on/off.\n");
fprintf(stderr, "EXAMPLES:\n");
fprintf(stderr, " gsr-global-hotkeys --all\n");
fprintf(stderr, " gsr-global-hotkeys --set-led \"Scroll Lock\" on\n");
fprintf(stderr, " gsr-global-hotkeys --set-led \"Scroll Lock\" off\n");
}
static bool is_gsr_global_hotkeys_already_running(void) {
@@ -38,6 +45,8 @@ int main(int argc, char **argv) {
setlocale(LC_ALL, "C"); /* Sigh... stupid C */
keyboard_grab_type grab_type = KEYBOARD_GRAB_TYPE_ALL;
const uid_t user_id = getuid();
if(argc == 2) {
const char *grab_type_arg = argv[1];
if(strcmp(grab_type_arg, "--all") == 0) {
@@ -46,11 +55,42 @@ int main(int argc, char **argv) {
grab_type = KEYBOARD_GRAB_TYPE_VIRTUAL;
} else if(strcmp(grab_type_arg, "--no-grab") == 0) {
grab_type = KEYBOARD_GRAB_TYPE_NO_GRAB;
} else if(strcmp(grab_type_arg, "--set-led") == 0) {
fprintf(stderr, "Error: missing led name and on/off argument to --set-led\n");
usage();
return 1;
} else {
fprintf(stderr, "gsr-global-hotkeys error: expected --all, --virtual or --no-grab, got %s\n", grab_type_arg);
fprintf(stderr, "gsr-global-hotkeys error: expected --all, --virtual, --no-grab or --set-led, got %s\n", grab_type_arg);
usage();
return 1;
}
} else if(argc == 4) {
/* It's not ideal to use gsr-global-hotkeys for leds, but we do that for now because it's a mess to create another binary for flatpak and distros */
const char *led_name = argv[2];
const char *led_enabled_str = argv[3];
bool led_enabled = false;
if(strcmp(led_enabled_str, "on") == 0) {
led_enabled = true;
} else if(strcmp(led_enabled_str, "off") == 0) {
led_enabled = false;
} else {
fprintf(stderr, "Error: expected \"on\" or \"off\" for --set-led option, got: \"%s\"", led_enabled_str);
usage();
return 1;
}
if(geteuid() != 0) {
if(setuid(0) == -1) {
fprintf(stderr, "gsr-global-hotkeys error: failed to change user to root, global hotkeys will not work. Make sure to set the correct capability on gsr-global-hotkeys\n");
return 1;
}
}
const bool success = set_leds(led_name, led_enabled);
setuid(user_id);
return success ? 0 : 1;
} else if(argc != 1) {
fprintf(stderr, "gsr-global-hotkeys error: expected 0 or 1 arguments, got %d argument(s)\n", argc);
usage();
@@ -62,7 +102,6 @@ int main(int argc, char **argv) {
return 1;
}
const uid_t user_id = getuid();
if(geteuid() != 0) {
if(setuid(0) == -1) {
fprintf(stderr, "gsr-global-hotkeys error: failed to change user to root, global hotkeys will not work. Make sure to set the correct capability on gsr-global-hotkeys\n");