mirror of
https://repo.dec05eba.com/gpu-screen-recorder-ui
synced 2026-04-08 04:18:08 +09:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e938241fe8 | ||
|
|
a9637f87e7 | ||
|
|
eb4ce76f01 | ||
|
|
d4d61b8c93 | ||
|
|
35a2fcc615 | ||
|
|
bb54b67956 | ||
|
|
35578e79ec | ||
|
|
ce6c924f58 | ||
|
|
77dd32a3ff | ||
|
|
b0def958c7 | ||
|
|
698538ac84 | ||
|
|
affa44e387 | ||
|
|
df2eec24a3 | ||
|
|
9a5c20836a | ||
|
|
ee123ceb0a | ||
|
|
f2544020b3 | ||
|
|
0c6e2aff07 | ||
|
|
26ff639f25 | ||
|
|
a812d7dbbb | ||
|
|
57ae00063c | ||
|
|
fa5b7a0c75 | ||
|
|
52ce22ae22 | ||
|
|
f379b87b33 |
28
README.md
28
README.md
@@ -7,9 +7,10 @@ Note: This software is still in early alpha. Expect bugs, and please report any
|
||||
You can report an issue by emailing the issue to dec05eba@protonmail.com.
|
||||
|
||||
# Usage
|
||||
Run `gsr-ui` and press `Alt+Z` to show/hide the UI. You can start the overlay UI at system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`.
|
||||
Run `gsr-ui` and press `Left Alt+Z` to show/hide the UI. You can start the overlay UI at system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`.
|
||||
There is also an option in the settings to enable/disable starting the program on system startup. This option only works on systems that use systemd.
|
||||
You have to manually add `gsr-ui` to system startup on systems that uses another init system.
|
||||
You have to manually add `gsr-ui` to system startup on systems that uses another init system.\
|
||||
A program called `gsr-ui-cli` is also installed when installing this software. This can be used to remotely control the UI. Run `gsr-ui-cli --help` to list the available commands.
|
||||
|
||||
# Installation
|
||||
If you are using an Arch Linux based distro then you can find gpu screen recorder ui on aur under the name gpu-screen-recorder-ui (`yay -S gpu-screen-recorder-ui`).\
|
||||
@@ -34,10 +35,8 @@ There are also additional dependencies needed at runtime:
|
||||
* [GPU Screen Recorder Notification](https://git.dec05eba.com/gpu-screen-recorder-notification/)
|
||||
|
||||
## Program behavior notes
|
||||
At the moment different keyboard layouts are not supported. The physical layout of keys are used for global hotkeys. If your Z and Y keys are swapped for example then you need to press Alt+Y instead of Alt+Z to open/hide the UI.\
|
||||
If you experience this issue then please email dec05eba@protonmail.com to get it fixed.\
|
||||
This program has to grab all keyboards and create a virtual keyboard (`gsr-ui virtual keyboard`) to make global hotkeys work on all Wayland compositors.
|
||||
This might cause issues for you if you use input remapping software. To workaround this you can go into settings and disable hotkeys and use the included `gsr-ui-cli` tool to control the UI remotely. You can combine `gsr-ui-cli` commands to hotkeys in your desktop environments hotkey settings.
|
||||
This might cause issues for you if you use input remapping software. To workaround this you can go into settings and select "Only grab virtual devices"
|
||||
|
||||
# License
|
||||
This software is licensed under GPL3.0-only. Files under `fonts/` directory belong to the Noto Sans Google fonts project and they are licensed under `SIL Open Font License`.
|
||||
@@ -55,6 +54,19 @@ If you want to donate you can donate via bitcoin or monero.
|
||||
* Monero: 4An9kp2qW1C9Gah7ewv4JzcNFQ5TAX7ineGCqXWK6vQnhsGGcRpNgcn8r9EC3tMcgY7vqCKs3nSRXhejMHBaGvFdN2egYet
|
||||
|
||||
# Known issues
|
||||
* When the UI is open the wallpaper is shown instead of the game on Hyprland and Sway. This is an issue with Hyprland and Sway. It cant be fixed until the UI is redesigned to not be a fullscreen overlay.
|
||||
* Different keyboard layouts are not supported at the moment. The physical layout of keys are used for global hotkeys. If your Z and Y keys are swapped for example then you need to press Alt+Y instead of Alt+Z to open/hide the UI. If you experience this issue then please email dec05eba@protonmail.com to get it fixed.
|
||||
* The mouse position can sometimes shift when opening the UI.
|
||||
* When the UI is open the wallpaper is shown instead of the game on Hyprland. This is an issue with Hyprland. It cant be fixed until the UI is redesigned to not be a fullscreen overlay.
|
||||
* Opening the UI when a game is fullscreened can mess up the game window a bit on Hyprland. I believe this is an issue with Hyprland.
|
||||
|
||||
# FAQ
|
||||
## I get an error when trying to start the gpu-screen-recorder-ui.service systemd service
|
||||
If you have previously used the flatpak version of GPU Screen Recorder with the new UI then non-flatpak version of the systemd service will conflict with that.
|
||||
Run these commands to first remove the flatpak version of the systemd service:
|
||||
```
|
||||
systemctl stop --user gpu-screen-recorder-ui
|
||||
rm ~/.local/share/systemd/user/gpu-screen-recorder-ui.service
|
||||
systemctl --user daemon-reload
|
||||
```
|
||||
and then start and enable the non-flatpak systemd service:
|
||||
```
|
||||
systemctl enable --now --user gpu-screen-recorder-ui`
|
||||
```
|
||||
15
TODO
15
TODO
@@ -20,8 +20,6 @@ Make hotkeys configurable.
|
||||
|
||||
Filechooser should have the option to select list view, search bar and common folders/mounted drives on the left side for quick navigation. Also a button to create a new directory.
|
||||
|
||||
Support wayland (excluding gnome, or force xwayland on gnome).
|
||||
|
||||
Restart replay on system start if monitor resolution changes.
|
||||
|
||||
Show warning when selecting hevc/av1 on amd because of amd driver/ffmpeg bug.
|
||||
@@ -92,7 +90,7 @@ Move ui hover code from ::draw to ::on_event, to properly handle widget event st
|
||||
|
||||
Save audio devices by name instead of id. This is more robust since audio id can change(?).
|
||||
|
||||
Improve linux global hotkeys startup time by parsing /proc/bus/input/devices instead of ioctl.
|
||||
Improve linux global hotkeys startup time by parsing /proc/bus/input/devices instead of ioctl. <- Do this!
|
||||
|
||||
We can get the name of the running steam game without x11 by listing processes and finding the one that runs a program called "reaper" with the arguments SteamLaunch AppId=<number>. The binary comes after the -- argument, get the name of the game by parsing out name from that, in the format steamapps/common/<name>/.
|
||||
|
||||
@@ -105,4 +103,13 @@ Show warning if another instance of gpu screen recorder is already running when
|
||||
Keyboard leds get turned off when stopping gsr-global-hotkeys (for example numlock). The numlock key has to be pressed twice again to make it look correct to match its state.
|
||||
|
||||
Implement hotkey changing in global settings by getting mgl key events. During this time gsr-global-hotkey would either need to be paused or add code in the callback handler for the existing hotkeys since they are grabbing hotkeys.
|
||||
This can only be done after gsr-global-hotkeys properly handle different keyboard layouts to make sure mgl keys match gsr-global-hotkey keys.
|
||||
This can only be done after gsr-global-hotkeys properly handle different keyboard layouts to make sure mgl keys match gsr-global-hotkey keys.
|
||||
|
||||
Re-enable hotkey disable option for flatpak.
|
||||
|
||||
Make gsr-ui flatpak systemd work nicely with non-flatpak gsr-ui. Maybe change ExecStart to do flatpak run ... || gsr-ui, but make it run as a shell command first with /bin/sh -c "".
|
||||
|
||||
When enabling X11 global hotkey again only grab lalt, not ralt.
|
||||
|
||||
When adding window capture only add it to recording and streaming and do the window selection when recording starts, to make it more ergonomic with hotkeys.
|
||||
If hotkey for recording/streaming start is pressed on the button for start is clicked then hide the ui if it's visible and show the window selection option (cursor).
|
||||
Submodule depends/mglpp updated: 3c33f0f813...e776c85d19
@@ -43,7 +43,7 @@ namespace gsr {
|
||||
struct MainConfig {
|
||||
int32_t config_file_version = 0;
|
||||
bool software_encoding_warning_shown = false;
|
||||
bool enable_hotkeys = true;
|
||||
std::string hotkeys_enable_option = "enable_hotkeys";
|
||||
std::string tint_color;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
namespace gsr {
|
||||
class GlobalHotkeysLinux : public GlobalHotkeys {
|
||||
public:
|
||||
GlobalHotkeysLinux();
|
||||
enum class GrabType {
|
||||
ALL,
|
||||
VIRTUAL
|
||||
};
|
||||
|
||||
GlobalHotkeysLinux(GrabType grab_type);
|
||||
GlobalHotkeysLinux(const GlobalHotkeysLinux&) = delete;
|
||||
GlobalHotkeysLinux& operator=(const GlobalHotkeysLinux&) = delete;
|
||||
~GlobalHotkeysLinux() override;
|
||||
@@ -20,5 +25,6 @@ namespace gsr {
|
||||
int pipes[2];
|
||||
FILE *read_file = nullptr;
|
||||
std::unordered_map<std::string, GlobalHotkeyCallback> bound_actions_by_id;
|
||||
GrabType grab_type;
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "GsrInfo.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "window_texture.h"
|
||||
#include "WindowUtils.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
#include <mglpp/window/Event.hpp>
|
||||
@@ -97,7 +98,7 @@ namespace gsr {
|
||||
void on_press_start_replay(bool disable_notification);
|
||||
void on_press_start_record();
|
||||
void on_press_start_stream();
|
||||
bool update_compositor_texture(const mgl_monitor *monitor);
|
||||
bool update_compositor_texture(const Monitor &monitor);
|
||||
|
||||
void force_window_on_top();
|
||||
private:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <mglpp/system/vec.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
namespace gsr {
|
||||
@@ -9,6 +11,16 @@ namespace gsr {
|
||||
CURSOR
|
||||
};
|
||||
|
||||
struct Monitor {
|
||||
mgl::vec2i position;
|
||||
mgl::vec2i size;
|
||||
};
|
||||
|
||||
Window get_focused_window(Display *dpy, WindowCaptureType cap_type);
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type);
|
||||
mgl::vec2i get_cursor_position(Display *dpy, Window *window);
|
||||
mgl::vec2i create_window_get_center_position(Display *display);
|
||||
std::string get_window_manager_name(Display *display);
|
||||
bool is_compositor_running(Display *dpy, int screen);
|
||||
std::vector<Monitor> get_monitors(Display *dpy);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.0.4', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.0.7', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
|
||||
|
||||
if get_option('buildtype') == 'debug'
|
||||
add_project_arguments('-g3', language : ['c', 'cpp'])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "gsr-ui"
|
||||
type = "executable"
|
||||
version = "1.0.4"
|
||||
version = "1.0.7"
|
||||
platforms = ["posix"]
|
||||
|
||||
[lang.cpp]
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace gsr {
|
||||
return {
|
||||
{"main.config_file_version", &config.main_config.config_file_version},
|
||||
{"main.software_encoding_warning_shown", &config.main_config.software_encoding_warning_shown},
|
||||
{"main.enable_hotkeys", &config.main_config.enable_hotkeys},
|
||||
{"main.hotkeys_enable_option", &config.main_config.hotkeys_enable_option},
|
||||
{"main.tint_color", &config.main_config.tint_color},
|
||||
|
||||
{"streaming.record_options.record_area_option", &config.streaming_config.record_options.record_area_option},
|
||||
|
||||
@@ -9,7 +9,15 @@
|
||||
#define PIPE_WRITE 1
|
||||
|
||||
namespace gsr {
|
||||
GlobalHotkeysLinux::GlobalHotkeysLinux() {
|
||||
static const char* grab_type_to_arg(GlobalHotkeysLinux::GrabType grab_type) {
|
||||
switch(grab_type) {
|
||||
case GlobalHotkeysLinux::GrabType::ALL: return "--all";
|
||||
case GlobalHotkeysLinux::GrabType::VIRTUAL: return "--virtual";
|
||||
}
|
||||
return "--all";
|
||||
}
|
||||
|
||||
GlobalHotkeysLinux::GlobalHotkeysLinux(GrabType grab_type) : grab_type(grab_type) {
|
||||
for(int i = 0; i < 2; ++i) {
|
||||
pipes[i] = -1;
|
||||
}
|
||||
@@ -32,6 +40,7 @@ namespace gsr {
|
||||
}
|
||||
|
||||
bool GlobalHotkeysLinux::start() {
|
||||
const char *grab_type_arg = grab_type_to_arg(grab_type);
|
||||
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
|
||||
const char *user_homepath = getenv("HOME");
|
||||
if(!user_homepath)
|
||||
@@ -61,10 +70,10 @@ namespace gsr {
|
||||
}
|
||||
|
||||
if(inside_flatpak) {
|
||||
const char *args[] = { "flatpak-spawn", "--host", "--", gsr_global_hotkeys_flatpak, NULL };
|
||||
const char *args[] = { "flatpak-spawn", "--host", "--", gsr_global_hotkeys_flatpak, grab_type_arg, nullptr };
|
||||
execvp(args[0], (char* const*)args);
|
||||
} else {
|
||||
const char *args[] = { "gsr-global-hotkeys", NULL };
|
||||
const char *args[] = { "gsr-global-hotkeys", grab_type_arg, nullptr };
|
||||
execvp(args[0], (char* const*)args);
|
||||
}
|
||||
|
||||
|
||||
211
src/Overlay.cpp
211
src/Overlay.cpp
@@ -42,19 +42,6 @@ namespace gsr {
|
||||
static const double force_window_on_top_timeout_seconds = 1.0;
|
||||
static const double replay_status_update_check_timeout_seconds = 1.0;
|
||||
|
||||
static bool is_focused_application_wayland(Display *dpy) {
|
||||
return get_focused_window(dpy, WindowCaptureType::FOCUSED) == 0;
|
||||
}
|
||||
|
||||
static bool is_cursor_hovering_application_wayland(Display *dpy) {
|
||||
Window root_window = None;
|
||||
Window window = None;
|
||||
int dummy_i;
|
||||
unsigned int dummy_u;
|
||||
XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &dummy_i, &dummy_i, &dummy_i, &dummy_i, &dummy_u);
|
||||
return window == None;
|
||||
}
|
||||
|
||||
static mgl::Texture texture_from_ximage(XImage *img) {
|
||||
uint8_t *texture_data = (uint8_t*)malloc(img->width * img->height * 3);
|
||||
// TODO:
|
||||
@@ -177,7 +164,7 @@ namespace gsr {
|
||||
return std::abs(a - b) <= difference;
|
||||
}
|
||||
|
||||
static bool is_window_fullscreen_on_monitor(Display *display, Window window, const mgl_monitor *monitor) {
|
||||
static bool is_window_fullscreen_on_monitor(Display *display, Window window, const Monitor &monitor) {
|
||||
if(!window)
|
||||
return false;
|
||||
|
||||
@@ -186,8 +173,8 @@ namespace gsr {
|
||||
return false;
|
||||
|
||||
const int margin = 2;
|
||||
return diff_int(geometry.x, monitor->pos.x, margin) && diff_int(geometry.y, monitor->pos.y, margin)
|
||||
&& diff_int(geometry.width, monitor->size.x, margin) && diff_int(geometry.height, monitor->size.y, margin);
|
||||
return diff_int(geometry.x, monitor.position.x, margin) && diff_int(geometry.y, monitor.position.y, margin)
|
||||
&& diff_int(geometry.width, monitor.size.x, margin) && diff_int(geometry.height, monitor.size.y, margin);
|
||||
}
|
||||
|
||||
/*static bool is_window_fullscreen_on_monitor(Display *display, Window window, const mgl_monitor *monitors, int num_monitors) {
|
||||
@@ -292,100 +279,13 @@ namespace gsr {
|
||||
}
|
||||
|
||||
// Returns the first monitor if not found. Assumes there is at least one monitor connected.
|
||||
static const mgl_monitor* find_monitor_at_position(mgl::Window &window, mgl::vec2i pos) {
|
||||
const mgl_window *win = window.internal_window();
|
||||
assert(win->num_monitors > 0);
|
||||
for(int i = 0; i < win->num_monitors; ++i) {
|
||||
const mgl_monitor *mon = &win->monitors[i];
|
||||
if(mgl::IntRect({ mon->pos.x, mon->pos.y }, { mon->size.x, mon->size.y }).contains(pos))
|
||||
return mon;
|
||||
static const Monitor* find_monitor_at_position(const std::vector<Monitor> &monitors, mgl::vec2i pos) {
|
||||
assert(!monitors.empty());
|
||||
for(const Monitor &monitor : monitors) {
|
||||
if(mgl::IntRect(monitor.position, monitor.size).contains(pos))
|
||||
return &monitor;
|
||||
}
|
||||
return &win->monitors[0];
|
||||
}
|
||||
|
||||
static mgl::vec2i get_cursor_position(Display *dpy) {
|
||||
Window root_window = None;
|
||||
Window window = None;
|
||||
int dummy_i;
|
||||
unsigned int dummy_u;
|
||||
mgl::vec2i root_pos;
|
||||
XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u);
|
||||
return root_pos;
|
||||
}
|
||||
|
||||
static mgl::vec2i create_window_get_center_position(Display *display) {
|
||||
const int size = 16;
|
||||
XSetWindowAttributes window_attr;
|
||||
window_attr.event_mask = StructureNotifyMask;
|
||||
window_attr.background_pixel = 0;
|
||||
const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, size, size, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr);
|
||||
if(!window)
|
||||
return {0, 0};
|
||||
|
||||
const Atom net_wm_window_type_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
|
||||
const Atom net_wm_window_type_notification_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE_NOTIFICATION", False);
|
||||
const Atom net_wm_window_type_utility = XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False);
|
||||
const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False);
|
||||
|
||||
const Atom window_type_atoms[2] = {
|
||||
net_wm_window_type_notification_atom,
|
||||
net_wm_window_type_utility
|
||||
};
|
||||
XChangeProperty(display, window, net_wm_window_type_atom, XA_ATOM, 32, PropModeReplace, (const unsigned char*)window_type_atoms, 2L);
|
||||
|
||||
const double alpha = 0.0;
|
||||
const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha);
|
||||
XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L);
|
||||
|
||||
XSizeHints *size_hints = XAllocSizeHints();
|
||||
size_hints->width = size;
|
||||
size_hints->min_width = size;
|
||||
size_hints->max_width = size;
|
||||
size_hints->height = size;
|
||||
size_hints->min_height = size;
|
||||
size_hints->max_height = size;
|
||||
size_hints->flags = PSize | PMinSize | PMaxSize;
|
||||
XSetWMNormalHints(display, window, size_hints);
|
||||
XFree(size_hints);
|
||||
|
||||
XMapWindow(display, window);
|
||||
XFlush(display);
|
||||
|
||||
const int x_fd = XConnectionNumber(display);
|
||||
mgl::vec2i window_pos;
|
||||
XEvent xev;
|
||||
while(true) {
|
||||
struct pollfd poll_fd;
|
||||
poll_fd.fd = x_fd;
|
||||
poll_fd.events = POLLIN;
|
||||
poll_fd.revents = 0;
|
||||
const int fds_ready = poll(&poll_fd, 1, 1000);
|
||||
if(fds_ready == 0) {
|
||||
fprintf(stderr, "Error: timed out waiting for ConfigureNotify after XCreateWindow\n");
|
||||
break;
|
||||
} else if(fds_ready == -1 || !(poll_fd.revents & POLLIN)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
XNextEvent(display, &xev);
|
||||
if(xev.type == ConfigureNotify) {
|
||||
window_pos.x = xev.xconfigure.x + xev.xconfigure.width / 2;
|
||||
window_pos.y = xev.xconfigure.y + xev.xconfigure.height / 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
XDestroyWindow(display, window);
|
||||
XFlush(display);
|
||||
|
||||
return window_pos;
|
||||
}
|
||||
|
||||
static bool is_compositor_running(Display *dpy, int screen) {
|
||||
char prop_name[20];
|
||||
snprintf(prop_name, sizeof(prop_name), "_NET_WM_CM_S%d", screen);
|
||||
Atom prop_atom = XInternAtom(dpy, prop_name, False);
|
||||
return XGetSelectionOwner(dpy, prop_atom) != None;
|
||||
return &monitors.front();
|
||||
}
|
||||
|
||||
static std::string get_power_supply_online_filepath() {
|
||||
@@ -810,25 +710,55 @@ namespace gsr {
|
||||
mgl_context *context = mgl_get_context();
|
||||
Display *display = (Display*)context->connection;
|
||||
|
||||
const std::vector<Monitor> monitors = get_monitors(display);
|
||||
if(monitors.empty()) {
|
||||
fprintf(stderr, "gsr warning: no monitors found, not showing overlay\n");
|
||||
window.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string wm_name = get_window_manager_name(display);
|
||||
const bool is_kwin = wm_name == "KWin";
|
||||
const bool is_wlroots = wm_name.find("wlroots") != std::string::npos;
|
||||
|
||||
// The cursor position is wrong on wayland if an x11 window is not focused. On wayland we instead create a window and get the position where the wayland compositor puts it
|
||||
Window x11_cursor_window = None;
|
||||
const mgl::vec2i cursor_position = get_cursor_position(display, &x11_cursor_window);
|
||||
const mgl::vec2i monitor_position_query_value = (x11_cursor_window || gsr_info.system_info.display_server != DisplayServer::WAYLAND) ? cursor_position : create_window_get_center_position(display);
|
||||
|
||||
const Monitor *focused_monitor = find_monitor_at_position(monitors, monitor_position_query_value);
|
||||
if(is_wlroots) {
|
||||
window_pos = focused_monitor->position;
|
||||
window_size = focused_monitor->size;
|
||||
} else {
|
||||
window_pos = {0, 0};
|
||||
window_size = {32, 32};
|
||||
}
|
||||
|
||||
// Wayland doesn't allow XGrabPointer/XGrabKeyboard when a wayland application is focused.
|
||||
// If the focused window is a wayland application then don't use override redirect and instead create
|
||||
// a fullscreen window for the ui.
|
||||
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || !is_focused_application_wayland(display);
|
||||
|
||||
window_size = { 1280, 720 };
|
||||
window_pos = { 0, 0 };
|
||||
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window;
|
||||
|
||||
mgl::Window::CreateParams window_create_params;
|
||||
window_create_params.size = window_size;
|
||||
window_create_params.min_size = window_size;
|
||||
window_create_params.max_size = window_size;
|
||||
if(is_wlroots || prevent_game_minimizing) {
|
||||
window_create_params.min_size = window_size;
|
||||
window_create_params.max_size = window_size;
|
||||
}
|
||||
window_create_params.position = window_pos;
|
||||
window_create_params.hidden = true;
|
||||
window_create_params.hidden = prevent_game_minimizing;
|
||||
window_create_params.override_redirect = prevent_game_minimizing;
|
||||
window_create_params.background_color = bg_color;
|
||||
window_create_params.support_alpha = true;
|
||||
window_create_params.window_type = MGL_WINDOW_TYPE_NORMAL;
|
||||
window_create_params.render_api = MGL_RENDER_API_EGL;
|
||||
window_create_params.hide_decorations = true;
|
||||
// MGL_WINDOW_TYPE_DIALOG is needed for kde plasma wayland in some cases, otherwise the window will pop up on another activity
|
||||
// or may not be visible at all
|
||||
window_create_params.window_type = (is_kwin && gsr_info.system_info.display_server == DisplayServer::WAYLAND) ? MGL_WINDOW_TYPE_DIALOG : MGL_WINDOW_TYPE_NORMAL;
|
||||
// Nvidia + Wayland + Egl doesn't work on some systems properly and it instead falls back to software rendering.
|
||||
// Use Glx on Wayland to workaround this issue. This is fine since Egl is only needed for x11 to reliably get the texture of the fullscreen window on Nvidia
|
||||
// when a compositor isn't running.
|
||||
window_create_params.render_api = gsr_info.system_info.display_server == DisplayServer::WAYLAND ? MGL_RENDER_API_GLX : MGL_RENDER_API_EGL;
|
||||
|
||||
if(!window->create("gsr ui", window_create_params))
|
||||
fprintf(stderr, "error: failed to create window\n");
|
||||
@@ -839,36 +769,26 @@ namespace gsr {
|
||||
data = 1;
|
||||
XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "GAMESCOPE_EXTERNAL_OVERLAY", False), XA_CARDINAL, 32, PropModeReplace, &data, 1);
|
||||
|
||||
window_pos = focused_monitor->position;
|
||||
window_size = focused_monitor->size;
|
||||
if(!init_theme(resources_path)) {
|
||||
fprintf(stderr, "Error: failed to load theme\n");
|
||||
::exit(1);
|
||||
window.reset();
|
||||
return;
|
||||
}
|
||||
get_theme().set_window_size(window_size);
|
||||
|
||||
if(is_wlroots || prevent_game_minimizing) {
|
||||
window->set_size(window_size);
|
||||
window->set_size_limits(window_size, window_size);
|
||||
window->set_position(window_pos);
|
||||
}
|
||||
|
||||
mgl_window *win = window->internal_window();
|
||||
if(win->num_monitors == 0) {
|
||||
fprintf(stderr, "gsr warning: no monitors found, not showing overlay\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// The cursor position is wrong on wayland if an x11 window is not focused. On wayland we instead create a window and get the position where the wayland compositor puts it
|
||||
const mgl::vec2i cursor_position = get_cursor_position(display);
|
||||
const mgl::vec2i monitor_position_query_value = gsr_info.system_info.display_server == DisplayServer::WAYLAND ? create_window_get_center_position(display) : cursor_position;
|
||||
const mgl_monitor *focused_monitor = find_monitor_at_position(*window, monitor_position_query_value);
|
||||
window_pos = {focused_monitor->pos.x, focused_monitor->pos.y};
|
||||
window_size = {focused_monitor->size.x, focused_monitor->size.y};
|
||||
get_theme().set_window_size(window_size);
|
||||
|
||||
window->set_size(window_size);
|
||||
window->set_size_limits(window_size, window_size);
|
||||
window->set_position(window_pos);
|
||||
|
||||
win->cursor_position.x = cursor_position.x - window_pos.x;
|
||||
win->cursor_position.y = cursor_position.y - window_pos.y;
|
||||
|
||||
update_compositor_texture(focused_monitor);
|
||||
|
||||
top_bar_text = mgl::Text("GPU Screen Recorder", get_theme().top_bar_font);
|
||||
logo_sprite = mgl::Sprite(&get_theme().logo_texture);
|
||||
update_compositor_texture(*focused_monitor);
|
||||
|
||||
bg_screenshot_overlay = mgl::Rectangle(mgl::vec2f(get_theme().window_width, get_theme().window_height));
|
||||
top_bar_background = mgl::Rectangle(mgl::vec2f(get_theme().window_width, get_theme().window_height*0.06f).floor());
|
||||
@@ -1043,7 +963,7 @@ namespace gsr {
|
||||
|
||||
// The focused application can be an xwayland application but the cursor can hover over a wayland application.
|
||||
// This is even the case when hovering over the titlebar of the xwayland application.
|
||||
if(!is_cursor_hovering_application_wayland(display))
|
||||
if(prevent_game_minimizing)
|
||||
xi_setup();
|
||||
|
||||
//window->set_fullscreen(true);
|
||||
@@ -1076,7 +996,8 @@ namespace gsr {
|
||||
// XFlush(display);
|
||||
// }
|
||||
|
||||
window->set_fullscreen(true);
|
||||
if(!is_wlroots)
|
||||
window->set_fullscreen(true);
|
||||
|
||||
visible = true;
|
||||
|
||||
@@ -2039,7 +1960,7 @@ namespace gsr {
|
||||
show_notification("Streaming has started", 3.0, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM);
|
||||
}
|
||||
|
||||
bool Overlay::update_compositor_texture(const mgl_monitor *monitor) {
|
||||
bool Overlay::update_compositor_texture(const Monitor &monitor) {
|
||||
window_texture_deinit(&window_texture);
|
||||
window_texture_sprite.set_texture(nullptr);
|
||||
screenshot_texture.clear();
|
||||
@@ -2062,7 +1983,7 @@ namespace gsr {
|
||||
window_texture_texture = mgl::Texture(window_texture.texture_id, MGL_TEXTURE_FORMAT_RGB);
|
||||
window_texture_sprite.set_texture(&window_texture_texture);
|
||||
} else {
|
||||
XImage *img = XGetImage(display, DefaultRootWindow(display), monitor->pos.x, monitor->pos.y, monitor->size.x, monitor->size.y, AllPlanes, ZPixmap);
|
||||
XImage *img = XGetImage(display, DefaultRootWindow(display), monitor.position.x, monitor.position.y, monitor.size.x, monitor.size.y, AllPlanes, ZPixmap);
|
||||
if(!img)
|
||||
fprintf(stderr, "Error: failed to take a screenshot\n");
|
||||
|
||||
@@ -2088,4 +2009,4 @@ namespace gsr {
|
||||
XFlush(display);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/fcntl.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
namespace gsr {
|
||||
static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) {
|
||||
|
||||
@@ -4,11 +4,43 @@
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xutil.h>
|
||||
|
||||
#include <mglpp/system/Utf8.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <mgl/window/window.h>
|
||||
}
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <poll.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#define MAX_PROPERTY_VALUE_LEN 4096
|
||||
|
||||
namespace gsr {
|
||||
static unsigned char* window_get_property(Display *dpy, Window window, Atom property_type, const char *property_name, unsigned int *property_size) {
|
||||
Atom ret_property_type = None;
|
||||
int ret_format = 0;
|
||||
unsigned long num_items = 0;
|
||||
unsigned long num_remaining_bytes = 0;
|
||||
unsigned char *data = nullptr;
|
||||
const Atom atom = XInternAtom(dpy, property_name, False);
|
||||
if(XGetWindowProperty(dpy, window, atom, 0, MAX_PROPERTY_VALUE_LEN / 4, False, property_type, &ret_property_type, &ret_format, &num_items, &num_remaining_bytes, &data) != Success || !data) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(ret_property_type != property_type) {
|
||||
XFree(data);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
*property_size = (ret_format / (32 / sizeof(long))) * num_items;
|
||||
return data;
|
||||
}
|
||||
|
||||
static bool window_has_atom(Display *dpy, Window window, Atom atom) {
|
||||
Atom type;
|
||||
unsigned long len, bytes_left;
|
||||
@@ -111,7 +143,26 @@ namespace gsr {
|
||||
return None;
|
||||
}
|
||||
|
||||
static char* get_window_title(Display *dpy, Window window) {
|
||||
static std::string utf8_sanitize(const uint8_t *str, int size) {
|
||||
const uint32_t zero_width_space_codepoint = 0x200b; // Some games such as the finals has zero-width space characters
|
||||
std::string result;
|
||||
for(int i = 0; i < size;) {
|
||||
// Some games such as the finals has utf8-bom between each character, wtf?
|
||||
if(i + 3 < size && memcmp(str + i, "\xEF\xBB\xBF", 3) == 0) {
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t codepoint = 0;
|
||||
size_t codepoint_length = 1;
|
||||
if(mgl::utf8_decode(str + i, size - i, &codepoint, &codepoint_length) && codepoint != zero_width_space_codepoint)
|
||||
result.append((const char*)str + i, codepoint_length);
|
||||
i += codepoint_length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::optional<std::string> get_window_title(Display *dpy, Window window) {
|
||||
const Atom net_wm_name_atom = XInternAtom(dpy, "_NET_WM_NAME", False);
|
||||
const Atom wm_name_atom = XInternAtom(dpy, "WM_NAME", False);
|
||||
const Atom utf8_string_atom = XInternAtom(dpy, "UTF8_STRING", False);
|
||||
@@ -124,7 +175,7 @@ namespace gsr {
|
||||
XGetWindowProperty(dpy, window, net_wm_name_atom, 0, 1024, False, utf8_string_atom, &type, &format, &num_items, &bytes_left, &data);
|
||||
|
||||
if(type == utf8_string_atom && format == 8 && data)
|
||||
return (char*)data;
|
||||
return utf8_sanitize(data, num_items);
|
||||
|
||||
type = None;
|
||||
format = 0;
|
||||
@@ -134,16 +185,18 @@ namespace gsr {
|
||||
XGetWindowProperty(dpy, window, wm_name_atom, 0, 1024, False, 0, &type, &format, &num_items, &bytes_left, &data);
|
||||
|
||||
if((type == XA_STRING || type == utf8_string_atom) && data)
|
||||
return (char*)data;
|
||||
return utf8_sanitize(data, num_items);
|
||||
|
||||
return NULL;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static const char* strip(const char *str, int *len) {
|
||||
int str_len = strlen(str);
|
||||
static std::string strip(const std::string &str) {
|
||||
int start_index = 0;
|
||||
int str_len = str.size();
|
||||
|
||||
for(int i = 0; i < str_len; ++i) {
|
||||
if(str[i] != ' ') {
|
||||
str += i;
|
||||
start_index += i;
|
||||
str_len -= i;
|
||||
break;
|
||||
}
|
||||
@@ -156,14 +209,7 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
*len = str_len;
|
||||
return str;
|
||||
}
|
||||
|
||||
static std::string string_string(const char *str) {
|
||||
int len = 0;
|
||||
str = strip(str, &len);
|
||||
return std::string(str, len);
|
||||
return str.substr(start_index, str_len);
|
||||
}
|
||||
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) {
|
||||
@@ -173,20 +219,273 @@ namespace gsr {
|
||||
return result;
|
||||
|
||||
// Window title is not always ideal (for example for a browser), but for games its pretty much required
|
||||
char *window_title = get_window_title(dpy, focused_window);
|
||||
const std::optional<std::string> window_title = get_window_title(dpy, focused_window);
|
||||
if(window_title) {
|
||||
result = string_string(window_title);
|
||||
XFree(window_title);
|
||||
result = strip(window_title.value());
|
||||
return result;
|
||||
}
|
||||
|
||||
XClassHint class_hint = {nullptr, nullptr};
|
||||
XGetClassHint(dpy, focused_window, &class_hint);
|
||||
if(class_hint.res_class) {
|
||||
result = string_string(class_hint.res_class);
|
||||
result = strip(class_hint.res_class);
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
mgl::vec2i get_cursor_position(Display *dpy, Window *window) {
|
||||
Window root_window = None;
|
||||
*window = None;
|
||||
int dummy_i;
|
||||
unsigned int dummy_u;
|
||||
mgl::vec2i root_pos;
|
||||
XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u);
|
||||
|
||||
// This dumb shit is done to satisfy gnome wayland. Only set |window| if a valid x11 window is focused
|
||||
if(window) {
|
||||
XWindowAttributes attr;
|
||||
if(XGetWindowAttributes(dpy, *window, &attr) && attr.override_redirect)
|
||||
*window = None;
|
||||
|
||||
int revert_to = 0;
|
||||
Window input_focus_window = None;
|
||||
if(XGetInputFocus(dpy, &input_focus_window, &revert_to)) {
|
||||
if(input_focus_window) {
|
||||
if(XGetWindowAttributes(dpy, input_focus_window, &attr) && attr.override_redirect)
|
||||
*window = None;
|
||||
} else {
|
||||
*window = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root_pos;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
unsigned long flags;
|
||||
unsigned long functions;
|
||||
unsigned long decorations;
|
||||
long input_mode;
|
||||
unsigned long status;
|
||||
} MotifHints;
|
||||
|
||||
#define MWM_HINTS_DECORATIONS 2
|
||||
|
||||
#define MWM_DECOR_NONE 0
|
||||
#define MWM_DECOR_ALL 1
|
||||
|
||||
static void window_set_decorations_visible(Display *display, Window window, bool visible) {
|
||||
const Atom motif_wm_hints_atom = XInternAtom(display, "_MOTIF_WM_HINTS", False);
|
||||
MotifHints motif_hints;
|
||||
memset(&motif_hints, 0, sizeof(motif_hints));
|
||||
motif_hints.flags = MWM_HINTS_DECORATIONS;
|
||||
motif_hints.decorations = visible ? MWM_DECOR_ALL : MWM_DECOR_NONE;
|
||||
XChangeProperty(display, window, motif_wm_hints_atom, motif_wm_hints_atom, 32, PropModeReplace, (unsigned char*)&motif_hints, sizeof(motif_hints) / sizeof(long));
|
||||
}
|
||||
|
||||
static bool create_window_get_center_position_kde(Display *display, mgl::vec2i &position) {
|
||||
const int size = 1;
|
||||
XSetWindowAttributes window_attr;
|
||||
window_attr.event_mask = StructureNotifyMask;
|
||||
window_attr.background_pixel = 0;
|
||||
const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, size, size, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr);
|
||||
if(!window)
|
||||
return false;
|
||||
|
||||
const Atom net_wm_window_type_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
|
||||
const Atom net_wm_window_type_notification_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE_NOTIFICATION", False);
|
||||
const Atom net_wm_window_type_utility = XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False);
|
||||
const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False);
|
||||
|
||||
const Atom window_type_atoms[2] = {
|
||||
net_wm_window_type_notification_atom,
|
||||
net_wm_window_type_utility
|
||||
};
|
||||
XChangeProperty(display, window, net_wm_window_type_atom, XA_ATOM, 32, PropModeReplace, (const unsigned char*)window_type_atoms, 2L);
|
||||
|
||||
const double alpha = 0.0;
|
||||
const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha);
|
||||
XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L);
|
||||
|
||||
window_set_decorations_visible(display, window, false);
|
||||
|
||||
XSizeHints *size_hints = XAllocSizeHints();
|
||||
size_hints->width = size;
|
||||
size_hints->height = size;
|
||||
size_hints->min_width = size;
|
||||
size_hints->min_height = size;
|
||||
size_hints->max_width = size;
|
||||
size_hints->max_height = size;
|
||||
size_hints->flags = PSize | PMinSize | PMaxSize;
|
||||
XSetWMNormalHints(display, window, size_hints);
|
||||
XFree(size_hints);
|
||||
|
||||
XMapWindow(display, window);
|
||||
XFlush(display);
|
||||
|
||||
bool got_data = false;
|
||||
const int x_fd = XConnectionNumber(display);
|
||||
XEvent xev;
|
||||
while(true) {
|
||||
struct pollfd poll_fd;
|
||||
poll_fd.fd = x_fd;
|
||||
poll_fd.events = POLLIN;
|
||||
poll_fd.revents = 0;
|
||||
const int fds_ready = poll(&poll_fd, 1, 1000);
|
||||
if(fds_ready == 0) {
|
||||
fprintf(stderr, "Error: timed out waiting for ConfigureNotify after XCreateWindow\n");
|
||||
break;
|
||||
} else if(fds_ready == -1 || !(poll_fd.revents & POLLIN)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
XNextEvent(display, &xev);
|
||||
if(xev.type == ConfigureNotify && xev.xconfigure.window == window) {
|
||||
got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0;
|
||||
position.x = xev.xconfigure.x + xev.xconfigure.width / 2;
|
||||
position.y = xev.xconfigure.y + xev.xconfigure.height / 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
XDestroyWindow(display, window);
|
||||
XFlush(display);
|
||||
|
||||
return got_data;
|
||||
}
|
||||
|
||||
static bool create_window_get_center_position_gnome(Display *display, mgl::vec2i &position) {
|
||||
const int size = 32;
|
||||
XSetWindowAttributes window_attr;
|
||||
window_attr.event_mask = StructureNotifyMask | ExposureMask;
|
||||
window_attr.background_pixel = 0;
|
||||
const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, size, size, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr);
|
||||
if(!window)
|
||||
return false;
|
||||
|
||||
const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False);
|
||||
const double alpha = 0.0;
|
||||
const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha);
|
||||
XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L);
|
||||
|
||||
window_set_decorations_visible(display, window, false);
|
||||
|
||||
XSizeHints *size_hints = XAllocSizeHints();
|
||||
size_hints->width = size;
|
||||
size_hints->height = size;
|
||||
size_hints->min_width = size;
|
||||
size_hints->min_height = size;
|
||||
size_hints->max_width = size;
|
||||
size_hints->max_height = size;
|
||||
size_hints->flags = PSize | PMinSize | PMaxSize;
|
||||
XSetWMNormalHints(display, window, size_hints);
|
||||
XFree(size_hints);
|
||||
|
||||
XMapWindow(display, window);
|
||||
XFlush(display);
|
||||
|
||||
bool got_data = false;
|
||||
const int x_fd = XConnectionNumber(display);
|
||||
XEvent xev;
|
||||
while(true) {
|
||||
struct pollfd poll_fd;
|
||||
poll_fd.fd = x_fd;
|
||||
poll_fd.events = POLLIN;
|
||||
poll_fd.revents = 0;
|
||||
const int fds_ready = poll(&poll_fd, 1, 1000);
|
||||
if(fds_ready == 0) {
|
||||
fprintf(stderr, "Error: timed out waiting for MapNotify/ConfigureNotify after XCreateWindow\n");
|
||||
break;
|
||||
} else if(fds_ready == -1 || !(poll_fd.revents & POLLIN)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
XNextEvent(display, &xev);
|
||||
if(xev.type == MapNotify && xev.xmap.window == window) {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
Window w = None;
|
||||
XTranslateCoordinates(display, window, DefaultRootWindow(display), 0, 0, &x, &y, &w);
|
||||
|
||||
got_data = x > 0 && y > 0;
|
||||
position.x = x + size / 2;
|
||||
position.y = y + size / 2;
|
||||
if(got_data)
|
||||
break;
|
||||
} else if(xev.type == ConfigureNotify && xev.xconfigure.window == window) {
|
||||
got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0;
|
||||
position.x = xev.xconfigure.x + xev.xconfigure.width / 2;
|
||||
position.y = xev.xconfigure.y + xev.xconfigure.height / 2;
|
||||
if(got_data)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
XDestroyWindow(display, window);
|
||||
XFlush(display);
|
||||
|
||||
return got_data;
|
||||
}
|
||||
|
||||
mgl::vec2i create_window_get_center_position(Display *display) {
|
||||
mgl::vec2i pos;
|
||||
if(!create_window_get_center_position_kde(display, pos)) {
|
||||
pos.x = 0;
|
||||
pos.y = 0;
|
||||
create_window_get_center_position_gnome(display, pos);
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
std::string get_window_manager_name(Display *display) {
|
||||
std::string wm_name;
|
||||
unsigned int property_size = 0;
|
||||
Window window = None;
|
||||
|
||||
unsigned char *net_supporting_wm_check = window_get_property(display, DefaultRootWindow(display), XA_WINDOW, "_NET_SUPPORTING_WM_CHECK", &property_size);
|
||||
if(net_supporting_wm_check) {
|
||||
if(property_size == 8)
|
||||
window = *(Window*)net_supporting_wm_check;
|
||||
XFree(net_supporting_wm_check);
|
||||
}
|
||||
|
||||
if(!window) {
|
||||
unsigned char *win_supporting_wm_check = window_get_property(display, DefaultRootWindow(display), XA_WINDOW, "_WIN_SUPPORTING_WM_CHECK", &property_size);
|
||||
if(win_supporting_wm_check) {
|
||||
if(property_size == 8)
|
||||
window = *(Window*)win_supporting_wm_check;
|
||||
XFree(win_supporting_wm_check);
|
||||
}
|
||||
}
|
||||
|
||||
if(!window)
|
||||
return wm_name;
|
||||
|
||||
const std::optional<std::string> window_title = get_window_title(display, window);
|
||||
if(window_title)
|
||||
wm_name = strip(window_title.value());
|
||||
|
||||
return wm_name;
|
||||
}
|
||||
|
||||
bool is_compositor_running(Display *dpy, int screen) {
|
||||
char prop_name[20];
|
||||
snprintf(prop_name, sizeof(prop_name), "_NET_WM_CM_S%d", screen);
|
||||
const Atom prop_atom = XInternAtom(dpy, prop_name, False);
|
||||
return XGetSelectionOwner(dpy, prop_atom) != None;
|
||||
}
|
||||
|
||||
static void get_monitors_callback(const mgl_monitor *monitor, void *userdata) {
|
||||
std::vector<Monitor> *monitors = (std::vector<Monitor>*)userdata;
|
||||
monitors->push_back({mgl::vec2i(monitor->pos.x, monitor->pos.y), mgl::vec2i(monitor->size.x, monitor->size.y)});
|
||||
}
|
||||
|
||||
std::vector<Monitor> get_monitors(Display *dpy) {
|
||||
std::vector<Monitor> monitors;
|
||||
mgl_for_each_active_monitor_output(dpy, get_monitors_callback, &monitors);
|
||||
return monitors;
|
||||
}
|
||||
}
|
||||
@@ -89,11 +89,14 @@ namespace gsr {
|
||||
}
|
||||
|
||||
std::unique_ptr<Subsection> GlobalSettingsPage::create_hotkey_subsection(ScrollablePage *parent_page) {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
auto enable_hotkeys_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
|
||||
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
|
||||
|
||||
auto enable_hotkeys_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::VERTICAL);
|
||||
enable_hotkeys_radio_button_ptr = enable_hotkeys_radio_button.get();
|
||||
enable_hotkeys_radio_button->add_item("Enable hotkeys and restart", "enable_hotkeys");
|
||||
enable_hotkeys_radio_button->add_item("Disable hotkeys and restart", "disable_hotkeys");
|
||||
enable_hotkeys_radio_button->add_item("Enable hotkeys", "enable_hotkeys");
|
||||
if(!inside_flatpak)
|
||||
enable_hotkeys_radio_button->add_item("Disable hotkeys", "disable_hotkeys");
|
||||
enable_hotkeys_radio_button->add_item("Only grab virtual devices (supports input remapping software)", "enable_hotkeys_virtual_devices");
|
||||
enable_hotkeys_radio_button->on_selection_changed = [&](const std::string&, const std::string &id) {
|
||||
if(!on_click_exit_program_button)
|
||||
return true;
|
||||
@@ -102,11 +105,12 @@ namespace gsr {
|
||||
on_click_exit_program_button("restart");
|
||||
else if(id == "disable_hotkeys")
|
||||
on_click_exit_program_button("restart");
|
||||
else if(id == "enable_hotkeys_virtual_devices")
|
||||
on_click_exit_program_button("restart");
|
||||
|
||||
return true;
|
||||
};
|
||||
list->add_widget(std::move(enable_hotkeys_radio_button));
|
||||
return std::make_unique<Subsection>("Hotkeys", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
return std::make_unique<Subsection>("Hotkeys", std::move(enable_hotkeys_radio_button), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<Button> GlobalSettingsPage::create_exit_program_button() {
|
||||
@@ -165,12 +169,12 @@ namespace gsr {
|
||||
const int exit_status = exec_program_on_host_get_stdout(args, stdout_str);
|
||||
startup_radio_button_ptr->set_selected_item(exit_status == 0 ? "start_on_system_startup" : "dont_start_on_system_startup", false, false);
|
||||
|
||||
enable_hotkeys_radio_button_ptr->set_selected_item(config.main_config.enable_hotkeys ? "enable_hotkeys" : "disable_hotkeys", false, false);
|
||||
enable_hotkeys_radio_button_ptr->set_selected_item(config.main_config.hotkeys_enable_option, false, false);
|
||||
}
|
||||
|
||||
void GlobalSettingsPage::save() {
|
||||
config.main_config.tint_color = tint_color_radio_button_ptr->get_selected_id();
|
||||
config.main_config.enable_hotkeys = enable_hotkeys_radio_button_ptr->get_selected_id() == "enable_hotkeys";
|
||||
config.main_config.hotkeys_enable_option = enable_hotkeys_radio_button_ptr->get_selected_id();
|
||||
save_config(config);
|
||||
}
|
||||
}
|
||||
59
src/main.cpp
59
src/main.cpp
@@ -10,6 +10,7 @@
|
||||
#include <signal.h>
|
||||
#include <thread>
|
||||
#include <string.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include <X11/keysym.h>
|
||||
#include <mglpp/mglpp.hpp>
|
||||
@@ -37,6 +38,7 @@ static void disable_prime_run() {
|
||||
unsetenv("__NV_PRIME_RENDER_OFFLOAD_PROVIDER");
|
||||
unsetenv("__GLX_VENDOR_LIBRARY_NAME");
|
||||
unsetenv("__VK_LAYER_NV_optimus");
|
||||
unsetenv("DRI_PRIME");
|
||||
}
|
||||
|
||||
static std::unique_ptr<gsr::GlobalHotkeysX11> register_x11_hotkeys(gsr::Overlay *overlay) {
|
||||
@@ -95,8 +97,8 @@ static std::unique_ptr<gsr::GlobalHotkeysX11> register_x11_hotkeys(gsr::Overlay
|
||||
return global_hotkeys;
|
||||
}
|
||||
|
||||
static std::unique_ptr<gsr::GlobalHotkeysLinux> register_linux_hotkeys(gsr::Overlay *overlay) {
|
||||
auto global_hotkeys = std::make_unique<gsr::GlobalHotkeysLinux>();
|
||||
static std::unique_ptr<gsr::GlobalHotkeysLinux> register_linux_hotkeys(gsr::Overlay *overlay, gsr::GlobalHotkeysLinux::GrabType grab_type) {
|
||||
auto global_hotkeys = std::make_unique<gsr::GlobalHotkeysLinux>(grab_type);
|
||||
if(!global_hotkeys->start())
|
||||
fprintf(stderr, "error: failed to start global hotkeys\n");
|
||||
|
||||
@@ -188,6 +190,48 @@ static bool is_gsr_ui_virtual_keyboard_running() {
|
||||
return virtual_keyboard_running;
|
||||
}
|
||||
|
||||
static void install_flatpak_systemd_service() {
|
||||
const bool systemd_service_exists = system(
|
||||
"data_home=$(flatpak-spawn --host -- /bin/sh -c 'echo \"${XDG_DATA_HOME:-$HOME/.local/share}\"') && "
|
||||
"flatpak-spawn --host -- ls \"$data_home/systemd/user/gpu-screen-recorder-ui.service\"") == 0;
|
||||
if(systemd_service_exists)
|
||||
return;
|
||||
|
||||
bool service_install_successful = (system(
|
||||
"data_home=$(flatpak-spawn --host -- /bin/sh -c 'echo \"${XDG_DATA_HOME:-$HOME/.local/share}\"') && "
|
||||
"flatpak-spawn --host -- install -Dm644 /var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/share/gpu-screen-recorder/gpu-screen-recorder-ui.service \"$data_home/systemd/user/gpu-screen-recorder-ui.service\"") == 0);
|
||||
service_install_successful &= (system("flatpak-spawn --host -- systemctl --user daemon-reload") == 0);
|
||||
if(service_install_successful)
|
||||
fprintf(stderr, "Info: the systemd service file was missing. It has now been installed\n");
|
||||
else
|
||||
fprintf(stderr, "Error: the systemd service file is missing and failed to install it again\n");
|
||||
}
|
||||
|
||||
static void remove_flatpak_systemd_service() {
|
||||
char systemd_service_path[PATH_MAX];
|
||||
const char *xdg_data_home = getenv("XDG_DATA_HOME");
|
||||
const char *home = getenv("HOME");
|
||||
if(xdg_data_home) {
|
||||
snprintf(systemd_service_path, sizeof(systemd_service_path), "%s/systemd/user/gpu-screen-recorder-ui.service", xdg_data_home);
|
||||
} else if(home) {
|
||||
snprintf(systemd_service_path, sizeof(systemd_service_path), "%s/.local/share/systemd/user/gpu-screen-recorder-ui.service", home);
|
||||
} else {
|
||||
fprintf(stderr, "Error: failed to get user home directory\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if(access(systemd_service_path, F_OK) != 0)
|
||||
return;
|
||||
|
||||
remove(systemd_service_path);
|
||||
system("systemctl --user daemon-reload");
|
||||
fprintf(stderr, "Info: conflicting flatpak version of the systemd service for gsr-ui was found at \"%s\", it has now been removed\n", systemd_service_path);
|
||||
}
|
||||
|
||||
static bool is_flatpak() {
|
||||
return getenv("FLATPAK_ID") != nullptr;
|
||||
}
|
||||
|
||||
static void usage() {
|
||||
printf("usage: gsr-ui [action]\n");
|
||||
printf("OPTIONS:\n");
|
||||
@@ -227,6 +271,11 @@ int main(int argc, char **argv) {
|
||||
usage();
|
||||
}
|
||||
|
||||
if(is_flatpak())
|
||||
install_flatpak_systemd_service();
|
||||
else
|
||||
remove_flatpak_systemd_service();
|
||||
|
||||
// TODO: This is a shitty method to detect if multiple instances of gsr-ui is running but this will work properly even in flatpak
|
||||
// that uses pid sandboxing. Replace this with a better method once we no longer rely on linux global hotkeys on some platform.
|
||||
// TODO: This method doesn't work when disabling hotkeys and the method below with pidof gsr-ui doesn't work in flatpak.
|
||||
@@ -314,8 +363,10 @@ int main(int argc, char **argv) {
|
||||
rpc_add_commands(rpc.get(), overlay.get());
|
||||
|
||||
std::unique_ptr<gsr::GlobalHotkeys> global_hotkeys = nullptr;
|
||||
if(overlay->get_config().main_config.enable_hotkeys)
|
||||
global_hotkeys = register_linux_hotkeys(overlay.get());
|
||||
if(overlay->get_config().main_config.hotkeys_enable_option == "enable_hotkeys")
|
||||
global_hotkeys = register_linux_hotkeys(overlay.get(), gsr::GlobalHotkeysLinux::GrabType::ALL);
|
||||
else if(overlay->get_config().main_config.hotkeys_enable_option == "enable_hotkeys_virtual_devices")
|
||||
global_hotkeys = register_linux_hotkeys(overlay.get(), gsr::GlobalHotkeysLinux::GrabType::VIRTUAL);
|
||||
|
||||
// TODO: Add hotkeys in Overlay when using x11 global hotkeys. The hotkeys in Overlay should duplicate each key that is used for x11 global hotkeys.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/poll.h>
|
||||
#include <poll.h>
|
||||
|
||||
/* LINUX */
|
||||
#include <linux/input.h>
|
||||
@@ -25,6 +25,24 @@
|
||||
|
||||
#define KEY_STATES_SIZE (KEY_MAX/8 + 1)
|
||||
|
||||
#define KEYCODE_TO_XKB_KEYCODE(key) ((key) + 8)
|
||||
|
||||
#define XK_Shift_L 0xffe1 /* Left shift */
|
||||
#define XK_Shift_R 0xffe2 /* Right shift */
|
||||
#define XK_Control_L 0xffe3 /* Left control */
|
||||
#define XK_Control_R 0xffe4 /* Right control */
|
||||
#define XK_Alt_L 0xffe9 /* Left alt */
|
||||
#define XK_Alt_R 0xffea /* Right alt */
|
||||
#define XK_Super_L 0xffeb /* Left super */
|
||||
#define XK_Super_R 0xffec /* Right super */
|
||||
|
||||
#define XK_z 0x007a
|
||||
#define XK_F7 0xffc4
|
||||
#define XK_F8 0xffc5
|
||||
#define XK_F9 0xffc6
|
||||
#define XK_F10 0xffc7
|
||||
#define XK_F11 0xffc8
|
||||
|
||||
static inline int count_num_bits_set(unsigned char c) {
|
||||
int n = 0;
|
||||
n += (c & 1);
|
||||
@@ -117,6 +135,35 @@ static void keyboard_event_process_key_state_change(keyboard_event *self, struct
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t keycode_to_keysym(keyboard_event *self, uint16_t keycode) {
|
||||
const unsigned long xkb_keycode = KEYCODE_TO_XKB_KEYCODE(keycode);
|
||||
if(self->x_context.display && self->x_context.XKeycodeToKeysym && xkb_keycode <= 255)
|
||||
return self->x_context.XKeycodeToKeysym(self->x_context.display, xkb_keycode, 0);
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* TODO: Support more keys when needed */
|
||||
static uint32_t keysym_to_keycode(uint32_t keysym) {
|
||||
switch(keysym) {
|
||||
case XK_Control_L: return KEY_LEFTCTRL;
|
||||
case XK_Shift_L: return KEY_LEFTSHIFT;
|
||||
case XK_Alt_L: return KEY_LEFTALT;
|
||||
case XK_Super_L: return KEY_LEFTMETA;
|
||||
case XK_Control_R: return KEY_RIGHTCTRL;
|
||||
case XK_Shift_R: return KEY_RIGHTSHIFT;
|
||||
case XK_Alt_R: return KEY_RIGHTALT;
|
||||
case XK_Super_R: return KEY_RIGHTMETA;
|
||||
case XK_z: return KEY_Z;
|
||||
case XK_F7: return KEY_F7;
|
||||
case XK_F8: return KEY_F8;
|
||||
case XK_F9: return KEY_F9;
|
||||
case XK_F10: return KEY_F10;
|
||||
case XK_F11: return KEY_F11;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void keyboard_event_process_input_event_data(keyboard_event *self, event_extra_data *extra_data, int fd, key_callback callback, void *userdata) {
|
||||
struct input_event event;
|
||||
if(read(fd, &event, sizeof(event)) != sizeof(event)) {
|
||||
@@ -137,7 +184,13 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
|
||||
if(event.type == EV_KEY) {
|
||||
keyboard_event_process_key_state_change(self, event, extra_data, fd);
|
||||
|
||||
switch(event.code) {
|
||||
/* We do this conversion from keycode to keysym back to keycode to support different keyboard layouts in the X server (which Wayland also uses to support Xwayland) */
|
||||
uint32_t keycode = event.code;
|
||||
const uint32_t keysym = keycode_to_keysym(self, event.code);
|
||||
if(keysym)
|
||||
keycode = keysym_to_keycode(keysym);
|
||||
|
||||
switch(keycode) {
|
||||
case KEY_LEFTSHIFT:
|
||||
self->lshift_button_state = event.value >= 1 ? KEYBOARD_BUTTON_PRESSED : KEYBOARD_BUTTON_RELEASED;
|
||||
break;
|
||||
@@ -165,21 +218,24 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
|
||||
default: {
|
||||
const bool shift_pressed = self->lshift_button_state == KEYBOARD_BUTTON_PRESSED || self->rshift_button_state == KEYBOARD_BUTTON_PRESSED;
|
||||
const bool ctrl_pressed = self->lctrl_button_state == KEYBOARD_BUTTON_PRESSED || self->rctrl_button_state == KEYBOARD_BUTTON_PRESSED;
|
||||
const bool alt_pressed = self->lalt_button_state == KEYBOARD_BUTTON_PRESSED || self->ralt_button_state == KEYBOARD_BUTTON_PRESSED;
|
||||
const bool lalt_pressed = self->lalt_button_state == KEYBOARD_BUTTON_PRESSED;
|
||||
const bool ralt_pressed = self->ralt_button_state == KEYBOARD_BUTTON_PRESSED;
|
||||
const bool meta_pressed = self->lmeta_button_state == KEYBOARD_BUTTON_PRESSED || self->rmeta_button_state == KEYBOARD_BUTTON_PRESSED;
|
||||
//fprintf(stderr, "pressed key: %d, state: %d, shift: %s, ctrl: %s, alt: %s, meta: %s\n", event.code, event.value,
|
||||
//fprintf(stderr, "pressed key: %d, state: %d, shift: %s, ctrl: %s, alt: %s, meta: %s\n", keycode, event.value,
|
||||
// shift_pressed ? "yes" : "no", ctrl_pressed ? "yes" : "no", alt_pressed ? "yes" : "no", meta_pressed ? "yes" : "no");
|
||||
uint32_t modifiers = 0;
|
||||
if(shift_pressed)
|
||||
modifiers |= KEYBOARD_MODKEY_SHIFT;
|
||||
if(ctrl_pressed)
|
||||
modifiers |= KEYBOARD_MODKEY_CTRL;
|
||||
if(alt_pressed)
|
||||
modifiers |= KEYBOARD_MODKEY_ALT;
|
||||
if(lalt_pressed)
|
||||
modifiers |= KEYBOARD_MODKEY_LALT;
|
||||
if(ralt_pressed)
|
||||
modifiers |= KEYBOARD_MODKEY_RALT;
|
||||
if(meta_pressed)
|
||||
modifiers |= KEYBOARD_MODKEY_SUPER;
|
||||
|
||||
if(!callback(event.code, modifiers, event.value, userdata))
|
||||
if(!callback(keycode, modifiers, event.value, userdata))
|
||||
return;
|
||||
|
||||
break;
|
||||
@@ -213,11 +269,41 @@ static bool keyboard_event_has_event_with_dev_input_fd(keyboard_event *self, int
|
||||
return false;
|
||||
}
|
||||
|
||||
/* TODO: Is there a more efficient way to do this? */
|
||||
static bool dev_input_is_virtual(int dev_input_id) {
|
||||
DIR *dir = opendir("/sys/devices/virtual/input");
|
||||
if(!dir)
|
||||
return false;
|
||||
|
||||
bool is_virtual = false;
|
||||
char virtual_input_filepath[1024];
|
||||
for(;;) {
|
||||
struct dirent *entry = readdir(dir);
|
||||
if(!entry)
|
||||
break;
|
||||
|
||||
if(strncmp(entry->d_name, "input", 5) != 0)
|
||||
continue;
|
||||
|
||||
snprintf(virtual_input_filepath, sizeof(virtual_input_filepath), "/sys/devices/virtual/input/%s/event%d", entry->d_name, dev_input_id);
|
||||
if(access(virtual_input_filepath, F_OK) == 0) {
|
||||
is_virtual = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return is_virtual;
|
||||
}
|
||||
|
||||
static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, const char *dev_input_filepath) {
|
||||
const int dev_input_id = get_dev_input_id_from_filepath(dev_input_filepath);
|
||||
if(dev_input_id == -1)
|
||||
return false;
|
||||
|
||||
if(self->grab_type == KEYBOARD_GRAB_TYPE_VIRTUAL && !dev_input_is_virtual(dev_input_id))
|
||||
return false;
|
||||
|
||||
if(keyboard_event_has_event_with_dev_input_fd(self, dev_input_id))
|
||||
return false;
|
||||
|
||||
@@ -373,10 +459,12 @@ static int setup_virtual_keyboard_input(const char *name) {
|
||||
return fd;
|
||||
}
|
||||
|
||||
bool keyboard_event_init(keyboard_event *self, bool poll_stdout_error, bool exclusive_grab) {
|
||||
bool keyboard_event_init(keyboard_event *self, bool poll_stdout_error, bool exclusive_grab, keyboard_grab_type grab_type, x11_context x_context) {
|
||||
memset(self, 0, sizeof(*self));
|
||||
self->stdout_event_index = -1;
|
||||
self->hotplug_event_index = -1;
|
||||
self->grab_type = grab_type;
|
||||
self->x_context = x_context;
|
||||
|
||||
if(exclusive_grab) {
|
||||
self->uinput_fd = setup_virtual_keyboard_input(GSR_UI_VIRTUAL_KEYBOARD_NAME);
|
||||
@@ -456,7 +544,25 @@ static void on_device_added_callback(const char *devname, void *userdata) {
|
||||
keyboard_event_try_add_device_if_keyboard(keyboard_ev, dev_input_filepath);
|
||||
}
|
||||
|
||||
#define MappingNotify 34
|
||||
|
||||
static void keyboard_event_poll_x11_events(keyboard_event *self) {
|
||||
if(!self->x_context.display || !self->x_context.XPending || !self->x_context.XNextEvent || !self->x_context.XRefreshKeyboardMapping)
|
||||
return;
|
||||
|
||||
XEvent xev;
|
||||
while(self->x_context.XPending(self->x_context.display)) {
|
||||
xev.type = 0;
|
||||
self->x_context.XNextEvent(self->x_context.display, &xev);
|
||||
if(xev.type == MappingNotify)
|
||||
self->x_context.XRefreshKeyboardMapping(xev.data);
|
||||
}
|
||||
}
|
||||
|
||||
void keyboard_event_poll_events(keyboard_event *self, int timeout_milliseconds, key_callback callback, void *userdata) {
|
||||
/* TODO: Add the x11 connection to the below poll? */
|
||||
keyboard_event_poll_x11_events(self);
|
||||
|
||||
if(poll(self->event_polls, self->num_event_polls, timeout_milliseconds) <= 0)
|
||||
return;
|
||||
|
||||
|
||||
@@ -10,18 +10,39 @@
|
||||
#include <stdint.h>
|
||||
|
||||
/* POSIX */
|
||||
#include <sys/poll.h>
|
||||
#include <poll.h>
|
||||
|
||||
/* LINUX */
|
||||
#include <linux/input-event-codes.h>
|
||||
|
||||
#define MAX_EVENT_POLLS 32
|
||||
|
||||
typedef struct {
|
||||
union {
|
||||
int type;
|
||||
unsigned char data[192];
|
||||
};
|
||||
} XEvent;
|
||||
|
||||
typedef unsigned long (*XKeycodeToKeysym_FUNC)(void *display, unsigned char keycode, int index);
|
||||
typedef int (*XPending_FUNC)(void *display);
|
||||
typedef int (*XNextEvent_FUNC)(void *display, XEvent *event_return);
|
||||
typedef int (*XRefreshKeyboardMapping_FUNC)(void* event_map);
|
||||
|
||||
typedef struct {
|
||||
void *display;
|
||||
XKeycodeToKeysym_FUNC XKeycodeToKeysym;
|
||||
XPending_FUNC XPending;
|
||||
XNextEvent_FUNC XNextEvent;
|
||||
XRefreshKeyboardMapping_FUNC XRefreshKeyboardMapping;
|
||||
} x11_context;
|
||||
|
||||
typedef enum {
|
||||
KEYBOARD_MODKEY_ALT = 1 << 0,
|
||||
KEYBOARD_MODKEY_SUPER = 1 << 1,
|
||||
KEYBOARD_MODKEY_CTRL = 1 << 2,
|
||||
KEYBOARD_MODKEY_SHIFT = 1 << 3
|
||||
KEYBOARD_MODKEY_LALT = 1 << 0,
|
||||
KEYBOARD_MODKEY_RALT = 1 << 2,
|
||||
KEYBOARD_MODKEY_SUPER = 1 << 3,
|
||||
KEYBOARD_MODKEY_CTRL = 1 << 4,
|
||||
KEYBOARD_MODKEY_SHIFT = 1 << 5
|
||||
} keyboard_modkeys;
|
||||
|
||||
typedef enum {
|
||||
@@ -36,6 +57,11 @@ typedef struct {
|
||||
int num_keys_pressed;
|
||||
} event_extra_data;
|
||||
|
||||
typedef enum {
|
||||
KEYBOARD_GRAB_TYPE_ALL,
|
||||
KEYBOARD_GRAB_TYPE_VIRTUAL
|
||||
} keyboard_grab_type;
|
||||
|
||||
typedef struct {
|
||||
struct pollfd event_polls[MAX_EVENT_POLLS]; /* Current size is |num_event_polls| */
|
||||
event_extra_data event_extra_data[MAX_EVENT_POLLS]; /* Current size is |num_event_polls| */
|
||||
@@ -45,6 +71,8 @@ typedef struct {
|
||||
int hotplug_event_index;
|
||||
int uinput_fd;
|
||||
bool stdout_failed;
|
||||
keyboard_grab_type grab_type;
|
||||
x11_context x_context;
|
||||
|
||||
hotplug_event hotplug_ev;
|
||||
|
||||
@@ -62,7 +90,7 @@ typedef struct {
|
||||
/* Return true to allow other applications to receive the key input (when using exclusive grab) */
|
||||
typedef bool (*key_callback)(uint32_t key, uint32_t modifiers, int press_status, void *userdata);
|
||||
|
||||
bool keyboard_event_init(keyboard_event *self, bool poll_stdout_error, bool exclusive_grab);
|
||||
bool keyboard_event_init(keyboard_event *self, bool poll_stdout_error, bool exclusive_grab, keyboard_grab_type grab_type, x11_context x_context);
|
||||
void keyboard_event_deinit(keyboard_event *self);
|
||||
|
||||
/* If |timeout_milliseconds| is -1 then wait until an event is received */
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
/* C stdlib */
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
/* POSIX */
|
||||
#include <unistd.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
typedef struct {
|
||||
uint32_t key;
|
||||
@@ -15,12 +17,12 @@ typedef struct {
|
||||
|
||||
#define NUM_GLOBAL_HOTKEYS 6
|
||||
static global_hotkey global_hotkeys[NUM_GLOBAL_HOTKEYS] = {
|
||||
{ .key = KEY_Z, .modifiers = KEYBOARD_MODKEY_ALT, .action = "show_hide" },
|
||||
{ .key = KEY_F9, .modifiers = KEYBOARD_MODKEY_ALT, .action = "record" },
|
||||
{ .key = KEY_F7, .modifiers = KEYBOARD_MODKEY_ALT, .action = "pause" },
|
||||
{ .key = KEY_F8, .modifiers = KEYBOARD_MODKEY_ALT, .action = "stream" },
|
||||
{ .key = KEY_F10, .modifiers = KEYBOARD_MODKEY_ALT | KEYBOARD_MODKEY_SHIFT, .action = "replay_start" },
|
||||
{ .key = KEY_F10, .modifiers = KEYBOARD_MODKEY_ALT, .action = "replay_save" }
|
||||
{ .key = KEY_Z, .modifiers = KEYBOARD_MODKEY_LALT, .action = "show_hide" },
|
||||
{ .key = KEY_F9, .modifiers = KEYBOARD_MODKEY_LALT, .action = "record" },
|
||||
{ .key = KEY_F7, .modifiers = KEYBOARD_MODKEY_LALT, .action = "pause" },
|
||||
{ .key = KEY_F8, .modifiers = KEYBOARD_MODKEY_LALT, .action = "stream" },
|
||||
{ .key = KEY_F10, .modifiers = KEYBOARD_MODKEY_LALT | KEYBOARD_MODKEY_SHIFT, .action = "replay_start" },
|
||||
{ .key = KEY_F10, .modifiers = KEYBOARD_MODKEY_LALT, .action = "replay_save" }
|
||||
};
|
||||
|
||||
static bool on_key_callback(uint32_t key, uint32_t modifiers, int press_status, void *userdata) {
|
||||
@@ -37,7 +39,104 @@ static bool on_key_callback(uint32_t key, uint32_t modifiers, int press_status,
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
static void usage(void) {
|
||||
fprintf(stderr, "usage: gsr-global-hotkeys [--all|--virtual]\n");
|
||||
fprintf(stderr, "OPTIONS:\n");
|
||||
fprintf(stderr, " --all Grab all devices.\n");
|
||||
fprintf(stderr, " --virtual Grab all virtual devices only.\n");
|
||||
}
|
||||
|
||||
typedef void* (*XOpenDisplay_FUNC)(const char*);
|
||||
typedef int (*XErrorHandler_FUNC)(void *display, void* error_event);
|
||||
typedef XErrorHandler_FUNC (*XSetErrorHandler_FUNC)(XErrorHandler_FUNC handler);
|
||||
|
||||
static int x_ignore_error(void *display, void *ee) {
|
||||
(void)display;
|
||||
(void)ee;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static x11_context setup_x11_context(void) {
|
||||
x11_context x_context = {0};
|
||||
XSetErrorHandler_FUNC XSetErrorHandler = NULL;
|
||||
|
||||
void *x11_lib = dlopen("libX11.so.6", RTLD_LAZY);
|
||||
if(!x11_lib) {
|
||||
fprintf(stderr, "Warning: dlopen libX11.so.6 failed\n");
|
||||
return x_context;
|
||||
}
|
||||
|
||||
XOpenDisplay_FUNC XOpenDisplay = dlsym(x11_lib, "XOpenDisplay");
|
||||
if(!XOpenDisplay) {
|
||||
fprintf(stderr, "Warning: dlsym XOpenDisplay failed\n");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
x_context.XKeycodeToKeysym = dlsym(x11_lib, "XKeycodeToKeysym");
|
||||
if(!x_context.XKeycodeToKeysym) {
|
||||
fprintf(stderr, "Warning: dlsym XKeycodeToKeysym failed\n");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
x_context.XPending = dlsym(x11_lib, "XPending");
|
||||
if(!x_context.XPending) {
|
||||
fprintf(stderr, "Warning: dlsym XPending failed\n");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
x_context.XNextEvent = dlsym(x11_lib, "XNextEvent");
|
||||
if(!x_context.XNextEvent) {
|
||||
fprintf(stderr, "Warning: dlsym XNextEvent failed\n");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
x_context.XRefreshKeyboardMapping = dlsym(x11_lib, "XRefreshKeyboardMapping");
|
||||
if(!x_context.XRefreshKeyboardMapping) {
|
||||
fprintf(stderr, "Warning: dlsym XRefreshKeyboardMapping failed\n");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
x_context.display = XOpenDisplay(NULL);
|
||||
if(!x_context.display) {
|
||||
fprintf(stderr, "Warning: XOpenDisplay failed\n");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
XSetErrorHandler = dlsym(x11_lib, "XSetErrorHandler");
|
||||
if(XSetErrorHandler)
|
||||
XSetErrorHandler(x_ignore_error);
|
||||
else
|
||||
fprintf(stderr, "Warning: dlsym XSetErrorHandler failed\n");
|
||||
|
||||
return x_context;
|
||||
|
||||
fail:
|
||||
memset(&x_context, 0, sizeof(x_context));
|
||||
dlclose(x11_lib);
|
||||
return x_context;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
keyboard_grab_type grab_type = KEYBOARD_GRAB_TYPE_ALL;
|
||||
if(argc == 2) {
|
||||
const char *grab_type_arg = argv[1];
|
||||
if(strcmp(grab_type_arg, "--all") == 0) {
|
||||
grab_type = KEYBOARD_GRAB_TYPE_ALL;
|
||||
} else if(strcmp(grab_type_arg, "--virtual") == 0) {
|
||||
grab_type = KEYBOARD_GRAB_TYPE_VIRTUAL;
|
||||
} else {
|
||||
fprintf(stderr, "Error: expected --all or --virtual, got %s\n", grab_type_arg);
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
} else if(argc != 1) {
|
||||
fprintf(stderr, "Error: expected 0 or 1 arguments, got %d argument(s)\n", argc);
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
x11_context x_context = setup_x11_context();
|
||||
|
||||
const uid_t user_id = getuid();
|
||||
if(geteuid() != 0) {
|
||||
if(setuid(0) == -1) {
|
||||
@@ -47,7 +146,7 @@ int main(void) {
|
||||
}
|
||||
|
||||
keyboard_event keyboard_ev;
|
||||
if(!keyboard_event_init(&keyboard_ev, true, true)) {
|
||||
if(!keyboard_event_init(&keyboard_ev, true, true, grab_type, x_context)) {
|
||||
fprintf(stderr, "Error: failed to setup hotplugging and no keyboard input devices were found\n");
|
||||
setuid(user_id);
|
||||
return 1;
|
||||
|
||||
@@ -50,6 +50,7 @@ static void usage(void) {
|
||||
printf(" toggle-stream Start/stop streaming.\n");
|
||||
printf(" toggle-replay Start/stop replay.\n");
|
||||
printf(" replay-save Save replay.\n");
|
||||
printf("\n");
|
||||
printf("EXAMPLES:\n");
|
||||
printf(" gsr-ui-cli toggle-show\n");
|
||||
printf(" gsr-ui-cli toggle-record\n");
|
||||
|
||||
Reference in New Issue
Block a user