Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,34 @@ dependencies:
#### Linux requirements

- [`keybinder-3.0`](https://github.com/kupferlauncher/keybinder)
- [`xdg-desktop-portal`](https://flatpak.github.io/xdg-desktop-portal/) with
GlobalShortcuts support for Wayland sessions

Run the following command

```
sudo apt-get install keybinder-3.0
```

On X11, Linux global shortcuts are registered through `keybinder-3.0`. On
Wayland, they are registered through the desktop portal, so the compositor may
show a permission dialog the first time shortcuts are bound. Portal support for
`capsLock` and `fn` modifiers depends on the compositor and may be unavailable.
Give each system-scoped `HotKey` a stable, semantic `identifier` (for example,
`toggle-mute`) and reuse it across application launches. The portal owns the
saved binding and the plugin queries it on startup; applications only need to
persist their own `HotKey` definitions when those definitions are user-created.
The key combination is only a preference when an identifier is first bound;
later changes must be made through the desktop's shortcut configuration UI.
Adding a new identifier may show the portal dialog again. Wayland portals do
not provide a portable API for removing one saved shortcut, so `unregister`
only stops handling it in the current application session.
Recent versions of `xdg-desktop-portal` also require host applications to have
an installed `.desktop` file whose basename matches the Linux `application-id`
(for example, `com.example.MyApp.desktop` for `com.example.MyApp`). Compositors
without a GlobalShortcuts portal backend, such as some Niri/wlroots setups, will
still reject Wayland global shortcut registration.

### Usage

```dart
Expand Down Expand Up @@ -105,7 +126,8 @@ await hotKeyManager.register(
keyDownHandler: (hotKey) {
print('onKeyDown+${hotKey.toJson()}');
},
// Only works on macOS.
// Only works on macOS and on Linux Wayland sessions that support the
// GlobalShortcuts portal.
keyUpHandler: (hotKey){
print('onKeyUp+${hotKey.toJson()}');
} ,
Expand Down
10 changes: 10 additions & 0 deletions packages/hotkey_manager/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,15 @@ dev_dependencies:
sdk: flutter
mostly_reasonable_lints: ^0.1.1

dependency_overrides:
hotkey_manager_linux:
path: ../../hotkey_manager_linux
hotkey_manager_macos:
path: ../../hotkey_manager_macos
hotkey_manager_platform_interface:
path: ../../hotkey_manager_platform_interface
hotkey_manager_windows:
path: ../../hotkey_manager_windows

flutter:
uses-material-design: true
30 changes: 30 additions & 0 deletions packages/hotkey_manager_linux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,36 @@

The Linux implementation of [hotkey_manager](https://pub.dev/packages/hotkey_manager).

## Backends

On X11, global shortcuts are registered with
[`keybinder-3.0`](https://github.com/kupferlauncher/keybinder).

On Wayland, global shortcuts are registered with the
[`org.freedesktop.portal.GlobalShortcuts`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html)
desktop portal. The compositor may show a permission dialog the first time
shortcuts are bound. The portal backend emits both `onKeyDown` and `onKeyUp`
events when the compositor sends activation and deactivation signals.

Applications should reuse a stable, semantic `HotKey.identifier` for each
system shortcut across launches. The backend queries `ListShortcuts` and reuses
portal-owned bindings when all requested identifiers already exist; it calls
`BindShortcuts` only when an identifier is missing. Adding a shortcut can
therefore show the portal dialog again. A requested key combination is only a
preference for a new binding; change an existing binding through the desktop's
shortcut configuration UI. The portal has no portable API for removing one
persisted shortcut, so unregistering only stops handling that shortcut in the
current application session.

Recent versions of `xdg-desktop-portal` require host applications to have an
installed `.desktop` file whose basename matches the Linux `application-id`
(for example, `com.example.MyApp.desktop` for `com.example.MyApp`). Wayland
compositors without a GlobalShortcuts portal backend, such as some Niri/wlroots
setups, will still reject global shortcut registration.

The Wayland portal does not have portable equivalents for the `capsLock` and
`fn` modifiers, so those modifiers may be ignored by the compositor.

## License

[MIT](./LICENSE)
5 changes: 4 additions & 1 deletion packages/hotkey_manager_linux/linux/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ set(PLUGIN_NAME "hotkey_manager_linux_plugin")

# Any new source files that you add to the plugin should be added here.
list(APPEND PLUGIN_SOURCES
"hotkey_manager_keybinder_backend.cc"
"hotkey_manager_linux_backend.cc"
"hotkey_manager_linux_plugin.cc"
"hotkey_manager_portal_backend.cc"
)

# Define the plugin library target. Its name must not be changed (see comment
Expand All @@ -40,7 +43,6 @@ target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter)
target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)
target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::KEYBINDER)

pkg_check_modules(KEYBINDER IMPORTED_TARGET keybinder-3.0)
if(KEYBINDER_FOUND)
Expand Down Expand Up @@ -96,6 +98,7 @@ apply_standard_settings(${TEST_RUNNER})
target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(${TEST_RUNNER} PRIVATE flutter)
target_link_libraries(${TEST_RUNNER} PRIVATE PkgConfig::GTK)
target_link_libraries(${TEST_RUNNER} PRIVATE PkgConfig::KEYBINDER)
target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock)

# Enable automatic test discovery.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#include "hotkey_manager_keybinder_backend.h"

#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>
#include <keybinder.h>

#include <algorithm>
#include <string>
#include <vector>

namespace {

HotkeyManagerKeybinderBackend* keybinder_backend_instance = nullptr;

void HandleKeyDownCallback(const char* keystring, void* user_data) {
if (keybinder_backend_instance != nullptr) {
keybinder_backend_instance->HandleKeyDown(keystring);
}
}

guint GetMods(const std::vector<std::string>& modifiers) {
guint mods = 0;
for (const std::string& modifier : modifiers) {
guint mod = 0;
if (modifier == "alt")
mod = GDK_MOD1_MASK;
else if (modifier == "capsLock")
mod = GDK_LOCK_MASK;
else if (modifier == "control")
mod = GDK_CONTROL_MASK;
else if (modifier == "meta")
mod = GDK_META_MASK;
else if (modifier == "shift")
mod = GDK_SHIFT_MASK;
mods = mods | mod;
}
return mods;
}

} // namespace

HotkeyManagerKeybinderBackend::HotkeyManagerKeybinderBackend(
FlEventChannel* event_channel)
: event_channel_(event_channel) {
keybinder_backend_instance = this;
keybinder_init();
}

HotkeyManagerKeybinderBackend::~HotkeyManagerKeybinderBackend() {
UnbindAll();
if (keybinder_backend_instance == this) {
keybinder_backend_instance = nullptr;
}
}

FlMethodResponse* HotkeyManagerKeybinderBackend::Register(FlValue* args) {
HotkeyDefinition hotkey;
std::string error_message;
if (!ParseHotkeyDefinition(args, &hotkey, &error_message)) {
return ErrorResponse("bad-arguments", error_message);
}

g_autofree gchar* keystring = gtk_accelerator_name(
hotkey.key_code, static_cast<GdkModifierType>(GetMods(hotkey.modifiers)));
if (keystring == nullptr || keystring[0] == '\0') {
return ErrorResponse("invalid-hotkey", "Unable to create GTK accelerator.");
}

auto existing = hotkey_id_map_.find(hotkey.identifier);
if (existing != hotkey_id_map_.end()) {
keybinder_unbind(existing->second.c_str(), HandleKeyDownCallback);
hotkey_id_map_.erase(existing);
}

if (!keybinder_bind(keystring, HandleKeyDownCallback, nullptr)) {
return ErrorResponse("register-failed", "Unable to bind global hotkey.");
}

hotkey_id_map_.insert({hotkey.identifier, keystring});
return SuccessResponse();
}

FlMethodResponse* HotkeyManagerKeybinderBackend::Unregister(FlValue* args) {
FlValue* identifier_value = fl_value_lookup_string(args, "identifier");
if (identifier_value == nullptr ||
fl_value_get_type(identifier_value) != FL_VALUE_TYPE_STRING) {
return ErrorResponse("bad-arguments", "Expected a string identifier.");
}

std::string identifier = fl_value_get_string(identifier_value);
auto existing = hotkey_id_map_.find(identifier);
if (existing != hotkey_id_map_.end()) {
keybinder_unbind(existing->second.c_str(), HandleKeyDownCallback);
hotkey_id_map_.erase(existing);
}

return SuccessResponse();
}

FlMethodResponse* HotkeyManagerKeybinderBackend::UnregisterAll() {
UnbindAll();
return SuccessResponse();
}

void HotkeyManagerKeybinderBackend::HandleKeyDown(const char* keystring) {
if (keystring == nullptr) {
return;
}

std::string accelerator = keystring;
auto result = std::find_if(
hotkey_id_map_.begin(), hotkey_id_map_.end(),
[accelerator](const auto& hotkey) { return hotkey.second == accelerator; });

if (result == hotkey_id_map_.end()) {
return;
}

SendHotkeyEvent(event_channel_, "onKeyDown", result->first);
}

void HotkeyManagerKeybinderBackend::UnbindAll() {
for (const auto& hotkey : hotkey_id_map_) {
keybinder_unbind(hotkey.second.c_str(), HandleKeyDownCallback);
}
hotkey_id_map_.clear();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#ifndef FLUTTER_PLUGIN_HOTKEY_MANAGER_KEYBINDER_BACKEND_H_
#define FLUTTER_PLUGIN_HOTKEY_MANAGER_KEYBINDER_BACKEND_H_

#include <flutter_linux/flutter_linux.h>

#include <map>
#include <string>

#include "hotkey_manager_linux_backend.h"

class HotkeyManagerKeybinderBackend : public HotkeyManagerLinuxBackend {
public:
explicit HotkeyManagerKeybinderBackend(FlEventChannel* event_channel);
~HotkeyManagerKeybinderBackend() override;

FlMethodResponse* Register(FlValue* args) override;
FlMethodResponse* Unregister(FlValue* args) override;
FlMethodResponse* UnregisterAll() override;
void HandleKeyDown(const char* keystring);

private:
void UnbindAll();

FlEventChannel* event_channel_;
std::map<std::string, std::string> hotkey_id_map_;
};

#endif // FLUTTER_PLUGIN_HOTKEY_MANAGER_KEYBINDER_BACKEND_H_
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#include "hotkey_manager_linux_backend.h"

bool ParseHotkeyDefinition(FlValue* args,
HotkeyDefinition* hotkey,
std::string* error_message) {
if (args == nullptr || fl_value_get_type(args) != FL_VALUE_TYPE_MAP) {
*error_message = "Expected hotkey arguments.";
return false;
}

FlValue* identifier_value = fl_value_lookup_string(args, "identifier");
FlValue* key_code_value = fl_value_lookup_string(args, "keyCode");
FlValue* modifiers_value = fl_value_lookup_string(args, "modifiers");

if (identifier_value == nullptr ||
fl_value_get_type(identifier_value) != FL_VALUE_TYPE_STRING) {
*error_message = "Expected a string identifier.";
return false;
}
if (key_code_value == nullptr ||
fl_value_get_type(key_code_value) != FL_VALUE_TYPE_INT) {
*error_message = "Expected an integer keyCode.";
return false;
}
if (modifiers_value == nullptr ||
fl_value_get_type(modifiers_value) != FL_VALUE_TYPE_LIST) {
*error_message = "Expected a modifiers list.";
return false;
}

hotkey->identifier = fl_value_get_string(identifier_value);
hotkey->key_code = static_cast<guint>(fl_value_get_int(key_code_value));
hotkey->modifiers.clear();

for (gint i = 0; i < fl_value_get_length(modifiers_value); i++) {
FlValue* modifier_value = fl_value_get_list_value(modifiers_value, i);
if (modifier_value == nullptr ||
fl_value_get_type(modifier_value) != FL_VALUE_TYPE_STRING) {
*error_message = "Expected string modifiers.";
return false;
}
hotkey->modifiers.push_back(fl_value_get_string(modifier_value));
}

return true;
}

FlMethodResponse* SuccessResponse() {
return FL_METHOD_RESPONSE(
fl_method_success_response_new(fl_value_new_bool(true)));
}

FlMethodResponse* ErrorResponse(const char* code, const std::string& message) {
return FL_METHOD_RESPONSE(
fl_method_error_response_new(code, message.c_str(), nullptr));
}

void SendHotkeyEvent(FlEventChannel* event_channel,
const char* type,
const std::string& identifier) {
if (event_channel == nullptr || identifier.empty()) {
return;
}

FlValue* event_data = fl_value_new_map();
fl_value_set_string_take(event_data, "identifier",
fl_value_new_string(identifier.c_str()));

g_autoptr(FlValue) event = fl_value_new_map();
fl_value_set_string_take(event, "type", fl_value_new_string(type));
fl_value_set_string_take(event, "data", event_data);

fl_event_channel_send(event_channel, event, nullptr, nullptr);
}
34 changes: 34 additions & 0 deletions packages/hotkey_manager_linux/linux/hotkey_manager_linux_backend.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#ifndef FLUTTER_PLUGIN_HOTKEY_MANAGER_LINUX_BACKEND_H_
#define FLUTTER_PLUGIN_HOTKEY_MANAGER_LINUX_BACKEND_H_

#include <flutter_linux/flutter_linux.h>
#include <glib.h>

#include <string>
#include <vector>

struct HotkeyDefinition {
std::string identifier;
guint key_code;
std::vector<std::string> modifiers;
};

class HotkeyManagerLinuxBackend {
public:
virtual ~HotkeyManagerLinuxBackend() = default;

virtual FlMethodResponse* Register(FlValue* args) = 0;
virtual FlMethodResponse* Unregister(FlValue* args) = 0;
virtual FlMethodResponse* UnregisterAll() = 0;
};

bool ParseHotkeyDefinition(FlValue* args,
HotkeyDefinition* hotkey,
std::string* error_message);
FlMethodResponse* SuccessResponse();
FlMethodResponse* ErrorResponse(const char* code, const std::string& message);
void SendHotkeyEvent(FlEventChannel* event_channel,
const char* type,
const std::string& identifier);

#endif // FLUTTER_PLUGIN_HOTKEY_MANAGER_LINUX_BACKEND_H_
Loading