summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/KeybindsService.qml
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/KeybindsService.qml')
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/KeybindsService.qml528
1 files changed, 528 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/KeybindsService.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/KeybindsService.qml
new file mode 100644
index 0000000..470831b
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Services/KeybindsService.qml
@@ -0,0 +1,528 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import QtCore
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Wayland
+// ! Even though qmlls says this is unused, it is wrong
+import qs.Common
+import "../Common/KeybindActions.js" as Actions
+
+Singleton {
+ id: root
+
+ Component.onCompleted: {
+ if (!shortcutInhibitorAvailable) {
+ console.warn("[KeybindsService] ShortcutInhibitor is not available in this environment, keybinds editor disabled.");
+ }
+ }
+
+ readonly property bool shortcutInhibitorAvailable: {
+ try {
+ return typeof ShortcutInhibitor !== "undefined";
+ } catch (e) {
+ return false;
+ }
+ }
+
+ property bool available: (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl) && shortcutInhibitorAvailable
+ property string currentProvider: {
+ if (CompositorService.isNiri)
+ return "niri";
+ if (CompositorService.isHyprland)
+ return "hyprland";
+ if (CompositorService.isDwl)
+ return "mangowc";
+ return "";
+ }
+
+ readonly property string cheatsheetProvider: {
+ if (CompositorService.isNiri)
+ return "niri";
+ if (CompositorService.isHyprland)
+ return "hyprland";
+ if (CompositorService.isDwl)
+ return "mangowc";
+ return "";
+ }
+ property bool cheatsheetAvailable: cheatsheetProvider !== ""
+ property bool cheatsheetLoading: false
+ property var cheatsheet: ({})
+
+ property bool loading: false
+ property bool saving: false
+ property bool fixing: false
+ property string lastError: ""
+ property bool dmsBindsIncluded: true
+
+ property var dmsStatus: ({
+ "exists": true,
+ "included": true,
+ "includePosition": -1,
+ "totalIncludes": 0,
+ "bindsAfterDms": 0,
+ "effective": true,
+ "overriddenBy": 0,
+ "statusMessage": ""
+ })
+
+ property var _rawData: null
+ property var keybinds: ({})
+ property var _allBinds: ({})
+ property var _categories: []
+ property var _flatCache: []
+ property var displayList: []
+ property int _dataVersion: 0
+ property string _pendingSavedKey: ""
+
+ readonly property var categoryOrder: Actions.getCategoryOrder()
+ readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
+ readonly property string compositorConfigDir: {
+ switch (currentProvider) {
+ case "niri":
+ return configDir + "/niri";
+ case "hyprland":
+ return configDir + "/hypr";
+ case "mangowc":
+ return configDir + "/mango";
+ default:
+ return "";
+ }
+ }
+ readonly property string dmsBindsPath: {
+ switch (currentProvider) {
+ case "niri":
+ return compositorConfigDir + "/dms/binds.kdl";
+ case "hyprland":
+ case "mangowc":
+ return compositorConfigDir + "/dms/binds.conf";
+ default:
+ return "";
+ }
+ }
+ readonly property string mainConfigPath: {
+ switch (currentProvider) {
+ case "niri":
+ return compositorConfigDir + "/config.kdl";
+ case "hyprland":
+ return compositorConfigDir + "/hyprland.conf";
+ case "mangowc":
+ return compositorConfigDir + "/config.conf";
+ default:
+ return "";
+ }
+ }
+ readonly property var actionTypes: Actions.getActionTypes()
+ readonly property var dmsActions: getDmsActions()
+
+ signal bindsLoaded
+ signal bindSaved(string key)
+ signal bindSaveCompleted(bool success)
+ signal bindRemoved(string key)
+ signal dmsBindsFixed
+ signal cheatsheetLoaded
+
+ Connections {
+ target: CompositorService
+ function onCompositorChanged() {
+ if (!CompositorService.isNiri)
+ return;
+ Qt.callLater(root.loadBinds);
+ }
+ }
+
+ Connections {
+ target: NiriService
+ enabled: CompositorService.isNiri
+ function onConfigReloaded() {
+ Qt.callLater(root.loadBinds, false);
+ }
+ }
+
+ Process {
+ id: cheatsheetProcess
+ running: false
+
+ stdout: StdioCollector {
+ onStreamFinished: {
+ try {
+ root.cheatsheet = JSON.parse(text);
+ } catch (e) {
+ console.error("[KeybindsService] Failed to parse cheatsheet:", e);
+ root.cheatsheet = {};
+ }
+ root.cheatsheetLoading = false;
+ root.cheatsheetLoaded();
+ }
+ }
+
+ onExited: exitCode => {
+ if (exitCode === 0)
+ return;
+ console.warn("[KeybindsService] Cheatsheet load failed with code:", exitCode);
+ root.cheatsheetLoading = false;
+ }
+ }
+
+ Process {
+ id: loadProcess
+ running: false
+
+ stdout: StdioCollector {
+ onStreamFinished: {
+ try {
+ root._rawData = JSON.parse(text);
+ root._processData();
+ } catch (e) {
+ console.error("[KeybindsService] Failed to parse binds:", e);
+ }
+ root.loading = false;
+ }
+ }
+
+ onExited: exitCode => {
+ if (exitCode !== 0) {
+ console.warn("[KeybindsService] Load process failed with code:", exitCode);
+ root.loading = false;
+ }
+ }
+ }
+
+ Process {
+ id: saveProcess
+ running: false
+
+ stderr: StdioCollector {
+ onStreamFinished: {
+ if (!text.trim())
+ return;
+ root.lastError = text.trim();
+ ToastService.showError(I18n.tr("Failed to save keybind"), "", root.lastError, "keybinds");
+ }
+ }
+
+ onExited: exitCode => {
+ root.saving = false;
+ if (exitCode !== 0) {
+ console.error("[KeybindsService] Save failed with code:", exitCode);
+ root.bindSaveCompleted(false);
+ return;
+ }
+ root.lastError = "";
+ root.bindSaveCompleted(true);
+ root.loadBinds(false);
+ }
+ }
+
+ Process {
+ id: removeProcess
+ running: false
+
+ stderr: StdioCollector {
+ onStreamFinished: {
+ if (!text.trim())
+ return;
+ root.lastError = text.trim();
+ ToastService.showError(I18n.tr("Failed to remove keybind"), "", root.lastError, "keybinds");
+ }
+ }
+
+ onExited: exitCode => {
+ if (exitCode !== 0) {
+ console.error("[KeybindsService] Remove failed with code:", exitCode);
+ return;
+ }
+ root.lastError = "";
+ root.loadBinds(false);
+ }
+ }
+
+ Process {
+ id: fixProcess
+ running: false
+
+ stderr: StdioCollector {
+ onStreamFinished: {
+ if (!text.trim())
+ return;
+ root.lastError = text.trim();
+ ToastService.showError(I18n.tr("Failed to add binds include"), "", root.lastError, "keybinds");
+ }
+ }
+
+ onExited: exitCode => {
+ root.fixing = false;
+ if (exitCode !== 0) {
+ console.error("[KeybindsService] Fix failed with code:", exitCode);
+ return;
+ }
+ root.lastError = "";
+ root.dmsBindsIncluded = true;
+ root.dmsBindsFixed();
+ const bindsFile = root.currentProvider === "niri" ? "dms/binds.kdl" : "dms/binds.conf";
+ ToastService.showInfo(I18n.tr("Binds include added"), I18n.tr("%1 is now included in config").arg(bindsFile), "", "keybinds");
+ Qt.callLater(root.forceReload);
+ }
+ }
+
+ function fixDmsBindsInclude() {
+ if (fixing || dmsBindsIncluded || !compositorConfigDir)
+ return;
+ fixing = true;
+ const timestamp = Math.floor(Date.now() / 1000);
+ const backupPath = `${mainConfigPath}.dmsbackup${timestamp}`;
+ let script;
+ switch (currentProvider) {
+ case "niri":
+ script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.kdl" && cp "${mainConfigPath}" "${backupPath}" && echo 'include "dms/binds.kdl"' >> "${mainConfigPath}"`;
+ break;
+ case "hyprland":
+ script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.conf" && cp "${mainConfigPath}" "${backupPath}" && echo 'source = ./dms/binds.conf' >> "${mainConfigPath}"`;
+ break;
+ case "mangowc":
+ script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.conf" && cp "${mainConfigPath}" "${backupPath}" && echo 'source = ./dms/binds.conf' >> "${mainConfigPath}"`;
+ break;
+ default:
+ fixing = false;
+ return;
+ }
+ fixProcess.command = ["sh", "-c", script];
+ fixProcess.running = true;
+ }
+
+ function forceReload() {
+ _allBinds = {};
+ _flatCache = [];
+ _categories = [];
+ loadBinds(true);
+ }
+
+ function loadCheatsheet(provider) {
+ if (cheatsheetProcess.running)
+ return;
+ const target = provider || cheatsheetProvider;
+ if (!target)
+ return;
+ cheatsheetLoading = true;
+ cheatsheetProcess.command = ["dms", "keybinds", "show", target];
+ cheatsheetProcess.running = true;
+ }
+
+ function loadBinds(showLoading) {
+ if (loadProcess.running || !available)
+ return;
+ const hasData = Object.keys(_allBinds).length > 0;
+ loading = showLoading !== false && !hasData;
+ loadProcess.command = ["dms", "keybinds", "show", currentProvider];
+ loadProcess.running = true;
+ }
+
+ function _processData() {
+ keybinds = _rawData || {};
+ dmsBindsIncluded = _rawData?.dmsBindsIncluded ?? true;
+ const status = _rawData?.dmsStatus;
+ if (status) {
+ dmsStatus = {
+ "exists": status.exists ?? true,
+ "included": status.included ?? true,
+ "includePosition": status.includePosition ?? -1,
+ "totalIncludes": status.totalIncludes ?? 0,
+ "bindsAfterDms": status.bindsAfterDms ?? 0,
+ "effective": status.effective ?? true,
+ "overriddenBy": status.overriddenBy ?? 0,
+ "statusMessage": status.statusMessage ?? ""
+ };
+ }
+
+ if (!_rawData?.binds) {
+ _allBinds = {};
+ _categories = [];
+ _flatCache = [];
+ displayList = [];
+ _dataVersion++;
+ bindsLoaded();
+ if (_pendingSavedKey) {
+ bindSaved(_pendingSavedKey);
+ _pendingSavedKey = "";
+ }
+ return;
+ }
+
+ const processed = {};
+ const bindsData = _rawData.binds;
+ for (const cat in bindsData) {
+ const binds = bindsData[cat];
+ for (var i = 0; i < binds.length; i++) {
+ const bind = binds[i];
+ const targetCat = Actions.isDmsAction(bind.action) ? "DMS" : cat;
+ if (!processed[targetCat])
+ processed[targetCat] = [];
+ processed[targetCat].push(bind);
+ }
+ }
+
+ const sortedCats = Object.keys(processed).sort((a, b) => {
+ const ai = categoryOrder.indexOf(a);
+ const bi = categoryOrder.indexOf(b);
+ return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
+ });
+
+ const grouped = [];
+ const actionMap = {};
+ for (var ci = 0; ci < sortedCats.length; ci++) {
+ const category = sortedCats[ci];
+ const binds = processed[category];
+ if (!binds)
+ continue;
+ for (var i = 0; i < binds.length; i++) {
+ const bind = binds[i];
+ const action = bind.action || "";
+ const keyData = {
+ "key": bind.key || "",
+ "source": bind.source || "config",
+ "isOverride": bind.source === "dms",
+ "cooldownMs": bind.cooldownMs || 0,
+ "flags": bind.flags || "",
+ "allowWhenLocked": bind.allowWhenLocked || false,
+ "allowInhibiting": bind.allowInhibiting,
+ "repeat": bind.repeat
+ };
+ if (actionMap[action]) {
+ actionMap[action].keys.push(keyData);
+ if (!actionMap[action].desc && bind.desc)
+ actionMap[action].desc = bind.desc;
+ if (!actionMap[action].conflict && bind.conflict)
+ actionMap[action].conflict = bind.conflict;
+ } else {
+ const entry = {
+ "category": category,
+ "action": action,
+ "desc": bind.desc || "",
+ "keys": [keyData],
+ "conflict": bind.conflict || null
+ };
+ actionMap[action] = entry;
+ grouped.push(entry);
+ }
+ }
+ }
+
+ const list = [];
+ for (const cat of sortedCats) {
+ list.push({
+ "id": "cat:" + cat,
+ "type": "category",
+ "name": cat
+ });
+ const binds = processed[cat];
+ if (!binds)
+ continue;
+ for (const bind of binds)
+ list.push({
+ "id": "bind:" + bind.key,
+ "type": "bind",
+ "key": bind.key,
+ "desc": bind.desc
+ });
+ }
+
+ _allBinds = processed;
+ _categories = sortedCats;
+ _flatCache = grouped;
+ displayList = list;
+ _dataVersion++;
+ bindsLoaded();
+ if (_pendingSavedKey) {
+ bindSaved(_pendingSavedKey);
+ _pendingSavedKey = "";
+ }
+ }
+
+ function getCategories() {
+ return _categories;
+ }
+
+ function getFlatBinds() {
+ return _flatCache;
+ }
+
+ function saveBind(originalKey, bindData) {
+ if (!bindData.key || !Actions.isValidAction(bindData.action))
+ return;
+ saving = true;
+ const cmd = ["dms", "keybinds", "set", currentProvider, bindData.key, bindData.action, "--desc", bindData.desc || ""];
+ if (originalKey && originalKey !== bindData.key)
+ cmd.push("--replace-key", originalKey);
+ if (bindData.cooldownMs > 0)
+ cmd.push("--cooldown-ms", String(bindData.cooldownMs));
+ if (bindData.allowWhenLocked)
+ cmd.push("--allow-when-locked");
+ if (bindData.repeat === false)
+ cmd.push("--no-repeat");
+ if (bindData.allowInhibiting === false)
+ cmd.push("--no-inhibiting");
+ if (bindData.flags)
+ cmd.push("--flags", bindData.flags);
+ saveProcess.command = cmd;
+ saveProcess.running = true;
+ _pendingSavedKey = bindData.key;
+ }
+
+ function removeBind(key) {
+ if (!key)
+ return;
+ removeProcess.command = ["dms", "keybinds", "remove", currentProvider, key];
+ removeProcess.running = true;
+ bindRemoved(key);
+ }
+
+ function isDmsAction(action) {
+ return Actions.isDmsAction(action);
+ }
+
+ function isValidAction(action) {
+ return Actions.isValidAction(action);
+ }
+
+ function getActionType(action) {
+ return Actions.getActionType(action);
+ }
+
+ function getActionLabel(action) {
+ return Actions.getActionLabel(action, currentProvider);
+ }
+
+ function getCompositorCategories() {
+ return Actions.getCompositorCategories(currentProvider);
+ }
+
+ function getCompositorActions(category) {
+ return Actions.getCompositorActions(currentProvider, category);
+ }
+
+ function getDmsActions() {
+ return Actions.getDmsActions(CompositorService.isNiri, CompositorService.isHyprland);
+ }
+
+ function buildSpawnAction(command, args) {
+ return Actions.buildSpawnAction(command, args);
+ }
+
+ function buildShellAction(shellCmd, shell) {
+ return Actions.buildShellAction(currentProvider, shellCmd, shell);
+ }
+
+ function getShellFromAction(action) {
+ return Actions.getShellFromAction(action);
+ }
+
+ function parseSpawnCommand(action) {
+ return Actions.parseSpawnCommand(action);
+ }
+
+ function parseShellCommand(action) {
+ return Actions.parseShellCommand(action);
+ }
+}