hyprland/global_shortcuts: add GlobalShortcut

This commit is contained in:
outfoxxed 2024-05-06 22:05:46 -07:00
parent 87a884ca36
commit bba8cb8a7d
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
14 changed files with 569 additions and 6 deletions

View file

@ -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(

View file

@ -1 +1,3 @@
if (SERVICE_STATUS_NOTIFIER)
add_subdirectory(status_notifier)
endif()

View file

@ -1,7 +1,13 @@
qt_add_library(quickshell-hyprland STATIC)
qt_add_qml_module(quickshell-hyprland URI Quickshell.Hyprland VERSION 0.1)
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})

View file

@ -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
)

View file

@ -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
)

View file

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="hyprland_global_shortcuts_v1">
<copyright>
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.
</copyright>
<description summary="registering global shortcuts">
This protocol allows a client to register triggerable actions,
meant to be global shortcuts.
</description>
<interface name="hyprland_global_shortcuts_manager_v1" version="1">
<description summary="manager to register global shortcuts">
This object is a manager which offers requests to create global shortcuts.
</description>
<request name="register_shortcut">
<description summary="register a shortcut">
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.
</description>
<arg name="shortcut" type="new_id" interface="hyprland_global_shortcut_v1"/>
<arg name="id" type="string" summary="a unique id for the shortcut"/>
<arg name="app_id" type="string" summary="the app_id of the application requesting the shortcut"/>
<arg name="description" type="string" summary="user-readable text describing what the shortcut does."/>
<arg name="trigger_description" type="string" summary="user-readable text describing how to trigger the shortcut for the client to render."/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the manager">
All objects created by the manager will still remain valid, until their
appropriate destroy request has been called.
</description>
</request>
<enum name="error">
<entry name="already_taken" value="0"
summary="the app_id + id combination has already been registered."/>
</enum>
</interface>
<interface name="hyprland_global_shortcut_v1" version="1">
<description summary="a shortcut">
This object represents a single shortcut.
</description>
<event name="pressed">
<description summary="keystroke pressed">
The keystroke was pressed.
tv_ values hold the timestamp of the occurrence.
</description>
<arg name="tv_sec_hi" type="uint"
summary="high 32 bits of the seconds part of the timestamp"/>
<arg name="tv_sec_lo" type="uint"
summary="low 32 bits of the seconds part of the timestamp"/>
<arg name="tv_nsec" type="uint"
summary="nanoseconds part of the timestamp"/>
</event>
<event name="released">
<description summary="keystroke released">
The keystroke was released.
tv_ values hold the timestamp of the occurrence.
</description>
<arg name="tv_sec_hi" type="uint"
summary="high 32 bits of the seconds part of the timestamp"/>
<arg name="tv_sec_lo" type="uint"
summary="low 32 bits of the seconds part of the timestamp"/>
<arg name="tv_nsec" type="uint"
summary="nanoseconds part of the timestamp"/>
</event>
<request name="destroy" type="destructor">
<description summary="delete this object, used or not">
Destroys the shortcut. Can be sent at any time by the client.
</description>
</request>
</interface>
</protocol>

View file

@ -0,0 +1,20 @@
#include <qqml.h>
#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

View file

@ -0,0 +1,55 @@
#include "manager.hpp"
#include <qstring.h>
#include <qwaylandclientextension.h>
#include "shortcut.hpp"
namespace qs::hyprland::global_shortcuts::impl {
GlobalShortcutManager::GlobalShortcutManager()
: QWaylandClientExtensionTemplate<GlobalShortcutManager>(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

View file

@ -0,0 +1,34 @@
#pragma once
#include <qcontainerfwd.h>
#include <qhash.h>
#include <qstring.h>
#include <qwayland-hyprland-global-shortcuts-v1.h>
#include <qwaylandclientextension.h>
#include "shortcut.hpp"
namespace qs::hyprland::global_shortcuts::impl {
class GlobalShortcutManager
: public QWaylandClientExtensionTemplate<GlobalShortcutManager>
, 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<QString, QString>, QPair<qint32, GlobalShortcut*>> shortcuts;
};
} // namespace qs::hyprland::global_shortcuts::impl

View file

@ -0,0 +1,115 @@
#include "qml.hpp"
#include <utility>
#include <qlogging.h>
#include <qobject.h>
#include <qtmetamacros.h>
#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

View file

@ -0,0 +1,108 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#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 = <modifiers>, <key>, global, <appid>:<name>
/// ```
/// 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

View file

@ -0,0 +1,33 @@
#include "shortcut.hpp"
#include <qtmetamacros.h>
#include <qtypes.h>
#include <wayland-hyprland-global-shortcuts-v1-client-protocol.h>
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

View file

@ -0,0 +1,32 @@
#pragma once
#include <qobject.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwayland-hyprland-global-shortcuts-v1.h>
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

View file

@ -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",
]
-----