mirror of
https://repo.dec05eba.com/gpu-screen-recorder-ui
synced 2026-04-09 04:44:52 +09:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57448f6579 | ||
|
|
4d7526d21e | ||
|
|
fded9b8d57 | ||
|
|
b80e3f8beb | ||
|
|
b807712d79 | ||
|
|
2df417f23f | ||
|
|
a82d1a2dfc | ||
|
|
043b6df255 | ||
|
|
831f583f89 |
@@ -14,7 +14,7 @@ A program called `gsr-ui-cli` is also installed when installing this software. T
|
||||
# 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.\
|
||||
You can also install gpu screen recorder 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.
|
||||
You can also install gpu screen recorder from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder) which includes this UI.
|
||||
|
||||
# Dependencies
|
||||
GPU Screen Recorder UI uses meson build system so you need to install `meson` to build GPU Screen Recorder UI.
|
||||
@@ -28,6 +28,7 @@ These are the dependencies needed to build GPU Screen Recorder UI:
|
||||
* libpulse (libpulse-simple)
|
||||
* libdrm
|
||||
* wayland (wayland-client, wayland-egl, wayland-scanner)
|
||||
* setcap (libcap)
|
||||
|
||||
## Runtime dependencies
|
||||
There are also additional dependencies needed at runtime:
|
||||
|
||||
14
TODO
14
TODO
@@ -12,8 +12,6 @@ Handle events in draw function because the render position of elements is availa
|
||||
|
||||
Add nvidia overclock option.
|
||||
|
||||
Add support for window selection in capture.
|
||||
|
||||
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.
|
||||
|
||||
Restart replay on system start if monitor resolution changes.
|
||||
@@ -37,8 +35,6 @@ Fix first frame being black when running without a compositor.
|
||||
|
||||
Add support for systray.
|
||||
|
||||
Add option to take screenshot.
|
||||
|
||||
Move event callbacks to a global list instead of std::function object in each widget. This reduces the size of widgets,
|
||||
since most widgets wont have the event callback set.
|
||||
This event callback would pass the widget as an argument.
|
||||
@@ -72,8 +68,6 @@ Run `systemctl status --user gpu-screen-recorder` when starting recording and gi
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Support CJK.
|
||||
@@ -98,9 +92,6 @@ Make gsr-ui flatpak systemd work nicely with non-flatpak gsr-ui. Maybe change Ex
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -118,9 +109,6 @@ When clicking on current directory in file manager show a dropdown menu where yo
|
||||
|
||||
Maybe change gsr-ui startup retry time in the systemd service, from 5 seconds to 2 seconds.
|
||||
|
||||
Add support for window capture. This should not prompt for window selection directly but instead prompt for window selection when recording starts and hide the ui first.
|
||||
For screenshots window capture should exist but "follow focused" option should not exist.
|
||||
|
||||
Make it possible to take a screenshot through a button in the ui instead of having to use hotkey.
|
||||
|
||||
Handle failing to save a replay. gsr should output "failed to save replay, or something like that" to make it possible to detect that.
|
||||
@@ -141,8 +129,6 @@ Make inactive buttons gray (in dropdown boxes and in the front page with save, e
|
||||
|
||||
Add option to do screen-direct recording. But make it clear that it should not be used, except for gsync on x11 nvidia.
|
||||
|
||||
Add window capture option (for x11).
|
||||
|
||||
Add systray for recording status.
|
||||
|
||||
Add a desktop icon when gsr-ui has a window mode option (which should be the default launch option).
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace gsr {
|
||||
int32_t video_width = 0;
|
||||
int32_t video_height = 0;
|
||||
int32_t fps = 60;
|
||||
int32_t video_bitrate = 15000;
|
||||
int32_t video_bitrate = 8000;
|
||||
bool merge_audio_tracks = true; // TODO: Remove in the future
|
||||
bool application_audio_invert = false; // TODO: Remove in the future
|
||||
bool change_video_resolution = false;
|
||||
@@ -55,7 +55,7 @@ namespace gsr {
|
||||
std::string video_quality = "very_high";
|
||||
std::string video_codec = "auto";
|
||||
std::string audio_codec = "opus";
|
||||
std::string framerate_mode = "vfr";
|
||||
std::string framerate_mode = "auto";
|
||||
bool advanced_view = false;
|
||||
bool overclock = false;
|
||||
bool record_cursor = true;
|
||||
@@ -79,6 +79,10 @@ namespace gsr {
|
||||
std::string stream_key;
|
||||
};
|
||||
|
||||
struct RumbleStreamConfig {
|
||||
std::string stream_key;
|
||||
};
|
||||
|
||||
struct CustomStreamConfig {
|
||||
std::string url;
|
||||
std::string container = "flv";
|
||||
@@ -91,6 +95,7 @@ namespace gsr {
|
||||
std::string streaming_service = "twitch";
|
||||
YoutubeStreamConfig youtube;
|
||||
TwitchStreamConfig twitch;
|
||||
RumbleStreamConfig rumble;
|
||||
CustomStreamConfig custom;
|
||||
ConfigHotkey start_stop_hotkey;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "GlobalHotkeys/GlobalHotkeysJoystick.hpp"
|
||||
#include "AudioPlayer.hpp"
|
||||
#include "RegionSelector.hpp"
|
||||
#include "WindowSelector.hpp"
|
||||
#include "CursorTracker/CursorTracker.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
@@ -116,10 +117,10 @@ namespace gsr {
|
||||
void on_press_save_replay();
|
||||
void on_press_save_replay_1_min_replay();
|
||||
void on_press_save_replay_10_min_replay();
|
||||
bool on_press_start_replay(bool disable_notification, bool finished_region_selection);
|
||||
void on_press_start_record(bool finished_region_selection);
|
||||
void on_press_start_stream(bool finished_region_selection);
|
||||
void on_press_take_screenshot(bool finished_region_selection, bool force_region_capture);
|
||||
bool on_press_start_replay(bool disable_notification, bool finished_selection);
|
||||
void on_press_start_record(bool finished_selection);
|
||||
void on_press_start_stream(bool finished_selection);
|
||||
void on_press_take_screenshot(bool finished_selection, bool force_region_capture);
|
||||
bool update_compositor_texture(const Monitor &monitor);
|
||||
|
||||
std::string get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options);
|
||||
@@ -213,10 +214,15 @@ namespace gsr {
|
||||
int replay_save_duration_min = 0;
|
||||
|
||||
AudioPlayer audio_player;
|
||||
|
||||
RegionSelector region_selector;
|
||||
bool start_region_capture = false;
|
||||
std::function<void()> on_region_selected;
|
||||
|
||||
WindowSelector window_selector;
|
||||
bool start_window_capture = false;
|
||||
std::function<void()> on_window_selected;
|
||||
|
||||
std::string recording_capture_target;
|
||||
std::string screenshot_capture_target;
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace gsr {
|
||||
|
||||
bool failed() const;
|
||||
bool poll_events();
|
||||
bool is_selected() const;
|
||||
bool take_selection();
|
||||
bool take_canceled();
|
||||
Region get_selection() const;
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace gsr {
|
||||
void string_split_char(std::string_view str, char delimiter, StringSplitCallback callback_func);
|
||||
bool starts_with(std::string_view str, const char *substr);
|
||||
bool ends_with(std::string_view str, const char *substr);
|
||||
std::string strip(const std::string &str);
|
||||
|
||||
std::string get_home_dir();
|
||||
std::string get_config_dir();
|
||||
|
||||
33
include/WindowSelector.hpp
Normal file
33
include/WindowSelector.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
#include <mglpp/graphics/Color.hpp>
|
||||
|
||||
namespace gsr {
|
||||
class WindowSelector {
|
||||
public:
|
||||
WindowSelector();
|
||||
WindowSelector(const WindowSelector&) = delete;
|
||||
WindowSelector& operator=(const WindowSelector&) = delete;
|
||||
~WindowSelector();
|
||||
|
||||
bool start(mgl::Color border_color);
|
||||
void stop();
|
||||
bool is_started() const;
|
||||
|
||||
bool failed() const;
|
||||
bool poll_events();
|
||||
bool take_selection();
|
||||
bool take_canceled();
|
||||
Window get_selection() const;
|
||||
private:
|
||||
Display *dpy = nullptr;
|
||||
Cursor crosshair_cursor = None;
|
||||
Colormap border_window_colormap = None;
|
||||
Window border_window = None;
|
||||
Window selected_window = None;
|
||||
bool selected = false;
|
||||
bool canceled = false;
|
||||
};
|
||||
}
|
||||
@@ -24,6 +24,7 @@ namespace gsr {
|
||||
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);
|
||||
void set_window_size_not_resizable(Display *dpy, Window window, int width, int height);
|
||||
Window window_get_target_window_child(Display *display, Window 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);
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace gsr {
|
||||
private:
|
||||
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_image_width_entry();
|
||||
std::unique_ptr<Entry> create_image_height_entry();
|
||||
std::unique_ptr<List> create_image_resolution();
|
||||
@@ -56,7 +55,6 @@ namespace gsr {
|
||||
|
||||
GsrPage *content_page_ptr = nullptr;
|
||||
ScrollablePage *settings_scrollable_page_ptr = nullptr;
|
||||
List *select_window_list_ptr = nullptr;
|
||||
List *image_resolution_list_ptr = nullptr;
|
||||
List *restore_portal_session_list_ptr = nullptr;
|
||||
List *color_range_list_ptr = nullptr;
|
||||
|
||||
@@ -46,7 +46,6 @@ namespace gsr {
|
||||
std::unique_ptr<RadioButton> create_view_radio_button();
|
||||
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();
|
||||
std::unique_ptr<List> create_area_size();
|
||||
@@ -147,7 +146,6 @@ namespace gsr {
|
||||
GsrPage *content_page_ptr = nullptr;
|
||||
ScrollablePage *settings_scrollable_page_ptr = nullptr;
|
||||
List *settings_list_ptr = nullptr;
|
||||
List *select_window_list_ptr = nullptr;
|
||||
List *area_size_list_ptr = nullptr;
|
||||
List *video_resolution_list_ptr = nullptr;
|
||||
List *restore_portal_session_list_ptr = nullptr;
|
||||
@@ -192,6 +190,7 @@ namespace gsr {
|
||||
Button *save_directory_button_ptr = nullptr;
|
||||
Entry *twitch_stream_key_entry_ptr = nullptr;
|
||||
Entry *youtube_stream_key_entry_ptr = nullptr;
|
||||
Entry *rumble_stream_key_entry_ptr = nullptr;
|
||||
Entry *stream_url_entry_ptr = nullptr;
|
||||
Entry *replay_time_entry_ptr = nullptr;
|
||||
RadioButton *replay_storage_button_ptr = nullptr;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.6.4', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.6.6', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
|
||||
|
||||
if get_option('buildtype') == 'debug'
|
||||
add_project_arguments('-g3', language : ['c', 'cpp'])
|
||||
@@ -40,6 +40,7 @@ src = [
|
||||
'src/Utils.cpp',
|
||||
'src/WindowUtils.cpp',
|
||||
'src/RegionSelector.cpp',
|
||||
'src/WindowSelector.cpp',
|
||||
'src/Config.cpp',
|
||||
'src/GsrInfo.cpp',
|
||||
'src/Process.cpp',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "gsr-ui"
|
||||
type = "executable"
|
||||
version = "1.6.4"
|
||||
version = "1.6.6"
|
||||
platforms = ["posix"]
|
||||
|
||||
[lang.cpp]
|
||||
|
||||
@@ -119,7 +119,7 @@ namespace gsr {
|
||||
|
||||
streaming_config.record_options.video_quality = "custom";
|
||||
streaming_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false});
|
||||
streaming_config.record_options.video_bitrate = 15000;
|
||||
streaming_config.record_options.video_bitrate = 8000;
|
||||
|
||||
record_config.save_directory = default_videos_save_directory;
|
||||
record_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false});
|
||||
@@ -201,6 +201,7 @@ namespace gsr {
|
||||
{"streaming.service", &config.streaming_config.streaming_service},
|
||||
{"streaming.youtube.key", &config.streaming_config.youtube.stream_key},
|
||||
{"streaming.twitch.key", &config.streaming_config.twitch.stream_key},
|
||||
{"streaming.rumble.key", &config.streaming_config.rumble.stream_key},
|
||||
{"streaming.custom.url", &config.streaming_config.custom.url},
|
||||
{"streaming.custom.container", &config.streaming_config.custom.container},
|
||||
{"streaming.start_stop_hotkey", &config.streaming_config.start_stop_hotkey},
|
||||
|
||||
@@ -9,14 +9,8 @@
|
||||
|
||||
namespace gsr {
|
||||
static const int MAX_CONNECTORS = 32;
|
||||
static const int CONNECTOR_TYPE_COUNTS = 32;
|
||||
static const uint32_t plane_property_all = 0xF;
|
||||
|
||||
typedef struct {
|
||||
int type;
|
||||
int count;
|
||||
} drm_connector_type_count;
|
||||
|
||||
typedef enum {
|
||||
PLANE_PROPERTY_CRTC_X = 1 << 0,
|
||||
PLANE_PROPERTY_CRTC_Y = 1 << 1,
|
||||
@@ -105,22 +99,6 @@ namespace gsr {
|
||||
return get_drm_property_by_name(drm_fd, &properties, name, result);
|
||||
}
|
||||
|
||||
static drm_connector_type_count* drm_connector_types_get_index(drm_connector_type_count *type_counts, int *num_type_counts, int connector_type) {
|
||||
for(int i = 0; i < *num_type_counts; ++i) {
|
||||
if(type_counts[i].type == connector_type)
|
||||
return &type_counts[i];
|
||||
}
|
||||
|
||||
if(*num_type_counts == CONNECTOR_TYPE_COUNTS)
|
||||
return NULL;
|
||||
|
||||
const int index = *num_type_counts;
|
||||
type_counts[index].type = connector_type;
|
||||
type_counts[index].count = 0;
|
||||
++*num_type_counts;
|
||||
return &type_counts[index];
|
||||
}
|
||||
|
||||
// Note: this monitor name logic is kept in sync with gpu screen recorder
|
||||
static std::string get_monitor_name_from_crtc_id(int drm_fd, uint32_t crtc_id) {
|
||||
std::string result;
|
||||
@@ -128,27 +106,23 @@ namespace gsr {
|
||||
if(!resources)
|
||||
return result;
|
||||
|
||||
drm_connector_type_count type_counts[CONNECTOR_TYPE_COUNTS];
|
||||
int num_type_counts = 0;
|
||||
|
||||
for(int i = 0; i < resources->count_connectors; ++i) {
|
||||
uint64_t connector_crtc_id = 0;
|
||||
drmModeConnectorPtr connector = drmModeGetConnectorCurrent(drm_fd, resources->connectors[i]);
|
||||
if(!connector)
|
||||
continue;
|
||||
|
||||
drm_connector_type_count *connector_type = drm_connector_types_get_index(type_counts, &num_type_counts, connector->connector_type);
|
||||
const char *connection_name = drmModeGetConnectorTypeName(connector->connector_type);
|
||||
if(connector_type)
|
||||
++connector_type->count;
|
||||
if(!connection_name)
|
||||
goto next;
|
||||
|
||||
if(connector->connection != DRM_MODE_CONNECTED)
|
||||
goto next;
|
||||
|
||||
if(connector_type && connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &connector_crtc_id) && connector_crtc_id == crtc_id) {
|
||||
if(connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &connector_crtc_id) && connector_crtc_id == crtc_id) {
|
||||
result = connection_name;
|
||||
result += "-";
|
||||
result += std::to_string(connector_type->count);
|
||||
result += std::to_string(connector->connector_type_id);
|
||||
drmModeFreeConnector(connector);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,68 @@ namespace gsr {
|
||||
static constexpr int axis_up_down = 7;
|
||||
static constexpr int axis_left_right = 6;
|
||||
|
||||
struct DeviceId {
|
||||
uint16_t vendor;
|
||||
uint16_t product;
|
||||
};
|
||||
|
||||
static bool read_file_hex_number(const char *path, unsigned int *value) {
|
||||
*value = 0;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if(!f)
|
||||
return false;
|
||||
|
||||
fscanf(f, "%x", value);
|
||||
fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
static DeviceId joystick_get_device_id(const char *path) {
|
||||
DeviceId device_id;
|
||||
device_id.vendor = 0;
|
||||
device_id.product = 0;
|
||||
|
||||
const char *js_path_id = nullptr;
|
||||
const int len = strlen(path);
|
||||
for(int i = len - 1; i >= 0; --i) {
|
||||
if(path[i] == '/') {
|
||||
js_path_id = path + i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!js_path_id)
|
||||
return device_id;
|
||||
|
||||
unsigned int vendor = 0;
|
||||
unsigned int product = 0;
|
||||
char path_buf[1024];
|
||||
|
||||
snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/vendor", js_path_id);
|
||||
if(!read_file_hex_number(path_buf, &vendor))
|
||||
return device_id;
|
||||
|
||||
snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/product", js_path_id);
|
||||
if(!read_file_hex_number(path_buf, &product))
|
||||
return device_id;
|
||||
|
||||
device_id.vendor = vendor;
|
||||
device_id.product = product;
|
||||
return device_id;
|
||||
}
|
||||
|
||||
static bool is_ps4_controller(DeviceId device_id) {
|
||||
return device_id.vendor == 0x054C && (device_id.product == 0x09CC || device_id.product == 0x0BA0 || device_id.product == 0x05C4);
|
||||
}
|
||||
|
||||
static bool is_ps5_controller(DeviceId device_id) {
|
||||
return device_id.vendor == 0x054C && (device_id.product == 0x0DF2 || device_id.product == 0x0CE6);
|
||||
}
|
||||
|
||||
static bool is_stadia_controller(DeviceId device_id) {
|
||||
return device_id.vendor == 0x18D1 && (device_id.product == 0x9400);
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -276,6 +338,8 @@ namespace gsr {
|
||||
dev_input_id
|
||||
};
|
||||
|
||||
//const DeviceId device_id = joystick_get_device_id(dev_input_filepath);
|
||||
|
||||
++num_poll_fd;
|
||||
fprintf(stderr, "Info: added joystick: %s\n", dev_input_filepath);
|
||||
return true;
|
||||
|
||||
212
src/Overlay.cpp
212
src/Overlay.cpp
@@ -26,6 +26,7 @@
|
||||
#include <malloc.h>
|
||||
#include <stdexcept>
|
||||
#include <algorithm>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
@@ -37,6 +38,7 @@
|
||||
#include <X11/Xcursor/Xcursor.h>
|
||||
#include <mglpp/system/Rect.hpp>
|
||||
#include <mglpp/window/Event.hpp>
|
||||
#include <mglpp/system/Utf8.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <mgl/mgl.h>
|
||||
@@ -677,6 +679,22 @@ namespace gsr {
|
||||
on_region_selected = nullptr;
|
||||
}
|
||||
|
||||
window_selector.poll_events();
|
||||
if(window_selector.take_canceled()) {
|
||||
on_window_selected = nullptr;
|
||||
} else if(window_selector.take_selection() && on_window_selected) {
|
||||
mgl_context *context = mgl_get_context();
|
||||
Display *display = (Display*)context->connection;
|
||||
|
||||
const Window selected_window = window_selector.get_selection();
|
||||
if(selected_window && selected_window != DefaultRootWindow(display)) {
|
||||
on_window_selected();
|
||||
} else {
|
||||
show_notification("No window selected", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
|
||||
}
|
||||
on_window_selected = nullptr;
|
||||
}
|
||||
|
||||
if(!visible || !window)
|
||||
return;
|
||||
|
||||
@@ -722,7 +740,16 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
if(region_selector.is_started()) {
|
||||
if(start_window_capture) {
|
||||
start_window_capture = false;
|
||||
hide();
|
||||
if(!window_selector.start(get_color_theme().tint_color)) {
|
||||
show_notification("Failed to start window capture", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE);
|
||||
on_window_selected = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if(region_selector.is_started() || window_selector.is_started()) {
|
||||
usleep(5 * 1000); // 5 ms
|
||||
return true;
|
||||
}
|
||||
@@ -856,7 +883,7 @@ namespace gsr {
|
||||
if(visible)
|
||||
return;
|
||||
|
||||
if(region_selector.is_started())
|
||||
if(region_selector.is_started() || window_selector.is_started())
|
||||
return;
|
||||
|
||||
drawn_first_frame = false;
|
||||
@@ -903,6 +930,7 @@ namespace gsr {
|
||||
// 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.
|
||||
// TODO: (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor))
|
||||
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots;
|
||||
|
||||
if(prevent_game_minimizing) {
|
||||
@@ -1311,6 +1339,7 @@ namespace gsr {
|
||||
visible = false;
|
||||
drawn_first_frame = false;
|
||||
start_region_capture = false;
|
||||
start_window_capture = false;
|
||||
|
||||
if(xi_input_xev) {
|
||||
free(xi_input_xev);
|
||||
@@ -1433,6 +1462,24 @@ namespace gsr {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void truncate_string(std::string &str, int max_length) {
|
||||
int index = 0;
|
||||
size_t byte_index = 0;
|
||||
|
||||
while(index < max_length && byte_index < str.size()) {
|
||||
uint32_t codepoint = 0;
|
||||
size_t codepoint_length = 0;
|
||||
mgl::utf8_decode((const unsigned char*)str.c_str() + byte_index, str.size() - byte_index, &codepoint, &codepoint_length);
|
||||
if(codepoint_length == 0)
|
||||
codepoint_length = 1;
|
||||
|
||||
index += 1;
|
||||
byte_index += codepoint_length;
|
||||
}
|
||||
|
||||
str.erase(byte_index);
|
||||
}
|
||||
|
||||
static bool is_hex_num(char c) {
|
||||
return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9');
|
||||
}
|
||||
@@ -1460,8 +1507,44 @@ namespace gsr {
|
||||
return is_hex && !hex_start;
|
||||
}
|
||||
|
||||
static bool is_number(const char *str) {
|
||||
const char *p = str;
|
||||
while(*p) {
|
||||
char c = *p;
|
||||
if(c < '0' || c > '9')
|
||||
return false;
|
||||
++p;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool is_capture_target_monitor(const char *capture_target) {
|
||||
return strcmp(capture_target, "focused") != 0 && strcmp(capture_target, "region") != 0 && strcmp(capture_target, "portal") != 0 && contains_non_hex_number(capture_target);
|
||||
return strcmp(capture_target, "window") != 0 && strcmp(capture_target, "focused") != 0 && strcmp(capture_target, "region") != 0 && strcmp(capture_target, "portal") != 0 && contains_non_hex_number(capture_target);
|
||||
}
|
||||
|
||||
static std::string capture_target_get_notification_name(const char *capture_target) {
|
||||
std::string result;
|
||||
if(is_capture_target_monitor(capture_target)) {
|
||||
result = "this monitor";
|
||||
} else if(is_number(capture_target)) {
|
||||
mgl_context *context = mgl_get_context();
|
||||
Display *display = (Display*)context->connection;
|
||||
|
||||
int64_t window_id = None;
|
||||
sscanf(capture_target, "%" PRIi64, &window_id);
|
||||
|
||||
const std::optional<std::string> window_title = get_window_title(display, window_id);
|
||||
if(window_title) {
|
||||
result = strip(window_title.value());
|
||||
truncate_string(result, 20);
|
||||
result = "window \"" + result + "\"";
|
||||
} else {
|
||||
result = std::string("window ") + capture_target;
|
||||
}
|
||||
} else {
|
||||
result = capture_target;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string get_valid_monitor_x11(const std::string &target_monitor_name, const std::vector<Monitor> &monitors) {
|
||||
@@ -1640,11 +1723,6 @@ namespace gsr {
|
||||
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;
|
||||
@@ -1674,11 +1752,7 @@ namespace gsr {
|
||||
if(!config.record_config.show_video_saved_notifications)
|
||||
return;
|
||||
|
||||
if(is_capture_target_monitor(recording_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Saved a recording of this monitor to \"%s\"", focused_window_name.c_str());
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Saved a recording of %s to \"%s\"", recording_capture_target.c_str(), focused_window_name.c_str());
|
||||
|
||||
snprintf(msg, sizeof(msg), "Saved a recording of %s to \"%s\"", capture_target_get_notification_name(recording_capture_target.c_str()).c_str(), focused_window_name.c_str());
|
||||
capture_target = recording_capture_target.c_str();
|
||||
break;
|
||||
}
|
||||
@@ -1692,11 +1766,7 @@ namespace gsr {
|
||||
else
|
||||
snprintf(duration, sizeof(duration), " ");
|
||||
|
||||
if(is_capture_target_monitor(recording_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Saved a%sreplay of this monitor to \"%s\"", duration, focused_window_name.c_str());
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Saved a%sreplay of %s to \"%s\"", duration, recording_capture_target.c_str(), focused_window_name.c_str());
|
||||
|
||||
snprintf(msg, sizeof(msg), "Saved a%sreplay of %s to \"%s\"", duration, capture_target_get_notification_name(recording_capture_target.c_str()).c_str(), focused_window_name.c_str());
|
||||
capture_target = recording_capture_target.c_str();
|
||||
break;
|
||||
}
|
||||
@@ -1704,11 +1774,7 @@ namespace gsr {
|
||||
if(!config.screenshot_config.show_screenshot_saved_notifications)
|
||||
return;
|
||||
|
||||
if(is_capture_target_monitor(screenshot_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of this monitor to \"%s\"", focused_window_name.c_str());
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of %s to \"%s\"", screenshot_capture_target.c_str(), focused_window_name.c_str());
|
||||
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of %s to \"%s\"", capture_target_get_notification_name(screenshot_capture_target.c_str()).c_str(), focused_window_name.c_str());
|
||||
capture_target = screenshot_capture_target.c_str();
|
||||
break;
|
||||
}
|
||||
@@ -1741,10 +1807,7 @@ namespace gsr {
|
||||
snprintf(duration, sizeof(duration), " ");
|
||||
|
||||
char msg[512];
|
||||
if(is_capture_target_monitor(recording_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Saved a%sreplay of this monitor", duration);
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Saved a%sreplay of %s", duration, recording_capture_target.c_str());
|
||||
snprintf(msg, sizeof(msg), "Saved a%sreplay of %s", duration, capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
|
||||
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str());
|
||||
}
|
||||
}
|
||||
@@ -1865,10 +1928,7 @@ namespace gsr {
|
||||
save_video_in_current_game_directory(screenshot_filepath.c_str(), NotificationType::SCREENSHOT);
|
||||
} else if(config.screenshot_config.show_screenshot_saved_notifications) {
|
||||
char msg[512];
|
||||
if(is_capture_target_monitor(screenshot_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of this monitor");
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of %s", screenshot_capture_target.c_str());
|
||||
snprintf(msg, sizeof(msg), "Saved a screenshot of %s", capture_target_get_notification_name(screenshot_capture_target.c_str()).c_str());
|
||||
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::SCREENSHOT, screenshot_capture_target.c_str());
|
||||
}
|
||||
} else {
|
||||
@@ -1967,10 +2027,7 @@ namespace gsr {
|
||||
save_video_in_current_game_directory(video_filepath.c_str(), NotificationType::RECORD);
|
||||
} else if(config.record_config.show_video_saved_notifications) {
|
||||
char msg[512];
|
||||
if(is_capture_target_monitor(recording_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Saved a recording of this monitor");
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Saved a recording of %s", recording_capture_target.c_str());
|
||||
snprintf(msg, sizeof(msg), "Saved a recording of %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
|
||||
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
|
||||
}
|
||||
} else {
|
||||
@@ -2163,11 +2220,12 @@ namespace gsr {
|
||||
}
|
||||
|
||||
static bool validate_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) {
|
||||
// TODO: Also check x11 window when enabled (check if capture_target is a decminal/hex number)
|
||||
if(capture_target == "region") {
|
||||
return capture_options.region;
|
||||
if(capture_target == "window") {
|
||||
return capture_options.window;
|
||||
} else if(capture_target == "focused") {
|
||||
return capture_options.focused;
|
||||
} else if(capture_target == "region") {
|
||||
return capture_options.region;
|
||||
} else if(capture_target == "portal") {
|
||||
return capture_options.portal;
|
||||
} else if(capture_target == "focused_monitor") {
|
||||
@@ -2199,7 +2257,9 @@ namespace gsr {
|
||||
}
|
||||
|
||||
std::string Overlay::get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) {
|
||||
if(capture_target == "focused_monitor") {
|
||||
if(capture_target == "window") {
|
||||
return std::to_string(window_selector.get_selection());
|
||||
} else if(capture_target == "focused_monitor") {
|
||||
std::optional<CursorInfo> cursor_info;
|
||||
if(cursor_tracker) {
|
||||
cursor_tracker->update();
|
||||
@@ -2268,8 +2328,8 @@ namespace gsr {
|
||||
kill(gpu_screen_recorder_process, SIGRTMIN+5);
|
||||
}
|
||||
|
||||
bool Overlay::on_press_start_replay(bool disable_notification, bool finished_region_selection) {
|
||||
if(region_selector.is_started())
|
||||
bool Overlay::on_press_start_replay(bool disable_notification, bool finished_selection) {
|
||||
if(region_selector.is_started() || window_selector.is_started())
|
||||
return false;
|
||||
|
||||
switch(recording_status) {
|
||||
@@ -2319,7 +2379,7 @@ namespace gsr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(config.replay_config.record_options.record_area_option == "region" && !finished_region_selection) {
|
||||
if(config.replay_config.record_options.record_area_option == "region" && !finished_selection) {
|
||||
start_region_capture = true;
|
||||
on_region_selected = [disable_notification, this]() {
|
||||
on_press_start_replay(disable_notification, true);
|
||||
@@ -2327,6 +2387,14 @@ namespace gsr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(config.replay_config.record_options.record_area_option == "window" && !finished_selection) {
|
||||
start_window_capture = true;
|
||||
on_window_selected = [disable_notification, this]() {
|
||||
on_press_start_replay(disable_notification, true);
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -2406,18 +2474,15 @@ namespace gsr {
|
||||
// to see when the program has exit.
|
||||
if(!disable_notification && config.replay_config.show_replay_started_notifications) {
|
||||
char msg[256];
|
||||
if(is_capture_target_monitor(recording_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Started replaying this monitor");
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Started replaying %s", recording_capture_target.c_str());
|
||||
snprintf(msg, sizeof(msg), "Started replaying %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
|
||||
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Overlay::on_press_start_record(bool finished_region_selection) {
|
||||
if(region_selector.is_started())
|
||||
void Overlay::on_press_start_record(bool finished_selection) {
|
||||
if(region_selector.is_started() || window_selector.is_started())
|
||||
return;
|
||||
|
||||
switch(recording_status) {
|
||||
@@ -2493,7 +2558,7 @@ namespace gsr {
|
||||
return;
|
||||
}
|
||||
|
||||
if(config.record_config.record_options.record_area_option == "region" && !finished_region_selection) {
|
||||
if(config.record_config.record_options.record_area_option == "region" && !finished_selection) {
|
||||
start_region_capture = true;
|
||||
on_region_selected = [this]() {
|
||||
on_press_start_record(true);
|
||||
@@ -2501,6 +2566,14 @@ namespace gsr {
|
||||
return;
|
||||
}
|
||||
|
||||
if(config.record_config.record_options.record_area_option == "window" && !finished_selection) {
|
||||
start_window_capture = true;
|
||||
on_window_selected = [this]() {
|
||||
on_press_start_record(true);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
record_filepath.clear();
|
||||
|
||||
// TODO: Validate input, fallback to valid values
|
||||
@@ -2563,10 +2636,7 @@ namespace gsr {
|
||||
// 1...
|
||||
if(config.record_config.show_recording_started_notifications) {
|
||||
char msg[256];
|
||||
if(is_capture_target_monitor(recording_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Started recording this monitor");
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Started recording %s", recording_capture_target.c_str());
|
||||
snprintf(msg, sizeof(msg), "Started recording %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
|
||||
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
|
||||
}
|
||||
}
|
||||
@@ -2579,6 +2649,9 @@ namespace gsr {
|
||||
} else if(config.streaming_config.streaming_service == "youtube") {
|
||||
url += "rtmp://a.rtmp.youtube.com/live2/";
|
||||
url += config.streaming_config.youtube.stream_key;
|
||||
} else if(config.streaming_config.streaming_service == "rumble") {
|
||||
url += "rtmp://rtmp.rumble.com/live/";
|
||||
url += config.streaming_config.rumble.stream_key;
|
||||
} else if(config.streaming_config.streaming_service == "custom") {
|
||||
url = config.streaming_config.custom.url;
|
||||
if(url.size() >= 7 && strncmp(url.c_str(), "rtmp://", 7) == 0)
|
||||
@@ -2603,8 +2676,8 @@ namespace gsr {
|
||||
return url;
|
||||
}
|
||||
|
||||
void Overlay::on_press_start_stream(bool finished_region_selection) {
|
||||
if(region_selector.is_started())
|
||||
void Overlay::on_press_start_stream(bool finished_selection) {
|
||||
if(region_selector.is_started() || window_selector.is_started())
|
||||
return;
|
||||
|
||||
switch(recording_status) {
|
||||
@@ -2650,7 +2723,7 @@ namespace gsr {
|
||||
return;
|
||||
}
|
||||
|
||||
if(config.streaming_config.record_options.record_area_option == "region" && !finished_region_selection) {
|
||||
if(config.streaming_config.record_options.record_area_option == "region" && !finished_selection) {
|
||||
start_region_capture = true;
|
||||
on_region_selected = [this]() {
|
||||
on_press_start_stream(true);
|
||||
@@ -2658,6 +2731,14 @@ namespace gsr {
|
||||
return;
|
||||
}
|
||||
|
||||
if(config.streaming_config.record_options.record_area_option == "window" && !finished_selection) {
|
||||
start_window_capture = true;
|
||||
on_window_selected = [this]() {
|
||||
on_press_start_stream(true);
|
||||
};
|
||||
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);
|
||||
@@ -2733,16 +2814,13 @@ namespace gsr {
|
||||
// to see when the program has exit.
|
||||
if(config.streaming_config.show_streaming_started_notifications) {
|
||||
char msg[256];
|
||||
if(is_capture_target_monitor(recording_capture_target.c_str()))
|
||||
snprintf(msg, sizeof(msg), "Started streaming this monitor");
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Started streaming %s", recording_capture_target.c_str());
|
||||
snprintf(msg, sizeof(msg), "Started streaming %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str());
|
||||
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM, recording_capture_target.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Overlay::on_press_take_screenshot(bool finished_region_selection, bool force_region_capture) {
|
||||
if(region_selector.is_started())
|
||||
void Overlay::on_press_take_screenshot(bool finished_selection, bool force_region_capture) {
|
||||
if(region_selector.is_started() || window_selector.is_started())
|
||||
return;
|
||||
|
||||
if(gpu_screen_recorder_screenshot_process > 0) {
|
||||
@@ -2761,7 +2839,7 @@ namespace gsr {
|
||||
return;
|
||||
}
|
||||
|
||||
if(region_capture && !finished_region_selection) {
|
||||
if(region_capture && !finished_selection) {
|
||||
start_region_capture = true;
|
||||
on_region_selected = [this, force_region_capture]() {
|
||||
usleep(200 * 1000); // Hack: wait 0.2 seconds before taking a screenshot to allow user to move cursor away. TODO: Remove this
|
||||
@@ -2770,6 +2848,14 @@ namespace gsr {
|
||||
return;
|
||||
}
|
||||
|
||||
if(config.screenshot_config.record_area_option == "window" && !finished_selection) {
|
||||
start_window_capture = true;
|
||||
on_window_selected = [this, force_region_capture]() {
|
||||
on_press_take_screenshot(true, force_region_capture);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Validate input, fallback to valid values
|
||||
const std::string output_file = config.screenshot_config.save_directory + "/Screenshot_" + get_date_str() + "." + config.screenshot_config.image_format; // TODO: Validate image format
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ namespace gsr {
|
||||
window_attr.background_pixel = is_wayland ? 0 : border_color_x11;
|
||||
window_attr.border_pixel = 0;
|
||||
window_attr.override_redirect = true;
|
||||
window_attr.event_mask = StructureNotifyMask | PointerMotionMask;
|
||||
window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask;
|
||||
window_attr.colormap = region_window_colormap;
|
||||
|
||||
Screen *screen = XDefaultScreenOfDisplay(dpy);
|
||||
@@ -366,10 +366,6 @@ namespace gsr {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RegionSelector::is_selected() const {
|
||||
return selected;
|
||||
}
|
||||
|
||||
bool RegionSelector::take_selection() {
|
||||
const bool result = selected;
|
||||
selected = false;
|
||||
|
||||
@@ -32,6 +32,28 @@ namespace gsr {
|
||||
return str.size() >= len && memcmp(str.data() + str.size() - len, substr, len) == 0;
|
||||
}
|
||||
|
||||
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_home_dir() {
|
||||
const char *home_dir = getenv("HOME");
|
||||
if(!home_dir) {
|
||||
|
||||
229
src/WindowSelector.cpp
Normal file
229
src/WindowSelector.cpp
Normal file
@@ -0,0 +1,229 @@
|
||||
#include "../include/WindowSelector.hpp"
|
||||
#include "../include/WindowUtils.hpp"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <X11/extensions/shape.h>
|
||||
#include <X11/cursorfont.h>
|
||||
#include <X11/keysym.h>
|
||||
|
||||
namespace gsr {
|
||||
static const int rectangle_border_size = 2;
|
||||
|
||||
static int max_int(int a, int b) {
|
||||
return a >= b ? a : b;
|
||||
}
|
||||
|
||||
static void set_region_rectangle(Display *dpy, Window window, int x, int y, int width, int height, int border_size) {
|
||||
if(width < 0) {
|
||||
x += width;
|
||||
width = abs(width);
|
||||
}
|
||||
|
||||
if(height < 0) {
|
||||
y += height;
|
||||
height = abs(height);
|
||||
}
|
||||
|
||||
XRectangle rectangles[] = {
|
||||
{
|
||||
(short)max_int(0, x), (short)max_int(0, y),
|
||||
(unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
|
||||
}, // Left
|
||||
{
|
||||
(short)max_int(0, x + width - border_size), (short)max_int(0, y),
|
||||
(unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
|
||||
}, // Right
|
||||
{
|
||||
(short)max_int(0, x + border_size), (short)max_int(0, y),
|
||||
(unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
|
||||
}, // Top
|
||||
{
|
||||
(short)max_int(0, x + border_size), (short)max_int(0, y + height - border_size),
|
||||
(unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
|
||||
}, // Bottom
|
||||
};
|
||||
XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted);
|
||||
XFlush(dpy);
|
||||
}
|
||||
|
||||
static unsigned long mgl_color_to_x11_color(mgl::Color color) {
|
||||
if(color.a == 0)
|
||||
return 0;
|
||||
return ((uint32_t)color.a << 24) | (((uint32_t)color.r * color.a / 0xFF) << 16) | (((uint32_t)color.g * color.a / 0xFF) << 8) | ((uint32_t)color.b * color.a / 0xFF);
|
||||
}
|
||||
|
||||
static Window get_cursor_window(Display *dpy) {
|
||||
Window root_window = None;
|
||||
Window window = None;
|
||||
int dummy_i;
|
||||
unsigned int dummy_u;
|
||||
mgl::vec2i root_pos;
|
||||
XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u);
|
||||
return window;
|
||||
}
|
||||
|
||||
static void get_window_geometry(Display *dpy, Window window, mgl::vec2i &pos, mgl::vec2i &size) {
|
||||
Window root_window;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
unsigned int w = 0;
|
||||
unsigned int h = 0;
|
||||
unsigned int dummy_border, dummy_depth;
|
||||
XGetGeometry(dpy, window, &root_window, &x, &y, &w, &h, &dummy_border, &dummy_depth);
|
||||
pos.x = x;
|
||||
pos.y = y;
|
||||
size.x = w;
|
||||
size.y = h;
|
||||
}
|
||||
|
||||
WindowSelector::WindowSelector() {
|
||||
|
||||
}
|
||||
|
||||
WindowSelector::~WindowSelector() {
|
||||
stop();
|
||||
}
|
||||
|
||||
bool WindowSelector::start(mgl::Color border_color) {
|
||||
if(dpy)
|
||||
return false;
|
||||
|
||||
const unsigned long border_color_x11 = mgl_color_to_x11_color(border_color);
|
||||
dpy = XOpenDisplay(nullptr);
|
||||
if(!dpy) {
|
||||
fprintf(stderr, "Error: WindowSelector::start: failed to connect to the X11 server\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
const Window cursor_window = get_cursor_window(dpy);
|
||||
mgl::vec2i cursor_window_pos, cursor_window_size;
|
||||
get_window_geometry(dpy, cursor_window, cursor_window_pos, cursor_window_size);
|
||||
|
||||
XVisualInfo vinfo;
|
||||
memset(&vinfo, 0, sizeof(vinfo));
|
||||
XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo);
|
||||
border_window_colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, AllocNone);
|
||||
|
||||
XSetWindowAttributes window_attr;
|
||||
window_attr.background_pixel = border_color_x11;
|
||||
window_attr.border_pixel = 0;
|
||||
window_attr.override_redirect = true;
|
||||
window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask;
|
||||
window_attr.colormap = border_window_colormap;
|
||||
|
||||
Screen *screen = XDefaultScreenOfDisplay(dpy);
|
||||
border_window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, XWidthOfScreen(screen), XHeightOfScreen(screen), 0,
|
||||
vinfo.depth, InputOutput, vinfo.visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr);
|
||||
if(!border_window) {
|
||||
fprintf(stderr, "Error: WindowSelector::start: failed to create region window\n");
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
set_window_size_not_resizable(dpy, border_window, XWidthOfScreen(screen), XHeightOfScreen(screen));
|
||||
if(cursor_window && cursor_window != DefaultRootWindow(dpy))
|
||||
set_region_rectangle(dpy, border_window, cursor_window_pos.x, cursor_window_pos.y, cursor_window_size.x, cursor_window_size.y, rectangle_border_size);
|
||||
else
|
||||
set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0);
|
||||
make_window_click_through(dpy, border_window);
|
||||
XMapWindow(dpy, border_window);
|
||||
|
||||
crosshair_cursor = XCreateFontCursor(dpy, XC_crosshair);
|
||||
XGrabPointer(dpy, DefaultRootWindow(dpy), True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, GrabModeAsync, GrabModeAsync, None, crosshair_cursor, CurrentTime);
|
||||
XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, GrabModeAsync, CurrentTime);
|
||||
XFlush(dpy);
|
||||
|
||||
selected = false;
|
||||
canceled = false;
|
||||
selected_window = None;
|
||||
return true;
|
||||
}
|
||||
|
||||
void WindowSelector::stop() {
|
||||
if(!dpy)
|
||||
return;
|
||||
|
||||
XUngrabPointer(dpy, CurrentTime);
|
||||
XUngrabKeyboard(dpy, CurrentTime);
|
||||
|
||||
if(border_window_colormap) {
|
||||
XFreeColormap(dpy, border_window_colormap);
|
||||
border_window_colormap = 0;
|
||||
}
|
||||
|
||||
if(border_window) {
|
||||
XDestroyWindow(dpy, border_window);
|
||||
border_window = 0;
|
||||
}
|
||||
|
||||
if(crosshair_cursor) {
|
||||
XFreeCursor(dpy, crosshair_cursor);
|
||||
crosshair_cursor = None;
|
||||
}
|
||||
|
||||
XFlush(dpy);
|
||||
XCloseDisplay(dpy);
|
||||
dpy = nullptr;
|
||||
}
|
||||
|
||||
bool WindowSelector::is_started() const {
|
||||
return dpy != nullptr;
|
||||
}
|
||||
|
||||
bool WindowSelector::failed() const {
|
||||
return !dpy;
|
||||
}
|
||||
|
||||
bool WindowSelector::poll_events() {
|
||||
if(!dpy || selected)
|
||||
return false;
|
||||
|
||||
XEvent xev;
|
||||
while(XPending(dpy)) {
|
||||
XNextEvent(dpy, &xev);
|
||||
|
||||
if(xev.type == MotionNotify) {
|
||||
const Window motion_window = xev.xmotion.subwindow;
|
||||
mgl::vec2i motion_window_pos, motion_window_size;
|
||||
get_window_geometry(dpy, motion_window, motion_window_pos, motion_window_size);
|
||||
if(motion_window && motion_window != DefaultRootWindow(dpy))
|
||||
set_region_rectangle(dpy, border_window, motion_window_pos.x, motion_window_pos.y, motion_window_size.x, motion_window_size.y, rectangle_border_size);
|
||||
else
|
||||
set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0);
|
||||
XFlush(dpy);
|
||||
} else if(xev.type == ButtonRelease && xev.xbutton.button == Button1) {
|
||||
selected_window = xev.xbutton.subwindow;
|
||||
const Window clicked_window_real = window_get_target_window_child(dpy, selected_window);
|
||||
if(clicked_window_real)
|
||||
selected_window = clicked_window_real;
|
||||
selected = true;
|
||||
|
||||
stop();
|
||||
break;
|
||||
} else if(xev.type == KeyRelease && XKeycodeToKeysym(dpy, xev.xkey.keycode, 0) == XK_Escape) {
|
||||
canceled = true;
|
||||
selected = false;
|
||||
stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowSelector::take_selection() {
|
||||
const bool result = selected;
|
||||
selected = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool WindowSelector::take_canceled() {
|
||||
const bool result = canceled;
|
||||
canceled = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
Window WindowSelector::get_selection() const {
|
||||
return selected_window;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "../include/WindowUtils.hpp"
|
||||
#include "../include/Utils.hpp"
|
||||
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xutil.h>
|
||||
@@ -62,7 +63,7 @@ namespace gsr {
|
||||
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) {
|
||||
Window window_get_target_window_child(Display *display, Window window) {
|
||||
if(window == None)
|
||||
return None;
|
||||
|
||||
@@ -212,28 +213,6 @@ namespace gsr {
|
||||
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);
|
||||
|
||||
@@ -35,9 +35,8 @@ namespace gsr {
|
||||
std::unique_ptr<ComboBox> ScreenshotSettingsPage::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(capture_options.window)
|
||||
// record_area_box->add_item("Window", "window");
|
||||
if(capture_options.window)
|
||||
record_area_box->add_item("Window", "window");
|
||||
if(capture_options.region)
|
||||
record_area_box->add_item("Region", "region");
|
||||
if(!capture_options.monitors.empty())
|
||||
@@ -60,14 +59,6 @@ namespace gsr {
|
||||
return record_area_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> ScreenshotSettingsPage::create_select_window() {
|
||||
auto select_window_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
select_window_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Select window:", get_color_theme().text_color));
|
||||
select_window_list->add_widget(std::make_unique<Button>(&get_theme().body_font, "Click here to select a window...", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)));
|
||||
select_window_list_ptr = select_window_list.get();
|
||||
return select_window_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Entry> ScreenshotSettingsPage::create_image_width_entry() {
|
||||
auto image_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3);
|
||||
image_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15);
|
||||
@@ -124,7 +115,6 @@ namespace gsr {
|
||||
|
||||
auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
capture_target_list->add_widget(create_record_area());
|
||||
capture_target_list->add_widget(create_select_window());
|
||||
capture_target_list->add_widget(create_image_resolution_section());
|
||||
capture_target_list->add_widget(create_restore_portal_session_section());
|
||||
|
||||
@@ -258,9 +248,7 @@ namespace gsr {
|
||||
content_page_ptr->add_widget(create_settings());
|
||||
|
||||
record_area_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
const bool window_selected = id == "window";
|
||||
const bool portal_selected = id == "portal";
|
||||
select_window_list_ptr->set_visible(window_selected);
|
||||
image_resolution_list_ptr->set_visible(change_image_resolution_checkbox_ptr->is_checked());
|
||||
restore_portal_session_list_ptr->set_visible(portal_selected);
|
||||
return true;
|
||||
|
||||
@@ -65,13 +65,12 @@ namespace gsr {
|
||||
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(capture_options.window)
|
||||
// record_area_box->add_item("Window", "window");
|
||||
if(capture_options.region)
|
||||
record_area_box->add_item("Region", "region");
|
||||
if(capture_options.window)
|
||||
record_area_box->add_item("Window", "window");
|
||||
if(capture_options.focused)
|
||||
record_area_box->add_item("Follow focused window", "focused");
|
||||
if(capture_options.region)
|
||||
record_area_box->add_item("Region", "region");
|
||||
if(!capture_options.monitors.empty())
|
||||
record_area_box->add_item(gsr_info->system_info.display_server == DisplayServer::WAYLAND ? "Focused monitor (Experimental on Wayland)" : "Focused monitor", "focused_monitor");
|
||||
for(const auto &monitor : capture_options.monitors) {
|
||||
@@ -92,14 +91,6 @@ namespace gsr {
|
||||
return record_area_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> SettingsPage::create_select_window() {
|
||||
auto select_window_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
select_window_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Select window:", get_color_theme().text_color));
|
||||
select_window_list->add_widget(std::make_unique<Button>(&get_theme().body_font, "Click here to select a window...", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)));
|
||||
select_window_list_ptr = select_window_list.get();
|
||||
return select_window_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Entry> SettingsPage::create_area_width_entry() {
|
||||
auto area_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3);
|
||||
area_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15);
|
||||
@@ -186,7 +177,6 @@ namespace gsr {
|
||||
|
||||
auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
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());
|
||||
capture_target_list->add_widget(create_restore_portal_session_section());
|
||||
@@ -451,13 +441,13 @@ namespace gsr {
|
||||
|
||||
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));
|
||||
auto video_bitrate_entry = std::make_unique<Entry>(&get_theme().body_font, "8000", (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();
|
||||
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.64MB", get_color_theme().text_color);
|
||||
auto size_mb_label = std::make_unique<Label>(&get_theme().body_font, "", get_color_theme().text_color);
|
||||
Label *size_mb_label_ptr = size_mb_label.get();
|
||||
list->add_widget(std::move(size_mb_label));
|
||||
|
||||
@@ -634,10 +624,8 @@ namespace gsr {
|
||||
content_page_ptr->add_widget(create_settings());
|
||||
|
||||
record_area_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
const bool window_selected = id == "window";
|
||||
const bool focused_selected = id == "focused";
|
||||
const bool portal_selected = id == "portal";
|
||||
select_window_list_ptr->set_visible(window_selected);
|
||||
area_size_list_ptr->set_visible(focused_selected);
|
||||
video_resolution_list_ptr->set_visible(!focused_selected && change_video_resolution_checkbox_ptr->is_checked());
|
||||
change_video_resolution_checkbox_ptr->set_visible(!focused_selected);
|
||||
@@ -964,6 +952,7 @@ namespace gsr {
|
||||
auto streaming_service_box = std::make_unique<ComboBox>(&get_theme().body_font);
|
||||
streaming_service_box->add_item("Twitch", "twitch");
|
||||
streaming_service_box->add_item("YouTube", "youtube");
|
||||
streaming_service_box->add_item("Rumble", "rumble");
|
||||
streaming_service_box->add_item("Custom", "custom");
|
||||
streaming_service_box_ptr = streaming_service_box.get();
|
||||
return streaming_service_box;
|
||||
@@ -988,6 +977,10 @@ namespace gsr {
|
||||
youtube_stream_key_entry_ptr = youtube_stream_key_entry.get();
|
||||
stream_key_list->add_widget(std::move(youtube_stream_key_entry));
|
||||
|
||||
auto rumble_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
|
||||
rumble_stream_key_entry_ptr = rumble_stream_key_entry.get();
|
||||
stream_key_list->add_widget(std::move(rumble_stream_key_entry));
|
||||
|
||||
stream_key_list_ptr = stream_key_list.get();
|
||||
return stream_key_list;
|
||||
}
|
||||
@@ -1049,12 +1042,14 @@ namespace gsr {
|
||||
streaming_service_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
const bool twitch_option = id == "twitch";
|
||||
const bool youtube_option = id == "youtube";
|
||||
const bool rumble_option = id == "rumble";
|
||||
const bool custom_option = id == "custom";
|
||||
stream_key_list_ptr->set_visible(!custom_option);
|
||||
stream_url_list_ptr->set_visible(custom_option);
|
||||
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);
|
||||
rumble_stream_key_entry_ptr->set_visible(rumble_option);
|
||||
return true;
|
||||
};
|
||||
streaming_service_box_ptr->on_selection_changed("Twitch", "twitch");
|
||||
@@ -1256,6 +1251,7 @@ namespace gsr {
|
||||
streaming_service_box_ptr->set_selected_item(config.streaming_config.streaming_service);
|
||||
youtube_stream_key_entry_ptr->set_text(config.streaming_config.youtube.stream_key);
|
||||
twitch_stream_key_entry_ptr->set_text(config.streaming_config.twitch.stream_key);
|
||||
rumble_stream_key_entry_ptr->set_text(config.streaming_config.rumble.stream_key);
|
||||
stream_url_entry_ptr->set_text(config.streaming_config.custom.url);
|
||||
container_box_ptr->set_selected_item(config.streaming_config.custom.container);
|
||||
}
|
||||
@@ -1397,6 +1393,7 @@ namespace gsr {
|
||||
config.streaming_config.streaming_service = streaming_service_box_ptr->get_selected_id();
|
||||
config.streaming_config.youtube.stream_key = youtube_stream_key_entry_ptr->get_text();
|
||||
config.streaming_config.twitch.stream_key = twitch_stream_key_entry_ptr->get_text();
|
||||
config.streaming_config.rumble.stream_key = rumble_stream_key_entry_ptr->get_text();
|
||||
config.streaming_config.custom.url = stream_url_entry_ptr->get_text();
|
||||
config.streaming_config.custom.container = container_box_ptr->get_selected_id();
|
||||
}
|
||||
|
||||
@@ -44,42 +44,42 @@ int main(int argc, char **argv) {
|
||||
} 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);
|
||||
fprintf(stderr, "gsr-global-hotkeys error: expected --all or --virtual, got %s\n", grab_type_arg);
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
} else if(argc != 1) {
|
||||
fprintf(stderr, "Error: expected 0 or 1 arguments, got %d argument(s)\n", argc);
|
||||
fprintf(stderr, "gsr-global-hotkeys error: expected 0 or 1 arguments, got %d argument(s)\n", argc);
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(is_gsr_global_hotkeys_already_running()) {
|
||||
fprintf(stderr, "Error: gsr-global-hotkeys is already running\n");
|
||||
fprintf(stderr, "gsr-global-hotkeys error: gsr-global-hotkeys is already running\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
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, "gsr-global-hotkeys error: failed to change user to root, global hotkeys will not work. Make sure to set the correct capability on gsr-global-hotkeys\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
fprintf(stderr, "gsr-global-hotkeys error: failed to setup hotplugging and no keyboard input devices were found\n");
|
||||
setuid(user_id);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Info: global hotkeys setup, waiting for hotkeys to be pressed\n");
|
||||
fprintf(stderr, "gsr-global-hotkeys info: global hotkeys setup, waiting for hotkeys to be pressed\n");
|
||||
|
||||
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");
|
||||
fprintf(stderr, "gsr-global-hotkeys info: stdin closed (parent process likely closed this process), exiting...\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user