diff --git a/CMakeLists.txt b/CMakeLists.txt
index e5f2042f..7eb81f5e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -15,6 +15,8 @@ option(WAYLAND "Enable wayland support" ON)
option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON)
option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON)
option(HYPRLAND "Support hyprland specific features" ON)
+option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON)
+option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
message(STATUS "Quickshell configuration")
@@ -29,6 +31,10 @@ endif ()
message(STATUS " Services")
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
message(STATUS " Hyprland: ${HYPRLAND}")
+if (HYPRLAND)
+ message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
+ message(STATUS " Global Shortcuts: ${HYPRLAND_GLOBAL_SHORTCUTS}")
+endif()
if (NOT DEFINED GIT_REVISION)
execute_process(
diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt
index 909acc00..56d7f669 100644
--- a/src/services/CMakeLists.txt
+++ b/src/services/CMakeLists.txt
@@ -1 +1,3 @@
-add_subdirectory(status_notifier)
+if (SERVICE_STATUS_NOTIFIER)
+ add_subdirectory(status_notifier)
+endif()
diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt
index 4bc0eeaa..06121a7e 100644
--- a/src/wayland/hyprland/CMakeLists.txt
+++ b/src/wayland/hyprland/CMakeLists.txt
@@ -1,7 +1,13 @@
qt_add_library(quickshell-hyprland STATIC)
qt_add_qml_module(quickshell-hyprland URI Quickshell.Hyprland VERSION 0.1)
-add_subdirectory(focus_grab)
+if (HYPRLAND_FOCUS_GRAB)
+ add_subdirectory(focus_grab)
+endif()
+
+if (HYPRLAND_GLOBAL_SHORTCUTS)
+ add_subdirectory(global_shortcuts)
+endif()
target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS})
diff --git a/src/wayland/hyprland/focus_grab/CMakeLists.txt b/src/wayland/hyprland/focus_grab/CMakeLists.txt
index 7e826aa5..587ae939 100644
--- a/src/wayland/hyprland/focus_grab/CMakeLists.txt
+++ b/src/wayland/hyprland/focus_grab/CMakeLists.txt
@@ -4,11 +4,18 @@ qt_add_library(quickshell-hyprland-focus-grab STATIC
qml.cpp
)
-qt_add_qml_module(quickshell-hyprland-focus-grab URI Quickshell.Hyprland._FocusGrab VERSION 0.1)
+qt_add_qml_module(quickshell-hyprland-focus-grab
+ URI Quickshell.Hyprland._FocusGrab
+ VERSION 0.1
+)
add_library(quickshell-hyprland-focus-grab-init OBJECT init.cpp)
-wl_proto(quickshell-hyprland-focus-grab hyprland-focus-grab-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml")
+wl_proto(quickshell-hyprland-focus-grab
+ hyprland-focus-grab-v1
+ "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml"
+)
+
target_link_libraries(quickshell-hyprland-focus-grab PRIVATE ${QT_DEPS} wayland-client)
target_link_libraries(quickshell-hyprland-focus-grab-init PRIVATE ${QT_DEPS})
@@ -16,4 +23,7 @@ qs_pch(quickshell-hyprland-focus-grab)
qs_pch(quickshell-hyprland-focus-grabplugin)
qs_pch(quickshell-hyprland-focus-grab-init)
-target_link_libraries(quickshell PRIVATE quickshell-hyprland-focus-grabplugin quickshell-hyprland-focus-grab-init)
+target_link_libraries(quickshell PRIVATE
+ quickshell-hyprland-focus-grabplugin
+ quickshell-hyprland-focus-grab-init
+)
diff --git a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt
new file mode 100644
index 00000000..804c0a3c
--- /dev/null
+++ b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt
@@ -0,0 +1,29 @@
+qt_add_library(quickshell-hyprland-global-shortcuts STATIC
+ qml.cpp
+ manager.cpp
+ shortcut.cpp
+)
+
+qt_add_qml_module(quickshell-hyprland-global-shortcuts
+ URI Quickshell.Hyprland._GlobalShortcuts
+ VERSION 0.1
+)
+
+add_library(quickshell-hyprland-global-shortcuts-init OBJECT init.cpp)
+
+wl_proto(quickshell-hyprland-global-shortcuts
+ hyprland-global-shortcuts-v1
+ "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-global-shortcuts-v1.xml"
+)
+
+target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE ${QT_DEPS} wayland-client)
+target_link_libraries(quickshell-hyprland-global-shortcuts-init PRIVATE ${QT_DEPS})
+
+qs_pch(quickshell-hyprland-global-shortcuts)
+qs_pch(quickshell-hyprland-global-shortcutsplugin)
+qs_pch(quickshell-hyprland-global-shortcuts-init)
+
+target_link_libraries(quickshell PRIVATE
+ quickshell-hyprland-global-shortcutsplugin
+ quickshell-hyprland-global-shortcuts-init
+)
diff --git a/src/wayland/hyprland/global_shortcuts/hyprland-global-shortcuts-v1.xml b/src/wayland/hyprland/global_shortcuts/hyprland-global-shortcuts-v1.xml
new file mode 100644
index 00000000..784d887e
--- /dev/null
+++ b/src/wayland/hyprland/global_shortcuts/hyprland-global-shortcuts-v1.xml
@@ -0,0 +1,112 @@
+
+
+
+ Copyright © 2022 Vaxry
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ 3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+ This protocol allows a client to register triggerable actions,
+ meant to be global shortcuts.
+
+
+
+
+ This object is a manager which offers requests to create global shortcuts.
+
+
+
+
+ Register a new global shortcut.
+
+ A global shortcut is anonymous, meaning the app does not know what key(s) trigger it.
+
+ The shortcut's keybinding shall be dealt with by the compositor.
+
+ In the case of a duplicate app_id + id combination, the already_taken protocol error is raised.
+
+
+
+
+
+
+
+
+
+
+ All objects created by the manager will still remain valid, until their
+ appropriate destroy request has been called.
+
+
+
+
+
+
+
+
+
+
+ This object represents a single shortcut.
+
+
+
+
+ The keystroke was pressed.
+
+ tv_ values hold the timestamp of the occurrence.
+
+
+
+
+
+
+
+
+ The keystroke was released.
+
+ tv_ values hold the timestamp of the occurrence.
+
+
+
+
+
+
+
+
+ Destroys the shortcut. Can be sent at any time by the client.
+
+
+
+
diff --git a/src/wayland/hyprland/global_shortcuts/init.cpp b/src/wayland/hyprland/global_shortcuts/init.cpp
new file mode 100644
index 00000000..12fed07f
--- /dev/null
+++ b/src/wayland/hyprland/global_shortcuts/init.cpp
@@ -0,0 +1,20 @@
+#include
+
+#include "../../../core/plugin.hpp"
+
+namespace {
+
+class HyprlandFocusGrabPlugin: public QuickshellPlugin {
+ void registerTypes() override {
+ qmlRegisterModuleImport(
+ "Quickshell.Hyprland",
+ QQmlModuleImportModuleAny,
+ "Quickshell.Hyprland._GlobalShortcuts",
+ QQmlModuleImportLatest
+ );
+ }
+};
+
+QS_REGISTER_PLUGIN(HyprlandFocusGrabPlugin);
+
+} // namespace
diff --git a/src/wayland/hyprland/global_shortcuts/manager.cpp b/src/wayland/hyprland/global_shortcuts/manager.cpp
new file mode 100644
index 00000000..ce0f24c4
--- /dev/null
+++ b/src/wayland/hyprland/global_shortcuts/manager.cpp
@@ -0,0 +1,55 @@
+#include "manager.hpp"
+
+#include
+#include
+
+#include "shortcut.hpp"
+
+namespace qs::hyprland::global_shortcuts::impl {
+
+GlobalShortcutManager::GlobalShortcutManager()
+ : QWaylandClientExtensionTemplate(1) {
+ this->initialize();
+}
+
+GlobalShortcut* GlobalShortcutManager::registerShortcut(
+ const QString& appid,
+ const QString& name,
+ const QString& description,
+ const QString& triggerDescription
+) {
+ auto shortcut = this->shortcuts.value({appid, name});
+
+ if (shortcut.second != nullptr) {
+ this->shortcuts.insert({appid, name}, {shortcut.first + 1, shortcut.second});
+ return shortcut.second;
+ } else {
+ auto* shortcutObj = this->register_shortcut(name, appid, description, triggerDescription);
+ auto* managedObj = new GlobalShortcut(shortcutObj);
+ this->shortcuts.insert({appid, name}, {1, managedObj});
+ return managedObj;
+ }
+}
+
+void GlobalShortcutManager::unregisterShortcut(const QString& appid, const QString& name) {
+ auto shortcut = this->shortcuts.value({appid, name});
+
+ if (shortcut.first > 1) {
+ this->shortcuts.insert({appid, name}, {shortcut.first - 1, shortcut.second});
+ } else {
+ delete shortcut.second;
+ this->shortcuts.remove({appid, name});
+ }
+}
+
+GlobalShortcutManager* GlobalShortcutManager::instance() {
+ static GlobalShortcutManager* instance = nullptr; // NOLINT
+
+ if (instance == nullptr) {
+ instance = new GlobalShortcutManager();
+ }
+
+ return instance;
+}
+
+} // namespace qs::hyprland::global_shortcuts::impl
diff --git a/src/wayland/hyprland/global_shortcuts/manager.hpp b/src/wayland/hyprland/global_shortcuts/manager.hpp
new file mode 100644
index 00000000..0a165c53
--- /dev/null
+++ b/src/wayland/hyprland/global_shortcuts/manager.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "shortcut.hpp"
+
+namespace qs::hyprland::global_shortcuts::impl {
+
+class GlobalShortcutManager
+ : public QWaylandClientExtensionTemplate
+ , public QtWayland::hyprland_global_shortcuts_manager_v1 {
+public:
+ explicit GlobalShortcutManager();
+
+ GlobalShortcut* registerShortcut(
+ const QString& appid,
+ const QString& name,
+ const QString& description,
+ const QString& triggerDescription
+ );
+
+ void unregisterShortcut(const QString& appid, const QString& name);
+
+ static GlobalShortcutManager* instance();
+
+private:
+ QHash, QPair> shortcuts;
+};
+
+} // namespace qs::hyprland::global_shortcuts::impl
diff --git a/src/wayland/hyprland/global_shortcuts/qml.cpp b/src/wayland/hyprland/global_shortcuts/qml.cpp
new file mode 100644
index 00000000..ff957eaf
--- /dev/null
+++ b/src/wayland/hyprland/global_shortcuts/qml.cpp
@@ -0,0 +1,115 @@
+#include "qml.hpp"
+#include
+
+#include
+#include
+#include
+
+#include "manager.hpp"
+#include "shortcut.hpp"
+
+namespace qs::hyprland::global_shortcuts {
+using impl::GlobalShortcutManager;
+
+GlobalShortcut::~GlobalShortcut() {
+ auto* manager = GlobalShortcutManager::instance();
+ if (manager != nullptr) {
+ manager->unregisterShortcut(this->mAppid, this->mName);
+ }
+}
+
+void GlobalShortcut::onPostReload() {
+ if (this->mName.isEmpty()) {
+ qWarning() << "Unable to create GlobalShortcut with empty name.";
+ return;
+ }
+
+ auto* manager = GlobalShortcutManager::instance();
+ if (manager == nullptr) {
+ qWarning() << "The active compositor does not support hyprland_global_shortcuts_v1.";
+ qWarning() << "GlobalShortcut will not work.";
+ return;
+ }
+
+ this->shortcut = manager->registerShortcut(
+ this->mAppid,
+ this->mName,
+ this->mDescription,
+ this->mTriggerDescription
+ );
+
+ QObject::connect(this->shortcut, &ShortcutImpl::pressed, this, &GlobalShortcut::onPressed);
+ QObject::connect(this->shortcut, &ShortcutImpl::released, this, &GlobalShortcut::onReleased);
+}
+
+bool GlobalShortcut::isPressed() const { return this->mPressed; }
+
+QString GlobalShortcut::appid() const { return this->mAppid; }
+
+void GlobalShortcut::setAppid(QString appid) {
+ if (this->shortcut != nullptr) {
+ qWarning() << "GlobalShortcut cannot be modified after creation.";
+ return;
+ }
+
+ if (appid == this->mAppid) return;
+
+ this->mAppid = std::move(appid);
+ emit this->appidChanged();
+}
+
+QString GlobalShortcut::name() const { return this->mName; }
+
+void GlobalShortcut::setName(QString name) {
+ if (this->shortcut != nullptr) {
+ qWarning() << "GlobalShortcut cannot be modified after creation.";
+ return;
+ }
+
+ if (name == this->mName) return;
+
+ this->mName = std::move(name);
+ emit this->nameChanged();
+}
+
+QString GlobalShortcut::description() const { return this->mDescription; }
+
+void GlobalShortcut::setDescription(QString description) {
+ if (this->shortcut != nullptr) {
+ qWarning() << "GlobalShortcut cannot be modified after creation.";
+ return;
+ }
+
+ if (description == this->mDescription) return;
+
+ this->mDescription = std::move(description);
+ emit this->descriptionChanged();
+}
+
+QString GlobalShortcut::triggerDescription() const { return this->mTriggerDescription; }
+
+void GlobalShortcut::setTriggerDescription(QString triggerDescription) {
+ if (this->shortcut != nullptr) {
+ qWarning() << "GlobalShortcut cannot be modified after creation.";
+ return;
+ }
+
+ if (triggerDescription == this->mTriggerDescription) return;
+
+ this->mTriggerDescription = std::move(triggerDescription);
+ emit this->triggerDescriptionChanged();
+}
+
+void GlobalShortcut::onPressed() {
+ this->mPressed = true;
+ emit this->pressed();
+ emit this->pressedChanged();
+}
+
+void GlobalShortcut::onReleased() {
+ this->mPressed = false;
+ emit this->released();
+ emit this->pressedChanged();
+}
+
+} // namespace qs::hyprland::global_shortcuts
diff --git a/src/wayland/hyprland/global_shortcuts/qml.hpp b/src/wayland/hyprland/global_shortcuts/qml.hpp
new file mode 100644
index 00000000..a43d963e
--- /dev/null
+++ b/src/wayland/hyprland/global_shortcuts/qml.hpp
@@ -0,0 +1,108 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "../../../core/reload.hpp"
+#include "shortcut.hpp"
+
+namespace qs::hyprland::global_shortcuts {
+
+///! Hyprland global shortcut.
+/// Global shortcut implemented with [hyprland_global_shortcuts_v1].
+///
+/// You can use this within hyprland as a global shortcut:
+/// ```
+/// bind = , , global, :
+/// ```
+/// See [the wiki] for details.
+///
+/// > [!WARNING] The shortcuts protocol does not allow duplicate appid + name pairs.
+/// > Within a single instance of quickshell this is handled internally, and both
+/// > users will be notified, but multiple instances of quickshell or XDPH may collide.
+/// >
+/// > If that happens, whichever client that tries to register the shortcuts last will crash.
+///
+/// > [!INFO] This type does *not* use the xdg-desktop-portal global shortcuts protocol,
+/// > as it is not fully functional without flatpak and would cause a considerably worse
+/// > user experience from other limitations. It will only work with Hyprland.
+/// > Note that, as this type bypasses xdg-desktop-portal, XDPH is not required.
+///
+/// [hyprland_global_shortcuts_v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml
+/// [the wiki]: https://wiki.hyprland.org/Configuring/Binds/#dbus-global-shortcuts
+class GlobalShortcut
+ : public QObject
+ , public PostReloadHook {
+ using ShortcutImpl = impl::GlobalShortcut;
+
+ Q_OBJECT;
+ // clang-format off
+ /// If the keybind is currently pressed.
+ Q_PROPERTY(bool pressed READ isPressed NOTIFY pressedChanged);
+ /// The appid of the shortcut. Defaults to `quickshell`.
+ /// You cannot change this at runtime.
+ ///
+ /// If you have more than one shortcut we recommend subclassing
+ /// GlobalShortcut to set this.
+ Q_PROPERTY(QString appid READ appid WRITE setAppid NOTIFY appidChanged);
+ /// The name of the shortcut.
+ /// You cannot change this at runtime.
+ Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged);
+ /// The description of the shortcut that appears in `hyprctl globalshortcuts`.
+ /// You cannot change this at runtime.
+ Q_PROPERTY(QString description READ description WRITE setDescription NOTIFY descriptionChanged);
+ /// Have not seen this used ever, but included for completeness. Safe to ignore.
+ Q_PROPERTY(QString triggerDescription READ triggerDescription WRITE setTriggerDescription NOTIFY triggerDescriptionChanged);
+ // clang-format on
+ QML_ELEMENT;
+
+public:
+ explicit GlobalShortcut(QObject* parent = nullptr): QObject(parent) {}
+ ~GlobalShortcut() override;
+ Q_DISABLE_COPY_MOVE(GlobalShortcut);
+
+ void onPostReload() override;
+
+ [[nodiscard]] bool isPressed() const;
+
+ [[nodiscard]] QString appid() const;
+ void setAppid(QString appid);
+
+ [[nodiscard]] QString name() const;
+ void setName(QString name);
+
+ [[nodiscard]] QString description() const;
+ void setDescription(QString description);
+
+ [[nodiscard]] QString triggerDescription() const;
+ void setTriggerDescription(QString triggerDescription);
+
+signals:
+ /// Fired when the keybind is pressed.
+ void pressed();
+ /// Fired when the keybind is released.
+ void released();
+
+ void pressedChanged();
+ void appidChanged();
+ void nameChanged();
+ void descriptionChanged();
+ void triggerDescriptionChanged();
+
+private slots:
+ void onPressed();
+ void onReleased();
+
+private:
+ impl::GlobalShortcut* shortcut = nullptr;
+
+ bool mPressed = false;
+ QString mAppid = "quickshell";
+ QString mName;
+ QString mDescription;
+ QString mTriggerDescription;
+};
+
+} // namespace qs::hyprland::global_shortcuts
diff --git a/src/wayland/hyprland/global_shortcuts/shortcut.cpp b/src/wayland/hyprland/global_shortcuts/shortcut.cpp
new file mode 100644
index 00000000..2178d775
--- /dev/null
+++ b/src/wayland/hyprland/global_shortcuts/shortcut.cpp
@@ -0,0 +1,33 @@
+#include "shortcut.hpp"
+
+#include
+#include
+#include
+
+namespace qs::hyprland::global_shortcuts::impl {
+
+GlobalShortcut::GlobalShortcut(::hyprland_global_shortcut_v1* shortcut) { this->init(shortcut); }
+
+GlobalShortcut::~GlobalShortcut() {
+ if (this->isInitialized()) {
+ this->destroy();
+ }
+}
+
+void GlobalShortcut::hyprland_global_shortcut_v1_pressed(
+ quint32 /*unused*/,
+ quint32 /*unused*/,
+ quint32 /*unused*/
+) {
+ emit this->pressed();
+}
+
+void GlobalShortcut::hyprland_global_shortcut_v1_released(
+ quint32 /*unused*/,
+ quint32 /*unused*/,
+ quint32 /*unused*/
+) {
+ emit this->released();
+}
+
+} // namespace qs::hyprland::global_shortcuts::impl
diff --git a/src/wayland/hyprland/global_shortcuts/shortcut.hpp b/src/wayland/hyprland/global_shortcuts/shortcut.hpp
new file mode 100644
index 00000000..6aa8fd58
--- /dev/null
+++ b/src/wayland/hyprland/global_shortcuts/shortcut.hpp
@@ -0,0 +1,32 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+namespace qs::hyprland::global_shortcuts::impl {
+
+class GlobalShortcut
+ : public QObject
+ , public QtWayland::hyprland_global_shortcut_v1 {
+ Q_OBJECT;
+
+public:
+ explicit GlobalShortcut(::hyprland_global_shortcut_v1* shortcut);
+ ~GlobalShortcut() override;
+ Q_DISABLE_COPY_MOVE(GlobalShortcut);
+
+signals:
+ void pressed();
+ void released();
+
+private:
+ // clang-format off
+ void hyprland_global_shortcut_v1_pressed(quint32 tvSecHi, quint32 tvSecLo, quint32 tvNsec) override;
+ void hyprland_global_shortcut_v1_released(quint32 tvSecHi, quint32 tvSecLo, quint32 tvNsec) override;
+ // clang-format on
+};
+
+} // namespace qs::hyprland::global_shortcuts::impl
diff --git a/src/wayland/hyprland/module.md b/src/wayland/hyprland/module.md
index f00bce37..1b3e2fbf 100644
--- a/src/wayland/hyprland/module.md
+++ b/src/wayland/hyprland/module.md
@@ -1,6 +1,7 @@
name = "Quickshell.Hyprland"
description = "Hyprland specific Quickshell types"
headers = [
- "focus_grab/qml.hpp"
+ "focus_grab/qml.hpp",
+ "global_shortcuts/qml.hpp",
]
-----