From 2842315b1d189ec0b9e5ee34954e6095947afb14 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:11:47 +0800 Subject: [PATCH] Fix/linux shortcuts inhibit (#14302) * feat: Inhibit system shortcuts on Linux Fixes #13013. Signed-off-by: Max von Forell * fix(linux): shortcuts inhibit Signed-off-by: fufesou --------- Signed-off-by: Max von Forell Signed-off-by: fufesou Co-authored-by: Max von Forell --- flatpak/rustdesk.json | 1 + .../desktop/pages/desktop_setting_page.dart | 94 +++++++ flutter/linux/CMakeLists.txt | 60 ++++- flutter/linux/my_application.cc | 12 + flutter/linux/wayland_shortcuts_inhibit.cc | 244 ++++++++++++++++++ flutter/linux/wayland_shortcuts_inhibit.h | 22 ++ src/flutter_ffi.rs | 28 ++ src/lang/en.rs | 2 +- src/platform/linux.rs | 119 +++++++++ 9 files changed, 580 insertions(+), 2 deletions(-) create mode 100644 flutter/linux/wayland_shortcuts_inhibit.cc create mode 100644 flutter/linux/wayland_shortcuts_inhibit.h diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json index c4935e137..2418ac2a6 100644 --- a/flatpak/rustdesk.json +++ b/flatpak/rustdesk.json @@ -55,6 +55,7 @@ ], "finish-args": [ "--share=ipc", + "--socket=wayland", "--socket=x11", "--share=network", "--filesystem=home", diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index b513bd4d9..b26d909cb 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -2538,6 +2538,49 @@ class WaylandCard extends StatefulWidget { class _WaylandCardState extends State { final restoreTokenKey = 'wayland-restore-token'; + static const _kClearShortcutsInhibitorEventKey = + 'clear-gnome-shortcuts-inhibitor-permission-res'; + final _clearShortcutsInhibitorFailedMsg = ''.obs; + // Don't show the shortcuts permission reset button for now. + // Users can change it manually: + // "Settings" -> "Apps" -> "RustDesk" -> "Permissions" -> "Inhibit Shortcuts". + // For resetting(clearing) the permission from the portal permission store, you can + // use (replace with the RustDesk desktop file ID): + // busctl --user call org.freedesktop.impl.portal.PermissionStore \ + // /org/freedesktop/impl/portal/PermissionStore org.freedesktop.impl.portal.PermissionStore \ + // DeletePermission sss "gnome" "shortcuts-inhibitor" "" + // On a native install this is typically "rustdesk.desktop"; on Flatpak it is usually + // the exported desktop ID derived from the Flatpak app-id (e.g. "com.rustdesk.RustDesk.desktop"). + // + // We may add it back in the future if needed. + final showResetInhibitorPermission = false; + + @override + void initState() { + super.initState(); + if (showResetInhibitorPermission) { + platformFFI.registerEventHandler( + _kClearShortcutsInhibitorEventKey, _kClearShortcutsInhibitorEventKey, + (evt) async { + if (!mounted) return; + if (evt['success'] == true) { + setState(() {}); + } else { + _clearShortcutsInhibitorFailedMsg.value = + evt['msg'] as String? ?? 'Unknown error'; + } + }); + } + } + + @override + void dispose() { + if (showResetInhibitorPermission) { + platformFFI.unregisterEventHandler( + _kClearShortcutsInhibitorEventKey, _kClearShortcutsInhibitorEventKey); + } + super.dispose(); + } @override Widget build(BuildContext context) { @@ -2545,9 +2588,16 @@ class _WaylandCardState extends State { future: bind.mainHandleWaylandScreencastRestoreToken( key: restoreTokenKey, value: "get"), hasData: (restoreToken) { + final hasShortcutsPermission = showResetInhibitorPermission && + bind.mainGetCommonSync( + key: "has-gnome-shortcuts-inhibitor-permission") == + "true"; + final children = [ if (restoreToken.isNotEmpty) _buildClearScreenSelection(context, restoreToken), + if (hasShortcutsPermission) + _buildClearShortcutsInhibitorPermission(context), ]; return Offstage( offstage: children.isEmpty, @@ -2592,6 +2642,50 @@ class _WaylandCardState extends State { ), ); } + + Widget _buildClearShortcutsInhibitorPermission(BuildContext context) { + onConfirm() { + _clearShortcutsInhibitorFailedMsg.value = ''; + bind.mainSetCommon( + key: "clear-gnome-shortcuts-inhibitor-permission", value: ""); + gFFI.dialogManager.dismissAll(); + } + + showConfirmMsgBox() => msgBoxCommon( + gFFI.dialogManager, + 'Confirmation', + Text( + translate('confirm-clear-shortcuts-inhibitor-permission-tip'), + ), + [ + dialogButton('OK', onPressed: onConfirm), + dialogButton('Cancel', + onPressed: () => gFFI.dialogManager.dismissAll()) + ]); + + return Column(children: [ + Obx( + () => _clearShortcutsInhibitorFailedMsg.value.isEmpty + ? Offstage() + : Align( + alignment: Alignment.topLeft, + child: Text(_clearShortcutsInhibitorFailedMsg.value, + style: DefaultTextStyle.of(context) + .style + .copyWith(color: Colors.red)) + .marginOnly(bottom: 10.0)), + ), + _Button( + 'Reset keyboard shortcuts permission', + showConfirmMsgBox, + tip: 'clear-shortcuts-inhibitor-permission-tip', + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.error.withOpacity(0.75)), + ), + ), + ]); + } } // ignore: non_constant_identifier_names diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index d320f403c..56a8dbb70 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -1,6 +1,6 @@ # Project-level configuration. cmake_minimum_required(VERSION 3.10) -project(runner LANGUAGES CXX) +project(runner LANGUAGES C CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. @@ -54,6 +54,55 @@ add_subdirectory(${FLUTTER_MANAGED_DIR}) find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +# Wayland protocol for keyboard shortcuts inhibit +pkg_check_modules(WAYLAND_CLIENT IMPORTED_TARGET wayland-client) +pkg_check_modules(WAYLAND_PROTOCOLS_PKG QUIET wayland-protocols) +pkg_check_modules(WAYLAND_SCANNER_PKG QUIET wayland-scanner) + +if(WAYLAND_PROTOCOLS_PKG_FOUND) + pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) +endif() +if(WAYLAND_SCANNER_PKG_FOUND) + pkg_get_variable(WAYLAND_SCANNER wayland-scanner wayland_scanner) +endif() + +if(WAYLAND_CLIENT_FOUND AND WAYLAND_PROTOCOLS_DIR AND WAYLAND_SCANNER) + set(KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL + "${WAYLAND_PROTOCOLS_DIR}/unstable/keyboard-shortcuts-inhibit/keyboard-shortcuts-inhibit-unstable-v1.xml") + + if(EXISTS ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL}) + set(WAYLAND_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/wayland-protocols") + file(MAKE_DIRECTORY ${WAYLAND_GENERATED_DIR}) + + # Generate client header + add_custom_command( + OUTPUT "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h" + COMMAND ${WAYLAND_SCANNER} client-header + ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL} + "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h" + DEPENDS ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL} + VERBATIM + ) + + # Generate protocol code + add_custom_command( + OUTPUT "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-protocol.c" + COMMAND ${WAYLAND_SCANNER} private-code + ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL} + "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-protocol.c" + DEPENDS ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL} + VERBATIM + ) + + set(WAYLAND_PROTOCOL_SOURCES + "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h" + "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-protocol.c" + ) + + set(HAS_KEYBOARD_SHORTCUTS_INHIBIT TRUE) + endif() +endif() + add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") # Define the application target. To change its name, change BINARY_NAME above, @@ -63,9 +112,11 @@ add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") add_executable(${BINARY_NAME} "main.cc" "my_application.cc" + "wayland_shortcuts_inhibit.cc" "bump_mouse.cc" "bump_mouse_x11.cc" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + ${WAYLAND_PROTOCOL_SOURCES} ) # Apply the standard set of build settings. This can be removed for applications @@ -78,6 +129,13 @@ target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) target_link_libraries(${BINARY_NAME} PRIVATE ${CMAKE_DL_LIBS}) # target_link_libraries(${BINARY_NAME} PRIVATE librustdesk) +# Wayland support for keyboard shortcuts inhibit +if(HAS_KEYBOARD_SHORTCUTS_INHIBIT) + target_compile_definitions(${BINARY_NAME} PRIVATE HAS_KEYBOARD_SHORTCUTS_INHIBIT) + target_include_directories(${BINARY_NAME} PRIVATE ${WAYLAND_GENERATED_DIR}) + target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::WAYLAND_CLIENT) +endif() + # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index c84cbddba..a05bb7856 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -6,6 +6,11 @@ #ifdef GDK_WINDOWING_X11 #include #endif +#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) +#include "wayland_shortcuts_inhibit.h" +#endif + +#include #include "flutter/generated_plugin_registrant.h" @@ -91,6 +96,13 @@ static void my_application_activate(GApplication* application) { gtk_widget_show(GTK_WIDGET(window)); gtk_widget_show(GTK_WIDGET(view)); +#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) + // Register callback for sub-windows created by desktop_multi_window plugin + // Only sub-windows (remote windows) need keyboard shortcuts inhibition + desktop_multi_window_plugin_set_window_created_callback( + (WindowCreatedCallback)wayland_shortcuts_inhibit_init_for_subwindow); +#endif + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); diff --git a/flutter/linux/wayland_shortcuts_inhibit.cc b/flutter/linux/wayland_shortcuts_inhibit.cc new file mode 100644 index 000000000..76c45be4d --- /dev/null +++ b/flutter/linux/wayland_shortcuts_inhibit.cc @@ -0,0 +1,244 @@ +// Wayland keyboard shortcuts inhibit implementation +// Uses the zwp_keyboard_shortcuts_inhibit_manager_v1 protocol to request +// the compositor to disable system shortcuts for specific windows. + +#include "wayland_shortcuts_inhibit.h" + +#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) + +#include +#include +#include +#include "keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h" + +// Data structure to hold inhibitor state for each window +typedef struct { + struct zwp_keyboard_shortcuts_inhibit_manager_v1* manager; + struct zwp_keyboard_shortcuts_inhibitor_v1* inhibitor; +} ShortcutsInhibitData; + +// Cleanup function for ShortcutsInhibitData +static void shortcuts_inhibit_data_free(gpointer data) { + ShortcutsInhibitData* inhibit_data = static_cast(data); + if (inhibit_data->inhibitor != NULL) { + zwp_keyboard_shortcuts_inhibitor_v1_destroy(inhibit_data->inhibitor); + } + if (inhibit_data->manager != NULL) { + zwp_keyboard_shortcuts_inhibit_manager_v1_destroy(inhibit_data->manager); + } + g_free(inhibit_data); +} + +// Wayland registry handler to find the shortcuts inhibit manager +static void registry_handle_global(void* data, struct wl_registry* registry, + uint32_t name, const char* interface, + uint32_t /*version*/) { + ShortcutsInhibitData* inhibit_data = static_cast(data); + if (strcmp(interface, + zwp_keyboard_shortcuts_inhibit_manager_v1_interface.name) == 0) { + inhibit_data->manager = + static_cast(wl_registry_bind( + registry, name, &zwp_keyboard_shortcuts_inhibit_manager_v1_interface, + 1)); + } +} + +static void registry_handle_global_remove(void* /*data*/, struct wl_registry* /*registry*/, + uint32_t /*name*/) { + // Not needed for this use case +} + +static const struct wl_registry_listener registry_listener = { + registry_handle_global, + registry_handle_global_remove, +}; + +// Inhibitor event handlers +static void inhibitor_active(void* /*data*/, + struct zwp_keyboard_shortcuts_inhibitor_v1* /*inhibitor*/) { + // Inhibitor is now active, shortcuts are being captured +} + +static void inhibitor_inactive(void* /*data*/, + struct zwp_keyboard_shortcuts_inhibitor_v1* /*inhibitor*/) { + // Inhibitor is now inactive, shortcuts restored to compositor +} + +static const struct zwp_keyboard_shortcuts_inhibitor_v1_listener inhibitor_listener = { + inhibitor_active, + inhibitor_inactive, +}; + +// Forward declaration +static void uninhibit_keyboard_shortcuts(GtkWindow* window); + +// Inhibit keyboard shortcuts on Wayland for a specific window +static void inhibit_keyboard_shortcuts(GtkWindow* window) { + GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(window)); + if (!GDK_IS_WAYLAND_DISPLAY(display)) { + return; + } + + // Check if already inhibited for this window + if (g_object_get_data(G_OBJECT(window), "shortcuts-inhibit-data") != NULL) { + return; + } + + ShortcutsInhibitData* inhibit_data = g_new0(ShortcutsInhibitData, 1); + + struct wl_display* wl_display = gdk_wayland_display_get_wl_display(display); + if (wl_display == NULL) { + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + struct wl_registry* registry = wl_display_get_registry(wl_display); + if (registry == NULL) { + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + wl_registry_add_listener(registry, ®istry_listener, inhibit_data); + wl_display_roundtrip(wl_display); + + if (inhibit_data->manager == NULL) { + wl_registry_destroy(registry); + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(window)); + if (gdk_window == NULL) { + wl_registry_destroy(registry); + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + struct wl_surface* surface = gdk_wayland_window_get_wl_surface(gdk_window); + if (surface == NULL) { + wl_registry_destroy(registry); + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + GdkSeat* gdk_seat = gdk_display_get_default_seat(display); + if (gdk_seat == NULL) { + wl_registry_destroy(registry); + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + struct wl_seat* seat = gdk_wayland_seat_get_wl_seat(gdk_seat); + if (seat == NULL) { + wl_registry_destroy(registry); + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + inhibit_data->inhibitor = + zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts( + inhibit_data->manager, surface, seat); + + if (inhibit_data->inhibitor == NULL) { + wl_registry_destroy(registry); + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + // Add listener to monitor active/inactive state + zwp_keyboard_shortcuts_inhibitor_v1_add_listener( + inhibit_data->inhibitor, &inhibitor_listener, window); + + wl_display_roundtrip(wl_display); + wl_registry_destroy(registry); + + // Associate the inhibit data with the window for cleanup on destroy + g_object_set_data_full(G_OBJECT(window), "shortcuts-inhibit-data", + inhibit_data, shortcuts_inhibit_data_free); +} + +// Remove keyboard shortcuts inhibitor from a window +static void uninhibit_keyboard_shortcuts(GtkWindow* window) { + ShortcutsInhibitData* inhibit_data = static_cast( + g_object_get_data(G_OBJECT(window), "shortcuts-inhibit-data")); + + if (inhibit_data == NULL) { + return; + } + + // This will trigger shortcuts_inhibit_data_free via g_object_set_data + g_object_set_data(G_OBJECT(window), "shortcuts-inhibit-data", NULL); +} + +// Focus event handlers for dynamic inhibitor management +static gboolean on_window_focus_in(GtkWidget* widget, GdkEventFocus* /*event*/, gpointer /*user_data*/) { + if (GTK_IS_WINDOW(widget)) { + inhibit_keyboard_shortcuts(GTK_WINDOW(widget)); + } + return FALSE; // Continue event propagation +} + +static gboolean on_window_focus_out(GtkWidget* widget, GdkEventFocus* /*event*/, gpointer /*user_data*/) { + if (GTK_IS_WINDOW(widget)) { + uninhibit_keyboard_shortcuts(GTK_WINDOW(widget)); + } + return FALSE; // Continue event propagation +} + +// Key for marking window as having focus handlers connected +static const char* const kFocusHandlersConnectedKey = "shortcuts-inhibit-focus-handlers-connected"; +// Key for marking window as having a pending realize handler +static const char* const kRealizeHandlerConnectedKey = "shortcuts-inhibit-realize-handler-connected"; + +// Callback when window is realized (mapped to screen) +// Sets up focus-based inhibitor management +static void on_window_realize(GtkWidget* widget, gpointer /*user_data*/) { + if (GTK_IS_WINDOW(widget)) { + // Check if focus handlers are already connected to avoid duplicates + if (g_object_get_data(G_OBJECT(widget), kFocusHandlersConnectedKey) != NULL) { + return; + } + + // Connect focus events for dynamic inhibitor management + g_signal_connect(widget, "focus-in-event", + G_CALLBACK(on_window_focus_in), NULL); + g_signal_connect(widget, "focus-out-event", + G_CALLBACK(on_window_focus_out), NULL); + + // Mark as connected to prevent duplicate connections + g_object_set_data(G_OBJECT(widget), kFocusHandlersConnectedKey, GINT_TO_POINTER(1)); + + // If window already has focus, create inhibitor now + if (gtk_window_has_toplevel_focus(GTK_WINDOW(widget))) { + inhibit_keyboard_shortcuts(GTK_WINDOW(widget)); + } + } +} + +// Public API: Initialize shortcuts inhibit for a sub-window +void wayland_shortcuts_inhibit_init_for_subwindow(void* view) { + GtkWidget* widget = GTK_WIDGET(view); + GtkWidget* toplevel = gtk_widget_get_toplevel(widget); + + if (toplevel != NULL && GTK_IS_WINDOW(toplevel)) { + // Check if already initialized to avoid duplicate realize handlers + if (g_object_get_data(G_OBJECT(toplevel), kFocusHandlersConnectedKey) != NULL || + g_object_get_data(G_OBJECT(toplevel), kRealizeHandlerConnectedKey) != NULL) { + return; + } + + if (gtk_widget_get_realized(toplevel)) { + // Window is already realized, set up focus handlers now + on_window_realize(toplevel, NULL); + } else { + // Mark realize handler as connected to prevent duplicate connections + // if called again before window is realized + g_object_set_data(G_OBJECT(toplevel), kRealizeHandlerConnectedKey, GINT_TO_POINTER(1)); + // Wait for window to be realized + g_signal_connect(toplevel, "realize", + G_CALLBACK(on_window_realize), NULL); + } + } +} + +#endif // defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) diff --git a/flutter/linux/wayland_shortcuts_inhibit.h b/flutter/linux/wayland_shortcuts_inhibit.h new file mode 100644 index 000000000..c0996931a --- /dev/null +++ b/flutter/linux/wayland_shortcuts_inhibit.h @@ -0,0 +1,22 @@ +// Wayland keyboard shortcuts inhibit support +// This module provides functionality to inhibit system keyboard shortcuts +// on Wayland compositors, allowing remote desktop windows to capture all +// key events including Super, Alt+Tab, etc. + +#ifndef WAYLAND_SHORTCUTS_INHIBIT_H_ +#define WAYLAND_SHORTCUTS_INHIBIT_H_ + +#include + +#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) + +// Initialize shortcuts inhibit for a sub-window created by desktop_multi_window plugin. +// This sets up focus-based inhibitor management: inhibitor is created when +// the window gains focus and destroyed when it loses focus. +// +// @param view The FlView of the sub-window +void wayland_shortcuts_inhibit_init_for_subwindow(void* view); + +#endif // defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) + +#endif // WAYLAND_SHORTCUTS_INHIBIT_H_ diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a46cfd8b6..864002d24 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -2759,6 +2759,11 @@ pub fn main_get_common(key: String) -> String { None => "", } .to_string(); + } else if key == "has-gnome-shortcuts-inhibitor-permission" { + #[cfg(target_os = "linux")] + return crate::platform::linux::has_gnome_shortcuts_inhibitor_permission().to_string(); + #[cfg(not(target_os = "linux"))] + return false.to_string(); } else { if key.starts_with("download-data-") { let id = key.replace("download-data-", ""); @@ -2920,6 +2925,29 @@ pub fn main_set_common(_key: String, _value: String) { } else if _key == "cancel-downloader" { crate::hbbs_http::downloader::cancel(&_value); } + + #[cfg(target_os = "linux")] + if _key == "clear-gnome-shortcuts-inhibitor-permission" { + std::thread::spawn(move || { + let (success, msg) = + match crate::platform::linux::clear_gnome_shortcuts_inhibitor_permission() { + Ok(_) => (true, "".to_owned()), + Err(e) => (false, e.to_string()), + }; + let data = HashMap::from([ + ( + "name", + serde_json::json!("clear-gnome-shortcuts-inhibitor-permission-res"), + ), + ("success", serde_json::json!(success)), + ("msg", serde_json::json!(msg)), + ]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + }); + } } pub fn session_get_common_sync( diff --git a/src/lang/en.rs b/src/lang/en.rs index 1399601de..511ddff4a 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -220,7 +220,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("default_proxy_tip", "Default protocol and port are Socks5 and 1080"), ("no_audio_input_device_tip", "No audio input device found."), ("clear_Wayland_screen_selection_tip", "After clearing the screen selection, you can reselect the screen to share."), - ("confirm_clear_Wayland_screen_selection_tip", "Are you sure to clear the Wayland screen selection?"), + ("confirm_clear_Wayland_screen_selection_tip", "Are you sure you want to clear the Wayland screen selection?"), ("android_new_voice_call_tip", "A new voice call request was received. If you accept, the audio will switch to voice communication."), ("texture_render_tip", "Use texture rendering to make the pictures smoother. You could try disabling this option if you encounter rendering issues."), ("floating_window_tip", "It helps to keep RustDesk background service"), diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 382af72cf..9493e1cae 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -2088,3 +2088,122 @@ pub fn is_selinux_enforcing() -> bool { }, } } + +/// Get the app ID for shortcuts inhibitor permission. +/// Returns different ID based on whether running in Flatpak or native. +/// The ID must match the installed .desktop filename, as GNOME Shell's +/// inhibitShortcutsDialog uses `Shell.WindowTracker.get_window_app(window).get_id()`. +fn get_shortcuts_inhibitor_app_id() -> String { + if is_flatpak() { + // In Flatpak, FLATPAK_ID is set automatically by the runtime to the app ID + // (e.g., "com.rustdesk.RustDesk"). This is the most reliable source. + // Fall back to constructing from app name if not available. + match std::env::var("FLATPAK_ID") { + Ok(id) if !id.is_empty() => format!("{}.desktop", id), + _ => { + let app_name = crate::get_app_name(); + format!("com.{}.{}.desktop", app_name.to_lowercase(), app_name) + } + } + } else { + format!("{}.desktop", crate::get_app_name().to_lowercase()) + } +} + +const PERMISSION_STORE_DEST: &str = "org.freedesktop.impl.portal.PermissionStore"; +const PERMISSION_STORE_PATH: &str = "/org/freedesktop/impl/portal/PermissionStore"; +const PERMISSION_STORE_IFACE: &str = "org.freedesktop.impl.portal.PermissionStore"; + +/// Clear GNOME shortcuts inhibitor permission via D-Bus. +/// This allows the permission dialog to be shown again. +pub fn clear_gnome_shortcuts_inhibitor_permission() -> ResultType<()> { + let app_id = get_shortcuts_inhibitor_app_id(); + log::info!( + "Clearing shortcuts inhibitor permission for app_id: {}, is_flatpak: {}", + app_id, + is_flatpak() + ); + + let conn = dbus::blocking::Connection::new_session()?; + let proxy = conn.with_proxy( + PERMISSION_STORE_DEST, + PERMISSION_STORE_PATH, + std::time::Duration::from_secs(3), + ); + + // DeletePermission(s table, s id, s app) -> () + let result: Result<(), dbus::Error> = proxy.method_call( + PERMISSION_STORE_IFACE, + "DeletePermission", + ("gnome", "shortcuts-inhibitor", app_id.as_str()), + ); + + match result { + Ok(()) => { + log::info!("Successfully cleared GNOME shortcuts inhibitor permission"); + Ok(()) + } + Err(e) => { + let err_name = e.name().unwrap_or(""); + // If the permission doesn't exist, that's also fine + if err_name == "org.freedesktop.portal.Error.NotFound" + || err_name == "org.freedesktop.DBus.Error.UnknownObject" + || err_name == "org.freedesktop.DBus.Error.ServiceUnknown" + { + log::info!("GNOME shortcuts inhibitor permission was not set ({})", err_name); + Ok(()) + } else { + bail!("Failed to clear permission: {}", e) + } + } + } +} + +/// Check if GNOME shortcuts inhibitor permission exists. +pub fn has_gnome_shortcuts_inhibitor_permission() -> bool { + let app_id = get_shortcuts_inhibitor_app_id(); + + let conn = match dbus::blocking::Connection::new_session() { + Ok(c) => c, + Err(e) => { + log::debug!("Failed to connect to session bus: {}", e); + return false; + } + }; + let proxy = conn.with_proxy( + PERMISSION_STORE_DEST, + PERMISSION_STORE_PATH, + std::time::Duration::from_secs(3), + ); + + // Lookup(s table, s id) -> (a{sas} permissions, v data) + // We only need the permissions dict; check if app_id is a key. + let result: Result< + ( + std::collections::HashMap>, + dbus::arg::Variant>, + ), + dbus::Error, + > = proxy.method_call( + PERMISSION_STORE_IFACE, + "Lookup", + ("gnome", "shortcuts-inhibitor"), + ); + + match result { + Ok((permissions, _)) => { + let found = permissions.contains_key(&app_id); + log::debug!( + "Shortcuts inhibitor permission lookup: app_id={}, found={}, keys={:?}", + app_id, + found, + permissions.keys().collect::>() + ); + found + } + Err(e) => { + log::debug!("Failed to query shortcuts inhibitor permission: {}", e); + false + } + } +}