Compare commits

...

21 Commits

Author SHA1 Message Date
dec05eba
c6339ac9c2 Rename dbus from com.dec05eba.gsr_kwin_helper to com.dec05eba.gpu_screen_recorder.gsr_kwin_helper for flatpak access 2026-01-24 18:14:11 +01:00
dec05eba
0341930394 Update flatpak version reference 2026-01-24 17:58:47 +01:00
dec05eba
3d673247a7 Remove (x11 applications only) for window title text on kde plasma wayland and hyprland 2026-01-24 17:58:08 +01:00
dec05eba
ed671e9d7c 1.10.4 2026-01-24 17:47:58 +01:00
dec05eba
6a72717fe5 Wayland: fix game minimizing sometimes 2026-01-24 17:38:48 +01:00
dec05eba
6ea867b9d2 Revert "Test workaround flatpak issue related to broken flatpak-spawn --host environment missing wayland display"
This reverts commit ec98533f1b.
2026-01-24 16:41:06 +01:00
dec05eba
756b993078 Revert "Attempt workaround flatpak issue"
This reverts commit 007e2546a9.
2026-01-24 16:41:00 +01:00
dec05eba
007e2546a9 Attempt workaround flatpak issue 2026-01-24 15:50:04 +01:00
dec05eba
ec98533f1b Test workaround flatpak issue related to broken flatpak-spawn --host environment missing wayland display 2026-01-24 15:48:11 +01:00
dec05eba
ebc460ecc8 Revert "Test dont set environment variables"
This reverts commit 540e2df322.
2026-01-24 15:41:22 +01:00
dec05eba
540e2df322 Test dont set environment variables 2026-01-24 15:20:06 +01:00
dec05eba
d0c581684b 1.10.3 2026-01-24 12:43:10 +01:00
dec05eba
d4dbb27213 Fix window name on x11 2026-01-24 01:09:11 +01:00
dec05eba
bfc7df5c56 Mention that libdbus is a new dependency 2026-01-24 01:01:26 +01:00
dec05eba
9c5688f61b Simplify gsr-hyprland-helper, some cleanups 2026-01-24 00:55:47 +01:00
Andrew
9ccb4dd541 Removed flatpak KWin title blocker in the overlay 2026-01-23 00:03:02 +01:00
Andrew
1e3e76fcee Hyprland and KDE workarounds should work with flatpak now 2026-01-22 10:41:02 +01:00
Andrew
9c9df47d62 Fix to ignore GSR Overlay in KWin workaround 2026-01-22 10:40:58 +01:00
Andrew
00ceaa989d Added KWin workaround to get current window title 2026-01-22 10:40:51 +01:00
Andrew
23b1526092 Added Hyprland workaround to get current window title 2026-01-22 10:40:40 +01:00
dec05eba
9339d6760e Force dont restore session portal when using window capture (portal) with hotkey on wayland 2026-01-21 01:51:25 +01:00
15 changed files with 639 additions and 21 deletions

3
.gitignore vendored
View File

@@ -6,3 +6,6 @@ compile_commands.json
**/xdg-output-unstable-v1-protocol.c
depends/.wraplock
.cache
build/

View File

@@ -27,6 +27,7 @@ These are the dependencies needed to build GPU Screen Recorder UI:
* linux-api-headers
* libpulse (libpulse-simple)
* libdrm
* libdbus
* wayland (wayland-client, wayland-egl, wayland-scanner)
* setcap (libcap)

View File

@@ -0,0 +1,13 @@
#pragma once
#include <string>
namespace gsr {
struct ActiveHyprlandWindow {
std::string window_id = "";
std::string title = "Game";
};
void start_hyprland_listener_thread();
std::string get_current_hyprland_window_title();
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include <string>
namespace gsr {
struct ActiveKwinWindow {
std::string title = "Game";
};
void start_kwin_helper_thread();
std::string get_current_kwin_window_title();
}

View File

@@ -165,7 +165,7 @@ namespace gsr {
bool update_compositor_texture(const Monitor &monitor);
void add_region_command(std::vector<const char*> &args, char *region_str, int region_str_size);
void add_common_gpu_screen_recorder_args(std::vector<const char*> &args, const RecordOptions &record_options, const std::vector<std::string> &audio_tracks, const std::string &video_bitrate, const char *region, char *region_str, int region_str_size, const std::string &region_area_option);
void add_common_gpu_screen_recorder_args(std::vector<const char*> &args, const RecordOptions &record_options, const std::vector<std::string> &audio_tracks, const std::string &video_bitrate, const char *region, char *region_str, int region_str_size, const std::string &region_area_option, RecordForceType force_type = RecordForceType::NONE);
std::string get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options);

View File

@@ -247,5 +247,7 @@ namespace gsr {
std::optional<GsrCamera> selected_camera;
std::optional<GsrCameraSetup> selected_camera_setup;
bool supports_window_title = false;
};
}

View File

@@ -1,4 +1,4 @@
project('gsr-ui', ['c', 'cpp'], version : '1.10.2', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
project('gsr-ui', ['c', 'cpp'], version : '1.10.4', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
add_project_arguments('-D_FILE_OFFSET_BITS=64', language : ['c', 'cpp'])
@@ -41,6 +41,8 @@ src = [
'src/CursorTracker/CursorTrackerX11.cpp',
'src/CursorTracker/CursorTrackerWayland.cpp',
'src/Utils.cpp',
'src/HyprlandWorkaround.cpp',
'src/KwinWorkaround.cpp',
'src/WindowUtils.cpp',
'src/RegionSelector.cpp',
'src/WindowSelector.cpp',
@@ -68,7 +70,9 @@ gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui')
icons_path = join_paths(prefix, datadir, 'icons')
add_project_arguments('-DGSR_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.12.0"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.12.1"', language: ['c', 'cpp'])
add_project_arguments('-DKWIN_HELPER_SCRIPT_PATH="' + gsr_ui_resources_path + '/gsrkwinhelper.js"', language: ['c', 'cpp'])
executable(
meson.project_name(),
@@ -111,6 +115,31 @@ executable(
install : true
)
executable(
'gsr-kwin-helper',
[
'tools/gsr-kwin-helper/main.cpp'
],
install : true,
dependencies: [
dependency('dbus-1'),
]
)
install_data(
'tools/gsr-kwin-helper/gsrkwinhelper.js',
install_dir: gsr_ui_resources_path,
install_mode: 'rwxr-xr-x'
)
executable(
'gsr-hyprland-helper',
[
'tools/gsr-hyprland-helper/main.c'
],
install : true
)
install_subdir('images', install_dir : gsr_ui_resources_path)
install_subdir('fonts', install_dir : gsr_ui_resources_path)

View File

@@ -1,7 +1,7 @@
[package]
name = "gsr-ui"
type = "executable"
version = "1.10.2"
version = "1.10.4"
platforms = ["posix"]
[lang.cpp]

View File

@@ -0,0 +1,62 @@
#include "../include/HyprlandWorkaround.hpp"
#include <iostream>
#include <unistd.h>
#include <thread>
namespace gsr {
static ActiveHyprlandWindow active_hyprland_window;
static bool hyprland_listener_thread_started = false;
static void hyprland_listener_thread() {
const bool inside_flatpak = access("/app/manifest.json", F_OK) == 0;
const char *hyprland_helper_bin =
inside_flatpak ?
"flatpak-spawn --host -- /var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/bin/gsr-hyprland-helper"
: "gsr-hyprland-helper";
FILE* pipe = popen(hyprland_helper_bin, "r");
if (!pipe) {
std::cerr << "Failed to start gsr-hyprland-helper process\n";
return;
}
std::cerr << "Started Hyprland helper thread\n";
char buffer[4096];
const std::string prefix = "Window title changed: ";
std::string line;
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
line = buffer;
if (!line.empty() && line.back() == '\n') {
line.pop_back();
}
size_t pos = line.find(prefix);
if (pos != std::string::npos) {
active_hyprland_window.title = line.substr(pos + prefix.length());
}
}
pclose(pipe);
}
std::string get_current_hyprland_window_title() {
return active_hyprland_window.title;
}
void start_hyprland_listener_thread() {
if (hyprland_listener_thread_started) {
return;
}
hyprland_listener_thread_started = true;
std::thread([&] {
hyprland_listener_thread();
}).detach();
}
}

67
src/KwinWorkaround.cpp Normal file
View File

@@ -0,0 +1,67 @@
#include "../include/KwinWorkaround.hpp"
#include <cstddef>
#include <iostream>
#include <sys/types.h>
#include <thread>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
namespace gsr {
static ActiveKwinWindow active_kwin_window;
static bool kwin_helper_thread_started = false;
void kwin_script_thread() {
FILE* pipe = popen("gsr-kwin-helper", "r");
if (!pipe) {
std::cerr << "Failed to start gsr-kwin-helper process\n";
return;
}
std::cerr << "Started a KWin helper thread\n";
char buffer[4096];
const std::string prefix = "Active window title set to: ";
std::string line;
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
line = buffer;
if (!line.empty() && line.back() == '\n') {
line.pop_back();
}
size_t pos = line.find(prefix);
if (pos != std::string::npos) {
std::string title = line.substr(pos + prefix.length());
if (title == "gsr ui" || title == "gsr notify") {
continue; // ignore the overlay and notification
}
active_kwin_window.title = std::move(title);
}
}
pclose(pipe);
}
std::string get_current_kwin_window_title() {
return active_kwin_window.title;
}
void start_kwin_helper_thread() {
if (kwin_helper_thread_started) {
return;
}
kwin_helper_thread_started = true;
std::thread([&] {
kwin_script_thread();
}).detach();
}
}

View File

@@ -10,6 +10,8 @@
#include "../include/gui/ScreenshotSettingsPage.hpp"
#include "../include/gui/GlobalSettingsPage.hpp"
#include "../include/gui/Utils.hpp"
#include "../include/KwinWorkaround.hpp"
#include "../include/HyprlandWorkaround.hpp"
#include "../include/gui/PageStack.hpp"
#include "../include/WindowUtils.hpp"
#include "../include/GlobalHotkeys/GlobalHotkeys.hpp"
@@ -548,6 +550,14 @@ namespace gsr {
save_config(config);
show_notification("Wayland doesn't support GPU Screen Recorder UI properly,\nthings may not work as expected. Use X11 if you experience issues.", notification_error_timeout_seconds, mgl::Color(255, 255, 255), mgl::Color(255, 0, 0), NotificationType::NOTICE, nullptr, NotificationLevel::ERROR);
}
const std::string wm_name = get_window_manager_name(x11_dpy);
if (wm_name.find("Hyprland") != std::string::npos) {
start_hyprland_listener_thread();
} else if (wm_name == "KWin") {
start_kwin_helper_thread();
}
}
update_led_indicator_after_settings_change();
@@ -1053,8 +1063,9 @@ 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.
const Window x11_focused_window = get_focused_window(display, WindowCaptureType::FOCUSED, false);
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND
|| (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor) && get_focused_window(display, WindowCaptureType::FOCUSED, false) == x11_cursor_window)
|| (x11_focused_window && is_window_fullscreen_on_monitor(display, x11_focused_window, *focused_monitor))
|| is_wlroots
|| is_hyprland;
@@ -1092,7 +1103,6 @@ namespace gsr {
window.reset();
return;
}
//window->set_low_latency(true);
unsigned char data = 2; // Prefer being composed to allow transparency
@@ -1964,11 +1974,24 @@ namespace gsr {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
const std::string video_filename = filepath_get_filename(video_filepath.c_str());
const std::string wm_name = get_window_manager_name(display);
const bool is_hyprland = wm_name.find("Hyprland") != std::string::npos;
const bool is_kwin_wayland = wm_name == "KWin" && gsr_info.system_info.display_server == DisplayServer::WAYLAND;
std::string focused_window_name;
if (is_hyprland) {
focused_window_name = get_current_hyprland_window_title();
} else if (is_kwin_wayland) {
focused_window_name = get_current_kwin_window_title();
} else {
const Window gsr_ui_window = window ? (Window)window->get_system_handle() : None;
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, false);
}
const Window gsr_ui_window = 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, false);
if(focused_window_name.empty())
focused_window_name = "Game";
@@ -2532,7 +2555,7 @@ namespace gsr {
args.push_back(region_str);
}
void Overlay::add_common_gpu_screen_recorder_args(std::vector<const char*> &args, const RecordOptions &record_options, const std::vector<std::string> &audio_tracks, const std::string &video_bitrate, const char *region, char *region_str, int region_str_size, const std::string &region_area_option) {
void Overlay::add_common_gpu_screen_recorder_args(std::vector<const char*> &args, const RecordOptions &record_options, const std::vector<std::string> &audio_tracks, const std::string &video_bitrate, const char *region, char *region_str, int region_str_size, const std::string &region_area_option, RecordForceType force_type) {
if(record_options.video_quality == "custom") {
args.push_back("-bm");
args.push_back("cbr");
@@ -2553,7 +2576,7 @@ namespace gsr {
args.push_back(audio_track.c_str());
}
if(record_options.restore_portal_session) {
if(record_options.restore_portal_session && force_type != RecordForceType::WINDOW) {
args.push_back("-restore-portal-session");
args.push_back("yes");
}
@@ -3146,8 +3169,17 @@ namespace gsr {
"-o", output_file.c_str()
};
const std::string hotkey_window_capture_portal_session_token_filepath = get_config_dir() + "/gsr-ui-window-capture-token";
if(record_area_option == "portal") {
hide_ui = true;
if(force_type == RecordForceType::WINDOW) {
args.push_back("-portal-session-token-filepath");
args.push_back(hotkey_window_capture_portal_session_token_filepath.c_str());
}
}
char region_str[128];
add_common_gpu_screen_recorder_args(args, config.record_config.record_options, audio_tracks, video_bitrate, size, region_str, sizeof(region_str), record_area_option);
add_common_gpu_screen_recorder_args(args, config.record_config.record_options, audio_tracks, video_bitrate, size, region_str, sizeof(region_str), record_area_option, force_type);
args.push_back(nullptr);
@@ -3180,9 +3212,6 @@ namespace gsr {
show_notification(msg, short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
}
if(record_area_option == "portal")
hide_ui = true;
// TODO: This will be incorrect if the user uses portal capture, as capture wont start until the user has
// selected what to capture and accepted it.
recording_duration_clock.restart();

View File

@@ -10,8 +10,13 @@
#include "../../include/Theme.hpp"
#include "../../include/GsrInfo.hpp"
#include "../../include/Utils.hpp"
#include "mglpp/window/Window.hpp"
#include "mglpp/window/Event.hpp"
#include "../../include/WindowUtils.hpp"
#include <mglpp/window/Window.hpp>
#include <mglpp/window/Event.hpp>
extern "C" {
#include <mgl/mgl.h>
}
#include <algorithm>
#include <cmath>
@@ -46,6 +51,14 @@ namespace gsr {
application_audio = get_application_audio();
capture_options = get_supported_capture_options(*gsr_info);
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
const std::string wm_name = get_window_manager_name(display);
const bool is_hyprland = wm_name.find("Hyprland") != std::string::npos;
const bool is_kwin = wm_name == "KWin";
supports_window_title = gsr_info->system_info.display_server == DisplayServer::X11 || is_hyprland || is_kwin;
auto content_page = std::make_unique<GsrPage>(settings_page_type_to_title_text(type), "Settings");
content_page->add_button("Back", "back", get_color_theme().page_bg_color);
content_page->on_click = [page_stack](const std::string &id) {
@@ -1113,6 +1126,7 @@ namespace gsr {
}
std::unique_ptr<RadioButton> SettingsPage::create_start_replay_automatically() {
// TODO: Support kde plasma wayland and hyprland (same ones that support getting window title)
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 applications only)");
@@ -1127,7 +1141,7 @@ namespace gsr {
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 applications only)");
snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", supports_window_title ? "" : " (X11 applications only)");
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, text);
save_replay_in_game_folder_ptr = checkbox.get();
return checkbox;
@@ -1216,9 +1230,9 @@ namespace gsr {
std::unique_ptr<Widget> SettingsPage::create_low_power_mode() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
list->set_visible(gsr_info->gpu_info.vendor == GpuVendor::AMD);
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record in low-power mode");
checkbox->set_visible(gsr_info->gpu_info.vendor == GpuVendor::AMD);
low_power_mode_checkbox_ptr = checkbox.get();
list->add_widget(std::move(checkbox));
@@ -1280,7 +1294,7 @@ namespace gsr {
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 applications only)");
snprintf(text, sizeof(text), "Save video in a folder with the name of the game%s", supports_window_title ? "" : " (X11 applications only)");
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, text);
save_recording_in_game_folder_ptr = checkbox.get();
return checkbox;

View File

@@ -0,0 +1,95 @@
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <sys/socket.h>
#include <sys/un.h>
static bool get_hyprland_socket_path(char *path, int path_len) {
const char* xdg_runtime_dir = getenv("XDG_RUNTIME_DIR");
const char* instance_sig = getenv("HYPRLAND_INSTANCE_SIGNATURE");
if (!xdg_runtime_dir || !instance_sig) {
fprintf(stderr, "Error: gsr-hypland-helper: environment variables not set\n");
return false;
}
if (snprintf(path, path_len, "%s/hypr/%s/.socket2.sock", xdg_runtime_dir, instance_sig) >= path_len) {
fprintf(stderr, "Error: gsr-hypland-helper: path to hyprland socket (%s/hypr/%s/.socket2.sock) is more than %d characters long\n", xdg_runtime_dir, instance_sig, path_len);
return false;
}
return true;
}
static void print_window_title(const char *window_title) {
if (window_title[0] == 0) {
printf("Window title changed: %s\n", "Desktop");
fflush(stdout);
return;
}
printf("Window title changed: %s\n", window_title);
fflush(stdout);
}
static int handle_ipc(void) {
const int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("Error: gsr-hyprland-helper: socket");
return 1;
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
if (!get_hyprland_socket_path(addr.sun_path, sizeof(addr.sun_path))) {
return 1;
}
if (connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("Error: gsr-hyprland-helper: connect");
close(sock_fd);
return 1;
}
fprintf(stderr, "Info: gsr-hyprland-helper: connected to Hyprland socket: %s\n", addr.sun_path);
FILE *sock_file = fdopen(sock_fd, "r");
if (!sock_file) {
perror("Error: gsr-hyprland-helper: fdopen");
close(sock_fd);
return 1;
}
char buffer[1024];
while (fgets(buffer, sizeof(buffer), sock_file)) {
int line_len = strlen(buffer);
if(line_len > 0 && buffer[line_len - 1] == '\n') {
buffer[line_len - 1] = '\0';
line_len -= 1;
}
if(line_len >= 14 && memcmp(buffer, "activewindow>>", 14) == 0) {
char *window_id = buffer + 14;
char *window_title = strchr(buffer + 14, ',');
if(!window_title)
continue;
window_title[0] = '\0';
window_title += 1;
if(strcmp(window_id, "gsr-ui") == 0 || strcmp(window_id, "gsr-notify") == 0)
continue;
print_window_title(window_title);
}
}
fclose(sock_file);
return 0;
}
int main(void) {
return handle_ipc();
}

View File

@@ -0,0 +1,58 @@
const DAEMON_DBUS_NAME = "com.dec05eba.gpu_screen_recorder.gsr_kwin_helper";
// utils
function sendNewActiveWindowTitle(title) {
callDBus(
DAEMON_DBUS_NAME, "/", DAEMON_DBUS_NAME,
"setActiveWindowTitle", title
);
}
function sendNewActiveWindowFullscreen(isFullscreen) {
callDBus(
DAEMON_DBUS_NAME, "/", DAEMON_DBUS_NAME,
"setActiveWindowFullscreen", isFullscreen
);
}
// track handlers to avoid duplicates
const windowEventHandlers = new Map();
function subscribeToClient(client) {
if (!client || windowEventHandlers.has(client)) return;
const emitActiveTitle = () => {
if (workspace.activeWindow === client) {
sendNewActiveWindowTitle(client.caption || "");
}
};
const emitActiveFullscreen = () => {
if (workspace.activeWindow === client) {
sendNewActiveWindowFullscreen(client.fullScreen);
}
};
windowEventHandlers.set(client, {
title: emitActiveTitle,
fs: emitActiveFullscreen,
});
client.captionChanged.connect(emitActiveTitle);
client.fullScreenChanged.connect(emitActiveFullscreen);
}
function updateActiveWindow(client) {
if (!client) return;
sendNewActiveWindowTitle(client.caption || "");
sendNewActiveWindowFullscreen(client.fullScreen);
subscribeToClient(client);
}
// handle window focus changes
workspace.windowActivated.connect(updateActiveWindow);
// handle initial state
if (workspace.activeWindow) {
updateActiveWindow(workspace.activeWindow);
}

View File

@@ -0,0 +1,233 @@
#include <dbus/dbus.h>
#include <unistd.h>
#include <iostream>
#include <string>
static const char* INTROSPECTION_XML =
"<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n"
"\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n"
"<node>\n"
" <interface name='com.dec05eba.gpu_screen_recorder.gsr_kwin_helper'>\n"
" <method name='setActiveWindowTitle'>\n"
" <arg type='s' name='title' direction='in'/>\n"
" </method>\n"
" </interface>\n"
" <interface name='org.freedesktop.DBus.Introspectable'>\n"
" <method name='Introspect'>\n"
" <arg type='s' name='data' direction='out'/>\n"
" </method>\n"
" </interface>\n"
"</node>\n";
class GsrKwinHelper {
public:
std::string active_window_title;
DBusConnection* connection = nullptr;
DBusError err;
bool init() {
dbus_error_init(&err);
connection = dbus_bus_get(DBUS_BUS_SESSION, &err);
if (dbus_error_is_set(&err)) {
std::cerr << "Error: gsr-kwin-helper: failed to connect to session bus: " << err.message << "\n";
dbus_error_free(&err);
return false;
}
if (!connection) {
std::cerr << "Error: gsr-kwin-helper: connection is null\n";
return false;
}
int ret = dbus_bus_request_name(connection, "com.dec05eba.gpu_screen_recorder.gsr_kwin_helper",
DBUS_NAME_FLAG_REPLACE_EXISTING, &err);
if (dbus_error_is_set(&err)) {
std::cerr << "Error: gsr-kwin-helper: failed to request name: " << err.message << "\n";
dbus_error_free(&err);
return false;
}
if (ret != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
std::cerr << "Error: gsr-kwin-helper: not primary owner of the name\n";
return false;
}
std::cerr << "Info: gsr-kwin-helper: DBus server initialized on com.dec05eba.gpu_screen_recorder.gsr_kwin_helper\n";
const bool inside_flatpak = access("/app/manifest.json", F_OK) == 0;
const char *helper_path =
!inside_flatpak
? KWIN_HELPER_SCRIPT_PATH
: "/var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/share/gsr-ui/gsrkwinhelper.js";
std::cerr << "Info: gsr-kwin-helper: KWin script path: " << helper_path << std::endl;
if (!load_kwin_script(connection, helper_path)) {
std::cerr << "Warning: gsr-kwin-helper: failed to load KWin script\n";
}
return true;
}
void run() {
while (true) {
dbus_connection_read_write(connection, 100);
DBusMessage* msg = dbus_connection_pop_message(connection);
if (!msg) {
continue;
}
if (dbus_message_is_method_call(msg, "org.freedesktop.DBus.Introspectable", "Introspect")) {
handle_introspect(msg);
} else if (dbus_message_is_method_call(msg, "com.dec05eba.gpu_screen_recorder.gsr_kwin_helper", "setActiveWindowTitle")) {
handle_set_title(msg);
}
dbus_message_unref(msg);
}
}
void handle_introspect(DBusMessage* msg) {
DBusMessage* reply = dbus_message_new_method_return(msg);
if (!reply) return;
DBusMessageIter args;
dbus_message_iter_init_append(reply, &args);
if (!dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &INTROSPECTION_XML)) {
dbus_message_unref(reply);
return;
}
dbus_connection_send(connection, reply, nullptr);
dbus_connection_flush(connection);
dbus_message_unref(reply);
}
void handle_set_title(DBusMessage* msg) {
DBusMessageIter args;
const char* title = nullptr;
if (!dbus_message_iter_init(msg, &args)) {
send_error_reply(msg, "No arguments provided");
return;
}
if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_STRING) {
send_error_reply(msg, "Expected string argument");
return;
}
dbus_message_iter_get_basic(&args, &title);
if (title) {
active_window_title = title;
std::cout << "Active window title set to: " << active_window_title << "\n";
std::cout.flush();
send_success_reply(msg);
} else {
send_error_reply(msg, "Failed to read string");
}
}
void send_success_reply(DBusMessage* msg) {
DBusMessage* reply = dbus_message_new_method_return(msg);
if (reply) {
dbus_connection_send(connection, reply, nullptr);
dbus_connection_flush(connection);
dbus_message_unref(reply);
}
}
void send_error_reply(DBusMessage* msg, const char* error_msg) {
DBusMessage* reply = dbus_message_new_error(msg, "com.dec05eba.gpu_screen_recorder.gsr_kwin_helper.Error", error_msg);
if (reply) {
dbus_connection_send(connection, reply, nullptr);
dbus_connection_flush(connection);
dbus_message_unref(reply);
}
}
bool call_kwin_method(DBusConnection* conn, const char* method,
const char* arg1 = nullptr, const char* arg2 = nullptr) {
DBusMessage* msg = dbus_message_new_method_call(
"org.kde.KWin",
"/Scripting",
"org.kde.kwin.Scripting",
method
);
if (!msg) {
std::cerr << "Error: gsr-kwin-helper: failed to create message for " << method << "\n";
return false;
}
if (arg1) {
dbus_message_append_args(msg, DBUS_TYPE_STRING, &arg1, DBUS_TYPE_INVALID);
if (arg2) {
dbus_message_append_args(msg, DBUS_TYPE_STRING, &arg2, DBUS_TYPE_INVALID);
}
}
DBusError err;
dbus_error_init(&err);
// Send message and wait for reply (with 1 second timeout)
DBusMessage* reply = dbus_connection_send_with_reply_and_block(conn, msg, 1000, &err);
dbus_message_unref(msg);
if (dbus_error_is_set(&err)) {
std::cerr << "Error: gsr-kwin-helper: error calling " << method << ": " << err.message << "\n";
dbus_error_free(&err);
return false;
}
if (reply) {
dbus_message_unref(reply);
}
return true;
}
bool load_kwin_script(DBusConnection* conn, const char* script_path) {
// Unload existing script
call_kwin_method(conn, "unloadScript", "gsrkwinhelper");
if (!call_kwin_method(conn, "loadScript", script_path, "gsrkwinhelper")) {
std::cerr << "Error: gsr-kwin-helper: failed to load KWin script\n";
return false;
}
if (!call_kwin_method(conn, "start")) {
std::cerr << "Error: gsr-kwin-helper: failed to start KWin script\n";
return false;
}
std::cerr << "Info: gsr-kwin-helper: KWin script loaded and started successfully\n";
return true;
}
~GsrKwinHelper() {
if (connection) {
dbus_bus_release_name(connection, "com.dec05eba.gpu_screen_recorder.gsr_kwin_helper", nullptr);
dbus_connection_unref(connection);
}
}
};
int main() {
GsrKwinHelper helper;
if (!helper.init()) {
return 1;
}
helper.run();
return 0;
}