diff options
65 files changed, 5629 insertions, 1271 deletions
@@ -7,11 +7,15 @@ 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.\ +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`).\ -If you are running another distro then you can run `sudo ./install.sh`, but you need to manually install the dependencies, as described below. +If you are running another distro then you can run `sudo ./install.sh`, but you need to manually install the dependencies, as described below.\ +You can also install gpu screen recorder (the gtk gui version) from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder). This flatpak includes both this UI and gpu-screen-recorder so no need to install that first. # Dependencies GPU Screen Recorder UI uses meson build system so you need to install `meson` to build GPU Screen Recorder UI. @@ -19,28 +23,41 @@ GPU Screen Recorder UI uses meson build system so you need to install `meson` to ## Build dependencies These are the dependencies needed to build GPU Screen Recorder UI: -* x11 (libx11, libxrandr, libxrender, libxfixes, libxcomposite) +* x11 (libx11, libxrandr, libxrender, libxcomposite, libxfixes, libxi) +* libxcursor * libglvnd (which provides libgl, libglx and libegl) -* libevdev -* libudev (systemd-libs) -* libinput -* libxkbcommon +* linux-api-headers +* libpulse (libpulse-simple) ## Runtime dependencies -* [GPU Screen Recorder](https://git.dec05eba.com/gpu-screen-recorder/) +There are also additional dependencies needed at runtime: + +* [GPU Screen Recorder](https://git.dec05eba.com/gpu-screen-recorder/) (version 5.0.0 or later) * [GPU Screen Recorder Notification](https://git.dec05eba.com/gpu-screen-recorder-notification/) +## Program behavior notes +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 select "Only grab virtual devices" + # License -This software is licensed under GPL3.0-only. Files under `fonts/` directory are licensed under `SIL Open Font 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`. `images/default.cur` it part of the [Adwaita icon theme](https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/tree/master) which is licensed under `Creative Commons Attribution-Share Alike 3.0`. # Demo [![Click here to watch a demo video on youtube](https://img.youtube.com/vi/SOqXusCTXXA/0.jpg)](https://www.youtube.com/watch?v=SOqXusCTXXA) # Screenshots -![](https://dec05eba.com/images/gsr-overlay-screenshot-front.webp) -![](https://dec05eba.com/images/gsr-overlay-screenshot-settings.webp) +![](https://dec05eba.com/images/front_page.jpg) +![](https://dec05eba.com/images/settings_page.jpg) # Donations If you want to donate you can donate via bitcoin or monero. * Bitcoin: bc1qqvuqnwrdyppf707ge27fqz2n9y9gu7lf5ypyuf -* Monero: 4An9kp2qW1C9Gah7ewv4JzcNFQ5TAX7ineGCqXWK6vQnhsGGcRpNgcn8r9EC3tMcgY7vqCKs3nSRXhejMHBaGvFdN2egYet
\ No newline at end of file +* Monero: 4An9kp2qW1C9Gah7ewv4JzcNFQ5TAX7ineGCqXWK6vQnhsGGcRpNgcn8r9EC3tMcgY7vqCKs3nSRXhejMHBaGvFdN2egYet + +# Known issues +* 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 `gsr-ui` to fix that. @@ -1,12 +1,4 @@ -setcap nice for good performance when opening overlay when game is running below 60 fps. -Maybe grab cursor with xi, as that will prevent games from detecting movement with xi2 api. - -Fullscreen on wayland doesn't render windows behind because it's a compositor optimization, to not draw anything behind (only draw the window directly without compositing). -Fix this by drawing the window smaller, or have two windows (left and right half monitor width). -Maybe change design to have black triangles appear and get larger until they fill the screen, with even spaces being left with no triangles. - Exclude triangles from a diagonal line across the screen. - Have buttons appear slanted in 3D. - All of these things should be done with vertex buffer, for real 3D. +setcap nice for good performance when opening overlay when game is running below 60 fps (on amd). WAYLAND_DISPLAY gamescope-0, DISPLAY=:1 (gamescope xwayland) @@ -22,14 +14,10 @@ Add nvidia overclock option. Add support for window selection in capture. -Add option to record the focused monitor. This works on wayland too when using kms capture since we can get cursor position without root and see which monitor (crtc) the cursor is on. - -Make hotkeys configurable. +Add option to record the focused monitor. This works on wayland too when using kms capture since we can get cursor position without root and see which monitor (crtc) the cursor is on. Or use create_window_get_center_position. 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. @@ -47,7 +35,7 @@ Add global setting. In that setting there should be an option to enable/disable Add profiles and hotkey to switch between profiles (show notification when switching profile). -Fix first frame being black. +Fix first frame being black when running without a compositor. Add support for systray. @@ -70,31 +58,66 @@ On nvidia check if suspend fix is applied. If not, show a popup asking the user Show warning when using steam deck or when trying to capture hevc/av1 on amd (the same warnings as gpu screen recorder gtk). -Add option to capture application audio. This should show a popup where you can use one of the available applications or a custom one and choose to record that application or all applications except that one. - Add profile option. Convert view to profile, add an option at the bottom that says "Edit profiles..." which should show a popup where you can create/remove profiles. New profiles should always be in advanced view. Verify monitor/audio when starting recording. Give an error if the options are no longer valid. -Add option to record focused monitor. This is less error prone when plugging in monitors, etc. - -Get focused window when opening gsr-ui and pass that to the save replay script, to ignore gsr-ui when getting game name. - -gsr ui window has _NET_WM_STATE _NET_WM_STATE_ABOVE, not _NET_WM_STATE_FULLSCREEN +gsr ui window has _NET_WM_STATE _NET_WM_STATE_ABOVE, not _NET_WM_STATE_FULLSCREEN. For replay on fullscreen detect focused fullscreen window by checking if the window size is the same as the monitor size instead of _NET_WM_STATE_FULLSCREEN. -Add audio devices/app refresh button. - Play camera shutter sound when saving recording. When another sound when starting recording. Some games such as "The Finals" crashes/freezes when they lose focus when running them on x11 on a laptop with prime setup and the monitor runs on the iGPU while the game runs on the dGPU. -Try to reproduce this and if it happens try grab cursor and keyboard instead of setting gsr ui focus and make gsr ui click through like gsr notify. This might fix the issue. Run `systemctl status --user gpu-screen-recorder` when starting recording and give a notification warning if it returns 0 (running). Or run pidof gpu-screen-recorder. Add option to select which gpu to record with, or list all monitors and automatically use the gpu associated with the monitor. Do the same in gtk application. -Remove all dependencies from tools/gsr-global-hotkeys and roll our own keyboard events code. +Dont allow autostart of replay if capture option is window recording (when window recording is added). + +Use global shortcuts desktop portal protocol on wayland when available. + +When support for window capture is enabled on x11 then make sure to not save the window except temporary while the program is open. + +Support CJK. + +Move ui hover code from ::draw to ::on_event, to properly handle widget event stack. + +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. <- 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>/. + +All steam game names by ID are available at https://api.steampowered.com/ISteamApps/GetAppList/v2/. The name of a single game can be retrieved from http://store.steampowered.com/api/appdetails?appids=115800. + +Dont put widget position to int position when scrolling. This makes the UI jitter when it's coming to a halt. + +Show warning if another instance of gpu screen recorder is already running when starting recording? + +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. + +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). + +Show an error that prime run will be disabled when using desktop portal capture option. This can cause issues as the user may have selected a video codec option that isn't available on their iGPU but is available on the prime-run dGPU. + +Is it possible to configure hotkey and the new hotkey to get triggered immediately? + +For keyboards that report supporting mice the keyboard grab will be delayed until any key has been pressed (and then released), see: https://github.com/dec05eba/gpu-screen-recorder-issues/issues/97 + See if there is any way around this. + +Instead of installing gsr-global-hotkeys in flatpak use kms-server-proxy to launch gsr-global-hotkeys inside the flatpak with root, just like gsr-kms-server. This removes the need to update gsr-global-hotkeys everytime there is an update. + +Check if "modprobe uinput" is needed on some systems (old fedora?). + +Add recording timer to see duration of recording/streaming. + +Make folder with window name work when using gamescope. Gamescope runs x11 itself so to get the window name inside that we have to connect to the gamescope X11 server (DISPLAY=:1 on x11 and DISPLAY=:2 on wayland, but not always). -Test global hotkeys with azerty instead of qwerty.
\ No newline at end of file +When clicking on current directory in file manager show a dropdown menu where you can select common directories (HOME, Videos, Downloads and mounted drives) for quick navigation. Maybe even button to search. diff --git a/depends/mglpp b/depends/mglpp -Subproject 9800cff631f1a82ee87005aabdefb3f5da7fadb +Subproject d875a5c2b9cd3b123e4253ba48f8738ff5b08f1 diff --git a/flatpak/gpu-screen-recorder-ui.service b/flatpak/gpu-screen-recorder-ui.service new file mode 100644 index 0000000..3ed7f4b --- /dev/null +++ b/flatpak/gpu-screen-recorder-ui.service @@ -0,0 +1,11 @@ +[Unit] +Description=GPU Screen Recorder UI Service + +[Service] +ExecStart=flatpak run com.dec05eba.gpu_screen_recorder gsr-ui +KillSignal=SIGINT +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=default.target
\ No newline at end of file diff --git a/images/default.cur b/images/default.cur Binary files differnew file mode 100644 index 0000000..c7da315 --- /dev/null +++ b/images/default.cur diff --git a/images/replay.png b/images/replay.png Binary files differindex 65c9339..e9ec83b 100644 --- a/images/replay.png +++ b/images/replay.png diff --git a/images/settings.png b/images/settings.png Binary files differindex 5f8d203..efc19a1 100644 --- a/images/settings.png +++ b/images/settings.png diff --git a/images/settings_small.png b/images/settings_small.png Binary files differnew file mode 100644 index 0000000..dcb896d --- /dev/null +++ b/images/settings_small.png diff --git a/include/AudioPlayer.hpp b/include/AudioPlayer.hpp new file mode 100644 index 0000000..22c3be8 --- /dev/null +++ b/include/AudioPlayer.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include <thread> + +namespace gsr { + // Only plays raw stereo PCM audio in 48000hz in s16le format. + // Use this command to convert an audio file (input.wav) to a format playable by this class (output.pcm): + // ffmpeg -i input.wav -f s16le -acodec pcm_s16le -ar 48000 output.pcm + class AudioPlayer { + public: + AudioPlayer() = default; + ~AudioPlayer(); + AudioPlayer(const AudioPlayer&) = delete; + AudioPlayer& operator=(const AudioPlayer&) = delete; + + bool play(const char *filepath); + private: + std::thread thread; + bool stop_playing_audio = false; + int audio_file_fd = -1; + }; +}
\ No newline at end of file diff --git a/include/Config.hpp b/include/Config.hpp index 6044ab8..34c2010 100644 --- a/include/Config.hpp +++ b/include/Config.hpp @@ -6,12 +6,17 @@ #include <vector> #include <optional> +#define GSR_CONFIG_FILE_VERSION 1 + namespace gsr { - struct GsrInfo; + struct SupportedCaptureOptions; struct ConfigHotkey { - int64_t keysym = 0; - uint32_t modifiers = 0; + int64_t key = 0; // Mgl key + uint32_t modifiers = 0; // HotkeyModifier + + bool operator==(const ConfigHotkey &other) const; + bool operator!=(const ConfigHotkey &other) const; }; struct RecordOptions { @@ -22,7 +27,7 @@ namespace gsr { int32_t video_height = 0; int32_t fps = 60; int32_t video_bitrate = 15000; - bool merge_audio_tracks = true; + bool merge_audio_tracks = true; // Currently unused for streaming because all known streaming sites only support 1 audio track bool application_audio_invert = false; bool change_video_resolution = false; std::vector<std::string> audio_tracks; @@ -38,8 +43,12 @@ namespace gsr { }; struct MainConfig { - int32_t config_file_version = 0; + int32_t config_file_version = GSR_CONFIG_FILE_VERSION; bool software_encoding_warning_shown = false; + std::string hotkeys_enable_option = "enable_hotkeys"; + std::string joystick_hotkeys_enable_option = "disable_hotkeys"; + std::string tint_color; + ConfigHotkey show_hide_hotkey; }; struct YoutubeStreamConfig { @@ -63,7 +72,7 @@ namespace gsr { YoutubeStreamConfig youtube; TwitchStreamConfig twitch; CustomStreamConfig custom; - ConfigHotkey start_stop_recording_hotkey; + ConfigHotkey start_stop_hotkey; }; struct RecordConfig { @@ -73,26 +82,29 @@ namespace gsr { bool show_video_saved_notifications = true; std::string save_directory; std::string container = "mp4"; - ConfigHotkey start_stop_recording_hotkey; - ConfigHotkey pause_unpause_recording_hotkey; + ConfigHotkey start_stop_hotkey; + ConfigHotkey pause_unpause_hotkey; }; struct ReplayConfig { RecordOptions record_options; std::string turn_on_replay_automatically_mode = "dont_turn_on_automatically"; bool save_video_in_game_folder = false; + bool restart_replay_on_save = false; bool show_replay_started_notifications = true; bool show_replay_stopped_notifications = true; bool show_replay_saved_notifications = true; std::string save_directory; std::string container = "mp4"; int32_t replay_time = 60; - ConfigHotkey start_stop_recording_hotkey; - ConfigHotkey save_recording_hotkey; + ConfigHotkey start_stop_hotkey; + ConfigHotkey save_hotkey; }; struct Config { - Config(const GsrInfo &gsr_info); + Config(const SupportedCaptureOptions &capture_options); + bool operator==(const Config &other); + bool operator!=(const Config &other); MainConfig main_config; StreamingConfig streaming_config; @@ -100,6 +112,6 @@ namespace gsr { ReplayConfig replay_config; }; - std::optional<Config> read_config(const GsrInfo &gsr_info); + std::optional<Config> read_config(const SupportedCaptureOptions &capture_options); void save_config(Config &config); }
\ No newline at end of file diff --git a/include/GlobalHotkeys.hpp b/include/GlobalHotkeys.hpp index 662113e..2927fa7 100644 --- a/include/GlobalHotkeys.hpp +++ b/include/GlobalHotkeys.hpp @@ -4,10 +4,25 @@ #include <functional> #include <string> +namespace mgl { + class Event; +} + namespace gsr { + enum HotkeyModifier : uint32_t { + HOTKEY_MOD_LSHIFT = 1 << 0, + HOTKEY_MOD_RSHIFT = 1 << 1, + HOTKEY_MOD_LCTRL = 1 << 2, + HOTKEY_MOD_RCTRL = 1 << 3, + HOTKEY_MOD_LALT = 1 << 4, + HOTKEY_MOD_RALT = 1 << 5, + HOTKEY_MOD_LSUPER = 1 << 6, + HOTKEY_MOD_RSUPER = 1 << 7 + }; + struct Hotkey { - uint64_t key = 0; - uint32_t modifiers = 0; + uint32_t key = 0; // X11 keysym + uint32_t modifiers = 0; // HotkeyModifier }; using GlobalHotkeyCallback = std::function<void(const std::string &id)>; @@ -24,5 +39,7 @@ namespace gsr { virtual void unbind_all_keys() {} virtual bool bind_action(const std::string &id, GlobalHotkeyCallback callback) { (void)id; (void)callback; return false; }; virtual void poll_events() = 0; + // Returns true if the event wasn't consumed (if the event didn't match a key that has been bound) + virtual bool on_event(mgl::Event &event) { (void)event; return true; } }; }
\ No newline at end of file diff --git a/include/GlobalHotkeysJoystick.hpp b/include/GlobalHotkeysJoystick.hpp new file mode 100644 index 0000000..69f66df --- /dev/null +++ b/include/GlobalHotkeysJoystick.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "GlobalHotkeys.hpp" +#include "Hotplug.hpp" +#include <unordered_map> +#include <optional> +#include <thread> +#include <poll.h> +#include <mglpp/system/Clock.hpp> +#include <linux/joystick.h> + +namespace gsr { + static constexpr int max_js_poll_fd = 16; + + class GlobalHotkeysJoystick : public GlobalHotkeys { + class GlobalHotkeysJoystickHotplugDelegate; + public: + GlobalHotkeysJoystick() = default; + GlobalHotkeysJoystick(const GlobalHotkeysJoystick&) = delete; + GlobalHotkeysJoystick& operator=(const GlobalHotkeysJoystick&) = delete; + ~GlobalHotkeysJoystick() override; + + bool start(); + bool bind_action(const std::string &id, GlobalHotkeyCallback callback) override; + void poll_events() override; + private: + void read_events(); + void process_js_event(int fd, js_event &event); + bool add_device(const char *dev_input_filepath, bool print_error = true); + bool remove_device(const char *dev_input_filepath); + bool remove_poll_fd(int index); + // Returns -1 if not found + int get_poll_fd_index_by_dev_input_id(int dev_input_id) const; + private: + struct ExtraData { + int dev_input_id = 0; + }; + + std::unordered_map<std::string, GlobalHotkeyCallback> bound_actions_by_id; + std::thread read_thread; + + pollfd poll_fd[max_js_poll_fd]; + ExtraData extra_data[max_js_poll_fd]; + int num_poll_fd = 0; + int event_fd = -1; + int event_index = -1; + + mgl::Clock double_click_clock; + std::optional<double> prev_time_clicked; + bool save_replay = false; + int hotplug_poll_index = -1; + Hotplug hotplug; + }; +}
\ No newline at end of file diff --git a/include/GlobalHotkeysLinux.hpp b/include/GlobalHotkeysLinux.hpp index 62da74e..c9428de 100644 --- a/include/GlobalHotkeysLinux.hpp +++ b/include/GlobalHotkeysLinux.hpp @@ -7,18 +7,26 @@ 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; bool start(); - bool bind_action(const std::string &id, GlobalHotkeyCallback callback) override; + bool bind_key_press(Hotkey hotkey, const std::string &id, GlobalHotkeyCallback callback) override; + void unbind_all_keys() override; void poll_events() override; private: pid_t process_id = 0; - int pipes[2]; + int read_pipes[2]; + int write_pipes[2]; FILE *read_file = nullptr; std::unordered_map<std::string, GlobalHotkeyCallback> bound_actions_by_id; + GrabType grab_type; }; }
\ No newline at end of file diff --git a/include/GlobalHotkeysX11.hpp b/include/GlobalHotkeysX11.hpp index 427e9f0..610399a 100644 --- a/include/GlobalHotkeysX11.hpp +++ b/include/GlobalHotkeysX11.hpp @@ -12,12 +12,15 @@ namespace gsr { GlobalHotkeysX11& operator=(const GlobalHotkeysX11&) = delete; ~GlobalHotkeysX11() override; + // Hotkey key is a KeySym (XK_z for example) and modifiers is a bitmask of X11 modifier masks (for example ShiftMask | Mod1Mask) bool bind_key_press(Hotkey hotkey, const std::string &id, GlobalHotkeyCallback callback) override; void unbind_key_press(const std::string &id) override; void unbind_all_keys() override; void poll_events() override; + bool on_event(mgl::Event &event) override; private: - void call_hotkey_callback(Hotkey hotkey) const; + // Returns true if a key bind has been registered for the hotkey + bool call_hotkey_callback(Hotkey hotkey) const; private: struct HotkeyData { Hotkey hotkey; diff --git a/include/GsrInfo.hpp b/include/GsrInfo.hpp index cd6292c..a8c0742 100644 --- a/include/GsrInfo.hpp +++ b/include/GsrInfo.hpp @@ -2,6 +2,7 @@ #include <string> #include <vector> +#include <stdint.h> #include <mglpp/system/vec.hpp> @@ -24,10 +25,24 @@ namespace gsr { mgl::vec2i size; }; + struct GsrVersion { + uint8_t major = 0; + uint8_t minor = 0; + uint8_t patch = 0; + + bool operator>(const GsrVersion &other) const; + bool operator>=(const GsrVersion &other) const; + bool operator<(const GsrVersion &other) const; + bool operator<=(const GsrVersion &other) const; + bool operator==(const GsrVersion &other) const; + bool operator!=(const GsrVersion &other) const; + + std::string to_string() const; + }; + struct SupportedCaptureOptions { bool window = false; bool focused = false; - bool screen = false; bool portal = false; std::vector<GsrMonitor> monitors; }; @@ -41,6 +56,7 @@ namespace gsr { struct SystemInfo { DisplayServer display_server = DisplayServer::UNKNOWN; bool supports_app_audio = false; + GsrVersion gsr_version; }; enum class GpuVendor { @@ -52,13 +68,13 @@ namespace gsr { struct GpuInfo { GpuVendor vendor = GpuVendor::UNKNOWN; + std::string card_path; }; struct GsrInfo { SystemInfo system_info; GpuInfo gpu_info; SupportedVideoCodecs supported_video_codecs; - SupportedCaptureOptions supported_capture_options; }; enum class GsrInfoExitStatus { @@ -78,4 +94,5 @@ namespace gsr { std::vector<AudioDevice> get_audio_devices(); std::vector<std::string> get_application_audio(); + SupportedCaptureOptions get_supported_capture_options(const GsrInfo &gsr_info); }
\ No newline at end of file diff --git a/include/Hotplug.hpp b/include/Hotplug.hpp new file mode 100644 index 0000000..38fe25d --- /dev/null +++ b/include/Hotplug.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include <functional> + +namespace gsr { + enum class HotplugAction { + ADD, + REMOVE + }; + + using HotplugEventCallback = std::function<void(HotplugAction hotplug_action, const char *devname)>; + + class Hotplug { + public: + Hotplug() = default; + Hotplug(const Hotplug&) = delete; + Hotplug& operator=(const Hotplug&) = delete; + ~Hotplug(); + + bool start(); + int steal_fd(); + void process_event_data(int fd, const HotplugEventCallback &callback); + private: + void parse_netlink_data(const char *line, const HotplugEventCallback &callback); + private: + int fd = -1; + bool started = false; + bool event_is_add = false; + bool event_is_remove = false; + bool subsystem_is_input = false; + char event_data[1024]; + }; +}
\ No newline at end of file diff --git a/include/Overlay.hpp b/include/Overlay.hpp index 580759c..f3025b2 100644 --- a/include/Overlay.hpp +++ b/include/Overlay.hpp @@ -5,6 +5,10 @@ #include "GsrInfo.hpp" #include "Config.hpp" #include "window_texture.h" +#include "WindowUtils.hpp" +#include "GlobalHotkeysLinux.hpp" +#include "GlobalHotkeysJoystick.hpp" +#include "AudioPlayer.hpp" #include <mglpp/window/Window.hpp> #include <mglpp/window/Event.hpp> @@ -14,8 +18,11 @@ #include <mglpp/graphics/Text.hpp> #include <mglpp/system/Clock.hpp> +#include <array> + namespace gsr { class DropdownButton; + class GlobalHotkeys; enum class RecordingStatus { NONE, @@ -33,13 +40,12 @@ namespace gsr { class Overlay { public: - Overlay(std::string resources_path, GsrInfo gsr_info, egl_functions egl_funcs); + Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs); Overlay(const Overlay&) = delete; Overlay& operator=(const Overlay&) = delete; ~Overlay(); void handle_events(); - void on_event(mgl::Event &event); // Returns false if not visible bool draw(); @@ -53,16 +59,38 @@ namespace gsr { void save_replay(); void show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type); bool is_open() const; + bool should_exit(std::string &reason) const; + void exit(); + + const Config& get_config() const; + + void unbind_all_keyboard_hotkeys(); + void rebind_all_keyboard_hotkeys(); private: + void handle_keyboard_mapping_event(); + void on_event(mgl::Event &event); + + void xi_setup(); + void handle_xi_events(); void process_key_bindings(mgl::Event &event); + void grab_mouse_and_keyboard(); + void xi_setup_fake_cursor(); + void xi_grab_all_mouse_devices(); + + void close_gpu_screen_recorder_output(); void update_notification_process_status(); + void save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type); + void on_replay_saved(const char *replay_saved_filepath); + void update_gsr_replay_save(); void update_gsr_process_status(); void replay_status_update_status(); void update_focused_fullscreen_status(); void update_power_supply_status(); + void on_stop_recording(int exit_code); + void update_ui_recording_paused(); void update_ui_recording_unpaused(); @@ -79,7 +107,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: @@ -94,11 +122,20 @@ namespace gsr { std::string resources_path; GsrInfo gsr_info; egl_functions egl_funcs; + Config config; + + bool visible = false; + mgl::Texture window_texture_texture; mgl::Sprite window_texture_sprite; mgl::Texture screenshot_texture; mgl::Sprite screenshot_sprite; mgl::Rectangle bg_screenshot_overlay; + + mgl::Texture cursor_texture; + mgl::Sprite cursor_sprite; + mgl::vec2i cursor_hotspot; + WindowTexture window_texture; PageStack page_stack; mgl::Rectangle top_bar_background; @@ -106,14 +143,17 @@ namespace gsr { mgl::Sprite logo_sprite; CustomRendererWidget close_button_widget; bool close_button_pressed_inside = false; - bool visible = false; uint64_t default_cursor = 0; + pid_t gpu_screen_recorder_process = -1; pid_t notification_process = -1; - Config config; + int gpu_screen_recorder_process_output_fd = -1; + FILE *gpu_screen_recorder_process_output_file = nullptr; + DropdownButton *replay_dropdown_button_ptr = nullptr; DropdownButton *record_dropdown_button_ptr = nullptr; DropdownButton *stream_dropdown_button_ptr = nullptr; + mgl::Clock force_window_on_top_clock; RecordingStatus recording_status = RecordingStatus::NONE; @@ -124,6 +164,33 @@ namespace gsr { bool power_supply_connected = false; bool focused_window_is_fullscreen = false; + std::string record_filepath; + + Display *xi_display = nullptr; + int xi_opcode = 0; + XEvent *xi_input_xev = nullptr; + XEvent *xi_output_xev = nullptr; + std::array<KeyBinding, 1> key_bindings; + bool drawn_first_frame = false; + + bool do_exit = false; + std::string exit_reason; + + mgl::vec2i window_size = { 1280, 720 }; + mgl::vec2i window_pos = { 0, 0 }; + + mgl::Clock show_overlay_clock; + double show_overlay_timeout_seconds = 0.0; + + std::unique_ptr<GlobalHotkeys> global_hotkeys = nullptr; + std::unique_ptr<GlobalHotkeysJoystick> global_hotkeys_js = nullptr; + Display *x11_mapping_display = nullptr; + XEvent x11_mapping_xev; + + mgl::Clock replay_save_clock; + bool replay_save_show_notification = false; + + AudioPlayer audio_player; }; }
\ No newline at end of file diff --git a/include/Process.hpp b/include/Process.hpp index 125a880..072ef58 100644 --- a/include/Process.hpp +++ b/include/Process.hpp @@ -1,6 +1,7 @@ #pragma once #include <sys/types.h> +#include <string> namespace gsr { enum class GsrMode { @@ -12,8 +13,13 @@ namespace gsr { // Arguments ending with NULL bool exec_program_daemonized(const char **args); - // Arguments ending with NULL - pid_t exec_program(const char **args); - // |output_buffer| should be at least PATH_MAX in size - bool read_cmdline_arg0(const char *filepath, char *output_buffer); + // Arguments ending with NULL. |read_fd| can be NULL + pid_t exec_program(const char **args, int *read_fd); + // Arguments ending with NULL. Returns the exit status of the program or -1 on error + int exec_program_get_stdout(const char **args, std::string &result); + // Arguments ending with NULL. Returns the exit status of the program or -1 on error. + // This works the same as |exec_program_get_stdout|, except on flatpak where this runs the program on the + // host machine with flatpak-spawn --host + int exec_program_on_host_get_stdout(const char **args, std::string &result); + pid_t pidof(const char *process_name, pid_t ignore_pid); }
\ No newline at end of file diff --git a/include/Rpc.hpp b/include/Rpc.hpp new file mode 100644 index 0000000..d6db218 --- /dev/null +++ b/include/Rpc.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include <stddef.h> +#include <functional> +#include <unordered_map> +#include <string> + +typedef struct _IO_FILE FILE; + +namespace gsr { + using RpcCallback = std::function<void(const std::string &name)>; + + class Rpc { + public: + Rpc() = default; + Rpc(const Rpc&) = delete; + Rpc& operator=(const Rpc&) = delete; + ~Rpc(); + + bool create(const char *name); + bool open(const char *name); + bool write(const char *str, size_t size); + void poll(); + + bool add_handler(const std::string &name, RpcCallback callback); + private: + bool open_filepath(const char *filepath); + private: + int fd = 0; + FILE *file = nullptr; + std::string fifo_filepath; + std::unordered_map<std::string, RpcCallback> handlers_by_name; + }; +}
\ No newline at end of file diff --git a/include/Theme.hpp b/include/Theme.hpp index 23bcbb7..185bcdc 100644 --- a/include/Theme.hpp +++ b/include/Theme.hpp @@ -8,6 +8,7 @@ #include <string> namespace gsr { + struct Config; struct GsrInfo; struct Theme { @@ -26,6 +27,7 @@ namespace gsr { mgl::Texture combobox_arrow_texture; mgl::Texture settings_texture; + mgl::Texture settings_small_texture; mgl::Texture folder_texture; mgl::Texture up_arrow_texture; mgl::Texture replay_button_texture; @@ -56,7 +58,7 @@ namespace gsr { mgl::Color text_color = mgl::Color(255, 255, 255); }; - bool init_color_theme(const GsrInfo &gsr_info); + bool init_color_theme(const Config &config, const GsrInfo &gsr_info); void deinit_color_theme(); ColorTheme& get_color_theme(); }
\ No newline at end of file diff --git a/include/WindowUtils.hpp b/include/WindowUtils.hpp new file mode 100644 index 0000000..99b45e9 --- /dev/null +++ b/include/WindowUtils.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include <mglpp/system/vec.hpp> +#include <string> +#include <vector> +#include <optional> +#include <X11/Xlib.h> + +namespace gsr { + enum class WindowCaptureType { + FOCUSED, + CURSOR + }; + + struct Monitor { + mgl::vec2i position; + mgl::vec2i size; + }; + + std::optional<std::string> get_window_title(Display *dpy, Window window); + Window get_focused_window(Display *dpy, WindowCaptureType cap_type); + std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type); + std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window); + std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window); + 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); +}
\ No newline at end of file diff --git a/include/gui/Button.hpp b/include/gui/Button.hpp index bc1dd94..eb68e99 100644 --- a/include/gui/Button.hpp +++ b/include/gui/Button.hpp @@ -5,6 +5,7 @@ #include <mglpp/graphics/Color.hpp> #include <mglpp/graphics/Text.hpp> +#include <mglpp/graphics/Sprite.hpp> namespace gsr { class Button : public Widget { @@ -20,15 +21,21 @@ namespace gsr { mgl::vec2f get_size() override; void set_border_scale(float scale); + void set_bg_hover_color(mgl::Color color); + void set_icon(mgl::Texture *texture); const std::string& get_text() const; void set_text(std::string str); std::function<void()> on_click; private: + void scale_sprite_to_button_size(); + private: mgl::vec2f size; mgl::Color bg_color; + mgl::Color bg_hover_color; mgl::Text text; + mgl::Sprite sprite; float border_scale = 0.0015f; }; }
\ No newline at end of file diff --git a/include/gui/GlobalSettingsPage.hpp b/include/gui/GlobalSettingsPage.hpp new file mode 100644 index 0000000..580e943 --- /dev/null +++ b/include/gui/GlobalSettingsPage.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include "StaticPage.hpp" +#include "../GsrInfo.hpp" +#include "../Config.hpp" + +#include <functional> +#include <mglpp/window/Event.hpp> + +namespace gsr { + class Overlay; + class GsrPage; + class PageStack; + class ScrollablePage; + class Subsection; + class RadioButton; + class Button; + class List; + class CustomRendererWidget; + + enum ConfigureHotkeyType { + NONE, + REPLAY_START_STOP, + REPLAY_SAVE, + RECORD_START_STOP, + RECORD_PAUSE_UNPAUSE, + STREAM_START_STOP, + SHOW_HIDE + }; + + class GlobalSettingsPage : public StaticPage { + public: + GlobalSettingsPage(Overlay *overlay, const GsrInfo *gsr_info, Config &config, PageStack *page_stack); + GlobalSettingsPage(const GlobalSettingsPage&) = delete; + GlobalSettingsPage& operator=(const GlobalSettingsPage&) = delete; + + void load(); + void save(); + void on_navigate_away_from_page() override; + + bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override; + + std::function<void(bool enable, int exit_status)> on_startup_changed; + std::function<void(const char *reason)> on_click_exit_program_button; + std::function<void(const char *hotkey_option)> on_keyboard_hotkey_changed; + std::function<void(const char *hotkey_option)> on_joystick_hotkey_changed; + private: + void load_hotkeys(); + + std::unique_ptr<Subsection> create_appearance_subsection(ScrollablePage *parent_page); + std::unique_ptr<Subsection> create_startup_subsection(ScrollablePage *parent_page); + std::unique_ptr<RadioButton> create_enable_keyboard_hotkeys_button(); + std::unique_ptr<RadioButton> create_enable_joystick_hotkeys_button(); + std::unique_ptr<List> create_show_hide_hotkey_options(); + std::unique_ptr<List> create_replay_hotkey_options(); + std::unique_ptr<List> create_record_hotkey_options(); + std::unique_ptr<List> create_stream_hotkey_options(); + std::unique_ptr<List> create_hotkey_control_buttons(); + std::unique_ptr<Subsection> create_hotkey_subsection(ScrollablePage *parent_page); + std::unique_ptr<Button> create_exit_program_button(); + std::unique_ptr<Button> create_go_back_to_old_ui_button(); + std::unique_ptr<Subsection> create_application_options_subsection(ScrollablePage *parent_page); + std::unique_ptr<Subsection> create_application_info_subsection(ScrollablePage *parent_page); + void add_widgets(); + + Button* configure_hotkey_get_button_by_active_type(); + ConfigHotkey* configure_hotkey_get_config_by_active_type(); + void for_each_config_hotkey(std::function<void(ConfigHotkey *config_hotkey)> callback); + void configure_hotkey_start(ConfigureHotkeyType hotkey_type); + void configure_hotkey_cancel(); + void configure_hotkey_stop_and_save(); + private: + Overlay *overlay = nullptr; + Config &config; + const GsrInfo *gsr_info = nullptr; + + GsrPage *content_page_ptr = nullptr; + PageStack *page_stack = nullptr; + RadioButton *tint_color_radio_button_ptr = nullptr; + RadioButton *startup_radio_button_ptr = nullptr; + RadioButton *enable_keyboard_hotkeys_radio_button_ptr = nullptr; + RadioButton *enable_joystick_hotkeys_radio_button_ptr = nullptr; + + Button *turn_replay_on_off_button_ptr = nullptr; + Button *save_replay_button_ptr = nullptr; + Button *start_stop_recording_button_ptr = nullptr; + Button *pause_unpause_recording_button_ptr = nullptr; + Button *start_stop_streaming_button_ptr = nullptr; + Button *show_hide_button_ptr = nullptr; + + ConfigHotkey configure_config_hotkey; + ConfigureHotkeyType configure_hotkey_type = ConfigureHotkeyType::NONE; + + CustomRendererWidget *hotkey_overlay_ptr = nullptr; + std::string hotkey_configure_action_name; + }; +}
\ No newline at end of file diff --git a/include/gui/RadioButton.hpp b/include/gui/RadioButton.hpp index a009eab..16d638e 100644 --- a/include/gui/RadioButton.hpp +++ b/include/gui/RadioButton.hpp @@ -27,7 +27,8 @@ namespace gsr { mgl::vec2f get_size() override; - std::function<void(const std::string &text, const std::string &id)> on_selection_changed; + // Return false to revert the change + std::function<bool(const std::string &text, const std::string &id)> on_selection_changed; private: void update_if_dirty(); private: diff --git a/include/gui/SettingsPage.hpp b/include/gui/SettingsPage.hpp index f18ff65..ad5f05a 100644 --- a/include/gui/SettingsPage.hpp +++ b/include/gui/SettingsPage.hpp @@ -10,6 +10,8 @@ #include "../GsrInfo.hpp" #include "../Config.hpp" +#include <functional> + namespace gsr { class GsrPage; class PageStack; @@ -25,17 +27,19 @@ namespace gsr { STREAM }; - SettingsPage(Type type, const GsrInfo &gsr_info, Config &config, PageStack *page_stack); + SettingsPage(Type type, const GsrInfo *gsr_info, Config &config, PageStack *page_stack); SettingsPage(const SettingsPage&) = delete; SettingsPage& operator=(const SettingsPage&) = delete; - void load(const GsrInfo &gsr_info); + void load(); void save(); void on_navigate_away_from_page() override; + + std::function<void()> on_config_changed; private: std::unique_ptr<RadioButton> create_view_radio_button(); - std::unique_ptr<ComboBox> create_record_area_box(const GsrInfo &gsr_info); - std::unique_ptr<Widget> create_record_area(const GsrInfo &gsr_info); + std::unique_ptr<ComboBox> create_record_area_box(); + std::unique_ptr<Widget> create_record_area(); std::unique_ptr<List> create_select_window(); std::unique_ptr<Entry> create_area_width_entry(); std::unique_ptr<Entry> create_area_height_entry(); @@ -48,7 +52,7 @@ namespace gsr { std::unique_ptr<CheckBox> create_restore_portal_session_checkbox(); std::unique_ptr<List> create_restore_portal_session_section(); std::unique_ptr<Widget> create_change_video_resolution_section(); - std::unique_ptr<Widget> create_capture_target(const GsrInfo &gsr_info); + std::unique_ptr<Widget> create_capture_target(); std::unique_ptr<ComboBox> create_audio_device_selection_combobox(); std::unique_ptr<Button> create_remove_audio_device_button(List *audio_device_list_ptr); std::unique_ptr<List> create_audio_device(); @@ -65,13 +69,13 @@ namespace gsr { std::unique_ptr<Widget> create_audio_track_section(); std::unique_ptr<Widget> create_audio_section(); std::unique_ptr<List> create_video_quality_box(); - std::unique_ptr<Entry> create_video_bitrate_entry(); + std::unique_ptr<List> create_video_bitrate_entry(); std::unique_ptr<List> create_video_bitrate(); std::unique_ptr<ComboBox> create_color_range_box(); std::unique_ptr<List> create_color_range(); std::unique_ptr<List> create_video_quality_section(); - std::unique_ptr<ComboBox> create_video_codec_box(const GsrInfo &gsr_info); - std::unique_ptr<List> create_video_codec(const GsrInfo &gsr_info); + std::unique_ptr<ComboBox> create_video_codec_box(); + std::unique_ptr<List> create_video_codec(); std::unique_ptr<ComboBox> create_audio_codec_box(); std::unique_ptr<List> create_audio_codec(); std::unique_ptr<Entry> create_framerate_entry(); @@ -80,24 +84,28 @@ namespace gsr { std::unique_ptr<List> create_framerate_mode(); std::unique_ptr<List> create_framerate_section(); std::unique_ptr<Widget> create_record_cursor_section(); - std::unique_ptr<Widget> create_video_section(const GsrInfo &gsr_info); - std::unique_ptr<Widget> create_settings(const GsrInfo &gsr_info); - void add_widgets(const GsrInfo &gsr_info); + std::unique_ptr<Widget> create_video_section(); + std::unique_ptr<Widget> create_settings(); + void add_widgets(); - void add_page_specific_widgets(const GsrInfo &gsr_info); + void add_page_specific_widgets(); std::unique_ptr<List> create_save_directory(const char *label); std::unique_ptr<ComboBox> create_container_box(); std::unique_ptr<List> create_container_section(); - std::unique_ptr<Entry> create_replay_time_entry(); + std::unique_ptr<List> create_replay_time_entry(); std::unique_ptr<List> create_replay_time(); - std::unique_ptr<RadioButton> create_start_replay_automatically(const GsrInfo &gsr_info); - std::unique_ptr<CheckBox> create_save_replay_in_game_folder(const GsrInfo &gsr_info); - std::unique_ptr<Label> create_estimated_file_size(); - void update_estimated_file_size(); - std::unique_ptr<CheckBox> create_save_recording_in_game_folder(const GsrInfo &gsr_info); - void add_replay_widgets(const GsrInfo &gsr_info); - void add_record_widgets(const GsrInfo &gsr_info); + std::unique_ptr<RadioButton> create_start_replay_automatically(); + std::unique_ptr<CheckBox> create_save_replay_in_game_folder(); + std::unique_ptr<CheckBox> create_restart_replay_on_save(); + std::unique_ptr<Label> create_estimated_replay_file_size(); + void update_estimated_replay_file_size(); + void update_replay_time_text(); + std::unique_ptr<CheckBox> create_save_recording_in_game_folder(); + std::unique_ptr<Label> create_estimated_record_file_size(); + void update_estimated_record_file_size(); + void add_replay_widgets(); + void add_record_widgets(); std::unique_ptr<ComboBox> create_streaming_service_box(); std::unique_ptr<List> create_streaming_service_section(); @@ -105,13 +113,13 @@ namespace gsr { std::unique_ptr<List> create_stream_url_section(); std::unique_ptr<ComboBox> create_stream_container_box(); std::unique_ptr<List> create_stream_container_section(); - void add_stream_widgets(const GsrInfo &gsr_info); + void add_stream_widgets(); - void load_audio_tracks(const RecordOptions &record_options, const GsrInfo &gsr_info); - void load_common(RecordOptions &record_options, const GsrInfo &gsr_info); - void load_replay(const GsrInfo &gsr_info); - void load_record(const GsrInfo &gsr_info); - void load_stream(const GsrInfo &gsr_info); + void load_audio_tracks(const RecordOptions &record_options); + void load_common(RecordOptions &record_options); + void load_replay(); + void load_record(); + void load_stream(); void save_common(RecordOptions &record_options); void save_replay(); @@ -120,8 +128,10 @@ namespace gsr { private: Type type; Config &config; + const GsrInfo *gsr_info = nullptr; std::vector<AudioDevice> audio_devices; std::vector<std::string> application_audio; + SupportedCaptureOptions capture_options; GsrPage *content_page_ptr = nullptr; ScrollablePage *settings_scrollable_page_ptr = nullptr; @@ -162,6 +172,7 @@ namespace gsr { List *stream_url_list_ptr = nullptr; List *container_list_ptr = nullptr; CheckBox *save_replay_in_game_folder_ptr = nullptr; + CheckBox *restart_replay_on_save = nullptr; Label *estimated_file_size_ptr = nullptr; CheckBox *show_replay_started_notification_checkbox_ptr = nullptr; CheckBox *show_replay_stopped_notification_checkbox_ptr = nullptr; @@ -176,10 +187,9 @@ namespace gsr { Entry *youtube_stream_key_entry_ptr = nullptr; Entry *stream_url_entry_ptr = nullptr; Entry *replay_time_entry_ptr = nullptr; + Label *replay_time_label_ptr = nullptr; RadioButton *turn_on_replay_automatically_mode_ptr = nullptr; PageStack *page_stack = nullptr; - - mgl::Text settings_title_text; }; }
\ No newline at end of file diff --git a/include/gui/Utils.hpp b/include/gui/Utils.hpp index 6963bc5..35b2bb7 100644 --- a/include/gui/Utils.hpp +++ b/include/gui/Utils.hpp @@ -15,4 +15,5 @@ namespace gsr { void draw_rectangle_outline(mgl::Window &window, mgl::vec2f pos, mgl::vec2f size, mgl::Color color, float border_size); double get_frame_delta_seconds(); void set_frame_delta_seconds(double frame_delta); + mgl::vec2f scale_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to); }
\ No newline at end of file diff --git a/meson.build b/meson.build index 056fea3..fc2f4f4 100644 --- a/meson.build +++ b/meson.build @@ -1,4 +1,4 @@ -project('gsr-ui', ['c', 'cpp'], version : '1.0.0', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends') +project('gsr-ui', ['c', 'cpp'], version : '1.1.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']) @@ -27,60 +27,72 @@ src = [ 'src/gui/CustomRendererWidget.cpp', 'src/gui/FileChooser.cpp', 'src/gui/SettingsPage.cpp', + 'src/gui/GlobalSettingsPage.cpp', 'src/gui/GsrPage.cpp', 'src/gui/Subsection.cpp', 'src/Utils.cpp', + 'src/WindowUtils.cpp', 'src/Config.cpp', 'src/GsrInfo.cpp', 'src/Process.cpp', 'src/Overlay.cpp', 'src/GlobalHotkeysX11.cpp', 'src/GlobalHotkeysLinux.cpp', + 'src/GlobalHotkeysJoystick.cpp', + 'src/AudioPlayer.cpp', + 'src/Hotplug.cpp', + 'src/Rpc.cpp', 'src/main.cpp', ] mglpp_proj = subproject('mglpp') mglpp_dep = mglpp_proj.get_variable('mglpp_dep') -dep = [ - mglpp_dep, - dependency('xcomposite'), -] - prefix = get_option('prefix') datadir = get_option('datadir') gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui') +add_project_arguments('-DGSR_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp']) +add_project_arguments('-DGSR_FLATPAK_VERSION="5.1.4"', language: ['c', 'cpp']) + executable( meson.project_name(), src, install : true, - dependencies : dep, + dependencies : [ + mglpp_dep, + dependency('threads'), + dependency('xcomposite'), + dependency('xfixes'), + dependency('xi'), + dependency('xcursor'), + dependency('libpulse-simple'), + ], cpp_args : '-DGSR_UI_RESOURCES_PATH="' + gsr_ui_resources_path + '"', ) executable( - 'gsr-window-name', - ['tools/gsr-window-name/main.c'], - install : true, - dependencies : [dependency('x11')], + 'gsr-global-hotkeys', + [ + 'tools/gsr-global-hotkeys/hotplug.c', + 'tools/gsr-global-hotkeys/keyboard_event.c', + 'tools/gsr-global-hotkeys/keys.c', + 'tools/gsr-global-hotkeys/main.c' + ], + c_args : '-fstack-protector-all', + install : true ) executable( - 'gsr-global-hotkeys', - ['tools/gsr-global-hotkeys/main.c'], - install : true, - dependencies : [ - dependency('libevdev'), - dependency('libudev'), - dependency('libinput'), - dependency('xkbcommon') + 'gsr-ui-cli', + [ + 'tools/gsr-ui-cli/main.c' ], + install : true ) install_subdir('images', install_dir : gsr_ui_resources_path) install_subdir('fonts', install_dir : gsr_ui_resources_path) -install_subdir('scripts', install_dir : gsr_ui_resources_path, install_mode : 'rwxr-xr-x') if get_option('systemd') == true install_data(files('extra/gpu-screen-recorder-ui.service'), install_dir : 'lib/systemd/user') @@ -88,4 +100,4 @@ endif if get_option('capabilities') == true meson.add_install_script('meson_post_install.sh') -endif
\ No newline at end of file +endif diff --git a/project.conf b/project.conf index 22bbc78..cbe812c 100644 --- a/project.conf +++ b/project.conf @@ -1,7 +1,7 @@ [package] name = "gsr-ui" type = "executable" -version = "0.1.0" +version = "1.1.7" platforms = ["posix"] [lang.cpp] @@ -11,4 +11,8 @@ version = "c++17" ignore_dirs = ["build", "tools"] [dependencies] -xcomposite = ">=0"
\ No newline at end of file +xcomposite = ">=0" +xfixes = ">=0" +xi = ">=0" +xcursor = ">=1" +libpulse-simple = ">=0"
\ No newline at end of file diff --git a/scripts/notify-saved-name.sh b/scripts/notify-saved-name.sh deleted file mode 100755 index c8ca399..0000000 --- a/scripts/notify-saved-name.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -[ "$GSR_SHOW_SAVED_NOTIFICATION" != "1" ] && exit 0 - -filepath="$1" -type="$2" - -file_name="$(basename "$filepath")" - -case "$type" in - "regular") - gsr-notify --text "Saved recording to '$file_name'" --timeout 3.0 --icon record --bg-color "$GSR_NOTIFY_BG_COLOR" - ;; - "replay") - gsr-notify --text "Saved replay to '$file_name'" --timeout 3.0 --icon replay --bg-color "$GSR_NOTIFY_BG_COLOR" - ;; -esac
\ No newline at end of file diff --git a/scripts/save-video-in-game-folder.sh b/scripts/save-video-in-game-folder.sh deleted file mode 100755 index 3d07d6a..0000000 --- a/scripts/save-video-in-game-folder.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -filepath="$1" -type="$2" - -file_name="$(basename "$filepath")" -file_dir="$(dirname "$filepath")" - -game_name=$(gsr-window-name focused || echo "Game") -game_name="$(echo "$game_name" | tr '/\\' '_')" -target_dir="$file_dir/$game_name" -new_filepath="$target_dir/$file_name" - -mkdir -p "$target_dir" -mv "$filepath" "$new_filepath" - -[ "$GSR_SHOW_SAVED_NOTIFICATION" != "1" ] && exit 0 - -case "$type" in - "regular") - gsr-notify --text "Saved recording to '$game_name/$file_name'" --timeout 3.0 --icon record --bg-color "$GSR_NOTIFY_BG_COLOR" - ;; - "replay") - gsr-notify --text "Saved replay to '$game_name/$file_name'" --timeout 3.0 --icon replay --bg-color "$GSR_NOTIFY_BG_COLOR" - ;; -esac
\ No newline at end of file diff --git a/src/AudioPlayer.cpp b/src/AudioPlayer.cpp new file mode 100644 index 0000000..cb6d1c7 --- /dev/null +++ b/src/AudioPlayer.cpp @@ -0,0 +1,86 @@ +#include "../include/AudioPlayer.hpp" + +#include <unistd.h> +#include <fcntl.h> +#include <stdio.h> +#include <string.h> + +#include <pulse/simple.h> +#include <pulse/error.h> + +#define BUFSIZE 4096 + +namespace gsr { + AudioPlayer::~AudioPlayer() { + if(thread.joinable()) { + stop_playing_audio = true; + thread.join(); + } + + if(audio_file_fd > 0) + close(audio_file_fd); + } + + bool AudioPlayer::play(const char *filepath) { + if(thread.joinable()) { + stop_playing_audio = true; + thread.join(); + } + + stop_playing_audio = false; + audio_file_fd = open(filepath, O_RDONLY); + if(audio_file_fd == -1) + return false; + + thread = std::thread([this]() { + pa_sample_spec ss; + ss.format = PA_SAMPLE_S16LE; + ss.rate = 48000; + ss.channels = 2; + + pa_simple *s = NULL; + int error; + + /* Create a new playback stream */ + if(!(s = pa_simple_new(NULL, "gsr-ui-audio-playback", PA_STREAM_PLAYBACK, NULL, "playback", &ss, NULL, NULL, &error))) { + fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error)); + goto finish; + } + + uint8_t buf[BUFSIZE]; + for(;;) { + ssize_t r; + + if(stop_playing_audio) + goto finish; + + if((r = read(audio_file_fd, buf, sizeof(buf))) <= 0) { + if(r == 0) /* EOF */ + break; + + fprintf(stderr, __FILE__": read() failed: %s\n", strerror(errno)); + goto finish; + } + + if(pa_simple_write(s, buf, (size_t) r, &error) < 0) { + fprintf(stderr, __FILE__": pa_simple_write() failed: %s\n", pa_strerror(error)); + goto finish; + } + } + + if(pa_simple_drain(s, &error) < 0) { + fprintf(stderr, __FILE__": pa_simple_drain() failed: %s\n", pa_strerror(error)); + goto finish; + } + + finish: + if(s) + pa_simple_free(s); + + close(audio_file_fd); + audio_file_fd = -1; + }); + + return true; + } +}
\ No newline at end of file diff --git a/src/Config.cpp b/src/Config.cpp index 112688a..b9e4cb7 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -1,38 +1,53 @@ #include "../include/Config.hpp" #include "../include/Utils.hpp" #include "../include/GsrInfo.hpp" +#include "../include/GlobalHotkeys.hpp" #include <variant> #include <limits.h> #include <inttypes.h> #include <libgen.h> +#include <mglpp/window/Keyboard.hpp> #define FORMAT_I32 "%" PRIi32 #define FORMAT_I64 "%" PRIi64 #define FORMAT_U32 "%" PRIu32 -#define CONFIG_FILE_VERSION 1 - namespace gsr { - Config::Config(const GsrInfo &gsr_info) { + bool ConfigHotkey::operator==(const ConfigHotkey &other) const { + return key == other.key && modifiers == other.modifiers; + } + + bool ConfigHotkey::operator!=(const ConfigHotkey &other) const { + return !operator==(other); + } + + Config::Config(const SupportedCaptureOptions &capture_options) { const std::string default_save_directory = get_videos_dir(); + streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT}; streaming_config.record_options.video_quality = "custom"; streaming_config.record_options.audio_tracks.push_back("default_output"); streaming_config.record_options.video_bitrate = 15000; + record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT}; + record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT}; record_config.save_directory = default_save_directory; record_config.record_options.audio_tracks.push_back("default_output"); record_config.record_options.video_bitrate = 45000; + replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT}; + replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT}; replay_config.record_options.video_quality = "custom"; replay_config.save_directory = default_save_directory; replay_config.record_options.audio_tracks.push_back("default_output"); replay_config.record_options.video_bitrate = 45000; - if(!gsr_info.supported_capture_options.monitors.empty()) { - streaming_config.record_options.record_area_option = gsr_info.supported_capture_options.monitors.front().name; - record_config.record_options.record_area_option = gsr_info.supported_capture_options.monitors.front().name; - replay_config.record_options.record_area_option = gsr_info.supported_capture_options.monitors.front().name; + main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT}; + + if(!capture_options.monitors.empty()) { + streaming_config.record_options.record_area_option = capture_options.monitors.front().name; + record_config.record_options.record_area_option = capture_options.monitors.front().name; + replay_config.record_options.record_area_option = capture_options.monitors.front().name; } } @@ -49,6 +64,10 @@ 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.hotkeys_enable_option", &config.main_config.hotkeys_enable_option}, + {"main.joystick_hotkeys_enable_option", &config.main_config.joystick_hotkeys_enable_option}, + {"main.tint_color", &config.main_config.tint_color}, + {"main.show_hide_hotkey", &config.main_config.show_hide_hotkey}, {"streaming.record_options.record_area_option", &config.streaming_config.record_options.record_area_option}, {"streaming.record_options.record_area_width", &config.streaming_config.record_options.record_area_width}, @@ -77,7 +96,7 @@ namespace gsr { {"streaming.twitch.key", &config.streaming_config.twitch.stream_key}, {"streaming.custom.url", &config.streaming_config.custom.url}, {"streaming.custom.container", &config.streaming_config.custom.container}, - {"streaming.start_stop_recording_hotkey", &config.streaming_config.start_stop_recording_hotkey}, + {"streaming.start_stop_hotkey", &config.streaming_config.start_stop_hotkey}, {"record.record_options.record_area_option", &config.record_config.record_options.record_area_option}, {"record.record_options.record_area_width", &config.record_config.record_options.record_area_width}, @@ -104,8 +123,8 @@ namespace gsr { {"record.show_video_saved_notifications", &config.record_config.show_video_saved_notifications}, {"record.save_directory", &config.record_config.save_directory}, {"record.container", &config.record_config.container}, - {"record.start_stop_recording_hotkey", &config.record_config.start_stop_recording_hotkey}, - {"record.pause_unpause_recording_hotkey", &config.record_config.pause_unpause_recording_hotkey}, + {"record.start_stop_hotkey", &config.record_config.start_stop_hotkey}, + {"record.pause_unpause_hotkey", &config.record_config.pause_unpause_hotkey}, {"replay.record_options.record_area_option", &config.replay_config.record_options.record_area_option}, {"replay.record_options.record_area_width", &config.replay_config.record_options.record_area_width}, @@ -129,18 +148,51 @@ namespace gsr { {"replay.record_options.restore_portal_session", &config.replay_config.record_options.restore_portal_session}, {"replay.turn_on_replay_automatically_mode", &config.replay_config.turn_on_replay_automatically_mode}, {"replay.save_video_in_game_folder", &config.replay_config.save_video_in_game_folder}, + {"replay.restart_replay_on_save", &config.replay_config.restart_replay_on_save}, {"replay.show_replay_started_notifications", &config.replay_config.show_replay_started_notifications}, {"replay.show_replay_stopped_notifications", &config.replay_config.show_replay_stopped_notifications}, {"replay.show_replay_saved_notifications", &config.replay_config.show_replay_saved_notifications}, {"replay.save_directory", &config.replay_config.save_directory}, {"replay.container", &config.replay_config.container}, {"replay.time", &config.replay_config.replay_time}, - {"replay.start_stop_recording_hotkey", &config.replay_config.start_stop_recording_hotkey}, - {"replay.save_recording_hotkey", &config.replay_config.save_recording_hotkey} + {"replay.start_stop_hotkey", &config.replay_config.start_stop_hotkey}, + {"replay.save_hotkey", &config.replay_config.save_hotkey} }; } - std::optional<Config> read_config(const GsrInfo &gsr_info) { + bool Config::operator==(const Config &other) { + const auto config_options = get_config_options(*this); + const auto config_options_other = get_config_options(const_cast<Config&>(other)); + for(auto it : config_options) { + auto it_other = config_options_other.find(it.first); + if(it_other == config_options_other.end() || it_other->second.index() != it.second.index()) + return false; + + if(std::holds_alternative<bool*>(it.second)) { + if(*std::get<bool*>(it.second) != *std::get<bool*>(it_other->second)) + return false; + } else if(std::holds_alternative<std::string*>(it.second)) { + if(*std::get<std::string*>(it.second) != *std::get<std::string*>(it_other->second)) + return false; + } else if(std::holds_alternative<int32_t*>(it.second)) { + if(*std::get<int32_t*>(it.second) != *std::get<int32_t*>(it_other->second)) + return false; + } else if(std::holds_alternative<ConfigHotkey*>(it.second)) { + if(*std::get<ConfigHotkey*>(it.second) != *std::get<ConfigHotkey*>(it_other->second)) + return false; + } else if(std::holds_alternative<std::vector<std::string>*>(it.second)) { + if(*std::get<std::vector<std::string>*>(it.second) != *std::get<std::vector<std::string>*>(it_other->second)) + return false; + } + } + return true; + } + + bool Config::operator!=(const Config &other) { + return !operator==(other); + } + + std::optional<Config> read_config(const SupportedCaptureOptions &capture_options) { std::optional<Config> config; const std::string config_path = get_config_dir() + "/config_ui"; @@ -150,7 +202,7 @@ namespace gsr { return config; } - config = Config(gsr_info); + config = Config(capture_options); config->streaming_config.record_options.audio_tracks.clear(); config->record_config.record_options.audio_tracks.clear(); config->replay_config.record_options.audio_tracks.clear(); @@ -185,9 +237,9 @@ namespace gsr { } else if(std::holds_alternative<ConfigHotkey*>(it->second)) { std::string value_str(key_value->value); ConfigHotkey *config_hotkey = std::get<ConfigHotkey*>(it->second); - if(sscanf(value_str.c_str(), FORMAT_I64 " " FORMAT_U32, &config_hotkey->keysym, &config_hotkey->modifiers) != 2) { + if(sscanf(value_str.c_str(), FORMAT_I64 " " FORMAT_U32, &config_hotkey->key, &config_hotkey->modifiers) != 2) { fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key_value->key.size(), key_value->key.data()); - config_hotkey->keysym = 0; + config_hotkey->key = 0; config_hotkey->modifiers = 0; } } else if(std::holds_alternative<std::vector<std::string>*>(it->second)) { @@ -198,7 +250,7 @@ namespace gsr { return true; }); - if(config->main_config.config_file_version != CONFIG_FILE_VERSION) { + if(config->main_config.config_file_version != GSR_CONFIG_FILE_VERSION) { fprintf(stderr, "Info: the config file is outdated, resetting it\n"); config = std::nullopt; } @@ -207,7 +259,7 @@ namespace gsr { } void save_config(Config &config) { - config.main_config.config_file_version = CONFIG_FILE_VERSION; + config.main_config.config_file_version = GSR_CONFIG_FILE_VERSION; const std::string config_path = get_config_dir() + "/config_ui"; @@ -236,7 +288,7 @@ namespace gsr { fprintf(file, "%.*s " FORMAT_I32 "\n", (int)it.first.size(), it.first.data(), *std::get<int32_t*>(it.second)); } else if(std::holds_alternative<ConfigHotkey*>(it.second)) { const ConfigHotkey *config_hotkey = std::get<ConfigHotkey*>(it.second); - fprintf(file, "%.*s " FORMAT_I64 " " FORMAT_U32 "\n", (int)it.first.size(), it.first.data(), config_hotkey->keysym, config_hotkey->modifiers); + fprintf(file, "%.*s " FORMAT_I64 " " FORMAT_U32 "\n", (int)it.first.size(), it.first.data(), config_hotkey->key, config_hotkey->modifiers); } else if(std::holds_alternative<std::vector<std::string>*>(it.second)) { std::vector<std::string> *array = std::get<std::vector<std::string>*>(it.second); for(const std::string &value : *array) { diff --git a/src/GlobalHotkeysJoystick.cpp b/src/GlobalHotkeysJoystick.cpp new file mode 100644 index 0000000..dfe1e6f --- /dev/null +++ b/src/GlobalHotkeysJoystick.cpp @@ -0,0 +1,243 @@ +#include "../include/GlobalHotkeysJoystick.hpp" +#include <string.h> +#include <errno.h> +#include <fcntl.h> +#include <unistd.h> +#include <sys/eventfd.h> + +namespace gsr { + static constexpr double double_click_timeout_seconds = 0.33; + + // Returns -1 on error + static int get_js_dev_input_id_from_filepath(const char *dev_input_filepath) { + if(strncmp(dev_input_filepath, "/dev/input/js", 13) != 0) + return -1; + + int dev_input_id = -1; + if(sscanf(dev_input_filepath + 13, "%d", &dev_input_id) == 1) + return dev_input_id; + return -1; + } + + GlobalHotkeysJoystick::~GlobalHotkeysJoystick() { + if(event_fd > 0) { + const uint64_t exit = 1; + write(event_fd, &exit, sizeof(exit)); + } + + if(read_thread.joinable()) + read_thread.join(); + + if(event_fd > 0) + close(event_fd); + + for(int i = 0; i < num_poll_fd; ++i) { + close(poll_fd[i].fd); + } + } + + bool GlobalHotkeysJoystick::start() { + if(num_poll_fd > 0) + return false; + + event_fd = eventfd(0, 0); + if(event_fd <= 0) + return false; + + event_index = num_poll_fd; + poll_fd[num_poll_fd] = { + event_fd, + POLLIN, + 0 + }; + extra_data[num_poll_fd] = { + -1 + }; + ++num_poll_fd; + + if(!hotplug.start()) { + fprintf(stderr, "Warning: failed to setup hotplugging\n"); + } else { + hotplug_poll_index = num_poll_fd; + poll_fd[num_poll_fd] = { + hotplug.steal_fd(), + POLLIN, + 0 + }; + extra_data[num_poll_fd] = { + -1 + }; + ++num_poll_fd; + } + + char dev_input_path[128]; + for(int i = 0; i < 8; ++i) { + snprintf(dev_input_path, sizeof(dev_input_path), "/dev/input/js%d", i); + add_device(dev_input_path, false); + } + + if(num_poll_fd == 0) + fprintf(stderr, "Info: no joysticks found, assuming they might be connected later\n"); + + read_thread = std::thread(&GlobalHotkeysJoystick::read_events, this); + return true; + } + + bool GlobalHotkeysJoystick::bind_action(const std::string &id, GlobalHotkeyCallback callback) { + if(num_poll_fd == 0) + return false; + return bound_actions_by_id.insert(std::make_pair(id, std::move(callback))).second; + } + + void GlobalHotkeysJoystick::poll_events() { + if(num_poll_fd == 0) + return; + + if(save_replay) { + save_replay = false; + auto it = bound_actions_by_id.find("save_replay"); + if(it != bound_actions_by_id.end()) + it->second("save_replay"); + } + } + + void GlobalHotkeysJoystick::read_events() { + js_event event; + while(poll(poll_fd, num_poll_fd, -1) > 0) { + for(int i = 0; i < num_poll_fd; ++i) { + if(poll_fd[i].revents & (POLLHUP|POLLERR|POLLNVAL)) { + if(i == event_index) + goto done; + + if(remove_poll_fd(i)) + --i; // This item was removed so we want to repeat the same index to continue to the next item + + continue; + } + + if(!(poll_fd[i].revents & POLLIN)) + continue; + + if(i == event_index) { + goto done; + } else if(i == hotplug_poll_index) { + hotplug.process_event_data(poll_fd[i].fd, [&](HotplugAction hotplug_action, const char *devname) { + char dev_input_filepath[1024]; + snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/%s", devname); + switch(hotplug_action) { + case HotplugAction::ADD: { + // Cant open the /dev/input device immediately or it fails. + // TODO: Remove this hack when a better solution is found. + usleep(50 * 1000); + add_device(dev_input_filepath); + break; + } + case HotplugAction::REMOVE: { + if(remove_device(dev_input_filepath)) + --i; // This item was removed so we want to repeat the same index to continue to the next item + break; + } + } + }); + } else { + process_js_event(poll_fd[i].fd, event); + } + } + } + + done: + ; + } + + void GlobalHotkeysJoystick::process_js_event(int fd, js_event &event) { + if(read(fd, &event, sizeof(event)) != sizeof(event)) + return; + + if((event.type & JS_EVENT_BUTTON) == 0) + return; + + if(event.number == 8 && event.value == 1) { + const double now = double_click_clock.get_elapsed_time_seconds(); + if(!prev_time_clicked.has_value()) { + prev_time_clicked = now; + return; + } + + if(prev_time_clicked.has_value()) { + const bool double_clicked = (now - prev_time_clicked.value()) < double_click_timeout_seconds; + if(double_clicked) { + save_replay = true; + prev_time_clicked.reset(); + } else { + prev_time_clicked = now; + } + } + } + } + + bool GlobalHotkeysJoystick::add_device(const char *dev_input_filepath, bool print_error) { + if(num_poll_fd >= max_js_poll_fd) { + fprintf(stderr, "Warning: failed to add joystick device %s, too many joysticks have been added\n", dev_input_filepath); + return false; + } + + const int dev_input_id = get_js_dev_input_id_from_filepath(dev_input_filepath); + if(dev_input_id == -1) + return false; + + const int fd = open(dev_input_filepath, O_RDONLY); + if(fd <= 0) { + if(print_error) + fprintf(stderr, "Error: failed to add joystick %s, error: %s\n", dev_input_filepath, strerror(errno)); + return false; + } + + poll_fd[num_poll_fd] = { + fd, + POLLIN, + 0 + }; + + extra_data[num_poll_fd] = { + dev_input_id + }; + + ++num_poll_fd; + fprintf(stderr, "Info: added joystick: %s\n", dev_input_filepath); + return true; + } + + bool GlobalHotkeysJoystick::remove_device(const char *dev_input_filepath) { + const int dev_input_id = get_js_dev_input_id_from_filepath(dev_input_filepath); + if(dev_input_id == -1) + return false; + + const int poll_fd_index = get_poll_fd_index_by_dev_input_id(dev_input_id); + if(poll_fd_index == -1) + return false; + + fprintf(stderr, "Info: removed joystick: %s\n", dev_input_filepath); + return remove_poll_fd(poll_fd_index); + } + + bool GlobalHotkeysJoystick::remove_poll_fd(int index) { + if(index < 0 || index >= num_poll_fd) + return false; + + close(poll_fd[index].fd); + for(int i = index + 1; i < num_poll_fd; ++i) { + poll_fd[i - 1] = poll_fd[i]; + extra_data[i - 1] = extra_data[i]; + } + --num_poll_fd; + return true; + } + + int GlobalHotkeysJoystick::get_poll_fd_index_by_dev_input_id(int dev_input_id) const { + for(int i = 0; i < num_poll_fd; ++i) { + if(dev_input_id == extra_data[i].dev_input_id) + return i; + } + return -1; + } +} diff --git a/src/GlobalHotkeysLinux.cpp b/src/GlobalHotkeysLinux.cpp index b0e8e52..4df6390 100644 --- a/src/GlobalHotkeysLinux.cpp +++ b/src/GlobalHotkeysLinux.cpp @@ -2,22 +2,76 @@ #include <signal.h> #include <sys/wait.h> #include <fcntl.h> +#include <limits.h> #include <string.h> +extern "C" { +#include <mgl/mgl.h> +} +#include <X11/Xlib.h> +#include <linux/input-event-codes.h> + #define PIPE_READ 0 #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"; + } + + static inline uint8_t x11_keycode_to_linux_keycode(uint8_t code) { + return code - 8; + } + + static std::vector<uint8_t> modifiers_to_linux_keys(uint32_t modifiers) { + std::vector<uint8_t> result; + if(modifiers & HOTKEY_MOD_LSHIFT) + result.push_back(KEY_LEFTSHIFT); + if(modifiers & HOTKEY_MOD_RSHIFT) + result.push_back(KEY_RIGHTSHIFT); + if(modifiers & HOTKEY_MOD_LCTRL) + result.push_back(KEY_LEFTCTRL); + if(modifiers & HOTKEY_MOD_RCTRL) + result.push_back(KEY_RIGHTCTRL); + if(modifiers & HOTKEY_MOD_LALT) + result.push_back(KEY_LEFTALT); + if(modifiers & HOTKEY_MOD_RALT) + result.push_back(KEY_RIGHTALT); + if(modifiers & HOTKEY_MOD_LSUPER) + result.push_back(KEY_LEFTMETA); + if(modifiers & HOTKEY_MOD_RSUPER) + result.push_back(KEY_RIGHTMETA); + return result; + } + + static std::string linux_keys_to_command_string(const uint8_t *keys, size_t size) { + std::string result; + for(size_t i = 0; i < size; ++i) { + if(!result.empty()) + result += "+"; + result += std::to_string(keys[i]); + } + return result; + } + + GlobalHotkeysLinux::GlobalHotkeysLinux(GrabType grab_type) : grab_type(grab_type) { for(int i = 0; i < 2; ++i) { - pipes[i] = -1; + read_pipes[i] = -1; + write_pipes[i] = -1; } } GlobalHotkeysLinux::~GlobalHotkeysLinux() { for(int i = 0; i < 2; ++i) { - if(pipes[i] > 0) - close(pipes[i]); + if(read_pipes[i] > 0) + close(read_pipes[i]); + + if(write_pipes[i] > 0) + close(write_pipes[i]); } if(read_file) @@ -31,77 +85,159 @@ 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) + user_homepath = "/tmp"; + if(process_id > 0) return false; - if(pipe(pipes) == -1) + if(pipe(read_pipes) == -1) return false; + if(pipe(write_pipes) == -1) { + for(int i = 0; i < 2; ++i) { + close(read_pipes[i]); + read_pipes[i] = -1; + } + return false; + } + const pid_t pid = vfork(); if(pid == -1) { perror("Failed to vfork"); for(int i = 0; i < 2; ++i) { - close(pipes[i]); - pipes[i] = -1; + close(read_pipes[i]); + close(write_pipes[i]); + read_pipes[i] = -1; + write_pipes[i] = -1; } return false; } else if(pid == 0) { /* child */ - dup2(pipes[PIPE_WRITE], STDOUT_FILENO); + dup2(read_pipes[PIPE_WRITE], STDOUT_FILENO); for(int i = 0; i < 2; ++i) { - close(pipes[i]); + close(read_pipes[i]); } - const char *args[] = { "gsr-global-hotkeys", NULL }; - execvp(args[0], (char* const*)args); - perror("execvp"); + dup2(write_pipes[PIPE_READ], STDIN_FILENO); + for(int i = 0; i < 2; ++i) { + close(write_pipes[i]); + } + + 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, grab_type_arg, nullptr }; + execvp(args[0], (char* const*)args); + } else { + const char *args[] = { "gsr-global-hotkeys", grab_type_arg, nullptr }; + execvp(args[0], (char* const*)args); + } + + perror("gsr-global-hotkeys"); _exit(127); } else { /* parent */ process_id = pid; - close(pipes[PIPE_WRITE]); - pipes[PIPE_WRITE] = -1; - const int fdl = fcntl(pipes[PIPE_READ], F_GETFL); - fcntl(pipes[PIPE_READ], F_SETFL, fdl | O_NONBLOCK); + close(read_pipes[PIPE_WRITE]); + read_pipes[PIPE_WRITE] = -1; + + close(write_pipes[PIPE_READ]); + write_pipes[PIPE_READ] = -1; - read_file = fdopen(pipes[PIPE_READ], "r"); + fcntl(read_pipes[PIPE_READ], F_SETFL, fcntl(read_pipes[PIPE_READ], F_GETFL) | O_NONBLOCK); + read_file = fdopen(read_pipes[PIPE_READ], "r"); if(read_file) - pipes[PIPE_READ] = -1; + read_pipes[PIPE_READ] = -1; else - fprintf(stderr, "fdopen failed, error: %s\n", strerror(errno)); + fprintf(stderr, "fdopen failed for read, error: %s\n", strerror(errno)); } return true; } - bool GlobalHotkeysLinux::bind_action(const std::string &id, GlobalHotkeyCallback callback) { - return bound_actions_by_id.insert(std::make_pair(id, callback)).second; + bool GlobalHotkeysLinux::bind_key_press(Hotkey hotkey, const std::string &id, GlobalHotkeyCallback callback) { + if(process_id <= 0) + return false; + + if(bound_actions_by_id.find(id) != bound_actions_by_id.end()) + return false; + + if(id.find(' ') != std::string::npos || id.find('\n') != std::string::npos) { + fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: id \"%s\" contains either space or newline\n", id.c_str()); + return false; + } + + if(hotkey.key == 0) { + //fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: hotkey requires a key\n"); + return false; + } + + if(hotkey.modifiers == 0) { + //fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: hotkey requires a modifier\n"); + return false; + } + + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + const uint8_t keycode = x11_keycode_to_linux_keycode(XKeysymToKeycode(display, hotkey.key)); + const std::vector<uint8_t> modifiers = modifiers_to_linux_keys(hotkey.modifiers); + const std::string modifiers_command = linux_keys_to_command_string(modifiers.data(), modifiers.size()); + + char command[256]; + const int command_size = snprintf(command, sizeof(command), "bind %s %d+%s\n", id.c_str(), (int)keycode, modifiers_command.c_str()); + if(write(write_pipes[PIPE_WRITE], command, command_size) != command_size) { + fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: failed to write command to gsr-global-hotkeys, error: %s\n", strerror(errno)); + return false; + } + + bound_actions_by_id[id] = std::move(callback); + return true; + } + + void GlobalHotkeysLinux::unbind_all_keys() { + if(process_id <= 0) + return; + + if(bound_actions_by_id.empty()) + return; + + char command[32]; + const int command_size = snprintf(command, sizeof(command), "unbind_all\n"); + if(write(write_pipes[PIPE_WRITE], command, command_size) != command_size) { + fprintf(stderr, "Error: GlobalHotkeysLinux::unbind_all_keys: failed to write command to gsr-global-hotkeys, error: %s\n", strerror(errno)); + } + bound_actions_by_id.clear(); } void GlobalHotkeysLinux::poll_events() { if(process_id <= 0) { - fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, process has not been started yet. Use GlobalHotkeysLinux::start to start the process first\n"); + //fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, process has not been started yet. Use GlobalHotkeysLinux::start to start the process first\n"); return; } if(!read_file) { - fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, read file hasn't opened\n"); + //fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, read file hasn't opened\n"); return; } + std::string action; char buffer[256]; while(true) { char *line = fgets(buffer, sizeof(buffer), read_file); if(!line) break; - const int line_len = strlen(line); + int line_len = strlen(line); if(line_len == 0) continue; - if(line[line_len - 1] == '\n') + if(line[line_len - 1] == '\n') { line[line_len - 1] = '\0'; + --line_len; + } - const std::string action = line; + action = line; auto it = bound_actions_by_id.find(action); if(it != bound_actions_by_id.end()) it->second(action); diff --git a/src/GlobalHotkeysX11.cpp b/src/GlobalHotkeysX11.cpp index 6b01bfd..9af2607 100644 --- a/src/GlobalHotkeysX11.cpp +++ b/src/GlobalHotkeysX11.cpp @@ -1,6 +1,7 @@ #include "../include/GlobalHotkeysX11.hpp" -#define XK_MISCELLANY -#include <X11/keysymdef.h> +#include <X11/keysym.h> +#include <mglpp/window/Event.hpp> +#include <assert.h> namespace gsr { static bool x_failed = false; @@ -25,6 +26,51 @@ namespace gsr { return numlockmask; } + static KeySym mgl_key_to_key_sym(mgl::Keyboard::Key key) { + switch(key) { + case mgl::Keyboard::Z: return XK_z; + case mgl::Keyboard::F7: return XK_F7; + case mgl::Keyboard::F8: return XK_F8; + case mgl::Keyboard::F9: return XK_F9; + case mgl::Keyboard::F10: return XK_F10; + default: return None; + } + } + + static uint32_t mgl_key_modifiers_to_x11_modifier_mask(const mgl::Event::KeyEvent &key_event) { + uint32_t mask = 0; + if(key_event.shift) + mask |= ShiftMask; + if(key_event.control) + mask |= ControlMask; + if(key_event.alt) + mask |= Mod1Mask; + if(key_event.system) + mask |= Mod4Mask; + return mask; + } + + static uint32_t modifiers_to_x11_modifiers(uint32_t modifiers) { + uint32_t result = 0; + if(modifiers & HOTKEY_MOD_LSHIFT) + result |= ShiftMask; + if(modifiers & HOTKEY_MOD_RSHIFT) + result |= ShiftMask; + if(modifiers & HOTKEY_MOD_LCTRL) + result |= ControlMask; + if(modifiers & HOTKEY_MOD_RCTRL) + result |= ControlMask; + if(modifiers & HOTKEY_MOD_LALT) + result |= Mod1Mask; + if(modifiers & HOTKEY_MOD_RALT) + result |= Mod5Mask; + if(modifiers & HOTKEY_MOD_LSUPER) + result |= Mod4Mask; + if(modifiers & HOTKEY_MOD_RSUPER) + result |= Mod4Mask; + return result; + } + GlobalHotkeysX11::GlobalHotkeysX11() { dpy = XOpenDisplay(NULL); if(!dpy) @@ -49,16 +95,17 @@ namespace gsr { x_failed = false; XErrorHandler prev_xerror = XSetErrorHandler(xerror_grab_error); + const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(hotkey.modifiers); unsigned int numlock_mask = x11_get_numlock_mask(dpy); unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask }; for(int i = 0; i < 4; ++i) { - XGrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy), False, GrabModeAsync, GrabModeAsync); + XGrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy), False, GrabModeAsync, GrabModeAsync); } XSync(dpy, False); if(x_failed) { for(int i = 0; i < 4; ++i) { - XUngrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy)); + XUngrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy)); } XSync(dpy, False); XSetErrorHandler(prev_xerror); @@ -81,10 +128,11 @@ namespace gsr { x_failed = false; XErrorHandler prev_xerror = XSetErrorHandler(xerror_grab_error); + const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(it->second.hotkey.modifiers); unsigned int numlock_mask = x11_get_numlock_mask(dpy); unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask }; for(int i = 0; i < 4; ++i) { - XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), it->second.hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy)); + XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy)); } XSync(dpy, False); @@ -102,8 +150,9 @@ namespace gsr { unsigned int numlock_mask = x11_get_numlock_mask(dpy); unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask }; for(auto it = bound_keys_by_id.begin(); it != bound_keys_by_id.end();) { + const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(it->second.hotkey.modifiers); for(int i = 0; i < 4; ++i) { - XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), it->second.hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy)); + XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy)); } } bound_keys_by_id.clear(); @@ -113,25 +162,40 @@ namespace gsr { } void GlobalHotkeysX11::poll_events() { + if(!dpy) + return; + while(XPending(dpy)) { XNextEvent(dpy, &xev); if(xev.type == KeyPress) { const KeySym key_sym = XLookupKeysym(&xev.xkey, 0); - call_hotkey_callback({ key_sym, xev.xkey.state }); + call_hotkey_callback({ (uint32_t)key_sym, xev.xkey.state }); } } } + bool GlobalHotkeysX11::on_event(mgl::Event &event) { + if(event.type != mgl::Event::KeyPressed) + return true; + + // Note: not all keys are mapped in mgl_key_to_key_sym. If more hotkeys are added or changed then add the key mapping there + const KeySym key_sym = mgl_key_to_key_sym(event.key.code); + const uint32_t modifiers = mgl_key_modifiers_to_x11_modifier_mask(event.key); + return !call_hotkey_callback(Hotkey{(uint32_t)key_sym, modifiers}); + } + static unsigned int key_state_without_locks(unsigned int key_state) { return key_state & ~(Mod2Mask|LockMask); } - void GlobalHotkeysX11::call_hotkey_callback(Hotkey hotkey) const { + bool GlobalHotkeysX11::call_hotkey_callback(Hotkey hotkey) const { + const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(hotkey.modifiers); for(const auto &[key, val] : bound_keys_by_id) { - if(val.hotkey.key == hotkey.key && key_state_without_locks(val.hotkey.modifiers) == key_state_without_locks(hotkey.modifiers)) { + if(val.hotkey.key == hotkey.key && key_state_without_locks(modifiers_to_x11_modifiers(val.hotkey.modifiers)) == key_state_without_locks(modifiers_x11)) { val.callback(key); - return; + return true; } } + return false; } }
\ No newline at end of file diff --git a/src/GsrInfo.cpp b/src/GsrInfo.cpp index 276870b..033757c 100644 --- a/src/GsrInfo.cpp +++ b/src/GsrInfo.cpp @@ -1,9 +1,98 @@ #include "../include/GsrInfo.hpp" #include "../include/Utils.hpp" +#include "../include/Process.hpp" + #include <optional> #include <string.h> namespace gsr { + bool GsrVersion::operator>(const GsrVersion &other) const { + return major > other.major || (major == other.major && minor > other.minor) || (major == other.major && minor == other.minor && patch > other.patch); + } + + bool GsrVersion::operator>=(const GsrVersion &other) const { + return major >= other.major || (major == other.major && minor >= other.minor) || (major == other.major && minor == other.minor && patch >= other.patch); + } + + bool GsrVersion::operator<(const GsrVersion &other) const { + return !operator>=(other); + } + + bool GsrVersion::operator<=(const GsrVersion &other) const { + return !operator>(other); + } + + bool GsrVersion::operator==(const GsrVersion &other) const { + return major == other.major && minor == other.minor && patch == other.patch; + } + + bool GsrVersion::operator!=(const GsrVersion &other) const { + return !operator==(other); + } + + std::string GsrVersion::to_string() const { + std::string result; + if(major == 0 && minor == 0 && patch == 0) + result = "Unknown"; + else + result = std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(patch); + return result; + } + + /* Returns -1 on error */ + static int parse_u8(const char *str, int size) { + if(size <= 0) + return -1; + + int result = 0; + for(int i = 0; i < size; ++i) { + char c = str[i]; + if(c >= '0' && c <= '9') { + result = result * 10 + (c - '0'); + if(result > 255) + return -1; + } else { + return -1; + } + } + return result; + } + + static GsrVersion parse_gsr_version(const std::string_view str) { + GsrVersion result; + uint8_t numbers[3]; + int number_index = 0; + + size_t index = 0; + while(true) { + size_t next_index = str.find('.', index); + if(next_index == std::string::npos) + next_index = str.size(); + + const int number = parse_u8(str.data() + index, next_index - index); + if(number == -1) { + fprintf(stderr, "Error: gpu-screen-recorder --info contains invalid gsr version: %.*s\n", (int)str.size(), str.data()); + return {0, 0, 0}; + } + + if(number_index >= 3) { + fprintf(stderr, "Error: gpu-screen-recorder --info contains invalid gsr version: %.*s\n", (int)str.size(), str.data()); + return {0, 0, 0}; + } + + numbers[number_index] = number; + ++number_index; + index = next_index + 1; + if(next_index == str.size()) + break; + } + + result.major = numbers[0]; + result.minor = numbers[1]; + result.patch = numbers[2]; + return result; + } + static std::optional<KeyValue> parse_key_value(std::string_view line) { const size_t space_index = line.find('|'); if(space_index == std::string_view::npos) @@ -23,6 +112,8 @@ namespace gsr { gsr_info->system_info.display_server = DisplayServer::WAYLAND; } else if(key_value->key == "supports_app_audio") { gsr_info->system_info.supports_app_audio = key_value->value == "yes"; + } else if(key_value->key == "gsr_version") { + gsr_info->system_info.gsr_version = parse_gsr_version(key_value->value); } } @@ -38,6 +129,8 @@ namespace gsr { gsr_info->gpu_info.vendor = GpuVendor::INTEL; else if(key_value->value == "nvidia") gsr_info->gpu_info.vendor = GpuVendor::NVIDIA; + } else if(key_value->key == "card_path") { + gsr_info->gpu_info.card_path = key_value->value; } } @@ -64,38 +157,6 @@ namespace gsr { gsr_info->supported_video_codecs.vp9 = true; } - static std::optional<GsrMonitor> capture_option_line_to_monitor(std::string_view line) { - std::optional<GsrMonitor> monitor; - const std::optional<KeyValue> key_value = parse_key_value(line); - if(!key_value) - return monitor; - - char value_buffer[256]; - snprintf(value_buffer, sizeof(value_buffer), "%.*s", (int)key_value->value.size(), key_value->value.data()); - - monitor = GsrMonitor{std::string(key_value->key), mgl::vec2i{0, 0}}; - if(sscanf(value_buffer, "%dx%d", &monitor->size.x, &monitor->size.y) != 2) - monitor->size = {0, 0}; - - return monitor; - } - - static void parse_capture_options_line(GsrInfo *gsr_info, std::string_view line) { - if(line == "window") - gsr_info->supported_capture_options.window = true; - else if(line == "focused") - gsr_info->supported_capture_options.focused = true; - else if(line == "screen") - gsr_info->supported_capture_options.screen = true; - else if(line == "portal") - gsr_info->supported_capture_options.portal = true; - else { - std::optional<GsrMonitor> monitor = capture_option_line_to_monitor(line); - if(monitor) - gsr_info->supported_capture_options.monitors.push_back(std::move(monitor.value())); - } - } - enum class GsrInfoSection { UNKNOWN, SYSTEM_INFO, @@ -112,23 +173,19 @@ namespace gsr { GsrInfoExitStatus get_gpu_screen_recorder_info(GsrInfo *gsr_info) { *gsr_info = GsrInfo{}; - FILE *f = popen("gpu-screen-recorder --info", "r"); - if(!f) { - fprintf(stderr, "error: 'gpu-screen-recorder --info' failed\n"); - return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND; - } - - char output[8192]; - ssize_t bytes_read = fread(output, 1, sizeof(output) - 1, f); - if(bytes_read < 0 || ferror(f)) { - fprintf(stderr, "error: failed to read 'gpu-screen-recorder --info' output\n"); - pclose(f); - return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND; + std::string stdout_str; + const char *args[] = { "gpu-screen-recorder", "--info", nullptr }; + const int exit_status = exec_program_get_stdout(args, stdout_str); + switch(exit_status) { + case 0: break; + case 14: return GsrInfoExitStatus::BROKEN_DRIVERS; + case 22: return GsrInfoExitStatus::OPENGL_FAILED; + case 23: return GsrInfoExitStatus::NO_DRM_CARD; + default: return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND; } - output[bytes_read] = '\0'; GsrInfoSection section = GsrInfoSection::UNKNOWN; - string_split_char({output, (size_t)bytes_read}, '\n', [&](std::string_view line) { + string_split_char(stdout_str, '\n', [&](std::string_view line) { if(starts_with(line, "section=")) { const std::string_view section_name = line.substr(8); if(section_name == "system_info") @@ -161,7 +218,7 @@ namespace gsr { break; } case GsrInfoSection::CAPTURE_OPTIONS: { - parse_capture_options_line(gsr_info, line); + // Intentionally ignore, get capture options with get_supported_capture_options instead break; } } @@ -169,18 +226,7 @@ namespace gsr { return true; }); - int status = pclose(f); - if(WIFEXITED(status)) { - switch(WEXITSTATUS(status)) { - case 0: return GsrInfoExitStatus::OK; - case 14: return GsrInfoExitStatus::BROKEN_DRIVERS; - case 22: return GsrInfoExitStatus::OPENGL_FAILED; - case 23: return GsrInfoExitStatus::NO_DRM_CARD; - default: return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND; - } - } - - return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND; + return GsrInfoExitStatus::OK; } static std::optional<AudioDevice> parse_audio_device_line(std::string_view line) { @@ -196,22 +242,14 @@ namespace gsr { std::vector<AudioDevice> get_audio_devices() { std::vector<AudioDevice> audio_devices; - FILE *f = popen("gpu-screen-recorder --list-audio-devices", "r"); - if(!f) { + std::string stdout_str; + const char *args[] = { "gpu-screen-recorder", "--list-audio-devices", nullptr }; + if(exec_program_get_stdout(args, stdout_str) != 0) { fprintf(stderr, "error: 'gpu-screen-recorder --list-audio-devices' failed\n"); return audio_devices; } - char output[16384]; - ssize_t bytes_read = fread(output, 1, sizeof(output) - 1, f); - if(bytes_read < 0 || ferror(f)) { - fprintf(stderr, "error: failed to read 'gpu-screen-recorder --list-audio-devices' output\n"); - pclose(f); - return audio_devices; - } - output[bytes_read] = '\0'; - - string_split_char({output, (size_t)bytes_read}, '\n', [&](std::string_view line) { + string_split_char(stdout_str, '\n', [&](std::string_view line) { std::optional<AudioDevice> audio_device = parse_audio_device_line(line); if(audio_device) audio_devices.push_back(std::move(audio_device.value())); @@ -224,26 +262,76 @@ namespace gsr { std::vector<std::string> get_application_audio() { std::vector<std::string> application_audio; - FILE *f = popen("gpu-screen-recorder --list-application-audio", "r"); - if(!f) { + std::string stdout_str; + const char *args[] = { "gpu-screen-recorder", "--list-application-audio", nullptr }; + if(exec_program_get_stdout(args, stdout_str) != 0) { fprintf(stderr, "error: 'gpu-screen-recorder --list-application-audio' failed\n"); return application_audio; } - char output[16384]; - ssize_t bytes_read = fread(output, 1, sizeof(output) - 1, f); - if(bytes_read < 0 || ferror(f)) { - fprintf(stderr, "error: failed to read 'gpu-screen-recorder --list-application-audio' output\n"); - pclose(f); - return application_audio; - } - output[bytes_read] = '\0'; - - string_split_char({output, (size_t)bytes_read}, '\n', [&](std::string_view line) { + string_split_char(stdout_str, '\n', [&](std::string_view line) { application_audio.emplace_back(line); return true; }); return application_audio; } + + static std::optional<GsrMonitor> capture_option_line_to_monitor(std::string_view line) { + std::optional<GsrMonitor> monitor; + const std::optional<KeyValue> key_value = parse_key_value(line); + if(!key_value) + return monitor; + + char value_buffer[256]; + snprintf(value_buffer, sizeof(value_buffer), "%.*s", (int)key_value->value.size(), key_value->value.data()); + + monitor = GsrMonitor{std::string(key_value->key), mgl::vec2i{0, 0}}; + if(sscanf(value_buffer, "%dx%d", &monitor->size.x, &monitor->size.y) != 2) + monitor->size = {0, 0}; + + return monitor; + } + + static void parse_capture_options_line(SupportedCaptureOptions &capture_options, std::string_view line) { + if(line == "window") + capture_options.window = true; + else if(line == "focused") + capture_options.focused = true; + else if(line == "portal") + capture_options.portal = true; + else { + std::optional<GsrMonitor> monitor = capture_option_line_to_monitor(line); + if(monitor) + capture_options.monitors.push_back(std::move(monitor.value())); + } + } + + static const char* gpu_vendor_to_string(GpuVendor vendor) { + switch(vendor) { + case GpuVendor::UNKNOWN: return "unknown"; + case GpuVendor::AMD: return "amd"; + case GpuVendor::INTEL: return "intel"; + case GpuVendor::NVIDIA: return "nvidia"; + } + return "unknown"; + } + + SupportedCaptureOptions get_supported_capture_options(const GsrInfo &gsr_info) { + SupportedCaptureOptions capture_options; + + std::string stdout_str; + const char *args[] = { "gpu-screen-recorder", "--list-capture-options", gsr_info.gpu_info.card_path.c_str(), gpu_vendor_to_string(gsr_info.gpu_info.vendor), nullptr }; + if(exec_program_get_stdout(args, stdout_str) != 0) { + fprintf(stderr, "error: 'gpu-screen-recorder --list-capture-options' failed\n"); + return capture_options; + } + + string_split_char(stdout_str, '\n', [&](std::string_view line) { + parse_capture_options_line(capture_options, line); + return true; + }); + + return capture_options; + } } diff --git a/src/Hotplug.cpp b/src/Hotplug.cpp new file mode 100644 index 0000000..84ed5bb --- /dev/null +++ b/src/Hotplug.cpp @@ -0,0 +1,81 @@ +#include "../include/Hotplug.hpp" + +#include <string.h> +#include <unistd.h> +#include <sys/socket.h> +#include <linux/types.h> +#include <linux/netlink.h> + +namespace gsr { + Hotplug::~Hotplug() { + if(fd > 0) + close(fd); + } + + bool Hotplug::start() { + if(started) + return false; + + struct sockaddr_nl nls = { + AF_NETLINK, + 0, + (unsigned int)getpid(), + (unsigned int)-1 + }; + + fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT); + if(fd == -1) + return false; /* Not root user */ + + if(bind(fd, (const struct sockaddr*)&nls, sizeof(struct sockaddr_nl))) { + close(fd); + fd = -1; + return false; + } + + started = true; + return true; + } + + int Hotplug::steal_fd() { + const int val = fd; + fd = -1; + return val; + } + + void Hotplug::process_event_data(int fd, const HotplugEventCallback &callback) { + const int bytes_read = read(fd, event_data, sizeof(event_data)); + if(bytes_read <= 0) + return; + + /* Hotplug data ends with a newline and a null terminator */ + int data_index = 0; + while(data_index < bytes_read) { + parse_netlink_data(event_data + data_index, callback); + data_index += strlen(event_data + data_index) + 1; /* Skip null terminator as well */ + } + } + + /* TODO: This assumes SUBSYSTEM= is output before DEVNAME=, is that always true? */ + void Hotplug::parse_netlink_data(const char *line, const HotplugEventCallback &callback) { + const char *at_symbol = strchr(line, '@'); + if(at_symbol) { + event_is_add = strncmp(line, "add@", 4) == 0; + event_is_remove = strncmp(line, "remove@", 7) == 0; + subsystem_is_input = false; + } else if(event_is_add || event_is_remove) { + if(strcmp(line, "SUBSYSTEM=input") == 0) + subsystem_is_input = true; + + if(subsystem_is_input && strncmp(line, "DEVNAME=", 8) == 0) { + if(event_is_add) + callback(HotplugAction::ADD, line+8); + else if(event_is_remove) + callback(HotplugAction::REMOVE, line+8); + + event_is_add = false; + event_is_remove = false; + } + } + } +} diff --git a/src/Overlay.cpp b/src/Overlay.cpp index 2475a77..919117d 100644 --- a/src/Overlay.cpp +++ b/src/Overlay.cpp @@ -7,20 +7,29 @@ #include "../include/gui/DropdownButton.hpp" #include "../include/gui/CustomRendererWidget.hpp" #include "../include/gui/SettingsPage.hpp" +#include "../include/gui/GlobalSettingsPage.hpp" #include "../include/gui/Utils.hpp" #include "../include/gui/PageStack.hpp" +#include "../include/gui/GsrPage.hpp" +#include "../include/WindowUtils.hpp" +#include "../include/GlobalHotkeys.hpp" #include <string.h> #include <assert.h> #include <sys/wait.h> #include <limits.h> #include <fcntl.h> +#include <poll.h> #include <stdexcept> #include <X11/Xlib.h> #include <X11/Xutil.h> #include <X11/Xatom.h> #include <X11/cursorfont.h> +#include <X11/extensions/Xfixes.h> +#include <X11/extensions/XInput2.h> +#include <X11/extensions/shape.h> +#include <X11/Xcursor/Xcursor.h> #include <mglpp/system/Rect.hpp> #include <mglpp/window/Event.hpp> @@ -32,65 +41,7 @@ namespace gsr { static const mgl::Color bg_color(0, 0, 0, 100); static const double force_window_on_top_timeout_seconds = 1.0; static const double replay_status_update_check_timeout_seconds = 1.0; - - static bool window_has_atom(Display *dpy, Window window, Atom atom) { - Atom type; - unsigned long len, bytes_left; - int format; - unsigned char *properties = NULL; - if(XGetWindowProperty(dpy, window, atom, 0, 1024, False, AnyPropertyType, &type, &format, &len, &bytes_left, &properties) < Success) - return false; - - if(properties) - XFree(properties); - - return type != None; - } - - static bool window_is_user_program(Display *dpy, Window window) { - const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False); - const Atom wm_state_atom = XInternAtom(dpy, "WM_STATE", False); - return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom); - } - - static Window get_window_at_cursor_position(Display *dpy) { - Window root_window = None; - Window window = None; - int dummy_i; - unsigned int dummy_u; - int cursor_pos_x = 0; - int cursor_pos_y = 0; - XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &dummy_i, &dummy_i, &cursor_pos_x, &cursor_pos_y, &dummy_u); - return window; - } - - static Window get_focused_window(Display *dpy) { - const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); - Window focused_window = None; - - // Atom type = None; - // int format = 0; - // unsigned long num_items = 0; - // unsigned long bytes_left = 0; - // unsigned char *data = NULL; - // XGetWindowProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, 0, 1, False, XA_WINDOW, &type, &format, &num_items, &bytes_left, &data); - - // fprintf(stderr, "focused window: %p\n", (void*)data); - - // if(type == XA_WINDOW && num_items == 1 && data) - // return *(Window*)data; - - int revert_to = 0; - XGetInputFocus(dpy, &focused_window, &revert_to); - if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window)) - return focused_window; - - focused_window = get_window_at_cursor_position(dpy); - if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window)) - return focused_window; - - return None; - } + static const double replay_saving_notification_timeout_seconds = 0.5; static mgl::Texture texture_from_ximage(XImage *img) { uint8_t *texture_data = (uint8_t*)malloc(img->width * img->height * 3); @@ -117,6 +68,53 @@ namespace gsr { return texture; } + static bool texture_from_x11_cursor(XcursorImage *x11_cursor_image, bool *visible, mgl::vec2i *hotspot, mgl::Texture &texture) { + uint8_t *cursor_data = NULL; + uint8_t *out = NULL; + const unsigned int *pixels = NULL; + *visible = false; + + if(!x11_cursor_image) + return false; + + if(!x11_cursor_image->pixels) + return false; + + hotspot->x = x11_cursor_image->xhot; + hotspot->y = x11_cursor_image->yhot; + + pixels = x11_cursor_image->pixels; + cursor_data = (uint8_t*)malloc((int)x11_cursor_image->width * (int)x11_cursor_image->height * 4); + if(!cursor_data) + return false; + + out = cursor_data; + /* Un-premultiply alpha */ + for(uint32_t y = 0; y < x11_cursor_image->height; ++y) { + for(uint32_t x = 0; x < x11_cursor_image->width; ++x) { + uint32_t pixel = *pixels++; + uint8_t *in = (uint8_t*)&pixel; + uint8_t alpha = in[3]; + if(alpha == 0) { + alpha = 1; + } else { + *visible = true; + } + + out[0] = (float)in[2] * 255.0/(float)alpha; + out[1] = (float)in[1] * 255.0/(float)alpha; + out[2] = (float)in[0] * 255.0/(float)alpha; + out[3] = in[3]; + out += 4; + in += 4; + } + } + + texture.load_from_memory(cursor_data, x11_cursor_image->width, x11_cursor_image->height, MGL_IMAGE_FORMAT_RGBA); + free(cursor_data); + return true; + } + static char hex_value_to_str(uint8_t v) { if(v <= 9) return '0' + v; @@ -167,7 +165,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; @@ -176,8 +174,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) { @@ -231,15 +229,6 @@ namespace gsr { return is_fullscreen; } - static void set_focused_window(Display *dpy, Window window) { - XSetInputFocus(dpy, window, RevertToPointerRoot, CurrentTime); - - const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); - XChangeProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, XA_WINDOW, 32, PropModeReplace, (const unsigned char*)&window, 1); - - XFlush(dpy); - } - #define _NET_WM_STATE_REMOVE 0 #define _NET_WM_STATE_ADD 1 #define _NET_WM_STATE_TOGGLE 2 @@ -265,6 +254,14 @@ namespace gsr { return True; } + static void make_window_click_through(Display *display, Window window) { + XRectangle rect; + memset(&rect, 0, sizeof(rect)); + XserverRegion region = XFixesCreateRegion(display, &rect, 1); + XFixesSetWindowShapeRegion(display, window, ShapeInput, 0, 0, region); + XFixesDestroyRegion(display, region); + } + static Bool make_window_sticky(Display *dpy, Window window) { return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_STICKY", False)); } @@ -274,22 +271,13 @@ namespace gsr { } // Returns the first monitor if not found. Assumes there is at least one monitor connected. - static const mgl_monitor* find_monitor_by_cursor_position(mgl::Window &window) { - 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({ win->cursor_position.x, win->cursor_position.y })) - 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 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() { @@ -320,17 +308,108 @@ namespace gsr { return is_connected; } - Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, egl_functions egl_funcs) : + static bool xinput_is_supported(Display *dpy, int *xi_opcode) { + *xi_opcode = 0; + int query_event = 0; + int query_error = 0; + if(!XQueryExtension(dpy, "XInputExtension", xi_opcode, &query_event, &query_error)) { + fprintf(stderr, "gsr-ui error: X Input extension not available\n"); + return false; + } + + int major = 2; + int minor = 1; + int retval = XIQueryVersion(dpy, &major, &minor); + if (retval != Success) { + fprintf(stderr, "gsr-ui error: XInput 2.1 is not supported\n"); + return false; + } + + return true; + } + + static Hotkey config_hotkey_to_hotkey(ConfigHotkey config_hotkey) { + return { + (uint32_t)mgl::Keyboard::key_to_x11_keysym((mgl::Keyboard::Key)config_hotkey.key), + config_hotkey.modifiers + }; + } + + static void bind_linux_hotkeys(GlobalHotkeysLinux *global_hotkeys, Overlay *overlay) { + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().main_config.show_hide_hotkey), + "show_hide", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_show(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().record_config.start_stop_hotkey), + "record", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_record(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().record_config.pause_unpause_hotkey), + "pause", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_pause(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().streaming_config.start_stop_hotkey), + "stream", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_stream(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().replay_config.start_stop_hotkey), + "replay_start", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_replay(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().replay_config.save_hotkey), + "replay_save", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay(); + }); + } + + static std::unique_ptr<GlobalHotkeysLinux> register_linux_hotkeys(Overlay *overlay, GlobalHotkeysLinux::GrabType grab_type) { + auto global_hotkeys = std::make_unique<GlobalHotkeysLinux>(grab_type); + if(!global_hotkeys->start()) + fprintf(stderr, "error: failed to start global hotkeys\n"); + + bind_linux_hotkeys(global_hotkeys.get(), overlay); + return global_hotkeys; + } + + static std::unique_ptr<GlobalHotkeysJoystick> register_joystick_hotkeys(Overlay *overlay) { + auto global_hotkeys_js = std::make_unique<GlobalHotkeysJoystick>(); + if(!global_hotkeys_js->start()) + fprintf(stderr, "Warning: failed to start joystick hotkeys\n"); + + global_hotkeys_js->bind_action("save_replay", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay(); + }); + + return global_hotkeys_js; + } + + Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs) : resources_path(std::move(resources_path)), - gsr_info(gsr_info), + gsr_info(std::move(gsr_info)), egl_funcs(egl_funcs), + config(capture_options), bg_screenshot_overlay({0.0f, 0.0f}), top_bar_background({0.0f, 0.0f}), - close_button_widget({0.0f, 0.0f}), - config(gsr_info) + close_button_widget({0.0f, 0.0f}) { - memset(&window_texture, 0, sizeof(window_texture)); - key_bindings[0].key_event.code = mgl::Keyboard::Escape; key_bindings[0].key_event.alt = false; key_bindings[0].key_event.control = false; @@ -340,19 +419,32 @@ namespace gsr { page_stack.pop(); }; - std::optional<Config> new_config = read_config(gsr_info); + memset(&window_texture, 0, sizeof(window_texture)); + + std::optional<Config> new_config = read_config(capture_options); if(new_config) config = std::move(new_config.value()); - init_color_theme(gsr_info); - // These environment variable are used by files in scripts/ folder - const std::string notify_bg_color_str = color_to_hex_str(get_color_theme().tint_color); - setenv("GSR_NOTIFY_BG_COLOR", notify_bg_color_str.c_str(), true); + init_color_theme(config, this->gsr_info); power_supply_online_filepath = get_power_supply_online_filepath(); if(config.replay_config.turn_on_replay_automatically_mode == "turn_on_at_system_startup") on_press_start_replay(true); + + if(config.main_config.hotkeys_enable_option == "enable_hotkeys") + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL); + else if(config.main_config.hotkeys_enable_option == "enable_hotkeys_virtual_devices") + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::VIRTUAL); + + if(config.main_config.joystick_hotkeys_enable_option == "enable_hotkeys") + global_hotkeys_js = register_joystick_hotkeys(this); + + x11_mapping_display = XOpenDisplay(nullptr); + if(x11_mapping_display) + XKeysymToKeycode(x11_mapping_display, XK_F1); // If we dont call we will never get a MappingNotify + else + fprintf(stderr, "Warning: XOpenDisplay failed to mapping notify\n"); } Overlay::~Overlay() { @@ -377,6 +469,134 @@ namespace gsr { } gpu_screen_recorder_process = -1; } + + close_gpu_screen_recorder_output(); + deinit_color_theme(); + + if(x11_mapping_display) + XCloseDisplay(x11_mapping_display); + } + + void Overlay::xi_setup() { + xi_display = XOpenDisplay(nullptr); + if(!xi_display) { + fprintf(stderr, "gsr-ui error: failed to setup XI connection\n"); + return; + } + + if(!xinput_is_supported(xi_display, &xi_opcode)) + goto error; + + xi_input_xev = (XEvent*)calloc(1, sizeof(XEvent)); + if(!xi_input_xev) + throw std::runtime_error("gsr-ui error: failed to allocate XEvent data"); + + xi_output_xev = (XEvent*)calloc(1, sizeof(XEvent)); + if(!xi_output_xev) + throw std::runtime_error("gsr-ui error: failed to allocate XEvent data"); + + unsigned char mask[XIMaskLen(XI_LASTEVENT)]; + memset(mask, 0, sizeof(mask)); + XISetMask(mask, XI_Motion); + //XISetMask(mask, XI_RawMotion); + XISetMask(mask, XI_ButtonPress); + XISetMask(mask, XI_ButtonRelease); + XISetMask(mask, XI_KeyPress); + XISetMask(mask, XI_KeyRelease); + + XIEventMask xi_masks; + xi_masks.deviceid = XIAllMasterDevices; + xi_masks.mask_len = sizeof(mask); + xi_masks.mask = mask; + if(XISelectEvents(xi_display, DefaultRootWindow(xi_display), &xi_masks, 1) != Success) { + fprintf(stderr, "gsr-ui error: XISelectEvents failed\n"); + goto error; + } + + XFlush(xi_display); + return; + + error: + free(xi_input_xev); + xi_input_xev = nullptr; + free(xi_output_xev); + xi_output_xev = nullptr; + if(xi_display) { + XCloseDisplay(xi_display); + xi_display = nullptr; + } + } + + void Overlay::close_gpu_screen_recorder_output() { + if(gpu_screen_recorder_process_output_file) { + fclose(gpu_screen_recorder_process_output_file); + gpu_screen_recorder_process_output_file = nullptr; + } + + if(gpu_screen_recorder_process_output_fd > 0) { + close(gpu_screen_recorder_process_output_fd); + gpu_screen_recorder_process_output_fd = -1; + } + } + + void Overlay::handle_xi_events() { + if(!xi_display) + return; + + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + while(XPending(xi_display)) { + XNextEvent(xi_display, xi_input_xev); + XGenericEventCookie *cookie = &xi_input_xev->xcookie; + if(cookie->type == GenericEvent && cookie->extension == xi_opcode && XGetEventData(xi_display, cookie)) { + const XIDeviceEvent *de = (XIDeviceEvent*)cookie->data; + if(cookie->evtype == XI_Motion) { + memset(xi_output_xev, 0, sizeof(*xi_output_xev)); + xi_output_xev->type = MotionNotify; + xi_output_xev->xmotion.display = display; + xi_output_xev->xmotion.window = window->get_system_handle(); + xi_output_xev->xmotion.subwindow = window->get_system_handle(); + xi_output_xev->xmotion.x = de->root_x - window_pos.x; + xi_output_xev->xmotion.y = de->root_y - window_pos.y; + xi_output_xev->xmotion.x_root = de->root_x; + xi_output_xev->xmotion.y_root = de->root_y; + //xi_output_xev->xmotion.state = // modifiers // TODO: + if(window->inject_x11_event(xi_output_xev, event)) + on_event(event); + } else if(cookie->evtype == XI_ButtonPress || cookie->evtype == XI_ButtonRelease) { + memset(xi_output_xev, 0, sizeof(*xi_output_xev)); + xi_output_xev->type = cookie->evtype == XI_ButtonPress ? ButtonPress : ButtonRelease; + xi_output_xev->xbutton.display = display; + xi_output_xev->xbutton.window = window->get_system_handle(); + xi_output_xev->xbutton.subwindow = window->get_system_handle(); + xi_output_xev->xbutton.x = de->root_x - window_pos.x; + xi_output_xev->xbutton.y = de->root_y - window_pos.y; + xi_output_xev->xbutton.x_root = de->root_x; + xi_output_xev->xbutton.y_root = de->root_y; + //xi_output_xev->xbutton.state = // modifiers // TODO: + xi_output_xev->xbutton.button = de->detail; + if(window->inject_x11_event(xi_output_xev, event)) + on_event(event); + } else if(cookie->evtype == XI_KeyPress || cookie->evtype == XI_KeyRelease) { + memset(xi_output_xev, 0, sizeof(*xi_output_xev)); + xi_output_xev->type = cookie->evtype == XI_KeyPress ? KeyPress : KeyRelease; + xi_output_xev->xkey.display = display; + xi_output_xev->xkey.window = window->get_system_handle(); + xi_output_xev->xkey.subwindow = window->get_system_handle(); + xi_output_xev->xkey.x = de->root_x - window_pos.x; + xi_output_xev->xkey.y = de->root_y - window_pos.y; + xi_output_xev->xkey.x_root = de->root_x; + xi_output_xev->xkey.y_root = de->root_y; + xi_output_xev->xkey.state = de->mods.effective; + xi_output_xev->xkey.keycode = de->detail; + if(window->inject_x11_event(xi_output_xev, event)) + on_event(event); + } + //fprintf(stderr, "got xi event: %d\n", cookie->evtype); + XFreeEventData(xi_display, cookie); + } + } } static uint32_t key_event_to_bitmask(mgl::Event::KeyEvent key_event) { @@ -397,11 +617,42 @@ namespace gsr { } } + void Overlay::handle_keyboard_mapping_event() { + if(!x11_mapping_display) + return; + + bool mapping_updated = false; + while(XPending(x11_mapping_display)) { + XNextEvent(x11_mapping_display, &x11_mapping_xev); + if(x11_mapping_xev.type == MappingNotify) { + XRefreshKeyboardMapping(&x11_mapping_xev.xmapping); + mapping_updated = true; + } + } + + if(mapping_updated) + rebind_all_keyboard_hotkeys(); + } + void Overlay::handle_events() { + if(global_hotkeys) + global_hotkeys->poll_events(); + + if(global_hotkeys_js) + global_hotkeys_js->poll_events(); + + handle_keyboard_mapping_event(); + if(!visible || !window) return; + handle_xi_events(); + while(window->poll_event(event)) { + if(global_hotkeys) { + if(!global_hotkeys->on_event(event)) + continue; + } on_event(event); } } @@ -410,7 +661,9 @@ namespace gsr { if(!visible || !window) return; - close_button_widget.on_event(event, *window, mgl::vec2f(0.0f, 0.0f)); + if(!close_button_widget.on_event(event, *window, mgl::vec2f(0.0f, 0.0f))) + return; + if(!page_stack.on_event(event, *window, mgl::vec2f(0.0f, 0.0f))) return; @@ -419,6 +672,7 @@ namespace gsr { bool Overlay::draw() { update_notification_process_status(); + update_gsr_replay_save(); update_gsr_process_status(); replay_status_update_status(); @@ -433,55 +687,229 @@ namespace gsr { if(!window) return false; + grab_mouse_and_keyboard(); + //force_window_on_top(); - window->clear(bg_color); + const bool draw_ui = show_overlay_clock.get_elapsed_time_seconds() >= show_overlay_timeout_seconds; - if(window_texture_sprite.get_texture() && window_texture.texture_id) { - window->draw(window_texture_sprite); - window->draw(bg_screenshot_overlay); - } else if(screenshot_texture.is_valid()) { - window->draw(screenshot_sprite); - window->draw(bg_screenshot_overlay); - } + window->clear(draw_ui ? bg_color : mgl::Color(0, 0, 0, 0)); + + if(draw_ui) { + if(window_texture_sprite.get_texture() && window_texture.texture_id) { + window->draw(window_texture_sprite); + window->draw(bg_screenshot_overlay); + } else if(screenshot_texture.is_valid()) { + window->draw(screenshot_sprite); + window->draw(bg_screenshot_overlay); + } + + window->draw(top_bar_background); + window->draw(top_bar_text); + window->draw(logo_sprite); - window->draw(top_bar_background); - window->draw(top_bar_text); - window->draw(logo_sprite); + close_button_widget.draw(*window, mgl::vec2f(0.0f, 0.0f)); + page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f)); - close_button_widget.draw(*window, mgl::vec2f(0.0f, 0.0f)); - page_stack.draw(*window, mgl::vec2f(0.0f, 0.0f)); + if(cursor_texture.is_valid()) { + cursor_sprite.set_position((window->get_mouse_position() - cursor_hotspot).to_vec2f()); + window->draw(cursor_sprite); + } + + if(!drawn_first_frame) { + drawn_first_frame = true; + mgl::Event event; + event.type = mgl::Event::MouseMoved; + event.mouse_move.x = window->get_mouse_position().x; + event.mouse_move.y = window->get_mouse_position().y; + on_event(event); + } + } window->display(); return true; } + void Overlay::grab_mouse_and_keyboard() { + // TODO: Remove these grabs when debugging with a debugger, or your X11 session will appear frozen. + // There should be a debug mode to not use these + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + XGrabPointer(display, window->get_system_handle(), True, + ButtonPressMask | ButtonReleaseMask | PointerMotionMask | + Button1MotionMask | Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask | + ButtonMotionMask, + GrabModeAsync, GrabModeAsync, None, default_cursor, CurrentTime); + // TODO: This breaks global hotkeys (when using x11 global hotkeys) + XGrabKeyboard(display, window->get_system_handle(), True, GrabModeAsync, GrabModeAsync, CurrentTime); + XFlush(display); + } + + void Overlay::xi_setup_fake_cursor() { + if(!xi_display) + return; + + XFixesHideCursor(xi_display, DefaultRootWindow(xi_display)); + XFlush(xi_display); + + // TODO: XCURSOR_SIZE and XCURSOR_THEME environment variables + const char *cursor_theme = XcursorGetTheme(xi_display); + if(!cursor_theme) { + //fprintf(stderr, "Warning: failed to get cursor theme, using \"default\" theme instead\n"); + cursor_theme = "default"; + } + + int cursor_size = XcursorGetDefaultSize(xi_display); + if(cursor_size <= 1) + cursor_size = 24; + + XcursorImage *cursor_image = nullptr; + for(int cursor_size_test : {cursor_size, 24}) { + for(const char *cursor_theme_test : {cursor_theme, "default", "Adwaita"}) { + for(unsigned int shape : {XC_left_ptr, XC_arrow}) { + cursor_image = XcursorShapeLoadImage(shape, cursor_theme_test, cursor_size_test); + if(cursor_image) + goto done; + } + } + } + + done: + if(!cursor_image) { + fprintf(stderr, "Error: failed to get cursor, loading bundled default cursor instead\n"); + const std::string default_cursor_path = resources_path + "images/default.cur"; + for(int cursor_size_test : {cursor_size, 24}) { + cursor_image = XcursorFilenameLoadImage(default_cursor_path.c_str(), cursor_size_test); + if(cursor_image) + break; + } + } + + if(!cursor_image) { + fprintf(stderr, "Error: failed to get cursor\n"); + XFixesShowCursor(xi_display, DefaultRootWindow(xi_display)); + XFlush(xi_display); + return; + } + + bool cursor_visible = false; + texture_from_x11_cursor(cursor_image, &cursor_visible, &cursor_hotspot, cursor_texture); + if(cursor_texture.is_valid()) + cursor_sprite.set_texture(&cursor_texture); + + XcursorImageDestroy(cursor_image); + } + + static bool device_is_mouse(const XIDeviceInfo *dev) { + for(int i = 0; i < dev->num_classes; ++i) { + if(dev->classes[i]->type == XIMasterPointer || dev->classes[i]->type == XISlavePointer) + return true; + } + return false; + } + + void Overlay::xi_grab_all_mouse_devices() { + if(!xi_display) + return; + + int num_devices = 0; + XIDeviceInfo *info = XIQueryDevice(xi_display, XIAllDevices, &num_devices); + if(!info) + return; + + unsigned char mask[XIMaskLen(XI_LASTEVENT)]; + memset(mask, 0, sizeof(mask)); + XISetMask(mask, XI_Motion); + //XISetMask(mask, XI_RawMotion); + XISetMask(mask, XI_ButtonPress); + XISetMask(mask, XI_ButtonRelease); + XISetMask(mask, XI_KeyPress); + XISetMask(mask, XI_KeyRelease); + + for (int i = 0; i < num_devices; ++i) { + const XIDeviceInfo *dev = &info[i]; + if(!device_is_mouse(dev)) + continue; + + XIEventMask xi_masks; + xi_masks.deviceid = dev->deviceid; + xi_masks.mask_len = sizeof(mask); + xi_masks.mask = mask; + XIGrabDevice(xi_display, dev->deviceid, window->get_system_handle(), CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, XIOwnerEvents, &xi_masks); + } + + XFlush(xi_display); + XIFreeDeviceInfo(info); + } + void Overlay::show() { + if(visible) + return; + + drawn_first_frame = false; window.reset(); window = std::make_unique<mgl::Window>(); deinit_theme(); - mgl::vec2i window_size = { 1280, 720 }; - mgl::vec2i window_pos = { 0, 0 }; + 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); + + // 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 || x11_cursor_window || is_wlroots; + + if(prevent_game_minimizing) { + window_pos = focused_monitor->position; + window_size = focused_monitor->size; + } else { + window_pos = {0, 0}; + window_size = focused_monitor->size / 2; + } 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; - window_create_params.position = window_pos; - window_create_params.hidden = true; - window_create_params.override_redirect = true; - window_create_params.background_color = bg_color; + if(prevent_game_minimizing) { + window_create_params.min_size = window_size; + window_create_params.max_size = window_size; + } + window_create_params.position = focused_monitor->position + focused_monitor->size / 2 - window_size / 2; + window_create_params.hidden = prevent_game_minimizing; + window_create_params.override_redirect = prevent_game_minimizing; + window_create_params.background_color = mgl::Color(0, 0, 0, 0); window_create_params.support_alpha = true; - window_create_params.window_type = MGL_WINDOW_TYPE_NOTIFICATION; - 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"); - mgl_context *context = mgl_get_context(); - Display *display = (Display*)context->connection; + //window->set_low_latency(true); unsigned char data = 2; // Prefer being composed to allow transparency XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "_NET_WM_BYPASS_COMPOSITOR", False), XA_CARDINAL, 32, PropModeReplace, &data, 1); @@ -489,30 +917,27 @@ namespace gsr { data = 1; XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "GAMESCOPE_EXTERNAL_OVERLAY", False), XA_CARDINAL, 32, PropModeReplace, &data, 1); + const auto original_window_size = window_size; + 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); - } - - mgl_window *win = window->internal_window(); - if(win->num_monitors == 0) { - fprintf(stderr, "gsr warning: no monitors found, not showing overlay\n"); + window.reset(); return; } - - const mgl_monitor *focused_monitor = find_monitor_by_cursor_position(*window); - 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); + if(prevent_game_minimizing) { + window->set_size(window_size); + window->set_size_limits(window_size, window_size); + } + window->set_position(focused_monitor->position + focused_monitor->size / 2 - original_window_size / 2); - update_compositor_texture(focused_monitor); + mgl_window *win = window->internal_window(); + win->cursor_position.x = cursor_position.x - window_pos.x; + win->cursor_position.y = cursor_position.y - window_pos.y; - 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()); @@ -545,6 +970,7 @@ namespace gsr { const int button_width = button_height; auto main_buttons_list = std::make_unique<List>(List::Orientation::HORIZONTAL); + List * main_buttons_list_ptr = main_buttons_list.get(); main_buttons_list->set_spacing(0.0f); { auto button = std::make_unique<DropdownButton>(&get_theme().title_font, &get_theme().body_font, "Instant Replay", "Off", &get_theme().replay_button_texture, @@ -555,9 +981,14 @@ namespace gsr { button->add_item("Settings", "settings"); button->set_item_icon("start", &get_theme().play_texture); button->set_item_icon("save", &get_theme().save_texture); + button->set_item_icon("settings", &get_theme().settings_small_texture); button->on_click = [this](const std::string &id) { if(id == "settings") { - auto replay_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::REPLAY, gsr_info, config, &page_stack); + auto replay_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::REPLAY, &gsr_info, config, &page_stack); + replay_settings_page->on_config_changed = [this]() { + if(recording_status == RecordingStatus::REPLAY) + show_notification("Replay settings have been modified.\nYou may need to restart replay to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + }; page_stack.push(std::move(replay_settings_page)); } else if(id == "save") { on_press_save_replay(); @@ -576,9 +1007,14 @@ namespace gsr { button->add_item("Settings", "settings"); button->set_item_icon("start", &get_theme().play_texture); button->set_item_icon("pause", &get_theme().pause_texture); + button->set_item_icon("settings", &get_theme().settings_small_texture); button->on_click = [this](const std::string &id) { if(id == "settings") { - auto record_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::RECORD, gsr_info, config, &page_stack); + auto record_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::RECORD, &gsr_info, config, &page_stack); + record_settings_page->on_config_changed = [this]() { + if(recording_status == RecordingStatus::RECORD) + show_notification("Recording settings have been modified.\nYou may need to restart recording to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); + }; page_stack.push(std::move(record_settings_page)); } else if(id == "pause") { toggle_pause(); @@ -595,9 +1031,14 @@ namespace gsr { button->add_item("Start", "start", "Alt+F8"); button->add_item("Settings", "settings"); button->set_item_icon("start", &get_theme().play_texture); + button->set_item_icon("settings", &get_theme().settings_small_texture); button->on_click = [this](const std::string &id) { if(id == "settings") { - auto stream_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::STREAM, gsr_info, config, &page_stack); + auto stream_settings_page = std::make_unique<SettingsPage>(SettingsPage::Type::STREAM, &gsr_info, config, &page_stack); + stream_settings_page->on_config_changed = [this]() { + if(recording_status == RecordingStatus::STREAM) + show_notification("Streaming settings have been modified.\nYou may need to restart streaming to apply the changes.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); + }; page_stack.push(std::move(stream_settings_page)); } else if(id == "start") { on_press_start_stream(); @@ -610,6 +1051,59 @@ namespace gsr { main_buttons_list->set_position((mgl::vec2f(window_size.x * 0.5f, window_size.y * 0.25f) - main_buttons_list_size * 0.5f).floor()); front_page_ptr->add_widget(std::move(main_buttons_list)); + { + const mgl::vec2f main_buttons_size = main_buttons_list_ptr->get_size(); + const int settings_button_size = main_buttons_size.y * 0.2f; + auto button = std::make_unique<Button>(&get_theme().title_font, "", mgl::vec2f(settings_button_size, settings_button_size), mgl::Color(0, 0, 0, 180)); + button->set_position((main_buttons_list_ptr->get_position() + main_buttons_size - mgl::vec2f(0.0f, settings_button_size) + mgl::vec2f(settings_button_size * 0.333f, 0.0f)).floor()); + button->set_bg_hover_color(mgl::Color(0, 0, 0, 255)); + button->set_icon(&get_theme().settings_small_texture); + button->on_click = [&]() { + auto settings_page = std::make_unique<GlobalSettingsPage>(this, &gsr_info, config, &page_stack); + + settings_page->on_startup_changed = [&](bool enable, int exit_status) { + if(exit_status == 0) + return; + + if(exit_status == 127) { + if(enable) + show_notification("Failed to add GPU Screen Recorder to system startup.\nThis option only works on systems that use systemd.\nYou have to manually add \"gsr-ui\" to system startup on systems that uses another init system.", 10.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + } else { + if(enable) + show_notification("Failed to add GPU Screen Recorder to system startup", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + else + show_notification("Failed to remove GPU Screen Recorder from system startup", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + } + }; + + settings_page->on_click_exit_program_button = [this](const char *reason) { + do_exit = true; + exit_reason = reason; + }; + + settings_page->on_keyboard_hotkey_changed = [this](const char *hotkey_option) { + global_hotkeys.reset(); + if(strcmp(hotkey_option, "enable_hotkeys") == 0) + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL); + else if(strcmp(hotkey_option, "enable_hotkeys_virtual_devices") == 0) + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::VIRTUAL); + else if(strcmp(hotkey_option, "disable_hotkeys") == 0) + global_hotkeys.reset(); + }; + + settings_page->on_joystick_hotkey_changed = [this](const char *hotkey_option) { + global_hotkeys_js.reset(); + if(strcmp(hotkey_option, "enable_hotkeys") == 0) + global_hotkeys_js = register_joystick_hotkeys(this); + else if(strcmp(hotkey_option, "disable_hotkeys") == 0) + global_hotkeys_js.reset(); + }; + + page_stack.push(std::move(settings_page)); + }; + front_page_ptr->add_widget(std::move(button)); + } + close_button_widget.draw_handler = [&](mgl::Window &window, mgl::vec2f pos, mgl::vec2f size) { const int border_size = std::max(1.0f, 0.0015f * get_theme().window_height); const float padding_size = std::max(1.0f, 0.003f * get_theme().window_height); @@ -637,8 +1131,18 @@ namespace gsr { return true; }; - window->set_fullscreen(true); + // 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. + const bool fake_cursor = is_wlroots ? x11_cursor_window != None : prevent_game_minimizing; + if(fake_cursor) + xi_setup(); + + //window->set_fullscreen(true); + if(gsr_info.system_info.display_server == DisplayServer::X11) + make_window_click_through(display, window->get_system_handle()); + window->set_visible(true); + make_window_sticky(display, window->get_system_handle()); hide_window_from_taskbar(display, window->get_system_handle()); @@ -646,33 +1150,23 @@ namespace gsr { XFreeCursor(display, default_cursor); default_cursor = 0; } - default_cursor = XCreateFontCursor(display, XC_arrow); + default_cursor = XCreateFontCursor(display, XC_left_ptr); + XFlush(display); - // TODO: Retry if these fail. - // TODO: Hmm, these dont work in owlboy. Maybe owlboy uses xi2 and that breaks this (does it?). - // Remove these grabs when debugging with a debugger, or your X11 session will appear frozen + grab_mouse_and_keyboard(); - // XGrabPointer(display, window->get_system_handle(), True, - // ButtonPressMask | ButtonReleaseMask | PointerMotionMask | - // Button1MotionMask | Button2MotionMask | Button3MotionMask | Button4MotionMask | Button5MotionMask | - // ButtonMotionMask, - // GrabModeAsync, GrabModeAsync, None, default_cursor, CurrentTime); - // TODO: This breaks global hotkeys - //XGrabKeyboard(display, window->get_system_handle(), True, GrabModeAsync, GrabModeAsync, CurrentTime); + // The real cursor doesn't move when all devices are grabbed, so we create our own cursor and diplay that while grabbed + xi_setup_fake_cursor(); - set_focused_window(display, window->get_system_handle()); - XFlush(display); + // We want to grab all devices to prevent any other application below the UI from receiving events. + // Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events. + xi_grab_all_mouse_devices(); - //window->set_fullscreen(true); + if(!is_wlroots) + window->set_fullscreen(true); visible = true; - mgl::Event event; - event.type = mgl::Event::MouseMoved; - event.mouse_move.x = window->get_mouse_position().x; - event.mouse_move.y = window->get_mouse_position().y; - on_event(event); - if(gpu_screen_recorder_process > 0) { switch(recording_status) { case RecordingStatus::NONE: @@ -691,9 +1185,18 @@ namespace gsr { if(paused) update_ui_recording_paused(); + + // Wayland compositors have retarded fullscreen animations that we cant disable in a proper way + // without messing up window position. + show_overlay_timeout_seconds = prevent_game_minimizing ? 0.0 : 0.15; + show_overlay_clock.restart(); + draw(); } void Overlay::hide() { + if(!visible) + return; + mgl_context *context = mgl_get_context(); Display *display = (Display*)context->connection; @@ -710,13 +1213,57 @@ namespace gsr { XUngrabPointer(display, CurrentTime); XFlush(display); + if(xi_display) { + cursor_texture.clear(); + cursor_sprite.set_texture(nullptr); + } + window_texture_deinit(&window_texture); window_texture_sprite.set_texture(nullptr); screenshot_texture.clear(); screenshot_sprite.set_texture(nullptr); visible = false; + drawn_first_frame = false; + + if(xi_input_xev) { + free(xi_input_xev); + xi_input_xev = nullptr; + } + + if(xi_output_xev) { + free(xi_output_xev); + xi_output_xev = nullptr; + } + + if(xi_display) { + XCloseDisplay(xi_display); + xi_display = nullptr; + + if(window) { + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + const mgl::vec2i new_cursor_position = mgl::vec2i(window->internal_window()->pos.x, window->internal_window()->pos.y) + window->get_mouse_position(); + XWarpPointer(display, DefaultRootWindow(display), DefaultRootWindow(display), 0, 0, 0, 0, new_cursor_position.x, new_cursor_position.y); + XFlush(display); + + XFixesShowCursor(display, DefaultRootWindow(display)); + XFlush(display); + } + } + if(window) { + if(show_overlay_timeout_seconds > 0.0001) { + window->clear(mgl::Color(0, 0, 0, 0)); + window->display(); + + mgl_context *context = mgl_get_context(); + context->gl.glFlush(); + context->gl.glFinish(); + usleep(50 * 1000); // EGL doesn't do an immediate flush for some reason + } + window->set_visible(false); window.reset(); } @@ -725,10 +1272,16 @@ namespace gsr { } void Overlay::toggle_show() { - if(visible) - hide(); - else + if(visible) { + //hide(); + // We dont want to hide immediately because hide is called in mgl event callback, in which it destroys the mgl window. + // Instead remove all pages and wait until next iteration to close the UI (which happens when there are no pages to render). + while(!page_stack.empty()) { + page_stack.pop(); + } + } else { show(); + } } void Overlay::toggle_record() { @@ -787,7 +1340,7 @@ namespace gsr { const char *notification_type_str = notification_type_to_string(notification_type); if(notification_type_str) { notification_args[9] = "--icon"; - notification_args[10] = "record", + notification_args[10] = notification_type_str; notification_args[11] = nullptr; } else { notification_args[9] = nullptr; @@ -799,13 +1352,40 @@ namespace gsr { waitpid(notification_process, &status, 0); } - notification_process = exec_program(notification_args); + notification_process = exec_program(notification_args, NULL); } bool Overlay::is_open() const { return visible; } + bool Overlay::should_exit(std::string &reason) const { + reason.clear(); + if(do_exit) + reason = exit_reason; + return do_exit; + } + + void Overlay::exit() { + do_exit = true; + } + + const Config& Overlay::get_config() const { + return config; + } + + void Overlay::unbind_all_keyboard_hotkeys() { + if(global_hotkeys) + global_hotkeys->unbind_all_keys(); + } + + void Overlay::rebind_all_keyboard_hotkeys() { + unbind_all_keyboard_hotkeys(); + // TODO: Check if type is GlobalHotkeysLinux + if(global_hotkeys) + bind_linux_hotkeys(static_cast<GlobalHotkeysLinux*>(global_hotkeys.get()), this); + } + void Overlay::update_notification_process_status() { if(notification_process <= 0) return; @@ -819,32 +1399,128 @@ namespace gsr { notification_process = -1; } + static void string_replace_characters(char *str, const char *characters_to_replace, char new_character) { + for(; *str != '\0'; ++str) { + for(const char *p = characters_to_replace; *p != '\0'; ++p) { + if(*str == *p) + *str = new_character; + } + } + } + + static std::string filepath_get_directory(const char *filepath) { + std::string result = filepath; + const size_t last_slash_index = result.rfind('/'); + if(last_slash_index == std::string::npos) + result = "."; + else + result.erase(last_slash_index); + return result; + } + + static std::string filepath_get_filename(const char *filepath) { + std::string result = filepath; + const size_t last_slash_index = result.rfind('/'); + if(last_slash_index != std::string::npos) + result.erase(0, last_slash_index + 1); + return result; + } + + static void truncate_string(std::string &str, int max_length) { + if((int)str.size() > max_length) + str.replace(str.begin() + max_length, str.end(), "..."); + } + + void Overlay::save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type) { + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + const std::string video_filename = filepath_get_filename(video_filepath); + + const Window gsr_ui_window = window ? window->get_system_handle() : None; + std::string focused_window_name = get_window_name_at_cursor_position(display, gsr_ui_window); + if(focused_window_name.empty()) + focused_window_name = get_focused_window_name(display, WindowCaptureType::FOCUSED); + if(focused_window_name.empty()) + focused_window_name = "Game"; + + string_replace_characters(focused_window_name.data(), "/\\", '_'); + + std::string video_directory = filepath_get_directory(video_filepath) + "/" + focused_window_name; + create_directory_recursive(video_directory.data()); + + const std::string new_video_filepath = video_directory + "/" + video_filename; + rename(video_filepath, new_video_filepath.c_str()); + + truncate_string(focused_window_name, 20); + std::string text; + switch(notification_type) { + case NotificationType::RECORD: { + if(!config.record_config.show_video_saved_notifications) + return; + text = "Saved recording to '" + focused_window_name + "/" + video_filename + "'"; + break; + } + case NotificationType::REPLAY: { + if(!config.replay_config.show_replay_saved_notifications) + return; + text = "Saved replay to '" + focused_window_name + "/" + video_filename + "'"; + break; + } + case NotificationType::NONE: + case NotificationType::STREAM: + break; + } + show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, notification_type); + } + + void Overlay::on_replay_saved(const char *replay_saved_filepath) { + replay_save_show_notification = false; + if(config.replay_config.save_video_in_game_folder) { + save_video_in_current_game_directory(replay_saved_filepath, NotificationType::REPLAY); + } else { + const std::string text = "Saved replay to '" + filepath_get_filename(replay_saved_filepath) + "'"; + show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + } + } + + void Overlay::update_gsr_replay_save() { + if(replay_save_show_notification && replay_save_clock.get_elapsed_time_seconds() >= replay_saving_notification_timeout_seconds) { + replay_save_show_notification = false; + show_notification("Saving replay, this might take some time", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + } + + if(gpu_screen_recorder_process_output_file) { + char buffer[1024]; + char *replay_saved_filepath = fgets(buffer, sizeof(buffer), gpu_screen_recorder_process_output_file); + if(!replay_saved_filepath || replay_saved_filepath[0] == '\0') + return; + + const int line_len = strlen(replay_saved_filepath); + if(replay_saved_filepath[line_len - 1] == '\n') + replay_saved_filepath[line_len - 1] = '\0'; + + on_replay_saved(replay_saved_filepath); + } else if(gpu_screen_recorder_process_output_fd > 0) { + char buffer[1024]; + read(gpu_screen_recorder_process_output_fd, buffer, sizeof(buffer)); + } + } + void Overlay::update_gsr_process_status() { if(gpu_screen_recorder_process <= 0) return; - errno = 0; int status; if(waitpid(gpu_screen_recorder_process, &status, WNOHANG) == 0) { // Still running return; } + close_gpu_screen_recorder_output(); + int exit_code = -1; - // The process is no longer a child process since gsr ui has restarted - if(errno == ECHILD) { - errno = 0; - kill(gpu_screen_recorder_process, 0); - if(errno != ESRCH) { - // Still running - return; - } - // We cant know the exit status, so we assume it succeeded - exit_code = 0; - } else { - if(WIFEXITED(status)) - exit_code = WEXITSTATUS(status); - } + if(WIFEXITED(status)) + exit_code = WEXITSTATUS(status); switch(recording_status) { case RecordingStatus::NONE: @@ -856,16 +1532,13 @@ namespace gsr { show_notification("Replay stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); } else { fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code); - show_notification("Replay stopped because of an error", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY); + show_notification("Replay stopped because of an error. Verify if settings are correct", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY); } break; } case RecordingStatus::RECORD: { update_ui_recording_stopped(); - if(exit_code != 0) { - fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code); - show_notification("Failed to start/save recording", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); - } + on_stop_recording(exit_code); break; } case RecordingStatus::STREAM: { @@ -875,7 +1548,7 @@ namespace gsr { show_notification("Streaming has stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); } else { fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code); - show_notification("Streaming stopped because of an error", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM); + show_notification("Streaming stopped because of an error. Verify if settings are correct", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM); } break; } @@ -901,7 +1574,7 @@ namespace gsr { mgl_context *context = mgl_get_context(); Display *display = (Display*)context->connection; - const Window focused_window = get_focused_window(display); + const Window focused_window = get_focused_window(display, WindowCaptureType::FOCUSED); if(window && focused_window == window->get_system_handle()) return; @@ -915,6 +1588,7 @@ namespace gsr { } } + // TODO: Instead of checking power supply status periodically listen to power supply event void Overlay::update_power_supply_status() { if(config.replay_config.turn_on_replay_automatically_mode != "turn_on_at_power_supply_connected") return; @@ -929,6 +1603,20 @@ namespace gsr { } } + void Overlay::on_stop_recording(int exit_code) { + if(exit_code == 0) { + if(config.record_config.save_video_in_game_folder) { + save_video_in_current_game_directory(record_filepath.c_str(), NotificationType::RECORD); + } else { + const std::string text = "Saved recording to '" + filepath_get_filename(record_filepath.c_str()) + "'"; + show_notification(text.c_str(), 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); + } + } else { + fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code); + show_notification("Failed to start/save recording. Verify if settings are correct", 3.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); + } + } + void Overlay::update_ui_recording_paused() { if(!visible || recording_status != RecordingStatus::RECORD) return; @@ -1088,12 +1776,35 @@ namespace gsr { args.push_back(audio_track.c_str()); } } + + if(record_options.restore_portal_session) { + args.push_back("-restore-portal-session"); + args.push_back("yes"); + } + } + + static bool validate_capture_target(const GsrInfo &gsr_info, const std::string &capture_target) { + const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info); + // TODO: Also check x11 window when enabled (check if capture_target is a decminal/hex number) + if(capture_target == "focused") { + return capture_options.focused; + } else if(capture_target == "portal") { + return capture_options.portal; + } else { + for(const GsrMonitor &monitor : capture_options.monitors) { + if(capture_target == monitor.name) + return true; + } + return false; + } } void Overlay::on_press_save_replay() { if(recording_status != RecordingStatus::REPLAY || gpu_screen_recorder_process <= 0) return; + replay_save_show_notification = true; + replay_save_clock.restart(); kill(gpu_screen_recorder_process, SIGUSR1); } @@ -1111,10 +1822,13 @@ namespace gsr { } paused = false; + replay_save_show_notification = false; // window->close(); // usleep(1000 * 50); // 50 milliseconds + close_gpu_screen_recorder_output(); + if(gpu_screen_recorder_process > 0) { kill(gpu_screen_recorder_process, SIGINT); int status; @@ -1133,6 +1847,13 @@ namespace gsr { return; } + if(!validate_capture_target(gsr_info, config.replay_config.record_options.record_area_option)) { + char err_msg[256]; + snprintf(err_msg, sizeof(err_msg), "Failed to start replay, capture target \"%s\" is invalid. Please change capture target in settings", config.replay_config.record_options.record_area_option.c_str()); + show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::REPLAY); + return; + } + // TODO: Validate input, fallback to valid values const std::string fps = std::to_string(config.replay_config.record_options.fps); const std::string video_bitrate = std::to_string(config.replay_config.record_options.video_bitrate); @@ -1149,7 +1870,9 @@ namespace gsr { } char region[64]; - snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.record_area_width, (int)config.replay_config.record_options.record_area_height); + region[0] = '\0'; + if(config.replay_config.record_options.record_area_option == "focused") + snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.record_area_width, (int)config.replay_config.record_options.record_area_height); if(config.replay_config.record_options.record_area_option != "focused" && config.replay_config.record_options.change_video_resolution) snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.video_width, (int)config.replay_config.record_options.video_height); @@ -1169,16 +1892,16 @@ namespace gsr { "-o", output_directory.c_str() }; - add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged); + if(config.replay_config.restart_replay_on_save && gsr_info.system_info.gsr_version >= GsrVersion{5, 0, 3}) { + args.push_back("-restart-replay-on-save"); + args.push_back("yes"); + } - setenv("GSR_SHOW_SAVED_NOTIFICATION", config.replay_config.show_replay_saved_notifications ? "1" : "0", true); - const std::string script_to_run_on_save = resources_path + (config.replay_config.save_video_in_game_folder ? "scripts/save-video-in-game-folder.sh" : "scripts/notify-saved-name.sh"); - args.push_back("-sc"); - args.push_back(script_to_run_on_save.c_str()); + add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged); args.push_back(nullptr); - gpu_screen_recorder_process = exec_program(args.data()); + gpu_screen_recorder_process = exec_program(args.data(), &gpu_screen_recorder_process_output_fd); if(gpu_screen_recorder_process == -1) { // TODO: Show notification failed to start } else { @@ -1186,6 +1909,12 @@ namespace gsr { update_ui_replay_started(); } + const int fdl = fcntl(gpu_screen_recorder_process_output_fd, F_GETFL); + fcntl(gpu_screen_recorder_process_output_fd, F_SETFL, fdl | O_NONBLOCK); + gpu_screen_recorder_process_output_file = fdopen(gpu_screen_recorder_process_output_fd, "r"); + if(gpu_screen_recorder_process_output_file) + gpu_screen_recorder_process_output_fd = -1; + // TODO: Start recording after this notification has disappeared to make sure it doesn't show up in the video. // Make clear to the user that the recording starts after the notification is gone. // Maybe have the option in notification to show timer until its getting hidden, then the notification can say: @@ -1223,17 +1952,29 @@ namespace gsr { if(waitpid(gpu_screen_recorder_process, &status, 0) == -1) { perror("waitpid failed"); /* Ignore... */ + } else { + int exit_code = -1; + if(WIFEXITED(status)) + exit_code = WEXITSTATUS(status); + on_stop_recording(exit_code); } - // window->set_visible(false); - // window->close(); - // return; - //exit(0); + gpu_screen_recorder_process = -1; recording_status = RecordingStatus::NONE; update_ui_recording_stopped(); + record_filepath.clear(); return; } + if(!validate_capture_target(gsr_info, config.record_config.record_options.record_area_option)) { + char err_msg[256]; + snprintf(err_msg, sizeof(err_msg), "Failed to start recording, capture target \"%s\" is invalid. Please change capture target in settings", config.record_config.record_options.record_area_option.c_str()); + show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::RECORD); + return; + } + + record_filepath.clear(); + // TODO: Validate input, fallback to valid values const std::string fps = std::to_string(config.record_config.record_options.fps); const std::string video_bitrate = std::to_string(config.record_config.record_options.video_bitrate); @@ -1249,7 +1990,9 @@ namespace gsr { } char region[64]; - snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.record_area_width, (int)config.record_config.record_options.record_area_height); + region[0] = '\0'; + if(config.record_config.record_options.record_area_option == "focused") + snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.record_area_width, (int)config.record_config.record_options.record_area_height); if(config.record_config.record_options.record_area_option != "focused" && config.record_config.record_options.change_video_resolution) snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.video_width, (int)config.record_config.record_options.video_height); @@ -1270,14 +2013,10 @@ namespace gsr { add_common_gpu_screen_recorder_args(args, config.record_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged); - setenv("GSR_SHOW_SAVED_NOTIFICATION", config.record_config.show_video_saved_notifications ? "1" : "0", true); - const std::string script_to_run_on_save = resources_path + (config.record_config.save_video_in_game_folder ? "scripts/save-video-in-game-folder.sh" : "scripts/notify-saved-name.sh"); - args.push_back("-sc"); - args.push_back(script_to_run_on_save.c_str()); - args.push_back(nullptr); - gpu_screen_recorder_process = exec_program(args.data()); + record_filepath = output_file; + gpu_screen_recorder_process = exec_program(args.data(), nullptr); if(gpu_screen_recorder_process == -1) { // TODO: Show notification failed to start } else { @@ -1363,6 +2102,13 @@ namespace gsr { return; } + if(!validate_capture_target(gsr_info, config.streaming_config.record_options.record_area_option)) { + char err_msg[256]; + snprintf(err_msg, sizeof(err_msg), "Failed to start streaming, capture target \"%s\" is invalid. Please change capture target in settings", config.streaming_config.record_options.record_area_option.c_str()); + show_notification(err_msg, 3.0, mgl::Color(255, 0, 0, 0), mgl::Color(255, 0, 0, 0), NotificationType::STREAM); + return; + } + // TODO: Validate input, fallback to valid values const std::string fps = std::to_string(config.streaming_config.record_options.fps); const std::string video_bitrate = std::to_string(config.streaming_config.record_options.video_bitrate); @@ -1383,7 +2129,9 @@ namespace gsr { const std::string url = streaming_get_url(config); char region[64]; - snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.record_area_width, (int)config.streaming_config.record_options.record_area_height); + region[0] = '\0'; + if(config.record_config.record_options.record_area_option == "focused") + snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.record_area_width, (int)config.streaming_config.record_options.record_area_height); if(config.record_config.record_options.record_area_option != "focused" && config.streaming_config.record_options.change_video_resolution) snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.video_width, (int)config.streaming_config.record_options.video_height); @@ -1397,16 +2145,16 @@ namespace gsr { "-fm", framerate_mode.c_str(), "-encoder", encoder, "-f", fps.c_str(), - "-f", fps.c_str(), "-v", "no", "-o", url.c_str() }; + config.streaming_config.record_options.merge_audio_tracks = true; add_common_gpu_screen_recorder_args(args, config.streaming_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged); args.push_back(nullptr); - gpu_screen_recorder_process = exec_program(args.data()); + gpu_screen_recorder_process = exec_program(args.data(), nullptr); if(gpu_screen_recorder_process == -1) { // TODO: Show notification failed to start } else { @@ -1427,7 +2175,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(); @@ -1440,15 +2188,17 @@ namespace gsr { return false; bool window_texture_loaded = false; - const Window window_at_cursor_position = get_window_at_cursor_position(display); - if(is_window_fullscreen_on_monitor(display, window_at_cursor_position, monitor) && window_at_cursor_position) - window_texture_loaded = window_texture_init(&window_texture, display, mgl_window_get_egl_display(window->internal_window()), window_at_cursor_position, egl_funcs) == 0; + Window focused_window = get_focused_window(display, WindowCaptureType::CURSOR); + if(!focused_window) + focused_window = get_focused_window(display, WindowCaptureType::FOCUSED); + if(focused_window && is_window_fullscreen_on_monitor(display, focused_window, monitor)) + window_texture_loaded = window_texture_init(&window_texture, display, mgl_window_get_egl_display(window->internal_window()), focused_window, egl_funcs) == 0; if(window_texture_loaded && window_texture.texture_id) { 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"); @@ -1474,4 +2224,4 @@ namespace gsr { XFlush(display); } } -}
\ No newline at end of file +} diff --git a/src/Process.cpp b/src/Process.cpp index 07d9dc6..c5fcf0f 100644 --- a/src/Process.cpp +++ b/src/Process.cpp @@ -9,6 +9,9 @@ #include <dirent.h> #include <stdlib.h> +#define PIPE_READ 0 +#define PIPE_WRITE 1 + namespace gsr { static void debug_print_args(const char **args) { fprintf(stderr, "gsr-ui info: running command:"); @@ -19,6 +22,24 @@ namespace gsr { fprintf(stderr, "\n"); } + static bool is_number(const char *str) { + for(int i = 0; str[i]; ++i) { + char c = str[i]; + if(c < '0' || c > '9') + return false; + } + return true; + } + + static int count_num_args(const char **args) { + int num_args = 0; + while(*args) { + ++num_args; + ++args; + } + return num_args; + } + bool exec_program_daemonized(const char **args) { /* 1 argument */ if(args[0] == nullptr) @@ -26,7 +47,7 @@ namespace gsr { debug_print_args(args); - pid_t pid = vfork(); + const pid_t pid = vfork(); if(pid == -1) { perror("Failed to vfork"); return false; @@ -35,10 +56,10 @@ namespace gsr { signal(SIGHUP, SIG_IGN); // Daemonize child to make the parent the init process which will reap the zombie child - pid_t second_child = vfork(); + const pid_t second_child = vfork(); if(second_child == 0) { // child execvp(args[0], (char* const*)args); - perror("execvp"); + perror(args[0]); _exit(127); } else if(second_child != -1) { // TODO: @@ -51,27 +72,112 @@ namespace gsr { return true; } - pid_t exec_program(const char **args) { + pid_t exec_program(const char **args, int *read_fd) { + if(read_fd) + *read_fd = -1; + /* 1 argument */ if(args[0] == nullptr) return -1; + int fds[2] = {-1, -1}; + if(pipe(fds) == -1) + return -1; + debug_print_args(args); - pid_t pid = vfork(); + const pid_t pid = vfork(); if(pid == -1) { + close(fds[PIPE_READ]); + close(fds[PIPE_WRITE]); perror("Failed to vfork"); return -1; } else if(pid == 0) { /* child */ + dup2(fds[PIPE_WRITE], STDOUT_FILENO); + close(fds[PIPE_READ]); + close(fds[PIPE_WRITE]); + execvp(args[0], (char* const*)args); - perror("execvp"); + perror(args[0]); _exit(127); } else { /* parent */ + close(fds[PIPE_WRITE]); + if(read_fd) + *read_fd = fds[PIPE_READ]; + else + close(fds[PIPE_READ]); return pid; } } - bool read_cmdline_arg0(const char *filepath, char *output_buffer) { + int exec_program_get_stdout(const char **args, std::string &result) { + result.clear(); + int read_fd = -1; + const pid_t process_id = exec_program(args, &read_fd); + if(process_id == -1) + return -1; + + int exit_status = 0; + char buffer[8192]; + for(;;) { + ssize_t bytes_read = read(read_fd, buffer, sizeof(buffer)); + if(bytes_read == 0) { + break; + } else if(bytes_read == -1) { + fprintf(stderr, "Failed to read from pipe to program %s, error: %s\n", args[0], strerror(errno)); + exit_status = -1; + break; + } + + buffer[bytes_read] = '\0'; + result.append(buffer, bytes_read); + } + + if(exit_status != 0) + kill(process_id, SIGKILL); + + int status = 0; + if(waitpid(process_id, &status, 0) == -1) { + perror("waitpid failed"); + exit_status = -1; + } + + if(!WIFEXITED(status)) + exit_status = -1; + + if(exit_status == 0) + exit_status = WEXITSTATUS(status); + + close(read_fd); + return exit_status; + } + + int exec_program_on_host_get_stdout(const char **args, std::string &result) { + if(count_num_args(args) > 64 - 3) { + fprintf(stderr, "Error: too many arguments when trying to launch \"%s\"\n", args[0]); + return -1; + } + + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + if(inside_flatpak) { + // Assumes programs wont need more than 64 - 3 args + const char *modified_args[64] = { "flatpak-spawn", "--host", "--" }; + for(int i = 3; i < 64; ++i) { + const char *arg = args[i - 3]; + if(!arg) { + modified_args[i] = nullptr; + break; + } + modified_args[i] = arg; + } + return exec_program_get_stdout(modified_args, result); + } else { + return exec_program_get_stdout(args, result); + } + } + + // |output_buffer| should be at least PATH_MAX in size + bool read_cmdline_arg0(const char *filepath, char *output_buffer, int output_buffer_size) { output_buffer[0] = '\0'; const char *arg0_end = NULL; @@ -88,13 +194,43 @@ namespace gsr { if(!arg0_end) goto err; - memcpy(output_buffer, buffer, arg0_end - buffer); - output_buffer[arg0_end - buffer] = '\0'; - close(fd); - return true; + if((arg0_end - buffer) + 1 <= output_buffer_size) { + memcpy(output_buffer, buffer, arg0_end - buffer); + output_buffer[arg0_end - buffer] = '\0'; + close(fd); + return true; + } err: close(fd); return false; } + + pid_t pidof(const char *process_name, pid_t ignore_pid) { + pid_t result = -1; + DIR *dir = opendir("/proc"); + if(!dir) + return -1; + + char cmdline_filepath[PATH_MAX]; + char arg0[PATH_MAX]; + + struct dirent *entry; + while((entry = readdir(dir)) != NULL) { + if(!is_number(entry->d_name)) + continue; + + snprintf(cmdline_filepath, sizeof(cmdline_filepath), "/proc/%s/cmdline", entry->d_name); + if(read_cmdline_arg0(cmdline_filepath, arg0, sizeof(arg0)) && strcmp(process_name, arg0) == 0) { + const pid_t pid = atoi(entry->d_name); + if(pid != ignore_pid) { + result = pid; + break; + } + } + } + + closedir(dir); + return result; + } }
\ No newline at end of file diff --git a/src/Rpc.cpp b/src/Rpc.cpp new file mode 100644 index 0000000..3eec98d --- /dev/null +++ b/src/Rpc.cpp @@ -0,0 +1,133 @@ +#include "../include/Rpc.hpp" +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <limits.h> +#include <string.h> +#include <errno.h> +#include <sys/stat.h> +#include <fcntl.h> + +namespace gsr { + static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) { + char dir[PATH_MAX]; + + const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + if(runtime_dir) + snprintf(dir, sizeof(dir), "%s", runtime_dir); + else + snprintf(dir, sizeof(dir), "/run/user/%d", geteuid()); + + if(access(dir, F_OK) != 0) + snprintf(dir, sizeof(dir), "/tmp"); + + snprintf(buffer, buffer_size, "%s/%s", dir, filename); + } + + Rpc::~Rpc() { + if(fd > 0) + close(fd); + + if(file) + fclose(file); + + if(!fifo_filepath.empty()) + remove(fifo_filepath.c_str()); + } + + bool Rpc::create(const char *name) { + if(file) { + fprintf(stderr, "Error: Rpc::create: already created/opened\n"); + return false; + } + + char fifo_filepath_tmp[PATH_MAX]; + get_runtime_filepath(fifo_filepath_tmp, sizeof(fifo_filepath_tmp), name); + fifo_filepath = fifo_filepath_tmp; + remove(fifo_filepath.c_str()); + + if(mkfifo(fifo_filepath.c_str(), 0600) != 0) { + fprintf(stderr, "Error: mkfifo failed, error: %s, %s\n", strerror(errno), fifo_filepath.c_str()); + return false; + } + + if(!open_filepath(fifo_filepath.c_str())) { + remove(fifo_filepath.c_str()); + fifo_filepath.clear(); + return false; + } + + return true; + } + + bool Rpc::open(const char *name) { + if(file) { + fprintf(stderr, "Error: Rpc::open: already created/opened\n"); + return false; + } + + char fifo_filepath_tmp[PATH_MAX]; + get_runtime_filepath(fifo_filepath_tmp, sizeof(fifo_filepath_tmp), name); + return open_filepath(fifo_filepath_tmp); + } + + bool Rpc::open_filepath(const char *filepath) { + fd = ::open(filepath, O_RDWR | O_NONBLOCK); + if(fd <= 0) + return false; + + file = fdopen(fd, "r+"); + if(!file) { + close(fd); + fd = 0; + return false; + } + fd = 0; + return true; + } + + bool Rpc::write(const char *str, size_t size) { + if(!file) { + fprintf(stderr, "Error: Rpc::write: fifo not created/opened yet\n"); + return false; + } + + ssize_t offset = 0; + while(offset < (ssize_t)size) { + const ssize_t bytes_written = fwrite(str + offset, 1, size - offset, file); + fflush(file); + if(bytes_written > 0) + offset += bytes_written; + } + return true; + } + + void Rpc::poll() { + if(!file) { + //fprintf(stderr, "Error: Rpc::poll: fifo not created/opened yet\n"); + return; + } + + std::string name; + char line[1024]; + while(fgets(line, sizeof(line), file)) { + int line_len = strlen(line); + if(line_len == 0) + continue; + + if(line[line_len - 1] == '\n') { + line[line_len - 1] = '\0'; + --line_len; + } + + name = line; + auto it = handlers_by_name.find(name); + if(it != handlers_by_name.end()) + it->second(name); + } + } + + bool Rpc::add_handler(const std::string &name, RpcCallback callback) { + return handlers_by_name.insert(std::make_pair(name, std::move(callback))).second; + } +}
\ No newline at end of file diff --git a/src/Theme.cpp b/src/Theme.cpp index a88aa1e..a6d1050 100644 --- a/src/Theme.cpp +++ b/src/Theme.cpp @@ -1,4 +1,5 @@ #include "../include/Theme.hpp" +#include "../include/Config.hpp" #include "../include/GsrInfo.hpp" #include <assert.h> @@ -7,6 +8,27 @@ namespace gsr { static Theme *theme = nullptr; static ColorTheme *color_theme = nullptr; + static mgl::Color gpu_vendor_to_color(GpuVendor vendor) { + switch(vendor) { + case GpuVendor::UNKNOWN: return mgl::Color(221, 0, 49); + case GpuVendor::AMD: return mgl::Color(221, 0, 49); + case GpuVendor::INTEL: return mgl::Color(8, 109, 183); + case GpuVendor::NVIDIA: return mgl::Color(118, 185, 0); + } + return mgl::Color(221, 0, 49); + } + + static mgl::Color color_name_to_color(const std::string &color_name) { + GpuVendor vendor = GpuVendor::UNKNOWN; + if(color_name == "amd") + vendor = GpuVendor::AMD; + else if(color_name == "intel") + vendor = GpuVendor::INTEL; + else if(color_name == "nvidia") + vendor = GpuVendor::NVIDIA; + return gpu_vendor_to_color(vendor); + } + bool Theme::set_window_size(mgl::vec2i window_size) { if(std::abs(window_size.x - window_width) < 0.1f && std::abs(window_size.y - window_height) < 0.1f) return true; @@ -44,6 +66,9 @@ namespace gsr { if(!theme->settings_texture.load_from_file((resources_path + "images/settings.png").c_str())) goto error; + if(!theme->settings_small_texture.load_from_file((resources_path + "images/settings_small.png").c_str())) + goto error; + if(!theme->folder_texture.load_from_file((resources_path + "images/folder.png").c_str())) goto error; @@ -102,29 +127,16 @@ namespace gsr { return *theme; } - bool init_color_theme(const GsrInfo &gsr_info) { + bool init_color_theme(const Config &config, const GsrInfo &gsr_info) { if(color_theme) return true; color_theme = new ColorTheme(); - switch(gsr_info.gpu_info.vendor) { - case GpuVendor::UNKNOWN: { - break; - } - case GpuVendor::AMD: { - color_theme->tint_color = mgl::Color(221, 0, 49); - break; - } - case GpuVendor::INTEL: { - color_theme->tint_color = mgl::Color(8, 109, 183); - break; - } - case GpuVendor::NVIDIA: { - color_theme->tint_color = mgl::Color(118, 185, 0); - break; - } - } + if(config.main_config.tint_color.empty()) + color_theme->tint_color = gpu_vendor_to_color(gsr_info.gpu_info.vendor); + else + color_theme->tint_color = color_name_to_color(config.main_config.tint_color); return true; } diff --git a/src/WindowUtils.cpp b/src/WindowUtils.cpp new file mode 100644 index 0000000..ec01e26 --- /dev/null +++ b/src/WindowUtils.cpp @@ -0,0 +1,530 @@ +#include "../include/WindowUtils.hpp" + +#include <X11/Xlib.h> +#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> + +#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; + int format; + unsigned char *properties = NULL; + if(XGetWindowProperty(dpy, window, atom, 0, 1024, False, AnyPropertyType, &type, &format, &len, &bytes_left, &properties) < Success) + return false; + + if(properties) + XFree(properties); + + return type != None; + } + + static bool window_is_user_program(Display *dpy, Window window) { + const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False); + const Atom wm_state_atom = XInternAtom(dpy, "WM_STATE", False); + return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom); + } + + static Window window_get_target_window_child(Display *display, Window window) { + if(window == None) + return None; + + if(window_is_user_program(display, window)) + return window; + + Window root; + Window parent; + Window *children = nullptr; + unsigned int num_children = 0; + if(!XQueryTree(display, window, &root, &parent, &children, &num_children) || !children) + return None; + + Window found_window = None; + for(int i = num_children - 1; i >= 0; --i) { + if(children[i] && window_is_user_program(display, children[i])) { + found_window = children[i]; + goto finished; + } + } + + for(int i = num_children - 1; i >= 0; --i) { + if(children[i]) { + Window win = window_get_target_window_child(display, children[i]); + if(win) { + found_window = win; + goto finished; + } + } + } + + finished: + XFree(children); + return found_window; + } + + 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); + + const Window direct_window = *window; + *window = window_get_target_window_child(dpy, *window); + // HACK: Count some other x11 windows as having an x11 window focused. Some games seem to create an Input window and that gets focused. + if(!*window) { + XWindowAttributes attr; + memset(&attr, 0, sizeof(attr)); + XGetWindowAttributes(dpy, direct_window, &attr); + if(attr.c_class == InputOnly && !get_window_title(dpy, direct_window)) + *window = direct_window; + } + return root_pos; + } + + Window get_focused_window(Display *dpy, WindowCaptureType cap_type) { + //const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); + Window focused_window = None; + + if(cap_type == WindowCaptureType::FOCUSED) { + // Atom type = None; + // int format = 0; + // unsigned long num_items = 0; + // unsigned long bytes_left = 0; + // unsigned char *data = NULL; + // XGetWindowProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, 0, 1, False, XA_WINDOW, &type, &format, &num_items, &bytes_left, &data); + + // if(type == XA_WINDOW && num_items == 1 && data) + // focused_window = *(Window*)data; + + // if(data) + // XFree(data); + + // if(focused_window) + // return focused_window; + + int revert_to = 0; + XGetInputFocus(dpy, &focused_window, &revert_to); + if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window)) + return focused_window; + } + + get_cursor_position(dpy, &focused_window); + if(focused_window && focused_window != DefaultRootWindow(dpy)) + return focused_window; + + return None; + } + + 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; + } + + std::optional<std::string> get_window_title(Display *dpy, Window window) { + std::optional<std::string> result; + 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); + + Atom type = None; + int format = 0; + unsigned long num_items = 0; + unsigned long bytes_left = 0; + unsigned char *data = NULL; + 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) { + result = utf8_sanitize(data, num_items); + goto done; + } + + if(data) + XFree(data); + + type = None; + format = 0; + num_items = 0; + bytes_left = 0; + data = NULL; + 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) { + result = utf8_sanitize(data, num_items); + goto done; + } + + done: + if(data) + XFree(data); + return result; + } + + 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] != ' ') { + start_index += i; + str_len -= i; + break; + } + } + + for(int i = str_len - 1; i >= 0; --i) { + if(str[i] != ' ') { + str_len = i + 1; + break; + } + } + + return str.substr(start_index, str_len); + } + + std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) { + std::string result; + const Window focused_window = get_focused_window(dpy, window_capture_type); + if(focused_window == None) + return result; + + // Window title is not always ideal (for example for a browser), but for games its pretty much required + const std::optional<std::string> window_title = get_window_title(dpy, focused_window); + if(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 = strip(class_hint.res_class); + return result; + } + + return result; + } + + std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window) { + std::string result; + + Window root; + Window parent; + Window *children = nullptr; + unsigned int num_children = 0; + if(!XQueryTree(dpy, DefaultRootWindow(dpy), &root, &parent, &children, &num_children) || !children) + return result; + + for(int i = (int)num_children - 1; i >= 0; --i) { + if(children[i] == ignore_window) + continue; + + XWindowAttributes attr; + memset(&attr, 0, sizeof(attr)); + XGetWindowAttributes(dpy, children[i], &attr); + if(attr.override_redirect || attr.c_class != InputOutput || attr.map_state != IsViewable) + continue; + + if(position.x >= attr.x && position.x <= attr.x + attr.width && position.y >= attr.y && position.y <= attr.y + attr.height) { + const Window real_window = window_get_target_window_child(dpy, children[i]); + if(!real_window || real_window == ignore_window) + continue; + + const std::optional<std::string> window_title = get_window_title(dpy, real_window); + if(window_title) + result = strip(window_title.value()); + + break; + } + } + + XFree(children); + return result; + } + + std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window) { + Window cursor_window; + const mgl::vec2i cursor_position = get_cursor_position(dpy, &cursor_window); + return get_window_name_at_position(dpy, cursor_position, ignore_window); + } + + 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, 200); + 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; + } + + while(XPending(display)) { + 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; + goto done; + } + } + } + + done: + 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, 200); + 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; + } + + while(XPending(display)) { + 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) + goto done; + } 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) + goto done; + } + } + } + + done: + 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; + } +}
\ No newline at end of file diff --git a/src/gui/Button.cpp b/src/gui/Button.cpp index fbf5cdd..54d1854 100644 --- a/src/gui/Button.cpp +++ b/src/gui/Button.cpp @@ -12,7 +12,15 @@ namespace gsr { static const float padding_left_scale = 0.007f; static const float padding_right_scale = 0.007f; - Button::Button(mgl::Font *font, const char *text, mgl::vec2f size, mgl::Color bg_color) : size(size), bg_color(bg_color), text(text, *font) { + // These are relative to the button size + static const float padding_top_icon_scale = 0.25f; + static const float padding_bottom_icon_scale = 0.25f; + static const float padding_left_icon_scale = 0.25f; + static const float padding_right_icon_scale = 0.25f; + + Button::Button(mgl::Font *font, const char *text, mgl::vec2f size, mgl::Color bg_color) : + size(size), bg_color(bg_color), bg_hover_color(bg_color), text(text, *font) + { } @@ -37,17 +45,23 @@ namespace gsr { return; const mgl::vec2f draw_pos = position + offset; - const mgl::vec2f item_size = get_size().floor(); + const bool mouse_inside = mgl::FloatRect(draw_pos, item_size).contains(window.get_mouse_position().to_vec2f()) && !has_parent_with_selected_child_widget(); + mgl::Rectangle background(item_size); background.set_position(draw_pos.floor()); - background.set_color(bg_color); + background.set_color(mouse_inside ? bg_hover_color : bg_color); window.draw(background); text.set_position((draw_pos + item_size * 0.5f - text.get_bounds().size * 0.5f).floor()); window.draw(text); - const bool mouse_inside = mgl::FloatRect(draw_pos, item_size).contains(window.get_mouse_position().to_vec2f()) && !has_parent_with_selected_child_widget(); + if(sprite.get_texture() && sprite.get_texture()->is_valid()) { + scale_sprite_to_button_size(); + sprite.set_position((background.get_position() + background.get_size() * 0.5f - sprite.get_size() * 0.5f).floor()); + window.draw(sprite); + } + if(mouse_inside) { const mgl::Color outline_color = (bg_color == get_color_theme().tint_color) ? mgl::Color(255, 255, 255) : get_color_theme().tint_color; draw_rectangle_outline(window, draw_pos, item_size, outline_color, std::max(1.0f, border_scale * get_theme().window_height)); @@ -76,6 +90,14 @@ namespace gsr { border_scale = scale; } + void Button::set_bg_hover_color(mgl::Color color) { + bg_hover_color = color; + } + + void Button::set_icon(mgl::Texture *texture) { + sprite.set_texture(texture); + } + const std::string& Button::get_text() const { return text.get_string(); } @@ -83,4 +105,18 @@ namespace gsr { void Button::set_text(std::string str) { text.set_string(std::move(str)); } + + void Button::scale_sprite_to_button_size() { + if(!sprite.get_texture() || !sprite.get_texture()->is_valid()) + return; + + const mgl::vec2f button_size = get_size(); + const int padding_icon_top = padding_top_icon_scale * button_size.y; + const int padding_icon_bottom = padding_bottom_icon_scale * button_size.y; + const int padding_icon_left = padding_left_icon_scale * button_size.y; + const int padding_icon_right = padding_right_icon_scale * button_size.y; + + const mgl::vec2f desired_size = button_size - mgl::vec2f(padding_icon_left + padding_icon_right, padding_icon_top + padding_icon_bottom); + sprite.set_size(scale_keep_aspect_ratio(sprite.get_texture()->get_size().to_vec2f(), desired_size).floor()); + } }
\ No newline at end of file diff --git a/src/gui/ComboBox.cpp b/src/gui/ComboBox.cpp index 62b2086..948e3a4 100644 --- a/src/gui/ComboBox.cpp +++ b/src/gui/ComboBox.cpp @@ -26,16 +26,21 @@ namespace gsr { return true; if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) { + const int padding_top = padding_top_scale * get_theme().window_height; + const int padding_bottom = padding_bottom_scale * get_theme().window_height; + const mgl::vec2f mouse_pos = { (float)event.mouse_button.x, (float)event.mouse_button.y }; - const mgl::vec2f item_size = get_size(); + mgl::vec2f item_size = get_size(); if(show_dropdown) { for(size_t i = 0; i < items.size(); ++i) { Item &item = items[i]; + item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom; if(mgl::FloatRect(item.position, item_size).contains(mouse_pos)) { const size_t prev_selected_item = selected_item; selected_item = i; show_dropdown = false; + dirty = true; remove_widget_as_selected_in_parent(); if(selected_item != prev_selected_item && on_selection_changed) @@ -47,6 +52,7 @@ namespace gsr { } const mgl::vec2f draw_pos = position + offset; + item_size = get_size(); if(mgl::FloatRect(draw_pos, item_size).contains(mouse_pos)) { show_dropdown = !show_dropdown; if(show_dropdown) @@ -66,9 +72,10 @@ namespace gsr { if(!visible) return; + //const mgl::Scissor scissor = window.get_scissor(); update_if_dirty(); - const mgl::vec2f draw_pos = (position + offset).floor(); + //max_size.x = std::min((scissor.position.x + scissor.size.x) - draw_pos.x, max_size.x); if(show_dropdown) draw_selected(window, draw_pos); @@ -78,6 +85,8 @@ namespace gsr { void ComboBox::add_item(const std::string &text, const std::string &id) { items.push_back({mgl::Text(text, *font), id, {0.0f, 0.0f}}); + items.back().text.set_max_width(font->get_character_size() * 22); // TODO: Make a proper solution + //items.back().text.set_max_rows(1); dirty = true; } @@ -87,6 +96,7 @@ namespace gsr { if(item.id == id) { const size_t prev_selected_item = selected_item; selected_item = i; + dirty = true; if(trigger_event && (trigger_event_even_if_selection_not_changed || selected_item != prev_selected_item) && on_selection_changed) on_selection_changed(item.text.get_string(), item.id); @@ -107,13 +117,13 @@ namespace gsr { void ComboBox::draw_selected(mgl::Window &window, mgl::vec2f draw_pos) { const int padding_top = padding_top_scale * get_theme().window_height; + const int padding_bottom = padding_bottom_scale * get_theme().window_height; const int padding_left = padding_left_scale * get_theme().window_height; - mgl_scissor scissor; - mgl_window_get_scissor(window.internal_window(), &scissor); + const mgl::Scissor scissor = window.get_scissor(); const bool bottom_is_outside_scissor = draw_pos.y + max_size.y > scissor.position.y + scissor.size.y; - const mgl::vec2f item_size = get_size(); + mgl::vec2f item_size = get_size(); mgl::vec2f items_draw_pos = draw_pos + mgl::vec2f(0.0f, item_size.y); mgl::Rectangle background(draw_pos, item_size.floor()); @@ -137,6 +147,9 @@ namespace gsr { const mgl::vec2f mouse_pos = window.get_mouse_position().to_vec2f(); for(size_t i = 0; i < items.size(); ++i) { + Item &item = items[i]; + item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom; + if(!cursor_inside) { cursor_inside = mgl::FloatRect(items_draw_pos, item_size).contains(mouse_pos); if(cursor_inside) { @@ -146,7 +159,6 @@ namespace gsr { } } - Item &item = items[i]; item.text.set_position((items_draw_pos + mgl::vec2f(padding_left, padding_top)).floor()); window.draw(item.text); @@ -160,7 +172,7 @@ namespace gsr { const int padding_left = padding_left_scale * get_theme().window_height; const int padding_right = padding_right_scale * get_theme().window_height; - const mgl::vec2f item_size = get_size(); + mgl::vec2f item_size = get_size(); mgl::Rectangle background(draw_pos.floor(), item_size.floor()); background.set_color(mgl::Color(0, 0, 0, 120)); window.draw(background); @@ -197,11 +209,12 @@ namespace gsr { const int padding_left = padding_left_scale * get_theme().window_height; const int padding_right = padding_right_scale * get_theme().window_height; - max_size = { 0.0f, font->get_character_size() + (float)padding_top + (float)padding_bottom }; + Item *selected_item_ptr = (selected_item < items.size()) ? &items[selected_item] : nullptr; + max_size = { 0.0f, padding_top + padding_bottom + (selected_item_ptr ? selected_item_ptr->text.get_bounds().size.y : 0.0f) }; for(Item &item : items) { const mgl::vec2f bounds = item.text.get_bounds().size; max_size.x = std::max(max_size.x, bounds.x + padding_left + padding_right); - max_size.y += bounds.y + padding_top + padding_bottom; + max_size.y += padding_top + bounds.y + padding_bottom; } if(max_size.x <= 0.001f) @@ -219,7 +232,8 @@ namespace gsr { const int padding_top = padding_top_scale * get_theme().window_height; const int padding_bottom = padding_bottom_scale * get_theme().window_height; - return { max_size.x, font->get_character_size() + (float)padding_top + (float)padding_bottom }; + Item *selected_item_ptr = (selected_item < items.size()) ? &items[selected_item] : nullptr; + return { max_size.x, padding_top + padding_bottom + (selected_item_ptr ? selected_item_ptr->text.get_bounds().size.y : 0.0f) }; } float ComboBox::get_dropdown_arrow_height() const { diff --git a/src/gui/CustomRendererWidget.cpp b/src/gui/CustomRendererWidget.cpp index cfb113b..5b6c809 100644 --- a/src/gui/CustomRendererWidget.cpp +++ b/src/gui/CustomRendererWidget.cpp @@ -17,19 +17,11 @@ namespace gsr { const mgl::vec2f draw_pos = position + offset; - mgl_scissor prev_scissor; - mgl_window_get_scissor(window.internal_window(), &prev_scissor); - - mgl_scissor new_scissor = { - mgl_vec2i{(int)draw_pos.x, (int)draw_pos.y}, - mgl_vec2i{(int)size.x, (int)size.y} - }; - mgl_window_set_scissor(window.internal_window(), &new_scissor); - + const mgl::Scissor prev_scissor = window.get_scissor(); + window.set_scissor({draw_pos.to_vec2i(), size.to_vec2i()}); if(draw_handler) draw_handler(window, draw_pos, size); - - mgl_window_set_scissor(window.internal_window(), &prev_scissor); + window.set_scissor(prev_scissor); } mgl::vec2f CustomRendererWidget::get_size() { diff --git a/src/gui/DropdownButton.cpp b/src/gui/DropdownButton.cpp index 4a2ae3a..81bc015 100644 --- a/src/gui/DropdownButton.cpp +++ b/src/gui/DropdownButton.cpp @@ -20,7 +20,7 @@ namespace gsr { { if(icon_texture && icon_texture->is_valid()) { icon_sprite.set_texture(icon_texture); - icon_sprite.set_height((int)(size.y * 0.5f)); + icon_sprite.set_height((int)(size.y * 0.45f)); } this->description.set_color(mgl::Color(150, 150, 150)); } @@ -242,4 +242,4 @@ namespace gsr { update_if_dirty(); return size; } -}
\ No newline at end of file +} diff --git a/src/gui/FileChooser.cpp b/src/gui/FileChooser.cpp index a58a582..ceb8c94 100644 --- a/src/gui/FileChooser.cpp +++ b/src/gui/FileChooser.cpp @@ -65,8 +65,7 @@ namespace gsr { if(!visible) return; - mgl_scissor scissor; - mgl_window_get_scissor(window.internal_window(), &scissor); + const mgl::Scissor scissor = window.get_scissor(); const mgl::vec2f draw_pos = position + offset; const mgl::vec2f mouse_pos = window.get_mouse_position().to_vec2f(); @@ -96,7 +95,12 @@ namespace gsr { selected_item_background.set_color(get_color_theme().tint_color); window.draw(selected_item_background); } - if(!has_parent_with_selected_child_widget() && mouse_over_item == -1 && mgl::FloatRect(item_pos, item_size).contains(mouse_pos)) { + + if(!has_parent_with_selected_child_widget() && mouse_over_item == -1 && + mouse_pos.x >= scissor.position.x && mouse_pos.x <= scissor.position.x + scissor.size.x && + mouse_pos.y >= scissor.position.y && mouse_pos.y <= scissor.position.y + scissor.size.y && + mgl::FloatRect(item_pos, item_size).contains(mouse_pos)) + { // mgl::Rectangle selected_item_background(item_size.floor()); // selected_item_background.set_position(item_pos.floor()); // selected_item_background.set_color(mgl::Color(20, 20, 20, 150)); @@ -106,7 +110,7 @@ namespace gsr { mouse_over_item = i; } - if(item_pos.y + item_size.y >= draw_pos.y && item_pos.y < scissor.position.y + scissor.size.y) { + if(item_pos.y + item_size.y >= scissor.position.y && item_pos.y < scissor.position.y + scissor.size.y) { window.draw(folder_sprite); // TODO: Dont allow text to go further left/right than item_pos (on the left side) and item_pos + item_size (on the right side). diff --git a/src/gui/GlobalSettingsPage.cpp b/src/gui/GlobalSettingsPage.cpp new file mode 100644 index 0000000..d00ad49 --- /dev/null +++ b/src/gui/GlobalSettingsPage.cpp @@ -0,0 +1,653 @@ +#include "../../include/gui/GlobalSettingsPage.hpp" + +#include "../../include/Overlay.hpp" +#include "../../include/GlobalHotkeys.hpp" +#include "../../include/Theme.hpp" +#include "../../include/Process.hpp" +#include "../../include/gui/GsrPage.hpp" +#include "../../include/gui/PageStack.hpp" +#include "../../include/gui/ScrollablePage.hpp" +#include "../../include/gui/Subsection.hpp" +#include "../../include/gui/List.hpp" +#include "../../include/gui/Label.hpp" +#include "../../include/gui/RadioButton.hpp" +#include "../../include/gui/LineSeparator.hpp" +#include "../../include/gui/CustomRendererWidget.hpp" + +#include <assert.h> +#include <X11/Xlib.h> +extern "C" { +#include <mgl/mgl.h> +} +#include <mglpp/window/Window.hpp> +#include <mglpp/graphics/Rectangle.hpp> +#include <mglpp/graphics/Text.hpp> + +#ifndef GSR_UI_VERSION +#define GSR_UI_VERSION "Unknown" +#endif + +#ifndef GSR_FLATPAK_VERSION +#define GSR_FLATPAK_VERSION "Unknown" +#endif + +namespace gsr { + static const char* gpu_vendor_to_color_name(GpuVendor vendor) { + switch(vendor) { + case GpuVendor::UNKNOWN: return "amd"; + case GpuVendor::AMD: return "amd"; + case GpuVendor::INTEL: return "intel"; + case GpuVendor::NVIDIA: return "nvidia"; + } + return "amd"; + } + + static const char* gpu_vendor_to_string(GpuVendor vendor) { + switch(vendor) { + case GpuVendor::UNKNOWN: return "Unknown"; + case GpuVendor::AMD: return "AMD"; + case GpuVendor::INTEL: return "Intel"; + case GpuVendor::NVIDIA: return "NVIDIA"; + } + return "unknown"; + } + + static uint32_t mgl_modifier_to_hotkey_modifier(mgl::Keyboard::Key modifier_key) { + switch(modifier_key) { + case mgl::Keyboard::LControl: return HOTKEY_MOD_LCTRL; + case mgl::Keyboard::LShift: return HOTKEY_MOD_LSHIFT; + case mgl::Keyboard::LAlt: return HOTKEY_MOD_LALT; + case mgl::Keyboard::LSystem: return HOTKEY_MOD_LSUPER; + case mgl::Keyboard::RControl: return HOTKEY_MOD_RCTRL; + case mgl::Keyboard::RShift: return HOTKEY_MOD_RSHIFT; + case mgl::Keyboard::RAlt: return HOTKEY_MOD_RALT; + case mgl::Keyboard::RSystem: return HOTKEY_MOD_RSUPER; + default: return 0; + } + return 0; + } + + static std::vector<mgl::Keyboard::Key> hotkey_modifiers_to_mgl_keys(uint32_t modifiers) { + std::vector<mgl::Keyboard::Key> result; + if(modifiers & HOTKEY_MOD_LCTRL) + result.push_back(mgl::Keyboard::LControl); + if(modifiers & HOTKEY_MOD_LSHIFT) + result.push_back(mgl::Keyboard::LShift); + if(modifiers & HOTKEY_MOD_LALT) + result.push_back(mgl::Keyboard::LAlt); + if(modifiers & HOTKEY_MOD_LSUPER) + result.push_back(mgl::Keyboard::LSystem); + if(modifiers & HOTKEY_MOD_RCTRL) + result.push_back(mgl::Keyboard::RControl); + if(modifiers & HOTKEY_MOD_RSHIFT) + result.push_back(mgl::Keyboard::RShift); + if(modifiers & HOTKEY_MOD_RALT) + result.push_back(mgl::Keyboard::RAlt); + if(modifiers & HOTKEY_MOD_RSUPER) + result.push_back(mgl::Keyboard::RSystem); + return result; + } + + static std::string config_hotkey_to_string(ConfigHotkey config_hotkey) { + std::string result; + + const std::vector<mgl::Keyboard::Key> modifier_keys = hotkey_modifiers_to_mgl_keys(config_hotkey.modifiers); + for(const mgl::Keyboard::Key modifier_key : modifier_keys) { + if(!result.empty()) + result += " + "; + result += mgl::Keyboard::key_to_string(modifier_key); + } + + if(config_hotkey.key != 0) { + if(!result.empty()) + result += " + "; + result += mgl::Keyboard::key_to_string((mgl::Keyboard::Key)config_hotkey.key); + } + + return result; + } + + GlobalSettingsPage::GlobalSettingsPage(Overlay *overlay, const GsrInfo *gsr_info, Config &config, PageStack *page_stack) : + StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()), + overlay(overlay), + config(config), + gsr_info(gsr_info), + page_stack(page_stack) + { + auto content_page = std::make_unique<GsrPage>(); + content_page->add_button("Back", "back", get_color_theme().page_bg_color); + content_page->on_click = [page_stack](const std::string &id) { + if(id == "back") + page_stack->pop(); + }; + content_page_ptr = content_page.get(); + add_widget(std::move(content_page)); + + add_widgets(); + load(); + + auto hotkey_overlay = std::make_unique<CustomRendererWidget>(get_size()); + hotkey_overlay->draw_handler = [this](mgl::Window &window, mgl::vec2f, mgl::vec2f) { + Button *configure_hotkey_button = configure_hotkey_get_button_by_active_type(); + if(!configure_hotkey_button) + return; + + mgl::Text title_text("Press a key combination to use for the hotkey \"" + hotkey_configure_action_name + "\":", get_theme().title_font); + mgl::Text hotkey_text(configure_hotkey_button->get_text(), get_theme().top_bar_font); + mgl::Text description_text("The hotkey has to contain one or more of these keys: Alt, Ctrl, Shift and Super. Press Esc to cancel.", get_theme().body_font); + const float text_max_width = std::max(title_text.get_bounds().size.x, std::max(hotkey_text.get_bounds().size.x, description_text.get_bounds().size.x)); + + const float padding_horizontal = int(get_theme().window_height * 0.01f); + const float padding_vertical = int(get_theme().window_height * 0.01f); + + const mgl::vec2f bg_size = mgl::vec2f(text_max_width + padding_horizontal*2.0f, get_theme().window_height * 0.1f).floor(); + mgl::Rectangle bg_rect(mgl::vec2f(get_theme().window_width*0.5f - bg_size.x*0.5f, get_theme().window_height*0.5f - bg_size.y*0.5f).floor(), bg_size); + bg_rect.set_color(get_color_theme().page_bg_color); + window.draw(bg_rect); + + const mgl::vec2f tint_size = mgl::vec2f(bg_size.x, 0.004f * get_theme().window_height).floor(); + mgl::Rectangle tint_rect(bg_rect.get_position() - mgl::vec2f(0.0f, tint_size.y), tint_size); + tint_rect.set_color(get_color_theme().tint_color); + window.draw(tint_rect); + + title_text.set_position(mgl::vec2f(bg_rect.get_position() + mgl::vec2f(bg_rect.get_size().x*0.5f - title_text.get_bounds().size.x*0.5f, padding_vertical)).floor()); + window.draw(title_text); + + hotkey_text.set_position(mgl::vec2f(bg_rect.get_position() + bg_rect.get_size()*0.5f - hotkey_text.get_bounds().size*0.5f).floor()); + window.draw(hotkey_text); + + const float caret_padding_x = int(0.001f * get_theme().window_height); + const mgl::vec2f caret_size = mgl::vec2f(std::max(2.0f, 0.002f * get_theme().window_height), hotkey_text.get_bounds().size.y).floor(); + mgl::Rectangle caret_rect(hotkey_text.get_position() + mgl::vec2f(hotkey_text.get_bounds().size.x + caret_padding_x, hotkey_text.get_bounds().size.y*0.5f - caret_size.y*0.5f).floor(), caret_size); + window.draw(caret_rect); + + description_text.set_position(mgl::vec2f(bg_rect.get_position() + mgl::vec2f(bg_rect.get_size().x*0.5f - description_text.get_bounds().size.x*0.5f, bg_rect.get_size().y - description_text.get_bounds().size.y - padding_vertical)).floor()); + window.draw(description_text); + }; + hotkey_overlay->set_visible(false); + hotkey_overlay_ptr = hotkey_overlay.get(); + add_widget(std::move(hotkey_overlay)); + } + + std::unique_ptr<Subsection> GlobalSettingsPage::create_appearance_subsection(ScrollablePage *parent_page) { + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Accent color", get_color_theme().text_color)); + auto tint_color_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL); + tint_color_radio_button_ptr = tint_color_radio_button.get(); + tint_color_radio_button->add_item("Red", "amd"); + tint_color_radio_button->add_item("Green", "nvidia"); + tint_color_radio_button->add_item("blue", "intel"); + tint_color_radio_button->on_selection_changed = [](const std::string&, const std::string &id) { + if(id == "amd") + get_color_theme().tint_color = mgl::Color(221, 0, 49); + else if(id == "nvidia") + get_color_theme().tint_color = mgl::Color(118, 185, 0); + else if(id == "intel") + get_color_theme().tint_color = mgl::Color(8, 109, 183); + return true; + }; + list->add_widget(std::move(tint_color_radio_button)); + return std::make_unique<Subsection>("Appearance", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<Subsection> GlobalSettingsPage::create_startup_subsection(ScrollablePage *parent_page) { + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start program on system startup?", get_color_theme().text_color)); + auto startup_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL); + startup_radio_button_ptr = startup_radio_button.get(); + startup_radio_button->add_item("Yes", "start_on_system_startup"); + startup_radio_button->add_item("No", "dont_start_on_system_startup"); + startup_radio_button->on_selection_changed = [&](const std::string&, const std::string &id) { + bool enable = false; + if(id == "dont_start_on_system_startup") + enable = false; + else if(id == "start_on_system_startup") + enable = true; + else + return false; + + const char *args[] = { "systemctl", enable ? "enable" : "disable", "--user", "gpu-screen-recorder-ui", nullptr }; + std::string stdout_str; + const int exit_status = exec_program_on_host_get_stdout(args, stdout_str); + if(on_startup_changed) + on_startup_changed(enable, exit_status); + return exit_status == 0; + }; + list->add_widget(std::move(startup_radio_button)); + return std::make_unique<Subsection>("Startup", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<RadioButton> GlobalSettingsPage::create_enable_keyboard_hotkeys_button() { + auto enable_hotkeys_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL); + enable_keyboard_hotkeys_radio_button_ptr = enable_hotkeys_radio_button.get(); + enable_hotkeys_radio_button->add_item("Yes", "enable_hotkeys"); + enable_hotkeys_radio_button->add_item("No", "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_keyboard_hotkey_changed) + on_keyboard_hotkey_changed(id.c_str()); + return true; + }; + return enable_hotkeys_radio_button; + } + + std::unique_ptr<RadioButton> GlobalSettingsPage::create_enable_joystick_hotkeys_button() { + auto enable_hotkeys_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL); + enable_joystick_hotkeys_radio_button_ptr = enable_hotkeys_radio_button.get(); + enable_hotkeys_radio_button->add_item("Yes", "enable_hotkeys"); + enable_hotkeys_radio_button->add_item("No", "disable_hotkeys"); + enable_hotkeys_radio_button->on_selection_changed = [&](const std::string&, const std::string &id) { + if(on_joystick_hotkey_changed) + on_joystick_hotkey_changed(id.c_str()); + return true; + }; + return enable_hotkeys_radio_button; + } + + std::unique_ptr<List> GlobalSettingsPage::create_show_hide_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Show/hide UI:", get_color_theme().text_color)); + auto show_hide_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + show_hide_button_ptr = show_hide_button.get(); + list->add_widget(std::move(show_hide_button)); + + show_hide_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::SHOW_HIDE); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_replay_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Turn replay on/off:", get_color_theme().text_color)); + auto turn_replay_on_off_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + turn_replay_on_off_button_ptr = turn_replay_on_off_button.get(); + list->add_widget(std::move(turn_replay_on_off_button)); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Save replay:", get_color_theme().text_color)); + auto save_replay_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + save_replay_button_ptr = save_replay_button.get(); + list->add_widget(std::move(save_replay_button)); + + turn_replay_on_off_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::REPLAY_START_STOP); + }; + + save_replay_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::REPLAY_SAVE); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_record_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start/stop recording:", get_color_theme().text_color)); + auto start_stop_recording_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + start_stop_recording_button_ptr = start_stop_recording_button.get(); + list->add_widget(std::move(start_stop_recording_button)); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Pause/unpause recording:", get_color_theme().text_color)); + auto pause_unpause_recording_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + pause_unpause_recording_button_ptr = pause_unpause_recording_button.get(); + list->add_widget(std::move(pause_unpause_recording_button)); + + start_stop_recording_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::RECORD_START_STOP); + }; + + pause_unpause_recording_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_stream_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start/stop streaming:", get_color_theme().text_color)); + auto start_stop_streaming_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + start_stop_streaming_button_ptr = start_stop_streaming_button.get(); + list->add_widget(std::move(start_stop_streaming_button)); + + start_stop_streaming_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::STREAM_START_STOP); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_hotkey_control_buttons() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + // auto clear_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Clear hotkeys", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + // clear_hotkeys_button->on_click = [this] { + // config.streaming_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0}; + // config.record_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0}; + // config.record_config.pause_unpause_hotkey = {mgl::Keyboard::Unknown, 0}; + // config.replay_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0}; + // config.replay_config.save_hotkey = {mgl::Keyboard::Unknown, 0}; + // config.main_config.show_hide_hotkey = {mgl::Keyboard::Unknown, 0}; + // load_hotkeys(); + // overlay->rebind_all_keyboard_hotkeys(); + // }; + // list->add_widget(std::move(clear_hotkeys_button)); + + auto reset_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Reset hotkeys to default", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + reset_hotkeys_button->on_click = [this] { + config.streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT}; + config.record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT}; + config.record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT}; + config.replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT}; + config.replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT}; + config.main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT}; + load_hotkeys(); + overlay->rebind_all_keyboard_hotkeys(); + }; + list->add_widget(std::move(reset_hotkeys_button)); + + return list; + } + + std::unique_ptr<Subsection> GlobalSettingsPage::create_hotkey_subsection(ScrollablePage *parent_page) { + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + List *list_ptr = list.get(); + auto subsection = std::make_unique<Subsection>("Hotkeys", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); + + list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Enable keyboard hotkeys?", get_color_theme().text_color)); + list_ptr->add_widget(create_enable_keyboard_hotkeys_button()); + list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Enable controller hotkeys?", get_color_theme().text_color)); + list_ptr->add_widget(create_enable_joystick_hotkeys_button()); + list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, subsection->get_inner_size().x)); + list_ptr->add_widget(create_show_hide_hotkey_options()); + list_ptr->add_widget(create_replay_hotkey_options()); + list_ptr->add_widget(create_record_hotkey_options()); + list_ptr->add_widget(create_stream_hotkey_options()); + list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Double-click the controller share button to save a replay", get_color_theme().text_color)); + list_ptr->add_widget(create_hotkey_control_buttons()); + return subsection; + } + + std::unique_ptr<Button> GlobalSettingsPage::create_exit_program_button() { + auto exit_program_button = std::make_unique<Button>(&get_theme().body_font, "Exit program", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + exit_program_button->on_click = [&]() { + if(on_click_exit_program_button) + on_click_exit_program_button("exit"); + }; + return exit_program_button; + } + + std::unique_ptr<Button> GlobalSettingsPage::create_go_back_to_old_ui_button() { + auto exit_program_button = std::make_unique<Button>(&get_theme().body_font, "Go back to the old UI", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + exit_program_button->on_click = [&]() { + if(on_click_exit_program_button) + on_click_exit_program_button("back-to-old-ui"); + }; + return exit_program_button; + } + + std::unique_ptr<Subsection> GlobalSettingsPage::create_application_options_subsection(ScrollablePage *parent_page) { + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL); + list->add_widget(create_exit_program_button()); + if(inside_flatpak) + list->add_widget(create_go_back_to_old_ui_button()); + return std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<Subsection> GlobalSettingsPage::create_application_info_subsection(ScrollablePage *parent_page) { + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + + char str[128]; + const std::string gsr_version = gsr_info->system_info.gsr_version.to_string(); + snprintf(str, sizeof(str), "GSR version: %s", gsr_version.c_str()); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color)); + + snprintf(str, sizeof(str), "GSR-UI version: %s", GSR_UI_VERSION); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color)); + + if(inside_flatpak) { + snprintf(str, sizeof(str), "Flatpak version: %s", GSR_FLATPAK_VERSION); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color)); + } + + snprintf(str, sizeof(str), "GPU vendor: %s", gpu_vendor_to_string(gsr_info->gpu_info.vendor)); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color)); + + return std::make_unique<Subsection>("Application info", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); + } + + void GlobalSettingsPage::add_widgets() { + auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size()); + + auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL); + settings_list->set_spacing(0.018f); + settings_list->add_widget(create_appearance_subsection(scrollable_page.get())); + settings_list->add_widget(create_startup_subsection(scrollable_page.get())); + settings_list->add_widget(create_hotkey_subsection(scrollable_page.get())); + settings_list->add_widget(create_application_options_subsection(scrollable_page.get())); + settings_list->add_widget(create_application_info_subsection(scrollable_page.get())); + scrollable_page->add_widget(std::move(settings_list)); + + content_page_ptr->add_widget(std::move(scrollable_page)); + } + + void GlobalSettingsPage::on_navigate_away_from_page() { + save(); + } + + void GlobalSettingsPage::load() { + if(config.main_config.tint_color.empty()) + tint_color_radio_button_ptr->set_selected_item(gpu_vendor_to_color_name(gsr_info->gpu_info.vendor)); + else + tint_color_radio_button_ptr->set_selected_item(config.main_config.tint_color); + + const char *args[] = { "systemctl", "is-enabled", "--quiet", "--user", "gpu-screen-recorder-ui", nullptr }; + std::string stdout_str; + 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_keyboard_hotkeys_radio_button_ptr->set_selected_item(config.main_config.hotkeys_enable_option, false, false); + enable_joystick_hotkeys_radio_button_ptr->set_selected_item(config.main_config.joystick_hotkeys_enable_option, false, false); + + load_hotkeys(); + } + + void GlobalSettingsPage::load_hotkeys() { + turn_replay_on_off_button_ptr->set_text(config_hotkey_to_string(config.replay_config.start_stop_hotkey)); + save_replay_button_ptr->set_text(config_hotkey_to_string(config.replay_config.save_hotkey)); + + start_stop_recording_button_ptr->set_text(config_hotkey_to_string(config.record_config.start_stop_hotkey)); + pause_unpause_recording_button_ptr->set_text(config_hotkey_to_string(config.record_config.pause_unpause_hotkey)); + + start_stop_streaming_button_ptr->set_text(config_hotkey_to_string(config.streaming_config.start_stop_hotkey)); + + show_hide_button_ptr->set_text(config_hotkey_to_string(config.main_config.show_hide_hotkey)); + } + + void GlobalSettingsPage::save() { + configure_hotkey_cancel(); + config.main_config.tint_color = tint_color_radio_button_ptr->get_selected_id(); + config.main_config.hotkeys_enable_option = enable_keyboard_hotkeys_radio_button_ptr->get_selected_id(); + config.main_config.joystick_hotkeys_enable_option = enable_joystick_hotkeys_radio_button_ptr->get_selected_id(); + save_config(config); + } + + bool GlobalSettingsPage::on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) { + if(!StaticPage::on_event(event, window, offset)) + return false; + + if(configure_hotkey_type == ConfigureHotkeyType::NONE) + return true; + + Button *configure_hotkey_button = configure_hotkey_get_button_by_active_type(); + if(!configure_hotkey_button) + return true; + + if(event.type == mgl::Event::KeyPressed) { + if(event.key.code == mgl::Keyboard::Escape) + return false; + + if(mgl::Keyboard::key_is_modifier(event.key.code)) { + configure_config_hotkey.modifiers |= mgl_modifier_to_hotkey_modifier(event.key.code); + configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey)); + } else if(configure_config_hotkey.modifiers != 0) { + configure_config_hotkey.key = event.key.code; + configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey)); + configure_hotkey_stop_and_save(); + } + + return false; + } else if(event.type == mgl::Event::KeyReleased) { + if(event.key.code == mgl::Keyboard::Escape) { + configure_hotkey_cancel(); + return false; + } + + if(mgl::Keyboard::key_is_modifier(event.key.code)) { + configure_config_hotkey.modifiers &= ~mgl_modifier_to_hotkey_modifier(event.key.code); + configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey)); + } + + return false; + } + + return true; + } + + Button* GlobalSettingsPage::configure_hotkey_get_button_by_active_type() { + switch(configure_hotkey_type) { + case ConfigureHotkeyType::NONE: + return nullptr; + case ConfigureHotkeyType::REPLAY_START_STOP: + return turn_replay_on_off_button_ptr; + case ConfigureHotkeyType::REPLAY_SAVE: + return save_replay_button_ptr; + case ConfigureHotkeyType::RECORD_START_STOP: + return start_stop_recording_button_ptr; + case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE: + return pause_unpause_recording_button_ptr; + case ConfigureHotkeyType::STREAM_START_STOP: + return start_stop_streaming_button_ptr; + case ConfigureHotkeyType::SHOW_HIDE: + return show_hide_button_ptr; + } + return nullptr; + } + + ConfigHotkey* GlobalSettingsPage::configure_hotkey_get_config_by_active_type() { + switch(configure_hotkey_type) { + case ConfigureHotkeyType::NONE: + return nullptr; + case ConfigureHotkeyType::REPLAY_START_STOP: + return &config.replay_config.start_stop_hotkey; + case ConfigureHotkeyType::REPLAY_SAVE: + return &config.replay_config.save_hotkey; + case ConfigureHotkeyType::RECORD_START_STOP: + return &config.record_config.start_stop_hotkey; + case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE: + return &config.record_config.pause_unpause_hotkey; + case ConfigureHotkeyType::STREAM_START_STOP: + return &config.streaming_config.start_stop_hotkey; + case ConfigureHotkeyType::SHOW_HIDE: + return &config.main_config.show_hide_hotkey; + } + return nullptr; + } + + void GlobalSettingsPage::for_each_config_hotkey(std::function<void(ConfigHotkey *config_hotkey)> callback) { + ConfigHotkey *config_hotkeys[] = { + &config.replay_config.start_stop_hotkey, + &config.replay_config.save_hotkey, + &config.record_config.start_stop_hotkey, + &config.record_config.pause_unpause_hotkey, + &config.streaming_config.start_stop_hotkey, + &config.main_config.show_hide_hotkey + }; + for(ConfigHotkey *config_hotkey : config_hotkeys) { + callback(config_hotkey); + } + } + + void GlobalSettingsPage::configure_hotkey_start(ConfigureHotkeyType hotkey_type) { + assert(hotkey_type != ConfigureHotkeyType::NONE); + configure_config_hotkey = {0, 0}; + configure_hotkey_type = hotkey_type; + + content_page_ptr->set_visible(false); + hotkey_overlay_ptr->set_visible(true); + overlay->unbind_all_keyboard_hotkeys(); + configure_hotkey_get_button_by_active_type()->set_text(""); + + switch(hotkey_type) { + case ConfigureHotkeyType::NONE: + hotkey_configure_action_name = ""; + break; + case ConfigureHotkeyType::REPLAY_START_STOP: + hotkey_configure_action_name = "Turn replay on/off"; + break; + case ConfigureHotkeyType::REPLAY_SAVE: + hotkey_configure_action_name = "Save replay"; + break; + case ConfigureHotkeyType::RECORD_START_STOP: + hotkey_configure_action_name = "Start/stop recording"; + break; + case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE: + hotkey_configure_action_name = "Pause/unpause recording"; + break; + case ConfigureHotkeyType::STREAM_START_STOP: + hotkey_configure_action_name = "Start/stop streaming"; + break; + case ConfigureHotkeyType::SHOW_HIDE: + hotkey_configure_action_name = "Show/hide UI"; + break; + } + } + + void GlobalSettingsPage::configure_hotkey_cancel() { + Button *config_hotkey_button = configure_hotkey_get_button_by_active_type(); + ConfigHotkey *config_hotkey = configure_hotkey_get_config_by_active_type(); + if(config_hotkey_button && config_hotkey) + config_hotkey_button->set_text(config_hotkey_to_string(*config_hotkey)); + + configure_config_hotkey = {0, 0}; + configure_hotkey_type = ConfigureHotkeyType::NONE; + content_page_ptr->set_visible(true); + hotkey_overlay_ptr->set_visible(false); + overlay->rebind_all_keyboard_hotkeys(); + } + + void GlobalSettingsPage::configure_hotkey_stop_and_save() { + Button *config_hotkey_button = configure_hotkey_get_button_by_active_type(); + ConfigHotkey *config_hotkey = configure_hotkey_get_config_by_active_type(); + if(config_hotkey_button && config_hotkey) { + bool hotkey_used_by_another_action = false; + for_each_config_hotkey([&](ConfigHotkey *config_hotkey_item) { + if(config_hotkey_item != config_hotkey && *config_hotkey_item == configure_config_hotkey) + hotkey_used_by_another_action = true; + }); + + if(hotkey_used_by_another_action) { + const std::string error_msg = "The hotkey \"" + config_hotkey_to_string(configure_config_hotkey) + " is already used for something else"; + overlay->show_notification(error_msg.c_str(), 3.0, mgl::Color(255, 0, 0, 255), mgl::Color(255, 0, 0, 255), NotificationType::NONE); + config_hotkey_button->set_text(config_hotkey_to_string(*config_hotkey)); + configure_config_hotkey = {0, 0}; + return; + } + + *config_hotkey = configure_config_hotkey; + } + + configure_config_hotkey = {0, 0}; + configure_hotkey_type = ConfigureHotkeyType::NONE; + content_page_ptr->set_visible(true); + hotkey_overlay_ptr->set_visible(false); + overlay->rebind_all_keyboard_hotkeys(); + } +} diff --git a/src/gui/GsrPage.cpp b/src/gui/GsrPage.cpp index e6ee5fc..c5fa263 100644 --- a/src/gui/GsrPage.cpp +++ b/src/gui/GsrPage.cpp @@ -102,15 +102,8 @@ namespace gsr { void GsrPage::draw_children(mgl::Window &window, mgl::vec2f position) { Widget *selected_widget = selected_child_widget; - mgl_scissor prev_scissor; - mgl_window_get_scissor(window.internal_window(), &prev_scissor); - - const mgl::vec2f inner_size = get_inner_size(); - mgl_scissor new_scissor = { - mgl_vec2i{(int)position.x, (int)position.y}, - mgl_vec2i{(int)inner_size.x, (int)inner_size.y} - }; - mgl_window_set_scissor(window.internal_window(), &new_scissor); + const mgl::Scissor prev_scissor = window.get_scissor(); + window.set_scissor({position.to_vec2i(), get_inner_size().to_vec2i()}); for(size_t i = 0; i < widgets.size(); ++i) { auto &widget = widgets[i]; @@ -121,7 +114,7 @@ namespace gsr { if(selected_widget) selected_widget->draw(window, position); - mgl_window_set_scissor(window.internal_window(), &prev_scissor); + window.set_scissor(prev_scissor); } mgl::vec2f GsrPage::get_size() { diff --git a/src/gui/RadioButton.cpp b/src/gui/RadioButton.cpp index 061d811..a6ef96a 100644 --- a/src/gui/RadioButton.cpp +++ b/src/gui/RadioButton.cpp @@ -35,12 +35,12 @@ namespace gsr { const bool mouse_inside = mgl::FloatRect(draw_pos, item_size).contains(mgl::vec2f(event.mouse_button.x, event.mouse_button.y)); if(mouse_inside) { - const size_t prev_selected_item = selected_item; - selected_item = i; - - if(selected_item != prev_selected_item && on_selection_changed) - on_selection_changed(item.text.get_string(), item.id); + if(selected_item != i && on_selection_changed) { + if(!on_selection_changed(item.text.get_string(), item.id)) + return false; + } + selected_item = i; return false; } @@ -158,12 +158,12 @@ namespace gsr { for(size_t i = 0; i < items.size(); ++i) { auto &item = items[i]; if(item.id == id) { - const size_t prev_selected_item = selected_item; - selected_item = i; - - if(trigger_event && (trigger_event_even_if_selection_not_changed || selected_item != prev_selected_item) && on_selection_changed) - on_selection_changed(item.text.get_string(), item.id); + if(trigger_event && (trigger_event_even_if_selection_not_changed || selected_item != i) && on_selection_changed) { + if(!on_selection_changed(item.text.get_string(), item.id)) + break; + } + selected_item = i; break; } } diff --git a/src/gui/ScrollablePage.cpp b/src/gui/ScrollablePage.cpp index 74dd715..d5e92d0 100644 --- a/src/gui/ScrollablePage.cpp +++ b/src/gui/ScrollablePage.cpp @@ -19,15 +19,37 @@ namespace gsr { if(!visible) return true; - offset = position + offset + mgl::vec2f(0.0f, scroll_y); + offset = position + offset; + + const mgl::vec2f content_size = get_inner_size(); + const mgl::vec2i scissor_pos(offset.x, offset.y); + const mgl::vec2i scissor_size(content_size.x, content_size.y); + + offset.y += scroll_y; Widget *selected_widget = selected_child_widget; + if(event.type == mgl::Event::MouseButtonPressed && scrollbar_rect.contains(mgl::vec2f(event.mouse_button.x, event.mouse_button.y))) { + set_widget_as_selected_in_parent(); + moving_scrollbar_with_cursor = true; + scrollbar_move_cursor_start_pos = mgl::vec2f(event.mouse_button.x, event.mouse_button.y); + scrollbar_move_cursor_scroll_y_start = scroll_y; + return false; + } + if(event.type == mgl::Event::MouseButtonReleased && moving_scrollbar_with_cursor) { moving_scrollbar_with_cursor = false; remove_widget_as_selected_in_parent(); return false; } + if(event.type == mgl::Event::MouseButtonPressed || event.type == mgl::Event::MouseButtonReleased) { + if(!mgl::IntRect(scissor_pos, scissor_size).contains({event.mouse_button.x, event.mouse_button.y})) + return true; + } else if(event.type == mgl::Event::MouseMoved) { + if(!mgl::IntRect(scissor_pos, scissor_size).contains({event.mouse_move.x, event.mouse_move.y})) + return true; + } + if(selected_widget) { if(!selected_widget->on_event(event, window, offset)) return false; @@ -51,14 +73,6 @@ namespace gsr { return false; } - if(event.type == mgl::Event::MouseButtonPressed && scrollbar_rect.contains(mgl::vec2f(event.mouse_button.x, event.mouse_button.y))) { - set_widget_as_selected_in_parent(); - moving_scrollbar_with_cursor = true; - scrollbar_move_cursor_start_pos = mgl::vec2f(event.mouse_button.x, event.mouse_button.y); - scrollbar_move_cursor_scroll_y_start = scroll_y; - return false; - } - return true; } @@ -75,11 +89,10 @@ namespace gsr { offset = position + offset; - mgl_scissor prev_scissor; - mgl_window_get_scissor(window.internal_window(), &prev_scissor); + const mgl::Scissor prev_scissor = window.get_scissor(); const mgl::vec2f content_size = get_inner_size(); - mgl_scissor new_scissor = { + const mgl_scissor new_scissor = { mgl_vec2i{(int)offset.x, (int)offset.y}, mgl_vec2i{(int)content_size.x, (int)content_size.y} }; @@ -136,7 +149,7 @@ namespace gsr { apply_animation(); limit_scroll(child_height); - mgl_window_set_scissor(window.internal_window(), &prev_scissor); + window.set_scissor(prev_scissor); double scrollbar_height = 1.0; if(child_height > 0.001) diff --git a/src/gui/SettingsPage.cpp b/src/gui/SettingsPage.cpp index 79f6c52..4d1109a 100644 --- a/src/gui/SettingsPage.cpp +++ b/src/gui/SettingsPage.cpp @@ -22,15 +22,16 @@ namespace gsr { APPLICATION_CUSTOM }; - SettingsPage::SettingsPage(Type type, const GsrInfo &gsr_info, Config &config, PageStack *page_stack) : + SettingsPage::SettingsPage(Type type, const GsrInfo *gsr_info, Config &config, PageStack *page_stack) : StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()), type(type), config(config), - page_stack(page_stack), - settings_title_text("Settings", get_theme().title_font) + gsr_info(gsr_info), + page_stack(page_stack) { audio_devices = get_audio_devices(); application_audio = get_application_audio(); + capture_options = get_supported_capture_options(*gsr_info); auto content_page = std::make_unique<GsrPage>(); content_page->add_button("Back", "back", get_color_theme().page_bg_color); @@ -41,9 +42,9 @@ namespace gsr { content_page_ptr = content_page.get(); add_widget(std::move(content_page)); - add_widgets(gsr_info); - add_page_specific_widgets(gsr_info); - load(gsr_info); + add_widgets(); + add_page_specific_widgets(); + load(); } std::unique_ptr<RadioButton> SettingsPage::create_view_radio_button() { @@ -55,31 +56,29 @@ namespace gsr { return view_radio_button; } - std::unique_ptr<ComboBox> SettingsPage::create_record_area_box(const GsrInfo &gsr_info) { + std::unique_ptr<ComboBox> SettingsPage::create_record_area_box() { auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font); // TODO: Show options not supported but disable them // TODO: Enable this - //if(gsr_info.supported_capture_options.window) + //if(capture_options.window) // record_area_box->add_item("Window", "window"); - if(gsr_info.supported_capture_options.focused) + if(capture_options.focused) record_area_box->add_item("Follow focused window", "focused"); - if(gsr_info.supported_capture_options.screen) - record_area_box->add_item("All monitors", "screen"); - for(const auto &monitor : gsr_info.supported_capture_options.monitors) { + for(const auto &monitor : capture_options.monitors) { char name[256]; snprintf(name, sizeof(name), "Monitor %s (%dx%d)", monitor.name.c_str(), monitor.size.x, monitor.size.y); record_area_box->add_item(name, monitor.name); } - if(gsr_info.supported_capture_options.portal) + if(capture_options.portal) record_area_box->add_item("Desktop portal", "portal"); record_area_box_ptr = record_area_box.get(); return record_area_box; } - std::unique_ptr<Widget> SettingsPage::create_record_area(const GsrInfo &gsr_info) { + std::unique_ptr<Widget> SettingsPage::create_record_area() { auto record_area_list = std::make_unique<List>(List::Orientation::VERTICAL); record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture target:", get_color_theme().text_color)); - record_area_list->add_widget(create_record_area_box(gsr_info)); + record_area_list->add_widget(create_record_area_box()); return record_area_list; } @@ -172,11 +171,11 @@ namespace gsr { return checkbox; } - std::unique_ptr<Widget> SettingsPage::create_capture_target(const GsrInfo &gsr_info) { + std::unique_ptr<Widget> SettingsPage::create_capture_target() { auto ll = std::make_unique<List>(List::Orientation::VERTICAL); auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); - capture_target_list->add_widget(create_record_area(gsr_info)); + capture_target_list->add_widget(create_record_area()); capture_target_list->add_widget(create_select_window()); capture_target_list->add_widget(create_area_size_section()); capture_target_list->add_widget(create_video_resolution_section()); @@ -305,7 +304,8 @@ namespace gsr { std::unique_ptr<Widget> SettingsPage::create_audio_section() { auto audio_device_section_list = std::make_unique<List>(List::Orientation::VERTICAL); audio_device_section_list->add_widget(create_audio_track_section()); - audio_device_section_list->add_widget(create_merge_audio_tracks_checkbox()); + if(type != Type::STREAM) + audio_device_section_list->add_widget(create_merge_audio_tracks_checkbox()); audio_device_section_list->add_widget(create_application_audio_invert_checkbox()); audio_device_section_list->add_widget(create_audio_codec()); return std::make_unique<Subsection>("Audio", std::move(audio_device_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); @@ -339,11 +339,27 @@ namespace gsr { return list; } - std::unique_ptr<Entry> SettingsPage::create_video_bitrate_entry() { + std::unique_ptr<List> SettingsPage::create_video_bitrate_entry() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); auto video_bitrate_entry = std::make_unique<Entry>(&get_theme().body_font, "15000", (int)(get_theme().body_font.get_character_size() * 4.0f)); video_bitrate_entry->validate_handler = create_entry_validator_integer_in_range(1, 500000); video_bitrate_entry_ptr = video_bitrate_entry.get(); - return video_bitrate_entry; + list->add_widget(std::move(video_bitrate_entry)); + + if(type == Type::STREAM) { + auto size_mb_label = std::make_unique<Label>(&get_theme().body_font, "1.92MB", get_color_theme().text_color); + Label *size_mb_label_ptr = size_mb_label.get(); + list->add_widget(std::move(size_mb_label)); + + video_bitrate_entry_ptr->on_changed = [size_mb_label_ptr](const std::string &text) { + const double video_bitrate_mb_per_seconds = (double)atoi(text.c_str()) / 1000LL / 8LL * 1.024; + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%.2fMB", video_bitrate_mb_per_seconds); + size_mb_label_ptr->set_text(buffer); + }; + } + + return list; } std::unique_ptr<List> SettingsPage::create_video_bitrate() { @@ -378,40 +394,40 @@ namespace gsr { return quality_list; } - std::unique_ptr<ComboBox> SettingsPage::create_video_codec_box(const GsrInfo &gsr_info) { + std::unique_ptr<ComboBox> SettingsPage::create_video_codec_box() { auto video_codec_box = std::make_unique<ComboBox>(&get_theme().body_font); // TODO: Show options not supported but disable them. // TODO: Show error if no encoders are supported. // TODO: Show warning (once) if only software encoder is available. video_codec_box->add_item("Auto (Recommended)", "auto"); - if(gsr_info.supported_video_codecs.h264) + if(gsr_info->supported_video_codecs.h264) video_codec_box->add_item("H264", "h264"); - if(gsr_info.supported_video_codecs.hevc) + if(gsr_info->supported_video_codecs.hevc) video_codec_box->add_item("HEVC", "hevc"); - if(gsr_info.supported_video_codecs.av1) + if(gsr_info->supported_video_codecs.hevc_10bit) + video_codec_box->add_item("HEVC (10 bit, reduces banding)", "hevc_10bit"); + if(gsr_info->supported_video_codecs.hevc_hdr) + video_codec_box->add_item("HEVC (HDR)", "hevc_hdr"); + if(gsr_info->supported_video_codecs.av1) video_codec_box->add_item("AV1", "av1"); - if(gsr_info.supported_video_codecs.vp8) + if(gsr_info->supported_video_codecs.av1_10bit) + video_codec_box->add_item("AV1 (10 bit, reduces banding)", "av1_10bit"); + if(gsr_info->supported_video_codecs.av1_hdr) + video_codec_box->add_item("AV1 (HDR)", "av1_hdr"); + if(gsr_info->supported_video_codecs.vp8) video_codec_box->add_item("VP8", "vp8"); - if(gsr_info.supported_video_codecs.vp9) + if(gsr_info->supported_video_codecs.vp9) video_codec_box->add_item("VP9", "vp9"); - if(gsr_info.supported_video_codecs.hevc_hdr) - video_codec_box->add_item("HEVC (HDR)", "hevc_hdr"); - if(gsr_info.supported_video_codecs.hevc_10bit) - video_codec_box->add_item("HEVC (10 bit, reduces banding)", "hevc_10bit"); - if(gsr_info.supported_video_codecs.av1_hdr) - video_codec_box->add_item("AV1 (HDR)", "av1_hdr"); - if(gsr_info.supported_video_codecs.av1_10bit) - video_codec_box->add_item("AV1 (10 bit, reduces banding)", "av1_10bit"); - if(gsr_info.supported_video_codecs.h264_software) + if(gsr_info->supported_video_codecs.h264_software) video_codec_box->add_item("H264 Software Encoder (Slow, not recommended)", "h264_software"); video_codec_box_ptr = video_codec_box.get(); return video_codec_box; } - std::unique_ptr<List> SettingsPage::create_video_codec(const GsrInfo &gsr_info) { + std::unique_ptr<List> SettingsPage::create_video_codec() { auto video_codec_list = std::make_unique<List>(List::Orientation::VERTICAL); video_codec_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Video codec:", get_color_theme().text_color)); - video_codec_list->add_widget(create_video_codec_box(gsr_info)); + video_codec_list->add_widget(create_video_codec_box()); video_codec_ptr = video_codec_list.get(); return video_codec_list; } @@ -477,16 +493,16 @@ namespace gsr { return record_cursor_checkbox; } - std::unique_ptr<Widget> SettingsPage::create_video_section(const GsrInfo &gsr_info) { + std::unique_ptr<Widget> SettingsPage::create_video_section() { auto video_section_list = std::make_unique<List>(List::Orientation::VERTICAL); video_section_list->add_widget(create_video_quality_section()); - video_section_list->add_widget(create_video_codec(gsr_info)); + video_section_list->add_widget(create_video_codec()); video_section_list->add_widget(create_framerate_section()); video_section_list->add_widget(create_record_cursor_section()); return std::make_unique<Subsection>("Video", std::move(video_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); } - std::unique_ptr<Widget> SettingsPage::create_settings(const GsrInfo &gsr_info) { + std::unique_ptr<Widget> SettingsPage::create_settings() { auto page_list = std::make_unique<List>(List::Orientation::VERTICAL); page_list->set_spacing(0.018f); page_list->add_widget(create_view_radio_button()); @@ -496,16 +512,16 @@ namespace gsr { auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL); settings_list->set_spacing(0.018f); - settings_list->add_widget(create_capture_target(gsr_info)); + settings_list->add_widget(create_capture_target()); settings_list->add_widget(create_audio_section()); - settings_list->add_widget(create_video_section(gsr_info)); + settings_list->add_widget(create_video_section()); settings_list_ptr = settings_list.get(); settings_scrollable_page_ptr->add_widget(std::move(settings_list)); return page_list; } - void SettingsPage::add_widgets(const GsrInfo &gsr_info) { - content_page_ptr->add_widget(create_settings(gsr_info)); + void SettingsPage::add_widgets() { + content_page_ptr->add_widget(create_settings()); record_area_box_ptr->on_selection_changed = [this](const std::string &text, const std::string &id) { (void)text; @@ -517,6 +533,7 @@ namespace gsr { video_resolution_list_ptr->set_visible(!focused_selected && change_video_resolution_checkbox_ptr->is_checked()); change_video_resolution_checkbox_ptr->set_visible(!focused_selected); restore_portal_session_list_ptr->set_visible(portal_selected); + return true; }; change_video_resolution_checkbox_ptr->on_changed = [this](bool checked) { @@ -531,35 +548,37 @@ namespace gsr { if(estimated_file_size_ptr) estimated_file_size_ptr->set_visible(custom_selected); + + return true; }; video_quality_box_ptr->on_selection_changed("", video_quality_box_ptr->get_selected_id()); - if(!gsr_info.supported_capture_options.monitors.empty()) - record_area_box_ptr->set_selected_item(gsr_info.supported_capture_options.monitors.front().name); - else if(gsr_info.supported_capture_options.portal) + if(!capture_options.monitors.empty()) + record_area_box_ptr->set_selected_item(capture_options.monitors.front().name); + else if(capture_options.portal) record_area_box_ptr->set_selected_item("portal"); - else if(gsr_info.supported_capture_options.window) + else if(capture_options.window) record_area_box_ptr->set_selected_item("window"); else record_area_box_ptr->on_selection_changed("", ""); - if(!gsr_info.system_info.supports_app_audio) { + if(!gsr_info->system_info.supports_app_audio) { add_application_audio_button_ptr->set_visible(false); add_custom_application_audio_button_ptr->set_visible(false); application_audio_invert_checkbox_ptr->set_visible(false); } } - void SettingsPage::add_page_specific_widgets(const GsrInfo &gsr_info) { + void SettingsPage::add_page_specific_widgets() { switch(type) { case Type::REPLAY: - add_replay_widgets(gsr_info); + add_replay_widgets(); break; case Type::RECORD: - add_record_widgets(gsr_info); + add_record_widgets(); break; case Type::STREAM: - add_stream_widgets(gsr_info); + add_stream_widgets(); break; } } @@ -579,10 +598,12 @@ namespace gsr { select_directory_page->add_widget(std::move(file_chooser)); select_directory_page->on_click = [this, file_chooser_ptr](const std::string &id) { - if(id == "save") + if(id == "save") { save_directory_button_ptr->set_text(file_chooser_ptr->get_current_directory()); - else if(id == "cancel") page_stack->pop(); + } else if(id == "cancel") { + page_stack->pop(); + } }; page_stack->push(std::move(select_directory_page)); @@ -608,70 +629,100 @@ namespace gsr { return container_list; } - std::unique_ptr<Entry> SettingsPage::create_replay_time_entry() { + std::unique_ptr<List> SettingsPage::create_replay_time_entry() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + auto replay_time_entry = std::make_unique<Entry>(&get_theme().body_font, "60", get_theme().body_font.get_character_size() * 3); - replay_time_entry->validate_handler = create_entry_validator_integer_in_range(1, 1200); + replay_time_entry->validate_handler = create_entry_validator_integer_in_range(1, 10800); replay_time_entry_ptr = replay_time_entry.get(); - return replay_time_entry; + list->add_widget(std::move(replay_time_entry)); + + auto replay_time_label = std::make_unique<Label>(&get_theme().body_font, "00h:00m:00s", get_color_theme().text_color); + replay_time_label_ptr = replay_time_label.get(); + list->add_widget(std::move(replay_time_label)); + + return list; } std::unique_ptr<List> SettingsPage::create_replay_time() { auto replay_time_list = std::make_unique<List>(List::Orientation::VERTICAL); - replay_time_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Replay time in seconds:", get_color_theme().text_color)); + replay_time_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Replay duration in seconds:", get_color_theme().text_color)); replay_time_list->add_widget(create_replay_time_entry()); return replay_time_list; } - std::unique_ptr<RadioButton> SettingsPage::create_start_replay_automatically(const GsrInfo &gsr_info) { + std::unique_ptr<RadioButton> SettingsPage::create_start_replay_automatically() { char fullscreen_text[256]; - snprintf(fullscreen_text, sizeof(fullscreen_text), "Turn on replay when starting a fullscreen application%s", gsr_info.system_info.display_server == DisplayServer::X11 ? "" : " (X11 only)"); + snprintf(fullscreen_text, sizeof(fullscreen_text), "Turn on replay when starting a fullscreen application%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)"); auto radiobutton = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::VERTICAL); radiobutton->add_item("Don't turn on replay automatically", "dont_turn_on_automatically"); - radiobutton->add_item("Turn on replay at system startup", "turn_on_at_system_startup"); + radiobutton->add_item("Turn on replay when this program starts", "turn_on_at_system_startup"); radiobutton->add_item(fullscreen_text, "turn_on_at_fullscreen"); radiobutton->add_item("Turn on replay when power supply is connected", "turn_on_at_power_supply_connected"); turn_on_replay_automatically_mode_ptr = radiobutton.get(); return radiobutton; } - std::unique_ptr<CheckBox> SettingsPage::create_save_replay_in_game_folder(const GsrInfo &gsr_info) { + std::unique_ptr<CheckBox> SettingsPage::create_save_replay_in_game_folder() { char text[256]; - snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", gsr_info.system_info.display_server == DisplayServer::X11 ? "" : " (X11 only)"); + snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)"); auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, text); save_replay_in_game_folder_ptr = checkbox.get(); return checkbox; } - std::unique_ptr<Label> SettingsPage::create_estimated_file_size() { - auto label = std::make_unique<Label>(&get_theme().body_font, "Estimated video max file size in RAM: 5.23MB", get_color_theme().text_color); + std::unique_ptr<CheckBox> SettingsPage::create_restart_replay_on_save() { + auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Restart replay on save"); + restart_replay_on_save = checkbox.get(); + return checkbox; + } + + std::unique_ptr<Label> SettingsPage::create_estimated_replay_file_size() { + auto label = std::make_unique<Label>(&get_theme().body_font, "Estimated video max file size in RAM: 57.60MB", get_color_theme().text_color); estimated_file_size_ptr = label.get(); return label; } - void SettingsPage::update_estimated_file_size() { + void SettingsPage::update_estimated_replay_file_size() { const int64_t replay_time_seconds = atoi(replay_time_entry_ptr->get_text().c_str()); const int64_t video_bitrate_bps = atoi(video_bitrate_entry_ptr->get_text().c_str()) * 1000LL / 8LL; - const double video_filesize_mb = ((double)replay_time_seconds * (double)video_bitrate_bps) / 1024.0 / 1024.0; + const double video_filesize_mb = ((double)replay_time_seconds * (double)video_bitrate_bps) / 1000.0 / 1000.0 * 1.024; - char buffer[512]; - snprintf(buffer, sizeof(buffer), "Estimated video max file size in RAM: %.2fMB", video_filesize_mb); + char buffer[256]; + snprintf(buffer, sizeof(buffer), "Estimated video max file size in RAM: %.2fMB.\nChange video bitrate or replay duration to change file size.", video_filesize_mb); estimated_file_size_ptr->set_text(buffer); } - void SettingsPage::add_replay_widgets(const GsrInfo &gsr_info) { + void SettingsPage::update_replay_time_text() { + int seconds = atoi(replay_time_entry_ptr->get_text().c_str()); + + const int hours = seconds / 60 / 60; + seconds -= (hours * 60 * 60); + + const int minutes = seconds / 60; + seconds -= (minutes * 60); + + char buffer[256]; + snprintf(buffer, sizeof(buffer), "%02dh:%02dm:%02ds", hours, minutes, seconds); + replay_time_label_ptr->set_text(buffer); + } + + void SettingsPage::add_replay_widgets() { auto file_info_list = std::make_unique<List>(List::Orientation::VERTICAL); auto file_info_data_list = std::make_unique<List>(List::Orientation::HORIZONTAL); file_info_data_list->add_widget(create_save_directory("Directory to save replays:")); file_info_data_list->add_widget(create_container_section()); file_info_data_list->add_widget(create_replay_time()); file_info_list->add_widget(std::move(file_info_data_list)); - file_info_list->add_widget(create_estimated_file_size()); + file_info_list->add_widget(create_estimated_replay_file_size()); settings_list_ptr->add_widget(std::make_unique<Subsection>("File info", std::move(file_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); auto general_list = std::make_unique<List>(List::Orientation::VERTICAL); - general_list->add_widget(create_start_replay_automatically(gsr_info)); - general_list->add_widget(create_save_replay_in_game_folder(gsr_info)); + general_list->add_widget(create_start_replay_automatically()); + general_list->add_widget(create_save_replay_in_game_folder()); + if(gsr_info->system_info.gsr_version >= GsrVersion{5, 0, 3}) + general_list->add_widget(create_restart_replay_on_save()); settings_list_ptr->add_widget(std::make_unique<Subsection>("General", std::move(general_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL); @@ -704,34 +755,54 @@ namespace gsr { framerate_mode_list_ptr->set_visible(advanced_view); notifications_subsection_ptr->set_visible(advanced_view); settings_scrollable_page_ptr->reset_scroll(); + return true; }; view_radio_button_ptr->on_selection_changed("Simple", "simple"); replay_time_entry_ptr->on_changed = [this](const std::string&) { - update_estimated_file_size(); + update_estimated_replay_file_size(); + update_replay_time_text(); }; video_bitrate_entry_ptr->on_changed = [this](const std::string&) { - update_estimated_file_size(); + update_estimated_replay_file_size(); }; } - std::unique_ptr<CheckBox> SettingsPage::create_save_recording_in_game_folder(const GsrInfo &gsr_info) { + std::unique_ptr<CheckBox> SettingsPage::create_save_recording_in_game_folder() { char text[256]; - snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", gsr_info.system_info.display_server == DisplayServer::X11 ? "" : " (X11 only)"); + snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)"); auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, text); save_recording_in_game_folder_ptr = checkbox.get(); return checkbox; } - void SettingsPage::add_record_widgets(const GsrInfo &gsr_info) { - auto file_list = std::make_unique<List>(List::Orientation::HORIZONTAL); - file_list->add_widget(create_save_directory("Directory to save the video:")); - file_list->add_widget(create_container_section()); - settings_list_ptr->add_widget(std::make_unique<Subsection>("File info", std::move(file_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); + std::unique_ptr<Label> SettingsPage::create_estimated_record_file_size() { + auto label = std::make_unique<Label>(&get_theme().body_font, "Estimated video file size per minute (excluding audio): 345.60MB", get_color_theme().text_color); + estimated_file_size_ptr = label.get(); + return label; + } + + void SettingsPage::update_estimated_record_file_size() { + const int64_t video_bitrate_bps = atoi(video_bitrate_entry_ptr->get_text().c_str()) * 1000LL / 8LL; + const double video_filesize_mb_per_minute = (60.0 * (double)video_bitrate_bps) / 1000.0 / 1000.0 * 1.024; + + char buffer[512]; + snprintf(buffer, sizeof(buffer), "Estimated video file size per minute (excluding audio): %.2fMB", video_filesize_mb_per_minute); + estimated_file_size_ptr->set_text(buffer); + } + + void SettingsPage::add_record_widgets() { + auto file_info_list = std::make_unique<List>(List::Orientation::VERTICAL); + auto file_info_data_list = std::make_unique<List>(List::Orientation::HORIZONTAL); + file_info_data_list->add_widget(create_save_directory("Directory to save the video:")); + file_info_data_list->add_widget(create_container_section()); + file_info_list->add_widget(std::move(file_info_data_list)); + file_info_list->add_widget(create_estimated_record_file_size()); + settings_list_ptr->add_widget(std::make_unique<Subsection>("File info", std::move(file_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); auto general_list = std::make_unique<List>(List::Orientation::VERTICAL); - general_list->add_widget(create_save_recording_in_game_folder(gsr_info)); + general_list->add_widget(create_save_recording_in_game_folder()); settings_list_ptr->add_widget(std::make_unique<Subsection>("General", std::move(general_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL); @@ -759,8 +830,13 @@ namespace gsr { framerate_mode_list_ptr->set_visible(advanced_view); notifications_subsection_ptr->set_visible(advanced_view); settings_scrollable_page_ptr->reset_scroll(); + return true; }; view_radio_button_ptr->on_selection_changed("Simple", "simple"); + + video_bitrate_entry_ptr->on_changed = [this](const std::string&) { + update_estimated_record_file_size(); + }; } std::unique_ptr<ComboBox> SettingsPage::create_streaming_service_box() { @@ -825,7 +901,7 @@ namespace gsr { return container_list; } - void SettingsPage::add_stream_widgets(const GsrInfo&) { + void SettingsPage::add_stream_widgets() { auto streaming_info_list = std::make_unique<List>(List::Orientation::HORIZONTAL); streaming_info_list->add_widget(create_streaming_service_section()); streaming_info_list->add_widget(create_stream_key_section()); @@ -859,6 +935,7 @@ namespace gsr { container_list_ptr->set_visible(custom_option); twitch_stream_key_entry_ptr->set_visible(twitch_option); youtube_stream_key_entry_ptr->set_visible(youtube_option); + return true; }; streaming_service_box_ptr->on_selection_changed("Twitch", "twitch"); @@ -871,6 +948,7 @@ namespace gsr { framerate_mode_list_ptr->set_visible(advanced_view); notifications_subsection_ptr->set_visible(advanced_view); settings_scrollable_page_ptr->reset_scroll(); + return true; }; view_radio_button_ptr->on_selection_changed("Simple", "simple"); } @@ -879,21 +957,22 @@ namespace gsr { save(); } - void SettingsPage::load(const GsrInfo &gsr_info) { + void SettingsPage::load() { switch(type) { case Type::REPLAY: - load_replay(gsr_info); + load_replay(); break; case Type::RECORD: - load_record(gsr_info); + load_record(); break; case Type::STREAM: - load_stream(gsr_info); + load_stream(); break; } } void SettingsPage::save() { + Config prev_config = config; switch(type) { case Type::REPLAY: save_replay(); @@ -906,6 +985,9 @@ namespace gsr { break; } save_config(config); + + if(on_config_changed && config != prev_config) + on_config_changed(); } static const std::string* get_application_audio_by_name_case_insensitive(const std::vector<std::string> &application_audio, const std::string &name) { @@ -921,11 +1003,11 @@ namespace gsr { return str.size() >= len && memcmp(str.data(), substr, len) == 0; } - void SettingsPage::load_audio_tracks(const RecordOptions &record_options, const GsrInfo &gsr_info) { + void SettingsPage::load_audio_tracks(const RecordOptions &record_options) { audio_track_list_ptr->clear(); for(const std::string &audio_track : record_options.audio_tracks) { if(starts_with(audio_track, "app:")) { - if(!gsr_info.system_info.supports_app_audio) + if(!gsr_info->system_info.supports_app_audio) continue; std::string audio_track_name = audio_track.substr(4); @@ -955,12 +1037,13 @@ namespace gsr { } } - void SettingsPage::load_common(RecordOptions &record_options, const GsrInfo &gsr_info) { + void SettingsPage::load_common(RecordOptions &record_options) { record_area_box_ptr->set_selected_item(record_options.record_area_option); - merge_audio_tracks_checkbox_ptr->set_checked(record_options.merge_audio_tracks); + if(merge_audio_tracks_checkbox_ptr) + merge_audio_tracks_checkbox_ptr->set_checked(record_options.merge_audio_tracks); application_audio_invert_checkbox_ptr->set_checked(record_options.application_audio_invert); change_video_resolution_checkbox_ptr->set_checked(record_options.change_video_resolution); - load_audio_tracks(record_options, gsr_info); + load_audio_tracks(record_options); color_range_box_ptr->set_selected_item(record_options.color_range); video_quality_box_ptr->set_selected_item(record_options.video_quality); video_codec_box_ptr->set_selected_item(record_options.video_codec); @@ -1009,23 +1092,27 @@ namespace gsr { video_bitrate_entry_ptr->set_text(std::to_string(record_options.video_bitrate)); } - void SettingsPage::load_replay(const GsrInfo &gsr_info) { - load_common(config.replay_config.record_options, gsr_info); + void SettingsPage::load_replay() { + load_common(config.replay_config.record_options); turn_on_replay_automatically_mode_ptr->set_selected_item(config.replay_config.turn_on_replay_automatically_mode); save_replay_in_game_folder_ptr->set_checked(config.replay_config.save_video_in_game_folder); + if(restart_replay_on_save) + restart_replay_on_save->set_checked(config.replay_config.restart_replay_on_save); show_replay_started_notification_checkbox_ptr->set_checked(config.replay_config.show_replay_started_notifications); show_replay_stopped_notification_checkbox_ptr->set_checked(config.replay_config.show_replay_stopped_notifications); show_replay_saved_notification_checkbox_ptr->set_checked(config.replay_config.show_replay_saved_notifications); save_directory_button_ptr->set_text(config.replay_config.save_directory); container_box_ptr->set_selected_item(config.replay_config.container); - if(config.replay_config.replay_time < 5) - config.replay_config.replay_time = 5; + if(config.replay_config.replay_time < 2) + config.replay_config.replay_time = 2; + if(config.replay_config.replay_time > 10800) + config.replay_config.replay_time = 10800; replay_time_entry_ptr->set_text(std::to_string(config.replay_config.replay_time)); } - void SettingsPage::load_record(const GsrInfo &gsr_info) { - load_common(config.record_config.record_options, gsr_info); + void SettingsPage::load_record() { + load_common(config.record_config.record_options); save_recording_in_game_folder_ptr->set_checked(config.record_config.save_video_in_game_folder); show_recording_started_notification_checkbox_ptr->set_checked(config.record_config.show_recording_started_notifications); show_video_saved_notification_checkbox_ptr->set_checked(config.record_config.show_video_saved_notifications); @@ -1033,8 +1120,8 @@ namespace gsr { container_box_ptr->set_selected_item(config.record_config.container); } - void SettingsPage::load_stream(const GsrInfo &gsr_info) { - load_common(config.streaming_config.record_options, gsr_info); + void SettingsPage::load_stream() { + load_common(config.streaming_config.record_options); show_streaming_started_notification_checkbox_ptr->set_checked(config.streaming_config.show_streaming_started_notifications); show_streaming_stopped_notification_checkbox_ptr->set_checked(config.streaming_config.show_streaming_stopped_notifications); streaming_service_box_ptr->set_selected_item(config.streaming_config.streaming_service); @@ -1078,7 +1165,8 @@ namespace gsr { record_options.video_height = atoi(video_height_entry_ptr->get_text().c_str()); record_options.fps = atoi(framerate_entry_ptr->get_text().c_str()); record_options.video_bitrate = atoi(video_bitrate_entry_ptr->get_text().c_str()); - record_options.merge_audio_tracks = merge_audio_tracks_checkbox_ptr->is_checked(); + if(merge_audio_tracks_checkbox_ptr) + record_options.merge_audio_tracks = merge_audio_tracks_checkbox_ptr->is_checked(); record_options.application_audio_invert = application_audio_invert_checkbox_ptr->is_checked(); record_options.change_video_resolution = change_video_resolution_checkbox_ptr->is_checked(); save_audio_tracks(record_options.audio_tracks, audio_track_list_ptr); @@ -1140,6 +1228,8 @@ namespace gsr { save_common(config.replay_config.record_options); config.replay_config.turn_on_replay_automatically_mode = turn_on_replay_automatically_mode_ptr->get_selected_id(); config.replay_config.save_video_in_game_folder = save_replay_in_game_folder_ptr->is_checked(); + if(restart_replay_on_save) + config.replay_config.restart_replay_on_save = restart_replay_on_save->is_checked(); config.replay_config.show_replay_started_notifications = show_replay_started_notification_checkbox_ptr->is_checked(); config.replay_config.show_replay_stopped_notifications = show_replay_stopped_notification_checkbox_ptr->is_checked(); config.replay_config.show_replay_saved_notifications = show_replay_saved_notification_checkbox_ptr->is_checked(); diff --git a/src/gui/StaticPage.cpp b/src/gui/StaticPage.cpp index a89fc42..182464c 100644 --- a/src/gui/StaticPage.cpp +++ b/src/gui/StaticPage.cpp @@ -36,14 +36,8 @@ namespace gsr { offset = draw_pos; Widget *selected_widget = selected_child_widget; - mgl_scissor prev_scissor; - mgl_window_get_scissor(window.internal_window(), &prev_scissor); - - mgl_scissor new_scissor = { - mgl_vec2i{(int)draw_pos.x, (int)draw_pos.y}, - mgl_vec2i{(int)size.x, (int)size.y} - }; - mgl_window_set_scissor(window.internal_window(), &new_scissor); + const mgl::Scissor prev_scissor = window.get_scissor(); + window.set_scissor({draw_pos.to_vec2i(), size.to_vec2i()}); for(size_t i = 0; i < widgets.size(); ++i) { auto &widget = widgets[i]; @@ -54,7 +48,7 @@ namespace gsr { if(selected_widget) selected_widget->draw(window, offset); - mgl_window_set_scissor(window.internal_window(), &prev_scissor); + window.set_scissor(prev_scissor); } mgl::vec2f StaticPage::get_size() { diff --git a/src/gui/Utils.cpp b/src/gui/Utils.cpp index e000b7a..d1643f2 100644 --- a/src/gui/Utils.cpp +++ b/src/gui/Utils.cpp @@ -50,4 +50,21 @@ namespace gsr { void set_frame_delta_seconds(double frame_delta) { frame_delta_seconds = frame_delta; } + + mgl::vec2f scale_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to) { + if(std::abs(from.x) <= 0.0001f || std::abs(from.y) <= 0.0001f) + return {0.0f, 0.0f}; + + const double height_to_width_ratio = (double)from.y / (double)from.x; + from.x = to.x; + from.y = from.x * height_to_width_ratio; + + if(from.y > to.y) { + const double width_height_ratio = (double)from.x / (double)from.y; + from.y = to.y; + from.x = from.y * width_height_ratio; + } + + return from; + } }
\ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 4a36fe7..ffaf596 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,21 +1,18 @@ #include "../include/GsrInfo.hpp" -#include "../include/Theme.hpp" -#include "../include/window_texture.h" #include "../include/Overlay.hpp" -#include "../include/GlobalHotkeysX11.hpp" -#include "../include/GlobalHotkeysLinux.hpp" #include "../include/gui/Utils.hpp" +#include "../include/Process.hpp" +#include "../include/Rpc.hpp" #include <unistd.h> #include <signal.h> -#include <sys/socket.h> -#include <thread> +#include <string.h> +#include <limits.h> -#include <X11/keysym.h> #include <mglpp/mglpp.hpp> #include <mglpp/system/Clock.hpp> -// TODO: Make keyboard controllable for steam deck (and other controllers). +// TODO: Make keyboard/controller controllable for steam deck (and other controllers). // TODO: Keep track of gpu screen recorder run by other programs to not allow recording at the same time, or something. // TODO: Add systray by using org.kde.StatusNotifierWatcher/etc dbus directly. // TODO: Make sure the overlay always stays on top. Test with starting the overlay and then opening youtube in fullscreen. @@ -37,16 +34,121 @@ 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 bool is_socket_disconnected(int socket) { - char buf = '\0'; - errno = 0; - const ssize_t bytes_read = recv(socket, &buf, 1, MSG_PEEK | MSG_DONTWAIT); - return bytes_read == 0 || (bytes_read == -1 && (errno == EBADF || errno == ENOTCONN)); +static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) { + rpc->add_handler("show_ui", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->show(); + }); + + rpc->add_handler("toggle-show", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->toggle_show(); + }); + + rpc->add_handler("toggle-record", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->toggle_record(); + }); + + rpc->add_handler("toggle-pause", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->toggle_pause(); + }); + + rpc->add_handler("toggle-stream", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->toggle_stream(); + }); + + rpc->add_handler("toggle-replay", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->toggle_replay(); + }); + + rpc->add_handler("replay-save", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->save_replay(); + }); } -int main(void) { +static bool is_gsr_ui_virtual_keyboard_running() { + FILE *f = fopen("/proc/bus/input/devices", "rb"); + if(!f) + return false; + + bool virtual_keyboard_running = false; + char line[1024]; + while(fgets(line, sizeof(line), f)) { + if(strstr(line, "gsr-ui virtual keyboard")) { + virtual_keyboard_running = true; + break; + } + } + + fclose(f); + 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"); + printf(" action The launch action. Should be either \"launch-show\" or \"launch-hide\". Optional, defaults to \"launch-hide\".\n"); + printf(" If \"launch-show\" is used then the program starts and the UI is immediately opened and can be shown/hidden with Alt+Z.\n"); + printf(" If \"launch-hide\" is used then the program starts but the UI is not opened until Alt+Z is pressed.\n"); + exit(1); +} + +enum class LaunchAction { + LAUNCH_SHOW, + LAUNCH_HIDE +}; + +int main(int argc, char **argv) { setlocale(LC_ALL, "C"); // Sigh... stupid C if(geteuid() == 0) { @@ -54,8 +156,44 @@ int main(void) { return 1; } - // Cant get window texture when prime-run is used - disable_prime_run(); + LaunchAction launch_action = LaunchAction::LAUNCH_HIDE; + if(argc == 1) { + launch_action = LaunchAction::LAUNCH_HIDE; + } else if(argc == 2) { + const char *launch_action_opt = argv[1]; + if(strcmp(launch_action_opt, "launch-show") == 0) { + launch_action = LaunchAction::LAUNCH_SHOW; + } else if(strcmp(launch_action_opt, "launch-hide") == 0) { + launch_action = LaunchAction::LAUNCH_HIDE; + } else { + printf("error: invalid action \"%s\", expected \"launch-show\" or \"launch-hide\".\n", launch_action_opt); + usage(); + } + } else { + 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. + // What do? creating a pid file doesn't work in flatpak either. + // TODO: This doesn't work in flatpak when disabling hotkeys. + if(is_gsr_ui_virtual_keyboard_running() || gsr::pidof("gsr-ui", getpid()) != -1) { + gsr::Rpc rpc; + if(rpc.open("gsr-ui") && rpc.write("show_ui\n", 8)) { + fprintf(stderr, "Error: another instance of gsr-ui is already running, opening that one instead\n"); + } else { + fprintf(stderr, "Error: failed to send command to running gsr-ui instance, user will have to open the UI manually with Alt+Z\n"); + const char *args[] = { "gsr-notify", "--text", "Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr }; + gsr::exec_program_daemonized(args); + } + return 1; + } // Stop nvidia driver from buffering frames setenv("__GL_MaxFramesAllowed", "1", true); @@ -69,24 +207,31 @@ int main(void) { signal(SIGINT, sigint_handler); - if(mgl_init() != 0) { - fprintf(stderr, "error: failed to initialize mgl. Either failed to connec to the X11 server or failed to setup opengl\n"); - exit(1); - } - gsr::GsrInfo gsr_info; // TODO: Show the error in ui gsr::GsrInfoExitStatus gsr_info_exit_status = gsr::get_gpu_screen_recorder_info(&gsr_info); if(gsr_info_exit_status != gsr::GsrInfoExitStatus::OK) { - fprintf(stderr, "error: failed to get gpu-screen-recorder info, error: %d\n", (int)gsr_info_exit_status); + fprintf(stderr, "Error: failed to get gpu-screen-recorder info, error: %d\n", (int)gsr_info_exit_status); + exit(1); + } + + const gsr::DisplayServer display_server = gsr_info.system_info.display_server; + if(display_server == gsr::DisplayServer::WAYLAND) { + fprintf(stderr, "Warning: Wayland doesn't support this program properly and XWayland is required. Things may not work as expected. Use X11 if you experience issues.\n"); + } else { + // Cant get window texture when prime-run is used + disable_prime_run(); + } + + if(mgl_init() != 0) { + fprintf(stderr, "Error: failed to initialize mgl. Failed to either connect to the X11 server or setup opengl\n"); exit(1); } - if(gsr_info.system_info.display_server == gsr::DisplayServer::WAYLAND) - fprintf(stderr, "warning: Wayland support is experimental and requires XWayland. Things may not work as expected.\n"); + gsr::SupportedCaptureOptions capture_options = gsr::get_supported_capture_options(gsr_info); std::string resources_path; - if(access("sibs-build", F_OK) == 0) { + if(access("sibs-build/linux_x86_64/debug/gsr-ui", F_OK) == 0) { resources_path = "./"; } else { #ifdef GSR_UI_RESOURCES_PATH @@ -97,7 +242,6 @@ int main(void) { } mgl_context *context = mgl_get_context(); - const int x11_socket = XConnectionNumber((Display*)context->connection); egl_functions egl_funcs; egl_funcs.eglGetError = (decltype(egl_funcs.eglGetError))context->gl.eglGetProcAddress("eglGetError"); @@ -110,115 +254,47 @@ int main(void) { exit(1); } - fprintf(stderr, "info: gsr ui is now ready, waiting for inputs. Press alt+z to show/hide the overlay\n"); + fprintf(stderr, "Info: gsr ui is now ready, waiting for inputs. Press alt+z to show/hide the overlay\n"); - auto overlay = std::make_unique<gsr::Overlay>(resources_path, gsr_info, egl_funcs); - //overlay.show(); + auto overlay = std::make_unique<gsr::Overlay>(resources_path, std::move(gsr_info), std::move(capture_options), egl_funcs); + if(launch_action == LaunchAction::LAUNCH_SHOW) + overlay->show(); - // gsr::GlobalHotkeysX11 global_hotkeys; - // const bool show_hotkey_registered = global_hotkeys.bind_key_press({ XK_z, Mod1Mask }, "show_hide", [&](const std::string &id) { - // fprintf(stderr, "pressed %s\n", id.c_str()); - // overlay->toggle_show(); - // }); + auto rpc = std::make_unique<gsr::Rpc>(); + if(!rpc->create("gsr-ui")) + fprintf(stderr, "Error: Failed to create rpc, commands won't be received\n"); - // const bool record_hotkey_registered = global_hotkeys.bind_key_press({ XK_F9, Mod1Mask }, "record", [&](const std::string &id) { - // fprintf(stderr, "pressed %s\n", id.c_str()); - // overlay->toggle_record(); - // }); + rpc_add_commands(rpc.get(), overlay.get()); - // const bool pause_hotkey_registered = global_hotkeys.bind_key_press({ XK_F7, Mod1Mask }, "pause", [&](const std::string &id) { - // fprintf(stderr, "pressed %s\n", id.c_str()); - // overlay->toggle_pause(); - // }); - - // const bool stream_hotkey_registered = global_hotkeys.bind_key_press({ XK_F8, Mod1Mask }, "stream", [&](const std::string &id) { - // fprintf(stderr, "pressed %s\n", id.c_str()); - // overlay->toggle_stream(); - // }); - - // const bool replay_hotkey_registered = global_hotkeys.bind_key_press({ XK_F10, ShiftMask | Mod1Mask }, "replay_start", [&](const std::string &id) { - // fprintf(stderr, "pressed %s\n", id.c_str()); - // overlay->toggle_replay(); - // }); - - // const bool replay_save_hotkey_registered = global_hotkeys.bind_key_press({ XK_F10, Mod1Mask }, "replay_save", [&](const std::string &id) { - // fprintf(stderr, "pressed %s\n", id.c_str()); - // overlay->save_replay(); - // }); - - gsr::GlobalHotkeysLinux global_hotkeys; - if(!global_hotkeys.start()) - fprintf(stderr, "error: failed to start global hotkeys\n"); - - const bool show_hotkey_registered = global_hotkeys.bind_action("show_hide", [&](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_show(); - }); - - const bool record_hotkey_registered = global_hotkeys.bind_action("record", [&](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_record(); - }); - - const bool pause_hotkey_registered = global_hotkeys.bind_action("pause", [&](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_pause(); - }); - - const bool stream_hotkey_registered = global_hotkeys.bind_action("stream", [&](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_stream(); - }); - - const bool replay_hotkey_registered = global_hotkeys.bind_action("replay_start", [&](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_replay(); - }); - - const bool replay_save_hotkey_registered = global_hotkeys.bind_action("replay_save", [&](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->save_replay(); - }); - - if(!show_hotkey_registered) - fprintf(stderr, "error: failed to register hotkey alt+z for showing the overlay because the hotkey is registered by another program\n"); - - if(!record_hotkey_registered) - fprintf(stderr, "error: failed to register hotkey alt+f9 for recording because the hotkey is registered by another program\n"); - - if(!pause_hotkey_registered) - fprintf(stderr, "error: failed to register hotkey alt+f7 for pausing because the hotkey is registered by another program\n"); - - if(!stream_hotkey_registered) - fprintf(stderr, "error: failed to register hotkey alt+f8 for streaming because the hotkey is registered by another program\n"); - - if(!replay_hotkey_registered) - fprintf(stderr, "error: failed to register hotkey alt+shift+f10 for starting replay because the hotkey is registered by another program\n"); - - if(!replay_save_hotkey_registered) - fprintf(stderr, "error: failed to register hotkey alt+f10 for saving replay because the hotkey is registered by another program\n"); + // 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. + std::string exit_reason; mgl::Clock frame_delta_clock; - while(running) { - if(is_socket_disconnected(x11_socket)) { - fprintf(stderr, "info: the X11 server has shutdown\n"); - break; - } + while(running && mgl_is_connected_to_display_server() && !overlay->should_exit(exit_reason)) { const double frame_delta_seconds = frame_delta_clock.restart(); gsr::set_frame_delta_seconds(frame_delta_seconds); - global_hotkeys.poll_events(); + rpc->poll(); overlay->handle_events(); - if(!overlay->draw()) - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if(!overlay->draw()) { + usleep(100 * 1000); // 100ms + mgl_ping_display_server(); + } } - fprintf(stderr, "info: shutting down!\n"); + fprintf(stderr, "Info: shutting down!\n"); + rpc.reset(); overlay.reset(); - gsr::deinit_theme(); - gsr::deinit_color_theme(); mgl_deinit(); + if(exit_reason == "back-to-old-ui") { + const char *args[] = { "gpu-screen-recorder-gtk", "use-old-ui", nullptr }; + execvp(args[0], (char* const*)args); + } else if(exit_reason == "restart") { + const char *args[] = { "gsr-ui", "launch-show", nullptr }; + execvp(args[0], (char* const*)args); + } + return 0; } diff --git a/tools/gsr-global-hotkeys/README.md b/tools/gsr-global-hotkeys/README.md new file mode 100644 index 0000000..8744107 --- /dev/null +++ b/tools/gsr-global-hotkeys/README.md @@ -0,0 +1,21 @@ +# About +Global hotkeys for X11 and all Wayland compositors by using linux device api. Keyboards are grabbed and only the non-hotkey keys are passed through to the system. +The program accepts text commands as input. Run the program with the option `--virtual` to only grab virtual devices. This is useful when using keyboard input mapping software such as +kanata, otherwise kanata may fail to launch or this program may fail to launch. +# Commands +## Bind +To add a key send `bind <action> <keycode+keycode+...><newline>` to the programs stdin, for example: +``` +bind show_hide 56+44 + +``` +which will bind alt+z. When alt+z is pressed the program will output `show_hide` (and a newline) to stdout. +The program only accepts one key for each keybind command but accepts a multiple modifier keys. +The keybinding requires at least one modifier key (ctrl, alt, super or shift) and a key to be used. +The keycodes are values from `<linux/input-event-codes.h>` linux api header (which is the same as X11 keycode value minus 8). +## Unbind +To unbind all keys send `unbind_all<newline>` to the programs stdin, for example: +``` +unbind_all + +```
\ No newline at end of file diff --git a/tools/gsr-global-hotkeys/hotplug.c b/tools/gsr-global-hotkeys/hotplug.c new file mode 100644 index 0000000..5ea2978 --- /dev/null +++ b/tools/gsr-global-hotkeys/hotplug.c @@ -0,0 +1,78 @@ +#include "hotplug.h" + +/* C stdlib */ +#include <string.h> + +/* POSIX */ +#include <unistd.h> +#include <sys/socket.h> + +/* LINUX */ +#include <linux/types.h> +#include <linux/netlink.h> + +bool hotplug_event_init(hotplug_event *self) { + memset(self, 0, sizeof(*self)); + + struct sockaddr_nl nls = { + .nl_family = AF_NETLINK, + .nl_pid = getpid(), + .nl_groups = -1 + }; + + const int fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT); + if(fd == -1) + return false; + + if(bind(fd, (void*)&nls, sizeof(struct sockaddr_nl))) { + close(fd); + return false; + } + + self->fd = fd; + return true; +} + +void hotplug_event_deinit(hotplug_event *self) { + if(self->fd > 0) { + close(self->fd); + self->fd = -1; + } +} + +int hotplug_event_steal_fd(hotplug_event *self) { + const int fd = self->fd; + self->fd = -1; + return fd; +} + +/* TODO: This assumes SUBSYSTEM= is output before DEVNAME=, is that always true? */ +static void hotplug_event_parse_netlink_data(hotplug_event *self, const char *line, hotplug_device_added_callback callback, void *userdata) { + const char *at_symbol = strchr(line, '@'); + if(at_symbol) { + self->event_is_add = strncmp(line, "add@", 4) == 0; + self->subsystem_is_input = false; + } else if(self->event_is_add) { + if(strcmp(line, "SUBSYSTEM=input") == 0) + self->subsystem_is_input = true; + + if(self->subsystem_is_input && strncmp(line, "DEVNAME=", 8) == 0) { + callback(line+8, userdata); + self->event_is_add = false; + } + } +} + +/* Netlink uevent structure is documented here: https://web.archive.org/web/20160127215232/https://www.kernel.org/doc/pending/hotplug.txt */ +void hotplug_event_process_event_data(hotplug_event *self, int fd, hotplug_device_added_callback callback, void *userdata) { + const int bytes_read = read(fd, self->event_data, sizeof(self->event_data)); + if(bytes_read <= 0) + return; + + /* Hotplug data ends with a newline and a null terminator */ + int data_index = 0; + while(data_index < bytes_read) { + hotplug_event_parse_netlink_data(self, self->event_data + data_index, callback, userdata); + data_index += strlen(self->event_data + data_index) + 1; /* Skip null terminator as well */ + } +} diff --git a/tools/gsr-global-hotkeys/hotplug.h b/tools/gsr-global-hotkeys/hotplug.h new file mode 100644 index 0000000..665485a --- /dev/null +++ b/tools/gsr-global-hotkeys/hotplug.h @@ -0,0 +1,22 @@ +#ifndef HOTPLUG_H +#define HOTPLUG_H + +/* C stdlib */ +#include <stdbool.h> + +typedef struct { + int fd; + bool event_is_add; + bool subsystem_is_input; + char event_data[1024]; +} hotplug_event; + +typedef void (*hotplug_device_added_callback)(const char *devname, void *userdata); + +bool hotplug_event_init(hotplug_event *self); +void hotplug_event_deinit(hotplug_event *self); + +int hotplug_event_steal_fd(hotplug_event *self); +void hotplug_event_process_event_data(hotplug_event *self, int fd, hotplug_device_added_callback callback, void *userdata); + +#endif /* HOTPLUG_H */ diff --git a/tools/gsr-global-hotkeys/keyboard_event.c b/tools/gsr-global-hotkeys/keyboard_event.c new file mode 100644 index 0000000..6973d4b --- /dev/null +++ b/tools/gsr-global-hotkeys/keyboard_event.c @@ -0,0 +1,740 @@ +#include "keyboard_event.h" +#include "keys.h" + +/* C stdlib */ +#include <stdio.h> +#include <string.h> +#include <errno.h> +#include <stdbool.h> +#include <stdlib.h> + +/* POSIX */ +#include <fcntl.h> +#include <unistd.h> +#include <dirent.h> +#include <poll.h> + +/* LINUX */ +#include <linux/input.h> +#include <linux/uinput.h> + +#define GSR_UI_VIRTUAL_KEYBOARD_NAME "gsr-ui virtual keyboard" + +#define KEY_RELEASE 0 +#define KEY_PRESS 1 +#define KEY_REPEAT 2 + +#define KEY_STATES_SIZE (KEY_MAX/8 + 1) + +static inline int count_num_bits_set(unsigned char c) { + int n = 0; + n += (c & 1); + c >>= 1; + n += (c & 1); + c >>= 1; + n += (c & 1); + c >>= 1; + n += (c & 1); + c >>= 1; + n += (c & 1); + c >>= 1; + n += (c & 1); + c >>= 1; + n += (c & 1); + c >>= 1; + n += (c & 1); + return n; +} + +static inline bool keyboard_event_has_exclusive_grab(const keyboard_event *self) { + return self->uinput_fd > 0; +} + +static int keyboard_event_get_num_keys_pressed(const unsigned char *key_states) { + if(!key_states) + return 0; + + int num_keys_pressed = 0; + for(int i = 0; i < KEY_STATES_SIZE; ++i) { + num_keys_pressed += count_num_bits_set(key_states[i]); + } + return num_keys_pressed; +} + +static void keyboard_event_fetch_update_key_states(keyboard_event *self, event_extra_data *extra_data, int fd) { + fsync(fd); + if(!extra_data->key_states) + return; + + if(ioctl(fd, EVIOCGKEY(KEY_STATES_SIZE), extra_data->key_states) == -1) + fprintf(stderr, "Warning: failed to fetch key states for device: /dev/input/event%d\n", extra_data->dev_input_id); + + if(!keyboard_event_has_exclusive_grab(self) || extra_data->grabbed) + return; + + extra_data->num_keys_pressed = keyboard_event_get_num_keys_pressed(extra_data->key_states); + if(extra_data->num_keys_pressed == 0) { + extra_data->grabbed = ioctl(fd, EVIOCGRAB, 1) != -1; + if(extra_data->grabbed) + fprintf(stderr, "Info: grabbed device: /dev/input/event%d\n", extra_data->dev_input_id); + else + fprintf(stderr, "Warning: failed to exclusively grab device: /dev/input/event%d. The focused application may receive keys used for global hotkeys\n", extra_data->dev_input_id); + } +} + +static void keyboard_event_process_key_state_change(keyboard_event *self, const struct input_event *event, event_extra_data *extra_data, int fd) { + if(event->type != EV_KEY) + return; + + if(!extra_data->key_states || event->code >= KEY_STATES_SIZE * 8) + return; + + const unsigned int byte_index = event->code / 8; + const unsigned char bit_index = event->code % 8; + unsigned char key_byte_state = extra_data->key_states[byte_index]; + const bool prev_key_pressed = (key_byte_state & (1 << bit_index)) != KEY_RELEASE; + + if(event->value == KEY_RELEASE) { + key_byte_state &= ~(1 << bit_index); + if(prev_key_pressed) + --extra_data->num_keys_pressed; + } else { + key_byte_state |= (1 << bit_index); + if(!prev_key_pressed) + ++extra_data->num_keys_pressed; + } + + extra_data->key_states[byte_index] = key_byte_state; + + if(!keyboard_event_has_exclusive_grab(self) || extra_data->grabbed) + return; + + if(extra_data->num_keys_pressed == 0) { + extra_data->grabbed = ioctl(fd, EVIOCGRAB, 1) != -1; + if(extra_data->grabbed) + fprintf(stderr, "Info: grabbed device: /dev/input/event%d\n", extra_data->dev_input_id); + else + fprintf(stderr, "Warning: failed to exclusively grab device: /dev/input/event%d. The focused application may receive keys used for global hotkeys\n", extra_data->dev_input_id); + } +} + +/* Return true if a global hotkey is assigned to the key combination */ +static bool keyboard_event_on_key_pressed(keyboard_event *self, const struct input_event *event, uint32_t modifiers) { + if(event->value != KEYBOARD_BUTTON_PRESSED) + return false; + + bool global_hotkey_match = false; + for(int i = 0; i < self->num_global_hotkeys; ++i) { + if(event->code == self->global_hotkeys[i].key && modifiers == self->global_hotkeys[i].modifiers) { + puts(self->global_hotkeys[i].action); + fflush(stdout); + global_hotkey_match = true; + } + } + return global_hotkey_match; +} + +static inline uint32_t set_bit(uint32_t value, uint32_t bit_flag, bool set) { + if(set) + return value | bit_flag; + else + return value & ~bit_flag; +} + +static uint32_t keycode_to_modifier_bit(uint32_t keycode) { + switch(keycode) { + case KEY_LEFTSHIFT: return KEYBOARD_MODKEY_LSHIFT; + case KEY_RIGHTSHIFT: return KEYBOARD_MODKEY_RSHIFT; + case KEY_LEFTCTRL: return KEYBOARD_MODKEY_LCTRL; + case KEY_RIGHTCTRL: return KEYBOARD_MODKEY_RCTRL; + case KEY_LEFTALT: return KEYBOARD_MODKEY_LALT; + case KEY_RIGHTALT: return KEYBOARD_MODKEY_RALT; + case KEY_LEFTMETA: return KEYBOARD_MODKEY_LSUPER; + case KEY_RIGHTMETA: return KEYBOARD_MODKEY_RSUPER; + } + return 0; +} + +static void keyboard_event_process_input_event_data(keyboard_event *self, event_extra_data *extra_data, int fd) { + struct input_event event; + if(read(fd, &event, sizeof(event)) != sizeof(event)) { + fprintf(stderr, "Error: failed to read input event data\n"); + return; + } + + if(event.type == EV_SYN && event.code == SYN_DROPPED) { + /* TODO: Don't do this on every SYN_DROPPED to prevent spamming this, instead wait until the next event or wait for timeout */ + keyboard_event_fetch_update_key_states(self, extra_data, fd); + return; + } + + //if(event.type == EV_KEY && event.code == KEY_A && event.value == KEY_PRESS) { + //fprintf(stderr, "fd: %d, type: %d, pressed %d, value: %d\n", fd, event.type, event.code, event.value); + //} + + if(event.type == EV_KEY && is_keyboard_key(event.code)) { + keyboard_event_process_key_state_change(self, &event, extra_data, fd); + const uint32_t modifier_bit = keycode_to_modifier_bit(event.code); + if(modifier_bit == 0) { + if(keyboard_event_on_key_pressed(self, &event, self->modifier_button_states)) + return; + } else { + self->modifier_button_states = set_bit(self->modifier_button_states, modifier_bit, event.value >= 1); + } + } + + if(extra_data->grabbed) { + /* TODO: What to do on error? */ + if(write(self->uinput_fd, &event, sizeof(event)) != sizeof(event)) + fprintf(stderr, "Error: failed to write event data to virtual keyboard for exclusively grabbed device\n"); + } +} + +/* Retarded linux takes very long time to close /dev/input/eventN files, even though they are virtual and opened read-only */ +static void* keyboard_event_close_fds_callback(void *userdata) { + keyboard_event *self = userdata; + while(self->running) { + pthread_mutex_lock(&self->close_dev_input_mutex); + for(int i = 0; i < self->num_close_fds; ++i) { + close(self->close_fds[i]); + } + self->num_close_fds = 0; + pthread_mutex_unlock(&self->close_dev_input_mutex); + + usleep(100 * 1000); /* 100 milliseconds */ + } + return NULL; +} + +static bool keyboard_event_try_add_close_fd(keyboard_event *self, int fd) { + bool success = false; + pthread_mutex_lock(&self->close_dev_input_mutex); + if(self->num_close_fds < MAX_CLOSE_FDS) { + self->close_fds[self->num_close_fds] = fd; + ++self->num_close_fds; + success = true; + } else { + success = false; + } + pthread_mutex_unlock(&self->close_dev_input_mutex); + return success; +} + +/* Returns -1 if invalid format. Expected |dev_input_filepath| to be in format /dev/input/eventN */ +static int get_dev_input_id_from_filepath(const char *dev_input_filepath) { + if(strncmp(dev_input_filepath, "/dev/input/event", 16) != 0) + return -1; + + int dev_input_id = -1; + if(sscanf(dev_input_filepath + 16, "%d", &dev_input_id) == 1) + return dev_input_id; + return -1; +} + +static bool keyboard_event_has_event_with_dev_input_fd(keyboard_event *self, int dev_input_id) { + for(int i = 0; i < self->num_event_polls; ++i) { + if(self->event_extra_data[i].dev_input_id == dev_input_id) + return true; + } + 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; + + const bool is_virtual_device = dev_input_is_virtual(dev_input_id); + if(self->grab_type == KEYBOARD_GRAB_TYPE_VIRTUAL && !is_virtual_device) + return false; + + if(keyboard_event_has_event_with_dev_input_fd(self, dev_input_id)) + return false; + + const int fd = open(dev_input_filepath, O_RDONLY); + if(fd == -1) + return false; + + char device_name[256]; + device_name[0] = '\0'; + ioctl(fd, EVIOCGNAME(sizeof(device_name)), device_name); + + unsigned long evbit = 0; + ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit); + const bool is_keyboard = (evbit & (1 << EV_SYN)) && (evbit & (1 << EV_KEY)); + + if(is_keyboard && strcmp(device_name, GSR_UI_VIRTUAL_KEYBOARD_NAME) != 0) { + unsigned char key_bits[KEY_MAX/8 + 1] = {0}; + ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(key_bits)), &key_bits); + + const bool supports_key_events = key_bits[KEY_A/8] & (1 << (KEY_A % 8)); + const bool supports_mouse_events = key_bits[BTN_MOUSE/8] & (1 << (BTN_MOUSE % 8)); + //const bool supports_touch_events = key_bits[BTN_TOUCH/8] & (1 << (BTN_TOUCH % 8)); + const bool supports_joystick_events = key_bits[BTN_JOYSTICK/8] & (1 << (BTN_JOYSTICK % 8)); + const bool supports_wheel_events = key_bits[BTN_WHEEL/8] & (1 << (BTN_WHEEL % 8)); + if(supports_key_events && (is_virtual_device || (!supports_joystick_events && !supports_wheel_events))) { + unsigned char *key_states = calloc(1, KEY_STATES_SIZE); + if(key_states && self->num_event_polls < MAX_EVENT_POLLS) { + //fprintf(stderr, "%s (%s) supports key inputs\n", dev_input_filepath, device_name); + self->event_polls[self->num_event_polls] = (struct pollfd) { + .fd = fd, + .events = POLLIN, + .revents = 0 + }; + + self->event_extra_data[self->num_event_polls] = (event_extra_data) { + .dev_input_id = dev_input_id, + .grabbed = false, + .key_states = key_states, + .num_keys_pressed = 0 + }; + + if(supports_mouse_events || supports_joystick_events || supports_wheel_events) { + fprintf(stderr, "Info: device not grabbed yet because it might be a mouse: /dev/input/event%d\n", dev_input_id); + fsync(fd); + if(ioctl(fd, EVIOCGKEY(KEY_STATES_SIZE), self->event_extra_data[self->num_event_polls].key_states) == -1) + fprintf(stderr, "Warning: failed to fetch key states for device: /dev/input/event%d\n", dev_input_id); + } else { + keyboard_event_fetch_update_key_states(self, &self->event_extra_data[self->num_event_polls], fd); + if(self->event_extra_data[self->num_event_polls].num_keys_pressed > 0) + fprintf(stderr, "Info: device not grabbed yet because some keys are still being pressed: /dev/input/event%d\n", dev_input_id); + } + + ++self->num_event_polls; + return true; + } else { + fprintf(stderr, "Warning: the maximum number of keyboard devices have been registered. The newly added keyboard will be ignored\n"); + } + } + } + + if(!keyboard_event_try_add_close_fd(self, fd)) { + fprintf(stderr, "Error: failed to add immediately, closing now\n"); + close(fd); + } + return false; +} + +static bool keyboard_event_add_dev_input_devices(keyboard_event *self) { + DIR *dir = opendir("/dev/input"); + if(!dir) { + fprintf(stderr, "error: failed to open /dev/input, error: %s\n", strerror(errno)); + return false; + } + + char dev_input_filepath[1024]; + for(;;) { + struct dirent *entry = readdir(dir); + if(!entry) + break; + + if(strncmp(entry->d_name, "event", 5) != 0) + continue; + + snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/%s", entry->d_name); + keyboard_event_try_add_device_if_keyboard(self, dev_input_filepath); + } + + closedir(dir); + return true; +} + +static void keyboard_event_remove_event(keyboard_event *self, int index) { + if(index < 0 || index >= self->num_event_polls) + return; + + ioctl(self->event_polls[index].fd, EVIOCGRAB, 0); + close(self->event_polls[index].fd); + free(self->event_extra_data[index].key_states); + + for(int i = index + 1; i < self->num_event_polls; ++i) { + self->event_polls[i - 1] = self->event_polls[i]; + self->event_extra_data[i - 1] = self->event_extra_data[i]; + } + --self->num_event_polls; +} + +/* Returns the fd to the uinput */ +/* Documented here: https://www.kernel.org/doc/html/v4.12/input/uinput.html */ +static int setup_virtual_keyboard_input(const char *name) { + /* TODO: O_NONBLOCK? */ + int fd = open("/dev/uinput", O_WRONLY); + if(fd == -1) { + fd = open("/dev/input/uinput", O_WRONLY); + if(fd == -1) { + fprintf(stderr, "Warning: failed to setup virtual device for exclusive grab (failed to open /dev/uinput or /dev/input/uinput), error: %s\n", strerror(errno)); + return -1; + } + } + + bool success = true; + success &= (ioctl(fd, UI_SET_EVBIT, EV_SYN) != -1); + success &= (ioctl(fd, UI_SET_EVBIT, EV_MSC) != -1); + success &= (ioctl(fd, UI_SET_EVBIT, EV_KEY) != -1); + success &= (ioctl(fd, UI_SET_EVBIT, EV_REP) != -1); + success &= (ioctl(fd, UI_SET_EVBIT, EV_REL) != -1); + success &= (ioctl(fd, UI_SET_EVBIT, EV_LED) != -1); + + success &= (ioctl(fd, UI_SET_MSCBIT, MSC_SCAN) != -1); + for(int i = 1; i < KEY_MAX; ++i) { + 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) { + success &= (ioctl(fd, UI_SET_LEDBIT, i) != -1); + } + + // success &= (ioctl(fd, UI_SET_EVBIT, EV_ABS) != -1); + // success &= (ioctl(fd, UI_SET_ABSBIT, ABS_X) != -1); + // success &= (ioctl(fd, UI_SET_ABSBIT, ABS_Y) != -1); + // success &= (ioctl(fd, UI_SET_ABSBIT, ABS_Z) != -1); + + int ui_version = 0; + success &= (ioctl(fd, UI_GET_VERSION, &ui_version) != -1); + + if(ui_version >= 5) { + struct uinput_setup usetup; + memset(&usetup, 0, sizeof(usetup)); + usetup.id.bustype = BUS_USB; + usetup.id.vendor = 0xdec0; + usetup.id.product = 0x5eba; + snprintf(usetup.name, sizeof(usetup.name), "%s", name); + success &= (ioctl(fd, UI_DEV_SETUP, &usetup) != -1); + } else { + struct uinput_user_dev uud; + memset(&uud, 0, sizeof(uud)); + snprintf(uud.name, UINPUT_MAX_NAME_SIZE, "%s", name); + if(write(fd, &uud, sizeof(uud)) != sizeof(uud)) + success = false; + } + + success &= (ioctl(fd, UI_DEV_CREATE) != -1); + if(!success) { + close(fd); + return -1; + } + + return fd; +} + +bool keyboard_event_init(keyboard_event *self, bool exclusive_grab, keyboard_grab_type grab_type) { + memset(self, 0, sizeof(*self)); + self->stdin_event_index = -1; + self->hotplug_event_index = -1; + self->grab_type = grab_type; + self->running = true; + + pthread_mutex_init(&self->close_dev_input_mutex, NULL); + if(pthread_create(&self->close_dev_input_fds_thread, NULL, keyboard_event_close_fds_callback, self) != 0) { + self->close_dev_input_fds_thread = 0; + fprintf(stderr, "Error: failed to create close fds thread\n"); + return false; + } + + if(exclusive_grab) { + self->uinput_fd = setup_virtual_keyboard_input(GSR_UI_VIRTUAL_KEYBOARD_NAME); + if(self->uinput_fd <= 0) + fprintf(stderr, "Warning: failed to setup virtual keyboard input for exclusive grab. The focused application will receive keys used for global hotkeys\n"); + } + + self->event_polls[self->num_event_polls] = (struct pollfd) { + .fd = STDIN_FILENO, + .events = POLLIN, + .revents = 0 + }; + + self->event_extra_data[self->num_event_polls] = (event_extra_data) { + .dev_input_id = -1, + .grabbed = false, + .key_states = NULL, + .num_keys_pressed = 0 + }; + + self->stdin_event_index = self->num_event_polls; + ++self->num_event_polls; + + if(hotplug_event_init(&self->hotplug_ev)) { + self->event_polls[self->num_event_polls] = (struct pollfd) { + .fd = hotplug_event_steal_fd(&self->hotplug_ev), + .events = POLLIN, + .revents = 0 + }; + + self->event_extra_data[self->num_event_polls] = (event_extra_data) { + .dev_input_id = -1, + .grabbed = false, + .key_states = NULL, + .num_keys_pressed = 0 + }; + + self->hotplug_event_index = self->num_event_polls; + ++self->num_event_polls; + } else { + fprintf(stderr, "Warning: failed to setup hotplugging\n"); + } + + keyboard_event_add_dev_input_devices(self); + + /* Neither hotplugging nor any keyboard devices were found. We will never listen to keyboard events so might as well fail */ + if(self->num_event_polls == 0) { + keyboard_event_deinit(self); + return false; + } + + return true; +} + +void keyboard_event_deinit(keyboard_event *self) { + self->running = false; + + for(int i = 0; i < self->num_global_hotkeys; ++i) { + free(self->global_hotkeys[i].action); + } + self->num_global_hotkeys = 0; + + if(self->uinput_fd > 0) { + ioctl(self->uinput_fd, UI_DEV_DESTROY); + close(self->uinput_fd); + self->uinput_fd = -1; + } + + for(int i = 0; i < self->num_event_polls; ++i) { + ioctl(self->event_polls[i].fd, EVIOCGRAB, 0); + close(self->event_polls[i].fd); + free(self->event_extra_data[i].key_states); + } + self->num_event_polls = 0; + + hotplug_event_deinit(&self->hotplug_ev); + + if(self->close_dev_input_fds_thread > 0) { + pthread_join(self->close_dev_input_fds_thread, NULL); + self->close_dev_input_fds_thread = 0; + } + + pthread_mutex_destroy(&self->close_dev_input_mutex); +} + +static void on_device_added_callback(const char *devname, void *userdata) { + keyboard_event *keyboard_ev = userdata; + char dev_input_filepath[256]; + snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/%s", devname); + keyboard_event_try_add_device_if_keyboard(keyboard_ev, dev_input_filepath); +} + +/* Returns -1 on error */ +static int parse_u8(const char *str, int size) { + if(size <= 0) + return -1; + + int result = 0; + for(int i = 0; i < size; ++i) { + char c = str[i]; + if(c >= '0' && c <= '9') { + result = result * 10 + (c - '0'); + if(result > 255) + return -1; + } else { + return -1; + } + } + return result; +} + +static bool keyboard_event_parse_bind_keys(const char *str, int size, uint8_t *key, uint32_t *modifiers) { + *key = 0; + *modifiers = 0; + + const char *number_start = str; + const char *end = str + size; + for(;;) { + const char *next = strchr(number_start, '+'); + if(!next) + next = end; + + const int number_len = next - number_start; + const int number = parse_u8(number_start, number_len); + if(number == -1) { + fprintf(stderr, "Error: bind command keys \"%s\" is in invalid format\n", str); + return false; + } + + const uint32_t modifier_bit = keycode_to_modifier_bit(number); + if(modifier_bit == 0) { + if(*key != 0) { + fprintf(stderr, "Error: can't bind hotkey with multiple non-modifier keys\n"); + return false; + } + *key = number; + } else { + *modifiers = set_bit(*modifiers, modifier_bit, true); + } + + number_start = next + 1; + if(next == end) + break; + } + + if(key == 0) { + fprintf(stderr, "Error: can't bind hotkey without a non-modifier key\n"); + return false; + } + + if(modifiers == 0) { + fprintf(stderr, "Error: can't bind hotkey without a modifier\n"); + return false; + } + + return true; +} + +/* |command| is null-terminated */ +static void keyboard_event_parse_stdin_command(keyboard_event *self, const char *command, int command_size) { + if(strncmp(command, "bind ", 5) == 0) { + /* Example: |bind show_hide 20+40| */ + if(self->num_global_hotkeys >= MAX_GLOBAL_HOTKEYS) { + fprintf(stderr, "Error: can't add another hotkey. The maximum number of hotkeys (%d) has been reached\n", MAX_GLOBAL_HOTKEYS); + return; + } + + const char *action_name_end = strchr(command + 5, ' '); + if(!action_name_end) { + fprintf(stderr, "Error: command \"%s\" is in invalid format\n", command); + return; + } + + const char *action_name = command + 5; + const int action_name_size = action_name_end - action_name; + + uint8_t key = 0; + uint32_t modifiers = 0; + const char *number_start = action_name_end + 1; + const char *end = command + command_size; + if(!keyboard_event_parse_bind_keys(number_start, end - number_start, &key, &modifiers)) + return; + + char *action = strndup(action_name, action_name_size); + if(!action) { + fprintf(stderr, "Error: failed to duplicate %.*s\n", action_name_size, action_name); + return; + } + + self->global_hotkeys[self->num_global_hotkeys] = (global_hotkey) { + .action = action, + .key = key, + .modifiers = modifiers + }; + ++self->num_global_hotkeys; + fprintf(stderr, "Info: binded hotkey: %s\n", action); + } else if(strncmp(command, "unbind_all", 10) == 0) { + for(int i = 0; i < self->num_global_hotkeys; ++i) { + free(self->global_hotkeys[i].action); + } + self->num_global_hotkeys = 0; + fprintf(stderr, "Info: unbinded all hotkeys\n"); + } else { + fprintf(stderr, "Warning: got invalid command: \"%s\", expected command to start with either \"bind\" or \"unbind_all\"\n", command); + } +} + +static void keyboard_event_process_stdin_command_data(keyboard_event *self, int fd) { + const int num_bytes_to_read = sizeof(self->stdin_command_data) - self->stdin_command_data_size; + if(num_bytes_to_read == 0) { + fprintf(stderr, "Error: failed to read data from stdin, buffer is full. Clearing buffer\n"); + self->stdin_command_data_size = 0; + return; + } + + const ssize_t bytes_read = read(fd, self->stdin_command_data + self->stdin_command_data_size, num_bytes_to_read); + if(bytes_read <= 0) + return; + + const char *command_start = self->stdin_command_data; + const char *search = self->stdin_command_data + self->stdin_command_data_size; + const char *end = search + bytes_read; + self->stdin_command_data_size += bytes_read; + + for(;;) { + char *next = memchr(search, '\n', end - search); + if(!next) + break; + + *next = '\0'; + keyboard_event_parse_stdin_command(self, command_start, next - command_start); + search = next + 1; + command_start = search; + if(next == end) + break; + } + + const int bytes_parsed = command_start - self->stdin_command_data; + if(bytes_parsed > 0) { + self->stdin_command_data_size -= bytes_parsed; + memmove(self->stdin_command_data, command_start, self->stdin_command_data_size); + } +} + +void keyboard_event_poll_events(keyboard_event *self, int timeout_milliseconds) { + if(poll(self->event_polls, self->num_event_polls, timeout_milliseconds) <= 0) + return; + + if(self->stdin_failed) + return; + + for(int i = 0; i < self->num_event_polls; ++i) { + if(i == self->stdin_event_index && (self->event_polls[i].revents & (POLLHUP|POLLERR))) + self->stdin_failed = true; + + if(self->event_polls[i].revents & POLLHUP) { /* TODO: What if this is the hotplug fd? */ + keyboard_event_remove_event(self, i); + --i; /* Repeat same index since the current element has been removed */ + continue; + } + + if(!(self->event_polls[i].revents & POLLIN)) + continue; + + if(i == self->hotplug_event_index) { + /* Device is added to end of |event_polls| so it's ok to add while iterating it via index */ + hotplug_event_process_event_data(&self->hotplug_ev, self->event_polls[i].fd, on_device_added_callback, self); + } else if(i == self->stdin_event_index) { + keyboard_event_process_stdin_command_data(self, self->event_polls[i].fd); + } else { + keyboard_event_process_input_event_data(self, &self->event_extra_data[i], self->event_polls[i].fd); + } + } +} + +bool keyboard_event_stdin_has_failed(const keyboard_event *self) { + return self->stdin_failed; +} diff --git a/tools/gsr-global-hotkeys/keyboard_event.h b/tools/gsr-global-hotkeys/keyboard_event.h new file mode 100644 index 0000000..a86b3dd --- /dev/null +++ b/tools/gsr-global-hotkeys/keyboard_event.h @@ -0,0 +1,92 @@ +#ifndef KEYBOARD_EVENT_H +#define KEYBOARD_EVENT_H + +/* Read keyboard input from linux /dev/input/eventN devices, with hotplug support */ + +#include "hotplug.h" + +/* C stdlib */ +#include <stdbool.h> +#include <stdint.h> + +/* POSIX */ +#include <poll.h> +#include <pthread.h> + +/* LINUX */ +#include <linux/input-event-codes.h> + +#define MAX_EVENT_POLLS 32 +#define MAX_CLOSE_FDS 256 +#define MAX_GLOBAL_HOTKEYS 32 + +typedef enum { + KEYBOARD_MODKEY_LALT = 1 << 0, + KEYBOARD_MODKEY_RALT = 1 << 1, + KEYBOARD_MODKEY_LSUPER = 1 << 2, + KEYBOARD_MODKEY_RSUPER = 1 << 3, + KEYBOARD_MODKEY_LCTRL = 1 << 4, + KEYBOARD_MODKEY_RCTRL = 1 << 5, + KEYBOARD_MODKEY_LSHIFT = 1 << 6, + KEYBOARD_MODKEY_RSHIFT = 1 << 7 +} keyboard_modkeys; + +typedef enum { + KEYBOARD_BUTTON_RELEASED, + KEYBOARD_BUTTON_PRESSED +} keyboard_button_state; + +typedef struct { + int dev_input_id; + bool grabbed; + unsigned char *key_states; + int num_keys_pressed; +} event_extra_data; + +typedef enum { + KEYBOARD_GRAB_TYPE_ALL, + KEYBOARD_GRAB_TYPE_VIRTUAL +} keyboard_grab_type; + +typedef struct { + uint32_t key; + uint32_t modifiers; /* keyboard_modkeys bitmask */ + char *action; +} global_hotkey; + +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| */ + int num_event_polls; + + int stdin_event_index; + int hotplug_event_index; + int uinput_fd; + bool stdin_failed; + keyboard_grab_type grab_type; + + pthread_t close_dev_input_fds_thread; + pthread_mutex_t close_dev_input_mutex; + int close_fds[MAX_CLOSE_FDS]; + int num_close_fds; + bool running; + + char stdin_command_data[512]; + int stdin_command_data_size; + + global_hotkey global_hotkeys[MAX_GLOBAL_HOTKEYS]; + int num_global_hotkeys; + + hotplug_event hotplug_ev; + + uint32_t modifier_button_states; +} keyboard_event; + +bool keyboard_event_init(keyboard_event *self, bool exclusive_grab, keyboard_grab_type grab_type); +void keyboard_event_deinit(keyboard_event *self); + +/* If |timeout_milliseconds| is -1 then wait until an event is received */ +void keyboard_event_poll_events(keyboard_event *self, int timeout_milliseconds); +bool keyboard_event_stdin_has_failed(const keyboard_event *self); + +#endif /* KEYBOARD_EVENT_H */ diff --git a/tools/gsr-global-hotkeys/keys.c b/tools/gsr-global-hotkeys/keys.c new file mode 100644 index 0000000..3b8fc8a --- /dev/null +++ b/tools/gsr-global-hotkeys/keys.c @@ -0,0 +1,21 @@ +#include "keys.h" +#include <linux/input-event-codes.h> + +bool is_keyboard_key(uint32_t keycode) { + return (keycode >= KEY_ESC && keycode <= KEY_KPDOT) + || (keycode >= KEY_ZENKAKUHANKAKU && keycode <= KEY_F24) + || (keycode >= KEY_PLAYCD && keycode <= KEY_MICMUTE) + || (keycode >= KEY_OK && keycode <= KEY_IMAGES) + || (keycode >= KEY_DEL_EOL && keycode <= KEY_DEL_LINE) + || (keycode >= KEY_FN && keycode <= KEY_FN_B) + || (keycode >= KEY_BRL_DOT1 && keycode <= KEY_BRL_DOT10) + || (keycode >= KEY_NUMERIC_0 && keycode <= KEY_LIGHTS_TOGGLE) + || (keycode == KEY_ALS_TOGGLE) + || (keycode >= KEY_BUTTONCONFIG && keycode <= KEY_VOICECOMMAND) + || (keycode >= KEY_BRIGHTNESS_MIN && keycode <= KEY_BRIGHTNESS_MAX) + || (keycode >= KEY_KBDINPUTASSIST_PREV && keycode <= KEY_ONSCREEN_KEYBOARD); +} + +bool is_mouse_button(uint32_t keycode) { + return (keycode >= BTN_MOUSE && keycode <= BTN_TASK); +} diff --git a/tools/gsr-global-hotkeys/keys.h b/tools/gsr-global-hotkeys/keys.h new file mode 100644 index 0000000..4f31882 --- /dev/null +++ b/tools/gsr-global-hotkeys/keys.h @@ -0,0 +1,10 @@ +#ifndef KEYS_H +#define KEYS_H + +#include <stdbool.h> +#include <stdint.h> + +bool is_keyboard_key(uint32_t keycode); +bool is_mouse_button(uint32_t keycode); + +#endif /* KEYS_H */ diff --git a/tools/gsr-global-hotkeys/main.c b/tools/gsr-global-hotkeys/main.c index 2823487..41e5ca5 100644 --- a/tools/gsr-global-hotkeys/main.c +++ b/tools/gsr-global-hotkeys/main.c @@ -1,272 +1,90 @@ +#include "keyboard_event.h" + +/* C stdlib */ #include <stdio.h> -#include <unistd.h> -#include <fcntl.h> #include <string.h> -#include <errno.h> -#include <stdbool.h> -#include <poll.h> - -#include <libudev.h> -#include <libinput.h> -#include <libevdev/libevdev.h> -#include <xkbcommon/xkbcommon.h> - -typedef struct { - struct xkb_context *xkb_context; - struct xkb_keymap *xkb_keymap; - struct xkb_state *xkb_state; -} key_mapper; - -typedef enum { - MODKEY_ALT = 1 << 0, - MODKEY_SUPER = 1 << 1, - MODKEY_CTRL = 1 << 2, - MODKEY_SHIFT = 1 << 3 -} modkeys; - -typedef struct { - uint32_t key; - uint32_t modifiers; /* modkeys */ - const char *action; -} global_hotkey; - -#define NUM_GLOBAL_HOTKEYS 6 -static global_hotkey global_hotkeys[NUM_GLOBAL_HOTKEYS] = { - { .key = XKB_KEY_z, .modifiers = MODKEY_ALT, .action = "show_hide" }, - { .key = XKB_KEY_F9, .modifiers = MODKEY_ALT, .action = "record" }, - { .key = XKB_KEY_F7, .modifiers = MODKEY_ALT, .action = "pause" }, - { .key = XKB_KEY_F8, .modifiers = MODKEY_ALT, .action = "stream" }, - { .key = XKB_KEY_F10, .modifiers = MODKEY_ALT | MODKEY_SHIFT, .action = "replay_start" }, - { .key = XKB_KEY_F10, .modifiers = MODKEY_ALT, .action = "replay_save" } -}; - -static int open_restricted(const char *path, int flags, void *user_data) { - (void)user_data; - int fd = open(path, flags); - if(fd < 0) - fprintf(stderr, "error: failed to open %s, error: %s\n", path, strerror(errno)); - return fd < 0 ? -errno : fd; -} - -static void close_restricted(int fd, void *user_data) { - (void)user_data; - close(fd); -} +#include <locale.h> -static const struct libinput_interface interface = { - .open_restricted = open_restricted, - .close_restricted = close_restricted, -}; +/* POSIX */ +#include <unistd.h> -static bool is_mod_key(xkb_keycode_t xkb_key_code) { - return xkb_key_code >= XKB_KEY_Shift_L && xkb_key_code <= XKB_KEY_Hyper_R; +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 struct { - const char *modname; - modkeys key; -} modname_to_modkey_map; - -static uint32_t xkb_state_to_modifiers(struct xkb_state *xkb_state) { - const modname_to_modkey_map modifier_keys[] = { - { .modname = XKB_MOD_NAME_ALT, .key = MODKEY_ALT }, - { .modname = XKB_MOD_NAME_LOGO, .key = MODKEY_SUPER }, - { .modname = XKB_MOD_NAME_SHIFT, .key = MODKEY_SHIFT }, - { .modname = XKB_MOD_NAME_CTRL, .key = MODKEY_CTRL } - }; - - uint32_t modifiers = 0; - for(int i = 0; i < 4; ++i) { - if(xkb_state_mod_name_is_active(xkb_state, modifier_keys[i].modname, XKB_STATE_MODS_EFFECTIVE) > 0) - modifiers |= modifier_keys[i].key; - } - return modifiers; -} - -#define KEY_CODE_EV_TO_XKB(key) ((key) + 8) - -static int print_key_event(struct libinput_event *event, key_mapper *mapper) { - struct libinput_event_keyboard *keyboard = libinput_event_get_keyboard_event(event); - const uint32_t key_code = libinput_event_keyboard_get_key(keyboard); - enum libinput_key_state state_code = libinput_event_keyboard_get_key_state(keyboard); - - const xkb_keycode_t xkb_key_code = KEY_CODE_EV_TO_XKB(key_code); - xkb_state_update_key(mapper->xkb_state, xkb_key_code, state_code == LIBINPUT_KEY_STATE_PRESSED ? XKB_KEY_DOWN : XKB_KEY_UP); - xkb_keysym_t xkb_key_sym = xkb_state_key_get_one_sym(mapper->xkb_state, xkb_key_code); - // char main_key[128]; - // main_key[0] = '\0'; - - // if(xkb_state_mod_name_is_active(mapper->xkb_state, XKB_MOD_NAME_LOGO, XKB_STATE_MODS_EFFECTIVE) > 0) - // strcat(main_key, "Super+"); - // if(xkb_state_mod_name_is_active(mapper->xkb_state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) > 0) - // strcat(main_key, "Ctrl+"); - // if(xkb_state_mod_name_is_active(mapper->xkb_state, XKB_MOD_NAME_ALT, XKB_STATE_MODS_EFFECTIVE) > 0 && strcmp(main_key, "Meta") != 0) - // strcat(main_key, "Alt+"); - // if(xkb_state_mod_name_is_active(mapper->xkb_state, XKB_MOD_NAME_SHIFT, XKB_STATE_MODS_EFFECTIVE) > 0) - // strcat(main_key, "Shift+"); - - // if(!is_mod_key(xkb_key_sym)) { - // char reg_key[64]; - // reg_key[0] = '\0'; - // xkb_keysym_get_name(xkb_key_sym, reg_key, sizeof(reg_key)); - // strcat(main_key, reg_key); - // } - - if(state_code != LIBINPUT_KEY_STATE_PRESSED) - return 0; +static bool is_gsr_global_hotkeys_already_running(void) { + FILE *f = fopen("/proc/bus/input/devices", "rb"); + if(!f) + return false; - const uint32_t current_modifiers = xkb_state_to_modifiers(mapper->xkb_state); - for(int i = 0; i < NUM_GLOBAL_HOTKEYS; ++i) { - if(xkb_key_sym == global_hotkeys[i].key && current_modifiers == global_hotkeys[i].modifiers) { - puts(global_hotkeys[i].action); - fflush(stdout); + bool virtual_keyboard_running = false; + char line[1024]; + while(fgets(line, sizeof(line), f)) { + if(strstr(line, "gsr-ui virtual keyboard")) { + virtual_keyboard_running = true; break; } } - return 0; -} - -static int handle_events(struct libinput *libinput, key_mapper *mapper) { - int result = -1; - struct libinput_event *event; - - if(libinput_dispatch(libinput) < 0) - return result; - - while((event = libinput_get_event(libinput)) != NULL) { - if(libinput_event_get_type(event) == LIBINPUT_EVENT_KEYBOARD_KEY) - print_key_event(event, mapper); - - libinput_event_destroy(event); - result = 0; - } - - return result; + fclose(f); + return virtual_keyboard_running; } -static int run_mainloop(struct libinput *libinput, key_mapper *mapper) { - struct pollfd fds[2] = { - { - .fd = libinput_get_fd(libinput), - .events = POLLIN, - .revents = 0 - }, - { - .fd = STDOUT_FILENO, - .events = 0, - .revents = 0 +int main(int argc, char **argv) { + setlocale(LC_ALL, "C"); /* Sigh... stupid C */ + + 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; } - }; - - if(handle_events(libinput, mapper) != 0) { - fprintf(stderr, "error: didn't receive device added events. Is this program not running as root?\n"); - return -1; - } - - while(poll(fds, 2, -1) >= 0) { - if(fds[0].revents & POLLIN) - handle_events(libinput, mapper); - if(fds[1].revents & (POLLHUP|POLLERR)) - break; - } - - return 0; -} - -static bool mapper_refresh_keymap(key_mapper *mapper) { - if(mapper->xkb_keymap != NULL) { - xkb_keymap_unref(mapper->xkb_keymap); - mapper->xkb_keymap = NULL; - } - - // TODO: - struct xkb_rule_names names = { - NULL, NULL, - NULL,//keymap_is_default(mapper->layout) ? NULL : mapper->layout, - NULL,//keymap_is_default(mapper->variant) ? NULL : mapper->variant, - NULL - }; - mapper->xkb_keymap = xkb_keymap_new_from_names(mapper->xkb_context, &names, XKB_KEYMAP_COMPILE_NO_FLAGS); - if(mapper->xkb_keymap == NULL) { - fprintf(stderr, "error: failed to create XKB keymap.\n"); - return false; - } - - if(mapper->xkb_state != NULL) { - xkb_state_unref(mapper->xkb_state); - mapper->xkb_state = NULL; + } else if(argc != 1) { + fprintf(stderr, "Error: expected 0 or 1 arguments, got %d argument(s)\n", argc); + usage(); + return 1; } - mapper->xkb_state = xkb_state_new(mapper->xkb_keymap); - if(mapper->xkb_state == NULL) { - fprintf(stderr, "error: failed to create XKB state.\n"); - return false; + if(is_gsr_global_hotkeys_already_running()) { + fprintf(stderr, "Error: gsr-global-hotkeys is already running\n"); + return 1; } - return true; -} - -int main(void) { - int result = 0; - struct udev *udev = NULL; - struct libinput *libinput = NULL; - const uid_t user_id = getuid(); if(geteuid() != 0) { if(setuid(0) == -1) { - fprintf(stderr, "error: failed to change user to root\n"); + fprintf(stderr, "Error: failed to change user to root\n"); return 1; } } - udev = udev_new(); - if(!udev) { - fprintf(stderr, "error: udev_new failed\n"); - result = 1; - goto done; - } - - libinput = libinput_udev_create_context(&interface, NULL, udev); - if(!libinput) { - fprintf(stderr, "error: libinput_udev_create_context failed\n"); - result = 1; - goto done; - } - - if(libinput_udev_assign_seat(libinput, "seat0") != 0) { - fprintf(stderr, "error: libinput_udev_assign_seat with seat0 failed\n"); - result = 1; - goto done; + keyboard_event keyboard_ev; + if(!keyboard_event_init(&keyboard_ev, true, grab_type)) { + fprintf(stderr, "Error: failed to setup hotplugging and no keyboard input devices were found\n"); + setuid(user_id); + return 1; } - key_mapper mapper; - mapper.xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - if(!mapper.xkb_context) { - fprintf(stderr, "error: xkb_context_new failed\n"); - result = 1; - goto done; - } + fprintf(stderr, "Info: global hotkeys setup, waiting for hotkeys to be pressed\n"); - if(!mapper_refresh_keymap(&mapper)) { - fprintf(stderr, "error: key mapper failed\n"); - result = 1; - goto done; - } - - if(run_mainloop(libinput, &mapper) < 0) { - fprintf(stderr, "error: failed to start main loop\n"); - result = 1; - goto done; + for(;;) { + keyboard_event_poll_events(&keyboard_ev, -1); + if(keyboard_event_stdin_has_failed(&keyboard_ev)) { + fprintf(stderr, "Info: stdin closed (parent process likely closed this process), exiting...\n"); + break; + } } - done: - if(libinput) - libinput_unref(libinput); - - if(udev) - udev_unref(udev); - + keyboard_event_deinit(&keyboard_ev); setuid(user_id); - return result; + return 0; } diff --git a/tools/gsr-ui-cli/main.c b/tools/gsr-ui-cli/main.c new file mode 100644 index 0000000..bcb5c81 --- /dev/null +++ b/tools/gsr-ui-cli/main.c @@ -0,0 +1,105 @@ +#include <limits.h> +#include <stdio.h> +#include <stddef.h> +#include <stdlib.h> +#include <stdbool.h> +#include <string.h> +#include <unistd.h> +#include <fcntl.h> + +static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) { + char dir[PATH_MAX]; + + const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + if(runtime_dir) + snprintf(dir, sizeof(dir), "%s", runtime_dir); + else + snprintf(dir, sizeof(dir), "/run/user/%d", geteuid()); + + if(access(dir, F_OK) != 0) + snprintf(dir, sizeof(dir), "/tmp"); + + snprintf(buffer, buffer_size, "%s/%s", dir, filename); +} + +/* Assumes |str| size is less than 256 */ +static void fifo_write_all(int file_fd, const char *str) { + char command[256]; + const ssize_t command_size = snprintf(command, sizeof(command), "%s\n", str); + if(command_size >= (ssize_t)sizeof(command)) { + fprintf(stderr, "Error: command too long: %s\n", str); + return; + } + + ssize_t offset = 0; + while(offset < (ssize_t)command_size) { + const ssize_t bytes_written = write(file_fd, str + offset, command_size - offset); + if(bytes_written > 0) + offset += bytes_written; + } +} + +static void usage(void) { + printf("usage: gsr-ui-cli <command>\n"); + printf("Run commands on the running gsr-ui instance.\n"); + printf("\n"); + printf("COMMANDS:\n"); + printf(" toggle-show Show/hide the UI.\n"); + printf(" toggle-record Start/stop recording.\n"); + printf(" toggle-pause Pause/unpause recording. Only applies to regular recording.\n"); + 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"); + exit(1); +} + +static bool is_valid_command(const char *command) { + const char *commands[] = { + "toggle-show", + "toggle-record", + "toggle-pause", + "toggle-stream", + "toggle-replay", + "replay-save", + NULL + }; + + for(int i = 0; commands[i]; ++i) { + if(strcmp(command, commands[i]) == 0) + return true; + } + + return false; +} + +int main(int argc, char **argv) { + if(argc != 2) { + printf("Error: expected 1 argument, %d provided\n", argc - 1); + usage(); + } + + const char *command = argv[1]; + if(strcmp(command, "-h") == 0 || strcmp(command, "--help") == 0) + usage(); + + if(!is_valid_command(command)) { + fprintf(stderr, "Error: invalid command: \"%s\"\n", command); + usage(); + } + + char fifo_filepath[PATH_MAX]; + get_runtime_filepath(fifo_filepath, sizeof(fifo_filepath), "gsr-ui"); + const int fifo_fd = open(fifo_filepath, O_RDWR | O_NONBLOCK); + if(fifo_fd <= 0) { + fprintf(stderr, "Error: failed to open fifo file %s. Maybe gsr-ui is not running?\n", fifo_filepath); + exit(2); + } + + fifo_write_all(fifo_fd, command); + close(fifo_fd); + return 0; +} diff --git a/tools/gsr-window-name/main.c b/tools/gsr-window-name/main.c deleted file mode 100644 index 8ebf1e0..0000000 --- a/tools/gsr-window-name/main.c +++ /dev/null @@ -1,187 +0,0 @@ -#include <X11/Xlib.h> -#include <X11/Xatom.h> -#include <X11/Xutil.h> - -#include <stdbool.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - -typedef enum { - CAPTURE_TYPE_FOCUSED, - CAPTURE_TYPE_CURSOR -} capture_type; - -static bool window_has_atom(Display *dpy, Window window, Atom atom) { - Atom type; - unsigned long len, bytes_left; - int format; - unsigned char *properties = NULL; - if(XGetWindowProperty(dpy, window, atom, 0, 1024, False, AnyPropertyType, &type, &format, &len, &bytes_left, &properties) < Success) - return false; - - if(properties) - XFree(properties); - - return type != None; -} - -static bool window_is_user_program(Display *dpy, Window window) { - const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False); - const Atom wm_state_atom = XInternAtom(dpy, "WM_STATE", False); - return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom); -} - -static Window get_window_at_cursor_position(Display *dpy) { - Window root_window = None; - Window window = None; - int dummy_i; - unsigned int dummy_u; - int cursor_pos_x = 0; - int cursor_pos_y = 0; - XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &dummy_i, &dummy_i, &cursor_pos_x, &cursor_pos_y, &dummy_u); - return window; -} - -static Window get_focused_window(Display *dpy, capture_type cap_type) { - const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); - Window focused_window = None; - - if(cap_type == CAPTURE_TYPE_FOCUSED) { - // Atom type = None; - // int format = 0; - // unsigned long num_items = 0; - // unsigned long bytes_left = 0; - // unsigned char *data = NULL; - // XGetWindowProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, 0, 1, False, XA_WINDOW, &type, &format, &num_items, &bytes_left, &data); - - // fprintf(stderr, "focused window: %p\n", (void*)data); - - // if(type == XA_WINDOW && num_items == 1 && data) - // return *(Window*)data; - - int revert_to = 0; - XGetInputFocus(dpy, &focused_window, &revert_to); - if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window)) - return focused_window; - } - - focused_window = get_window_at_cursor_position(dpy); - if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window)) - return focused_window; - - return None; -} - -static char* 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, "_NET_WM_NAME", False); - const Atom utf8_string_atom = XInternAtom(dpy, "UTF8_STRING", False); - - Atom type = None; - int format = 0; - unsigned long num_items = 0; - unsigned long bytes_left = 0; - unsigned char *data = NULL; - 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; - - type = None; - format = 0; - num_items = 0; - bytes_left = 0; - data = NULL; - 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 NULL; -} - -static const char* strip(const char *str, int *len) { - int str_len = strlen(str); - for(int i = 0; i < str_len; ++i) { - if(str[i] != ' ') { - str += i; - str_len -= i; - break; - } - } - - for(int i = str_len - 1; i >= 0; --i) { - if(str[i] != ' ') { - str_len = i + 1; - break; - } - } - - *len = str_len; - return str; -} - -static void print_str_strip(const char *str) { - int len = 0; - str = strip(str, &len); - printf("%.*s", len, str); -} - -static int x11_ignore_error(Display *dpy, XErrorEvent *error_event) { - (void)dpy; - (void)error_event; - return 0; -} - -static void usage(void) { - fprintf(stderr, "usage: gsr-window-name <focused|cursor>\n"); - fprintf(stderr, "options:\n"); - fprintf(stderr, " focused The class/name of the focused window is returned. If no window is focused then the window beneath the cursor is returned instead\n"); - fprintf(stderr, " cursor The class/name of the window beneath the cursor is returned\n"); - exit(1); -} - -int main(int argc, char **argv) { - if(argc != 2) - usage(); - - const char *cap_type_str = argv[1]; - capture_type cap_type = CAPTURE_TYPE_FOCUSED; - if(strcmp(cap_type_str, "focused") == 0) { - cap_type = CAPTURE_TYPE_FOCUSED; - } else if(strcmp(cap_type_str, "cursor") == 0) { - cap_type = CAPTURE_TYPE_CURSOR; - } else { - fprintf(stderr, "error: invalid option '%s', expected either 'focused' or 'cursor'\n", cap_type_str); - usage(); - } - - Display *dpy = XOpenDisplay(NULL); - if(!dpy) { - fprintf(stderr, "Error: failed to connect to the X11 server\n"); - exit(1); - } - - XSetErrorHandler(x11_ignore_error); - - const Window focused_window = get_focused_window(dpy, cap_type); - if(focused_window == None) - exit(2); - - // 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); - if(window_title) { - print_str_strip(window_title); - exit(0); - } - - XClassHint class_hint = {0}; - XGetClassHint(dpy, focused_window, &class_hint); - if(class_hint.res_class) { - print_str_strip(class_hint.res_class); - exit(0); - } - - return 2; -} |