diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f08cfcd..71a1437b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,6 +142,7 @@ target_link_libraries(Hyprland ${CMAKE_SOURCE_DIR}/ext-workspace-unstable-v1-protocol.o ${CMAKE_SOURCE_DIR}/wlr-foreign-toplevel-management-unstable-v1-protocol.o ${CMAKE_SOURCE_DIR}/hyprland-toplevel-export-v1-protocol.o + ${CMAKE_SOURCE_DIR}/hyprland-global-shortcuts-v1-protocol.o ${CMAKE_SOURCE_DIR}/fractional-scale-v1-protocol.o ${CMAKE_SOURCE_DIR}/text-input-unstable-v1-protocol.o ${CMAKE_SOURCE_DIR}/wlr-screencopy-unstable-v1-protocol.o diff --git a/Makefile b/Makefile index 61c11449..6bcbbbbd 100644 --- a/Makefile +++ b/Makefile @@ -101,6 +101,16 @@ hyprland-toplevel-export-v1-protocol.c: hyprland-toplevel-export-v1-protocol.o: hyprland-toplevel-export-v1-protocol.h +hyprland-global-shortcuts-v1-protocol.h: + $(WAYLAND_SCANNER) server-header \ + subprojects/hyprland-protocols/protocols/hyprland-global-shortcuts-v1.xml $@ + +hyprland-global-shortcuts-v1-protocol.c: + $(WAYLAND_SCANNER) private-code \ + subprojects/hyprland-protocols/protocols/hyprland-global-shortcuts-v1.xml $@ + +hyprland-global-shortcuts-v1-protocol.o: hyprland-global-shortcuts-v1-protocol.h + linux-dmabuf-unstable-v1-protocol.h: $(WAYLAND_SCANNER) server-header \ $(WAYLAND_PROTOCOLS)/unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml $@ @@ -206,7 +216,7 @@ uninstall: rm -f ${PREFIX}/share/man/man1/Hyprland.1 rm -f ${PREFIX}/share/man/man1/hyprctl.1 -protocols: xdg-shell-protocol.o wlr-layer-shell-unstable-v1-protocol.o wlr-screencopy-unstable-v1-protocol.o idle-protocol.o ext-workspace-unstable-v1-protocol.o pointer-constraints-unstable-v1-protocol.o tablet-unstable-v2-protocol.o wlr-output-power-management-unstable-v1-protocol.o linux-dmabuf-unstable-v1-protocol.o hyprland-toplevel-export-v1-protocol.o wlr-foreign-toplevel-management-unstable-v1-protocol.o fractional-scale-v1-protocol.o text-input-unstable-v1-protocol.o +protocols: xdg-shell-protocol.o wlr-layer-shell-unstable-v1-protocol.o wlr-screencopy-unstable-v1-protocol.o idle-protocol.o ext-workspace-unstable-v1-protocol.o pointer-constraints-unstable-v1-protocol.o tablet-unstable-v2-protocol.o wlr-output-power-management-unstable-v1-protocol.o linux-dmabuf-unstable-v1-protocol.o hyprland-toplevel-export-v1-protocol.o wlr-foreign-toplevel-management-unstable-v1-protocol.o fractional-scale-v1-protocol.o text-input-unstable-v1-protocol.o hyprland-global-shortcuts-v1-protocol.o fixwlr: sed -i -E 's/(soversion = 12)([^032]|$$)/soversion = 12032/g' subprojects/wlroots/meson.build diff --git a/flake.lock b/flake.lock index 396c1da3..663d7348 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1671839510, - "narHash": "sha256-+PY1qqJfmZzzROgcIY4I7AkCwpnC+qBIYk2eFoA9RWc=", + "lastModified": 1680997116, + "narHash": "sha256-nNyoatiHmTMczrCoHCH2LIRfSF8n9ZPZ1O7WNMxcbR4=", "owner": "hyprwm", "repo": "hyprland-protocols", - "rev": "b8f55e02a328c47ed373133c52483bbfa20a1b75", + "rev": "d7d403b711b60e8136295b0d4229e89a115e80cc", "type": "github" }, "original": { diff --git a/hyprctl/main.cpp b/hyprctl/main.cpp index 14753e71..7c939a4c 100644 --- a/hyprctl/main.cpp +++ b/hyprctl/main.cpp @@ -43,6 +43,7 @@ commands: setprop plugin notify + globalshortcuts flags: -j -> output in JSON @@ -345,6 +346,8 @@ int main(int argc, char** argv) { request(fullRequest); else if (fullRequest.contains("/animations")) request(fullRequest); + else if (fullRequest.contains("/globalshortcuts")) + request(fullRequest); else if (fullRequest.contains("/switchxkblayout")) request(fullRequest, 2); else if (fullRequest.contains("/seterror")) diff --git a/protocols/meson.build b/protocols/meson.build index c7a1c038..458de862 100644 --- a/protocols/meson.build +++ b/protocols/meson.build @@ -31,7 +31,8 @@ protocols = [ ['pointer-constraints-unstable-v1.xml'], ['tablet-unstable-v2.xml'], ['idle.xml'], - [hl_protocol_dir, 'protocols/hyprland-toplevel-export-v1.xml'] + [hl_protocol_dir, 'protocols/hyprland-toplevel-export-v1.xml'], + [hl_protocol_dir, 'protocols/hyprland-global-shortcuts-v1.xml'] ] wl_protos_src = [] wl_protos_headers = [] diff --git a/src/debug/HyprCtl.cpp b/src/debug/HyprCtl.cpp index d4b765e2..bef7c68e 100644 --- a/src/debug/HyprCtl.cpp +++ b/src/debug/HyprCtl.cpp @@ -536,6 +536,29 @@ std::string animationsRequest(HyprCtl::eHyprCtlOutputFormat format) { return ret; } +std::string globalShortcutsRequest(HyprCtl::eHyprCtlOutputFormat format) { + std::string ret = ""; + const auto SHORTCUTS = g_pProtocolManager->m_pGlobalShortcutsProtocolManager->getAllShortcuts(); + if (format == HyprCtl::eHyprCtlOutputFormat::FORMAT_NORMAL) { + for (auto& sh : SHORTCUTS) + ret += getFormat("%s:%s -> %s\n", sh.appid.c_str(), sh.id.c_str(), sh.description.c_str()); + } else { + ret += "["; + for (auto& sh : SHORTCUTS) { + ret += getFormat(R"#( +{ + "name": "%s", + "description": "%s" +},)#", + escapeJSONStrings(sh.appid + ":" + sh.id).c_str(), escapeJSONStrings(sh.description).c_str()); + } + ret.pop_back(); + ret += "]\n"; + } + + return ret; +} + std::string bindsRequest(HyprCtl::eHyprCtlOutputFormat format) { std::string ret = ""; if (format == HyprCtl::eHyprCtlOutputFormat::FORMAT_NORMAL) { @@ -1228,6 +1251,8 @@ std::string getReply(std::string request) { return cursorPosRequest(format); else if (request == "binds") return bindsRequest(format); + else if (request == "globalshortcuts") + return globalShortcutsRequest(format); else if (request == "animations") return animationsRequest(format); else if (request.find("plugin") == 0) diff --git a/src/managers/KeybindManager.cpp b/src/managers/KeybindManager.cpp index 51043b04..0f99f2e4 100644 --- a/src/managers/KeybindManager.cpp +++ b/src/managers/KeybindManager.cpp @@ -63,6 +63,7 @@ CKeybindManager::CKeybindManager() { m_mDispatchers["lockgroups"] = lockGroups; m_mDispatchers["moveintogroup"] = moveIntoGroup; m_mDispatchers["moveoutofgroup"] = moveOutOfGroup; + m_mDispatchers["global"] = global; m_tScrollTimer.reset(); } @@ -352,7 +353,7 @@ bool CKeybindManager::handleKeybinds(const uint32_t& modmask, const std::string& for (auto& k : m_lKeybinds) { if (modmask != k.modmask || (g_pCompositor->m_sSeat.exclusiveClient && !k.locked) || k.submap != m_szCurrentSelectedSubmap || - (!pressed && !k.release && k.handler != "pass" && k.handler != "mouse") || k.shadowed) + (!pressed && !k.release && k.handler != "pass" && k.handler != "mouse" && k.handler != "global") || k.shadowed) continue; if (!key.empty()) { @@ -425,7 +426,10 @@ void CKeybindManager::shadowKeybinds(const xkb_keysym_t& doesntHave, const int& for (auto& k : m_lKeybinds) { - bool shadow = false; + bool shadow = false; + + if (k.handler == "global") + continue; // can't be shadowed const auto KBKEY = xkb_keysym_from_name(k.key.c_str(), XKB_KEYSYM_CASE_INSENSITIVE); const auto KBKEYUPPER = xkb_keysym_to_upper(KBKEY); @@ -2139,3 +2143,16 @@ void CKeybindManager::moveOutOfGroup(std::string args) { g_pKeybindManager->m_bGroupsLocked = GROUPSLOCKEDPREV; } + +void CKeybindManager::global(std::string args) { + const auto APPID = args.substr(0, args.find_first_of(':')); + const auto NAME = args.substr(args.find_first_of(':') + 1); + + if (APPID.empty() || NAME.empty()) + return; + + if (!g_pProtocolManager->m_pGlobalShortcutsProtocolManager->globalShortcutExists(APPID, NAME)) + return; + + g_pProtocolManager->m_pGlobalShortcutsProtocolManager->sendGlobalShortcutEvent(APPID, NAME, g_pKeybindManager->m_iPassPressed); +} \ No newline at end of file diff --git a/src/managers/KeybindManager.hpp b/src/managers/KeybindManager.hpp index 6c773a8c..9fd299b2 100644 --- a/src/managers/KeybindManager.hpp +++ b/src/managers/KeybindManager.hpp @@ -139,6 +139,7 @@ class CKeybindManager { static void lockGroups(std::string); static void moveIntoGroup(std::string); static void moveOutOfGroup(std::string); + static void global(std::string); friend class CCompositor; friend class CInputManager; diff --git a/src/managers/ProtocolManager.cpp b/src/managers/ProtocolManager.cpp index d9aa299f..205f3d02 100644 --- a/src/managers/ProtocolManager.cpp +++ b/src/managers/ProtocolManager.cpp @@ -4,5 +4,6 @@ CProtocolManager::CProtocolManager() { m_pToplevelExportProtocolManager = std::make_unique(); m_pFractionalScaleProtocolManager = std::make_unique(); m_pTextInputV1ProtocolManager = std::make_unique(); + m_pGlobalShortcutsProtocolManager = std::make_unique(); m_pScreencopyProtocolManager = std::make_unique(); } \ No newline at end of file diff --git a/src/managers/ProtocolManager.hpp b/src/managers/ProtocolManager.hpp index 340785ec..bc6b776e 100644 --- a/src/managers/ProtocolManager.hpp +++ b/src/managers/ProtocolManager.hpp @@ -4,6 +4,7 @@ #include "../protocols/ToplevelExport.hpp" #include "../protocols/FractionalScale.hpp" #include "../protocols/TextInputV1.hpp" +#include "../protocols/GlobalShortcuts.hpp" #include "../protocols/Screencopy.hpp" class CProtocolManager { @@ -13,6 +14,7 @@ class CProtocolManager { std::unique_ptr m_pToplevelExportProtocolManager; std::unique_ptr m_pFractionalScaleProtocolManager; std::unique_ptr m_pTextInputV1ProtocolManager; + std::unique_ptr m_pGlobalShortcutsProtocolManager; std::unique_ptr m_pScreencopyProtocolManager; }; diff --git a/src/protocols/GlobalShortcuts.cpp b/src/protocols/GlobalShortcuts.cpp new file mode 100644 index 00000000..b376cae3 --- /dev/null +++ b/src/protocols/GlobalShortcuts.cpp @@ -0,0 +1,151 @@ +#include "GlobalShortcuts.hpp" +#include "../Compositor.hpp" + +#define GLOBAL_SHORTCUTS_VERSION 1 + +static void bindManagerInt(wl_client* client, void* data, uint32_t version, uint32_t id) { + g_pProtocolManager->m_pGlobalShortcutsProtocolManager->bindManager(client, data, version, id); +} + +static void handleDisplayDestroy(struct wl_listener* listener, void* data) { + g_pProtocolManager->m_pGlobalShortcutsProtocolManager->displayDestroy(); +} + +void CGlobalShortcutsProtocolManager::displayDestroy() { + wl_global_destroy(m_pGlobal); +} + +CGlobalShortcutsProtocolManager::CGlobalShortcutsProtocolManager() { + m_pGlobal = wl_global_create(g_pCompositor->m_sWLDisplay, &hyprland_global_shortcuts_manager_v1_interface, GLOBAL_SHORTCUTS_VERSION, this, bindManagerInt); + + if (!m_pGlobal) { + Debug::log(ERR, "GlobalShortcutsManager could not start!"); + return; + } + + m_liDisplayDestroy.notify = handleDisplayDestroy; + wl_display_add_destroy_listener(g_pCompositor->m_sWLDisplay, &m_liDisplayDestroy); + + Debug::log(LOG, "GlobalShortcutsManager started successfully!"); +} + +static void handleRegisterShortcut(wl_client* client, wl_resource* resource, uint32_t shortcut, const char* id, const char* app_id, const char* description, + const char* trigger_description) { + g_pProtocolManager->m_pGlobalShortcutsProtocolManager->registerShortcut(client, resource, shortcut, id, app_id, description, trigger_description); +} + +static void handleDestroy(wl_client* client, wl_resource* resource) { + wl_resource_destroy(resource); +} + +static const struct hyprland_global_shortcuts_manager_v1_interface globalShortcutsManagerImpl = { + .register_shortcut = handleRegisterShortcut, + .destroy = handleDestroy, +}; + +static const struct hyprland_global_shortcut_v1_interface shortcutImpl = { + .destroy = handleDestroy, +}; + +void CGlobalShortcutsProtocolManager::bindManager(wl_client* client, void* data, uint32_t version, uint32_t id) { + const auto RESOURCE = wl_resource_create(client, &hyprland_global_shortcuts_manager_v1_interface, version, id); + wl_resource_set_implementation(RESOURCE, &globalShortcutsManagerImpl, this, nullptr); + + Debug::log(LOG, "GlobalShortcutsManager bound successfully!"); + + m_vClients.emplace_back(std::make_unique(client)); +} + +SShortcutClient* CGlobalShortcutsProtocolManager::clientFromWlClient(wl_client* client) { + for (auto& c : m_vClients) { + if (c->client == client) { + return c.get(); + } + } + + return nullptr; +} + +static void onShortcutDestroy(wl_resource* pResource) { + g_pProtocolManager->m_pGlobalShortcutsProtocolManager->destroyShortcut(pResource); +} + +void CGlobalShortcutsProtocolManager::registerShortcut(wl_client* client, wl_resource* resource, uint32_t shortcut, const char* id, const char* app_id, const char* description, + const char* trigger_description) { + const auto PCLIENT = clientFromWlClient(client); + + if (!PCLIENT) { + Debug::log(ERR, "Error at global shortcuts: no client in register?"); + return; + } + + for (auto& c : m_vClients) { + for (auto& sh : c->shortcuts) { + if (sh->appid == app_id && sh->id == id) { + wl_resource_post_error(resource, HYPRLAND_GLOBAL_SHORTCUTS_MANAGER_V1_ERROR_ALREADY_TAKEN, "Combination is taken"); + return; + } + } + } + + const auto PSHORTCUT = PCLIENT->shortcuts.emplace_back(std::make_unique()).get(); + PSHORTCUT->id = id; + PSHORTCUT->description = description; + PSHORTCUT->appid = app_id; + PSHORTCUT->shortcut = shortcut; + + PSHORTCUT->resource = wl_resource_create(client, &hyprland_global_shortcut_v1_interface, 1, shortcut); + if (!PSHORTCUT->resource) { + wl_client_post_no_memory(client); + std::erase_if(PCLIENT->shortcuts, [&](const auto& other) { return other.get() == PSHORTCUT; }); + return; + } + + wl_resource_set_implementation(PSHORTCUT->resource, &shortcutImpl, this, &onShortcutDestroy); +} + +bool CGlobalShortcutsProtocolManager::globalShortcutExists(std::string appid, std::string trigger) { + for (auto& c : m_vClients) { + for (auto& sh : c->shortcuts) { + if (sh->appid == appid && sh->id == trigger) { + return true; + } + } + } + + return false; +} + +void CGlobalShortcutsProtocolManager::sendGlobalShortcutEvent(std::string appid, std::string trigger, bool pressed) { + for (auto& c : m_vClients) { + for (auto& sh : c->shortcuts) { + if (sh->appid == appid && sh->id == trigger) { + timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + uint32_t tvSecHi = (sizeof(now.tv_sec) > 4) ? now.tv_sec >> 32 : 0; + uint32_t tvSecLo = now.tv_sec & 0xFFFFFFFF; + if (pressed) + hyprland_global_shortcut_v1_send_pressed(sh->resource, tvSecHi, tvSecLo, now.tv_nsec); + else + hyprland_global_shortcut_v1_send_released(sh->resource, tvSecHi, tvSecLo, now.tv_nsec); + } + } + } +} + +std::vector CGlobalShortcutsProtocolManager::getAllShortcuts() { + std::vector copy; + for (auto& c : m_vClients) { + for (auto& sh : c->shortcuts) { + copy.push_back(*sh); + } + } + + return copy; +} + +void CGlobalShortcutsProtocolManager::destroyShortcut(wl_resource* resource) { + for (auto& c : m_vClients) { + std::erase_if(c->shortcuts, [&](const auto& other) { return other->resource == resource; }); + } +} \ No newline at end of file diff --git a/src/protocols/GlobalShortcuts.hpp b/src/protocols/GlobalShortcuts.hpp new file mode 100644 index 00000000..07c484c6 --- /dev/null +++ b/src/protocols/GlobalShortcuts.hpp @@ -0,0 +1,39 @@ +#pragma once +#include "../defines.hpp" +#include "hyprland-global-shortcuts-v1-protocol.h" +#include + +struct SShortcut { + wl_resource* resource; + std::string id, description, appid; + uint32_t shortcut = 0; +}; + +struct SShortcutClient { + wl_client* client = nullptr; + std::vector> shortcuts; +}; + +class CGlobalShortcutsProtocolManager { + public: + CGlobalShortcutsProtocolManager(); + void bindManager(wl_client* client, void* data, uint32_t version, uint32_t id); + void displayDestroy(); + + void registerShortcut(wl_client* client, wl_resource* resource, uint32_t shortcut, const char* id, const char* app_id, const char* description, + const char* trigger_description); + void destroyShortcut(wl_resource* resource); + + bool globalShortcutExists(std::string appid, std::string trigger); + void sendGlobalShortcutEvent(std::string appid, std::string trigger, bool pressed); + + std::vector getAllShortcuts(); + + private: + std::vector> m_vClients; + + SShortcutClient* clientFromWlClient(wl_client* client); + + wl_global* m_pGlobal = nullptr; + wl_listener m_liDisplayDestroy; +}; \ No newline at end of file diff --git a/subprojects/hyprland-protocols b/subprojects/hyprland-protocols index 301733ae..d7d403b7 160000 --- a/subprojects/hyprland-protocols +++ b/subprojects/hyprland-protocols @@ -1 +1 @@ -Subproject commit 301733ae466b229066ba15a53e6d8b91c5dcef5b +Subproject commit d7d403b711b60e8136295b0d4229e89a115e80cc