From e41961025e056b4d638e581c2a603bdb66bcdf1c Mon Sep 17 00:00:00 2001 From: blacku Date: Tue, 21 Jul 2026 03:36:15 +0200 Subject: [PATCH] feat: add Keymap plugin (#68) --- keymap/README.md | 358 ++ keymap/command_library.json | 2915 ++++++++++++++++ keymap/examples/hyprland.lua | 56 + keymap/examples/mangowc.conf | 56 + keymap/examples/niri.kdl | 57 + keymap/mangowc_service.luau | 967 ++++++ keymap/niri_service.luau | 959 +++++ keymap/panel.luau | 3281 ++++++++++++++++++ keymap/plugin.toml | 229 ++ keymap/screenshots/command-library.webp | Bin 0 -> 47838 bytes keymap/screenshots/creator.webp | Bin 0 -> 65496 bytes keymap/screenshots/editor.webp | Bin 0 -> 70100 bytes keymap/screenshots/keyboard-100.webp | Bin 0 -> 33634 bytes keymap/screenshots/mangowc-list.webp | Bin 0 -> 61526 bytes keymap/screenshots/niri-list.webp | Bin 0 -> 56584 bytes keymap/screenshots/settings.webp | Bin 0 -> 73950 bytes keymap/service.luau | 1109 ++++++ keymap/tests/category_marker_parser_test.lua | 121 + keymap/tests/command_library_test.py | 68 + keymap/tests/example_configs_test.lua | 166 + keymap/tests/hidden_sentinel_parser_test.lua | 115 + keymap/tests/hypr_command_parser_test.lua | 67 + keymap/tests/i18n_test.py | 41 + keymap/tests/keyboard_layouts_test.lua | 118 + keymap/tests/writer_create_test.lua | 413 +++ keymap/tests/writer_update_test.lua | 1009 ++++++ keymap/thumbnail.webp | Bin 0 -> 48214 bytes keymap/translations/en.json | 459 +++ keymap/widget.luau | 32 + keymap/writer_service.luau | 1951 +++++++++++ 30 files changed, 14547 insertions(+) create mode 100644 keymap/README.md create mode 100644 keymap/command_library.json create mode 100644 keymap/examples/hyprland.lua create mode 100644 keymap/examples/mangowc.conf create mode 100644 keymap/examples/niri.kdl create mode 100644 keymap/mangowc_service.luau create mode 100644 keymap/niri_service.luau create mode 100644 keymap/panel.luau create mode 100644 keymap/plugin.toml create mode 100644 keymap/screenshots/command-library.webp create mode 100644 keymap/screenshots/creator.webp create mode 100644 keymap/screenshots/editor.webp create mode 100644 keymap/screenshots/keyboard-100.webp create mode 100644 keymap/screenshots/mangowc-list.webp create mode 100644 keymap/screenshots/niri-list.webp create mode 100644 keymap/screenshots/settings.webp create mode 100644 keymap/service.luau create mode 100644 keymap/tests/category_marker_parser_test.lua create mode 100644 keymap/tests/command_library_test.py create mode 100644 keymap/tests/example_configs_test.lua create mode 100644 keymap/tests/hidden_sentinel_parser_test.lua create mode 100644 keymap/tests/hypr_command_parser_test.lua create mode 100644 keymap/tests/i18n_test.py create mode 100644 keymap/tests/keyboard_layouts_test.lua create mode 100644 keymap/tests/writer_create_test.lua create mode 100644 keymap/tests/writer_update_test.lua create mode 100644 keymap/thumbnail.webp create mode 100644 keymap/translations/en.json create mode 100644 keymap/widget.luau create mode 100644 keymap/writer_service.luau diff --git a/keymap/README.md b/keymap/README.md new file mode 100644 index 0000000..6861cec --- /dev/null +++ b/keymap/README.md @@ -0,0 +1,358 @@ +# Keymap + +Keymap is a searchable, theme-aware shortcut viewer and editor for Noctalia. +It supports Hyprland's native Lua configuration, Niri, and MangoWC, follows +split configuration files, and preserves user-defined categories. + +![Keymap shortcut editor](screenshots/editor.webp) + +## Features + +- Reads the active compositor automatically, with a manual override for custom + sessions and test setups. +- Searches shortcut descriptions, combinations, categories, actions, commands, + and MangoWC key modes. +- Renders ANSI 100%, 96%, 80% (TKL), 75%, 65%, and 60% keyboards. +- Shows whether a physical key is free, occupied on the selected modifier + layer, or used by another combination. +- Creates press and release shortcuts where the compositor supports them. +- Browses Noctalia IPC commands and native actions for the active compositor, + with source, category, readiness, and text filters. +- Includes a searchable library of Noctalia commands and native compositor + actions, while retaining free-form custom shell commands. +- Edits supported combinations, descriptions, commands, trigger modes, and + categories directly above the shortcut list. +- Reorders shortcuts and moves them between categories with drag and drop. +- Renames categories and can hide, restore, or permanently delete shortcuts. +- Uses Noctalia theme roles by default and accepts custom colors for cards, + headings, modifier pills, keys, and text. +- Validates and reloads configuration changes, with guarded rollback on + failure. + +![100% keyboard view](screenshots/keyboard-100.webp) + +## Plugin + +| Type | ID | +| --- | --- | +| Bar widget | `widget` | +| Panel | `panel` | +| Hyprland service | `service` | +| Niri service | `niri-service` | +| MangoWC service | `mangowc-service` | +| Configuration writer | `writer-service` | + +The complete plugin ID is `blackbartblues/keymap`. + +## Requirements + +- Noctalia v5 with plugin API 5. +- Hyprland with its native Lua configuration API, Niri, or MangoWC. +- The command-line tools for the selected compositor, plus `xdg-open` for the + optional configuration-folder action: Hyprland uses `hyprctl` and + `Hyprland`; Niri uses `niri`; MangoWC uses `mango` and `mmsg`. + +Classic Hyprland `.conf` keybinds are intentionally unsupported. Hyprland is +moving to its native Lua configuration API, which is the only Hyprland format +Keymap reads and writes. + +For read-only use, Niri and MangoWC are parsed without spawning their IPC +tools. The validator and reload commands are used only after an explicit edit. +The folder button uses `xdg-open` to open the directory containing the active +configuration file. + +## Usage + +Add the `widget` entry to a Noctalia bar and click it, or open the panel through +IPC: + +```sh +noctalia msg panel-toggle blackbartblues/keymap:panel +``` + +The header provides keyboard and list views, the shortcut creator, the +existing-shortcut editor, refresh, configuration-folder, settings, and close +actions. + +The settings button opens `Settings -> Plugins`. Select the gear beside +Keymap to edit its compositor, paths, keyboard size, columns, and colors. + +## Configuration discovery + +An existing path entered in Keymap settings always wins. If that path does not +exist, Keymap searches safe, compositor-specific locations: + +- Hyprland: `$XDG_CONFIG_HOME/hypr/hyprland.lua`, `init.lua`, and + `/etc/xdg/hypr/hyprland.lua`, followed by a scored scan of top-level `.lua` + files in the Hyprland configuration directory. +- Niri: `$NIRI_CONFIG`, `$XDG_CONFIG_HOME/niri/config.kdl`, followed by a + scored scan of top-level `.kdl` files in the Niri configuration directory. +- MangoWC: `$XDG_CONFIG_HOME/mango/config.conf`, `/etc/mango/config.conf`, + followed by a scored scan of top-level `.conf` files in the MangoWC + configuration directory. + +The generated `keymap.lua`, `keymap.kdl`, and `keymap.conf` files and files +whose names contain `backup` are excluded from automatic discovery. If no +usable shortcut file is found, the panel points to settings so the correct path +can be entered manually. + +## Browsing shortcuts + +In keyboard view, enable an exact Super, Ctrl, Shift, and Alt layer. Occupied +keys open the shortcuts assigned to that combination; unoccupied keys can be +sent directly to the creator. Change the physical layout from the keyboard +size selector while editing shortcuts, or set its default in plugin settings. + +In list view, type into the search box to filter the complete category tree. +Sequential shortcuts such as workspaces 1 through 9 can optionally be folded +into a single row. + +The example configurations below demonstrate the same feature set without +reading or modifying a personal setup: + +![Niri example shortcuts](screenshots/niri-list.webp) + +![MangoWC example shortcuts](screenshots/mangowc-list.webp) + +## Creating shortcuts + +Select **New shortcut**, choose modifiers and a key, select an existing or new +category, then enter a description and command. The keyboard view can fill the +combination by clicking a physical key. + +Select **Commands** beside the command field to open the known-command +library. It contains every command exposed by Noctalia's IPC help plus the +native Hyprland Lua, Niri KDL, or MangoWC actions for the active compositor. +Filter by source, category, whether an action still needs arguments, or free +text. Selecting an entry fills the command field; replace every +`{{placeholder}}` with a compositor-valid value before saving. **Custom +command** returns to an unrestricted shell command, so the library never +removes the option to type a command manually. + +Native actions are offered while creating a shortcut. In the editor, the +library offers Noctalia shell commands only: converting an existing shell bind +to a different native syntax cannot be rewritten safely across all supported +source forms. + +Hyprland supports multi-key sequences such as `Super + C + V`. Niri and +MangoWC accept one ordinary key in this writer. Niri exposes press activation; +Hyprland and MangoWC expose press and release activation. + +![Shortcut creator](screenshots/creator.webp) + +### Command library + +Select **Command library** beside the command field to browse 362 known +commands and native actions: + +| Source | Entries | Verified from | +| --- | ---: | --- | +| Noctalia | 98 | Runtime `noctalia msg --help` output | +| Hyprland | 51 | Native `hl.dsp.*` dispatcher bindings from Hyprland 0.56.0 | +| Niri | 135 | Configurable KDL actions from Niri 26.04 | +| MangoWC | 78 | Current parser dispatchers and official keybinding documentation | + +Search by action, syntax, source, or category. Source, category, and readiness +filters can narrow the results. **Ready** entries can be inserted directly; +**Needs input** entries contain visible `{{argument}}` placeholders that must +be replaced before saving. Selecting **Custom command** returns to a normal +shell command without restricting it to the catalog. + +Noctalia entries are saved as shell commands. When creating a shortcut, +compositor entries are emitted as native Hyprland Lua, Niri KDL, or MangoWC +actions rather than wrappers around an IPC command. The writer checks the +selected entry ID, compositor, source, and completed template again before it +touches a file. Existing native actions remain preserved but are not converted +to another native catalog action by the editor. + +![Known command library](screenshots/command-library.webp) + +The first created shortcut adds one marked include to the configured root and +creates a sibling managed file: + +| Compositor | Managed file | Marked include | +| --- | --- | --- | +| Hyprland | `keymap.lua` | `require("keymap")` | +| Niri | `keymap.kdl` | `include "keymap.kdl"` | +| MangoWC | `keymap.conf` | `source=./keymap.conf` | + +## Editing and organizing + +Select **Edit shortcuts** to expose actions on writable rows. The pencil opens +the inline editor above the category cards. The eye-slash action hides a +shortcut without losing its original text, and the trash action permanently +deletes it after confirmation. Hidden shortcuts remain available in the +editor's recovery section, where they can be restored or deleted. + +Drag a row handle to another position in the same category or into another +category. The same move can be performed with the Category field in the inline +editor. Select the pencil in a category heading to rename that category. + +Native compositor actions remain intact. Fields that cannot be rewritten +safely are disabled instead of being guessed. Generated, ranged, or otherwise +read-only entries are visibly locked. + +Before every create, update, move, reorder, category rename, hide, restore, or +delete operation, Keymap verifies that the source still matches the parsed +snapshot and refuses symbolic-link targets. Writes use a temporary sibling and +atomic rename. The candidate is then checked with the compositor's native +validator and reloaded: + +| Compositor | Validator | Reload | +| --- | --- | --- | +| Hyprland | `Hyprland --verify-config -c ` | `hyprctl reload` | +| Niri | `niri validate -c ` | `niri msg action load-config-file` | +| MangoWC | `mango -c -p` | `mmsg dispatch reload_config` | + +If validation or reload fails, Keymap attempts to restore each file it changed. +After a reload failure, it also attempts to reload the restored configuration. +Rollback or recovery-reload failures are reported explicitly. A source that +changes after parsing is never overwritten; the operation stops and asks the +user to refresh instead. + +## Categories and source formats + +### Hyprland Lua + +Place numbered headings before groups of native Lua bindings: + +```lua +-- 1. Applications +hl.bind("SUPER + RETURN", hl.dsp.exec_cmd("foot"), { description = "Open terminal" }) +``` + +Local modules loaded with `require` are scanned recursively. Literal +descriptions and literal prefixes such as `description = "Workspace " .. i` +are matched against the live bind registry. The live registry remains +authoritative; the Lua files supply source locations, editable snippets, and +category order. + +Hyprland versions whose `hyprctl binds -j` output cannot be decoded are handled +automatically through the complete plain-text `hyprctl binds` fallback. + +### Niri + +Category comments live inside `binds {}` blocks: + +```kdl +binds { + // #"Applications" + Mod+Return hotkey-overlay-title="Open terminal" { spawn-sh "foot"; } +} +``` + +Positional `include` nodes, optional includes, later overrides, disabled `/-` +nodes, custom overlay titles, and native actions are supported. Because Niri +does not expose an effective bind registry through IPC, its configuration tree +is the source of truth. + +### MangoWC + +Category comments and optional descriptions use the following form: + +```ini +# Applications +bind=SUPER,Return,spawn_shell,foot #"Open terminal" +``` + +Keymap supports `bind` with `l/s/r/p` flags, `axisbind`, `mousebind`, +`gesturebind`, `switchbind`, `keymode`, `source`, and `source-optional`. +MangoWC's configuration tree is the source of truth. + +Complete, non-loaded examples are included in the repository: + +- [`examples/hyprland.lua`](examples/hyprland.lua) — 40 shortcuts and a config + that passes `Hyprland --verify-config`. +- [`examples/niri.kdl`](examples/niri.kdl) — 39 shortcuts and a config that + passes `niri validate`. +- [`examples/mangowc.conf`](examples/mangowc.conf) — 40 shortcuts. + +They cover applications, window management, workspaces, screenshots, Noctalia, +media controls, utilities, release triggers, wheel bindings, and compositor +native actions. They are documentation and test fixtures; Keymap never loads +them automatically. + +## Settings + +| Setting | Default | Purpose | +| --- | --- | --- | +| `compositor` | `auto` | Detect the session or force Hyprland, Niri, or MangoWC. | +| `hyprland_config` | `~/.config/hypr/hyprland.lua` | Hyprland native Lua root. | +| `niri_config` | `~/.config/niri/config.kdl` | Niri KDL root. | +| `mangowc_config` | `~/.config/mango/config.conf` | MangoWC config root. | +| `merge_sequential` | `true` | Fold related numbered shortcuts into one row. | +| `show_undescribed` | `true` | Show Hyprland binds without descriptions. | +| `keyboard_layout` | `100` | Default 100%, 96%, 80%, 75%, 65%, or 60% view. | +| `columns` | `3` | One to four balanced category columns. | +| `card_color` / `card_opacity` | `surface_variant` / `35` | Card background role or custom color and opacity. | +| `category_color` | `primary` | Category heading role or custom color. | +| `description_color` | `on_surface` | Description role or custom color. | +| modifier color pairs | Noctalia theme roles | Background and text for Super, Ctrl, Shift, and Alt. | +| `key_color` / `key_text_color` | `surface` / `on_surface` | Ordinary key background and text. | +| widget `glyph` / `show_label` | `keyboard` / `false` | Bar appearance. | + +![Keymap settings](screenshots/settings.webp) + +Theme-role values follow Noctalia palette changes automatically. Every color +setting also accepts a custom color. + +## IPC + +Request an immediate refresh from the service for the active compositor: + +```sh +# Hyprland +noctalia msg plugin blackbartblues/keymap:service all refresh + +# Niri +noctalia msg plugin blackbartblues/keymap:niri-service all refresh + +# MangoWC +noctalia msg plugin blackbartblues/keymap:mangowc-service all refresh +``` + +The panel accepts `view-keyboard`, `view-list`, `creator-open`, +`creator-cancel`, `editor-open`, `editor-bind `, `clear-modifiers`, +`keyboard-key `, and `keyboard-layout ` events. For example: + +```sh +noctalia msg plugin blackbartblues/keymap:panel all keyboard-layout 75 +``` + +Valid layout payloads are `100`, `96`, `80`, `75`, `65`, and `60`. + +## Safety and limits + +- Keymap makes no network requests and never executes commands stored inside + shortcuts; it only passes explicitly saved configuration text to the active + compositor. +- Configuration traversal is cycle-safe and limited to 64 files of up to + 512 KiB each. Root files accepted for writing are limited to 2 MiB. +- Required missing Niri includes and MangoWC sources stop parsing rather than + silently presenting an incomplete list. Optional sources and non-fatal + parser issues are shown as warnings. +- All interface prose and settings metadata use Noctalia's translation API. + Physical key legends and standard modifier names remain technical labels. + The shipped English catalog is the source language for Weblate. + +## Tests + +From the `keymap` directory: + +```sh +for test_file in tests/*.lua; do lua "$test_file"; done +python tests/command_library_test.py +python tests/i18n_test.py +Hyprland --verify-config -c examples/hyprland.lua +niri validate -c examples/niri.kdl +``` + +The suite covers category markers, hidden-block recovery, Hyprland command +parsing and text fallback, all keyboard layouts, create/update/write rollback, +the three example configurations, command-library integrity and native action +creation, short DnD identifiers, automatic path discovery, and translation-key +coverage. + +## License + +MIT. diff --git a/keymap/command_library.json b/keymap/command_library.json new file mode 100644 index 0000000..b5cf95d --- /dev/null +++ b/keymap/command_library.json @@ -0,0 +1,2915 @@ +{ + "schema": 1, + "sources": { + "noctalia": { + "revision": "noctalia-core d07ed348f4948a2afd093a6a1ad683ed81616436, runtime msg --help" + }, + "hyprland": { + "revision": "v0.56.0, commit 36b2e0cfe0c6094dbc47bd42a437431315bb3087, LuaBindingsDispatchers.cpp" + }, + "niri": { + "revision": "v26.04, commit 8ed0da44d974c32c6877d2f4630c314da0717ecb, niri-config/src/binds.rs" + }, + "mangowc": { + "revision": "mangowm/mango commit b5efdd24e76c3c9aa2300e626fe013fcd3617cca, parse_config.h and docs/bindings/keys.md" + } + }, + "entries": [ + { + "id": "hyprland/exec_cmd", + "source": "hyprland", + "category": "applications", + "kind": "native", + "template": "hl.dsp.exec_cmd({{command}})", + "usage": "hl.dsp.exec_cmd(…)" + }, + { + "id": "hyprland/exec_raw", + "source": "hyprland", + "category": "applications", + "kind": "native", + "template": "hl.dsp.exec_raw({{command}})", + "usage": "hl.dsp.exec_raw(…)" + }, + { + "id": "hyprland/cursor.move", + "source": "hyprland", + "category": "cursor", + "kind": "native", + "template": "hl.dsp.cursor.move({ x = {{x}}, y = {{y}} })", + "usage": "hl.dsp.cursor.move(…)" + }, + { + "id": "hyprland/cursor.move_to_corner", + "source": "hyprland", + "category": "cursor", + "kind": "native", + "template": "hl.dsp.cursor.move_to_corner({ corner = {{corner}} })", + "usage": "hl.dsp.cursor.move_to_corner(…)" + }, + { + "id": "hyprland/dpms", + "source": "hyprland", + "category": "display", + "kind": "native", + "template": "hl.dsp.dpms()", + "usage": "hl.dsp.dpms()" + }, + { + "id": "hyprland/force_renderer_reload", + "source": "hyprland", + "category": "display", + "kind": "native", + "template": "hl.dsp.force_renderer_reload()", + "usage": "hl.dsp.force_renderer_reload()" + }, + { + "id": "hyprland/focus", + "source": "hyprland", + "category": "focus", + "kind": "native", + "template": "hl.dsp.focus({ direction = {{direction}} })", + "usage": "hl.dsp.focus(…)" + }, + { + "id": "hyprland/group.active", + "source": "hyprland", + "category": "groups", + "kind": "native", + "template": "hl.dsp.group.active({ index = {{index}} })", + "usage": "hl.dsp.group.active(…)" + }, + { + "id": "hyprland/group.lock", + "source": "hyprland", + "category": "groups", + "kind": "native", + "template": "hl.dsp.group.lock()", + "usage": "hl.dsp.group.lock()" + }, + { + "id": "hyprland/group.lock_active", + "source": "hyprland", + "category": "groups", + "kind": "native", + "template": "hl.dsp.group.lock_active()", + "usage": "hl.dsp.group.lock_active()" + }, + { + "id": "hyprland/group.move_window", + "source": "hyprland", + "category": "groups", + "kind": "native", + "template": "hl.dsp.group.move_window()", + "usage": "hl.dsp.group.move_window()" + }, + { + "id": "hyprland/group.next", + "source": "hyprland", + "category": "groups", + "kind": "native", + "template": "hl.dsp.group.next()", + "usage": "hl.dsp.group.next()" + }, + { + "id": "hyprland/group.prev", + "source": "hyprland", + "category": "groups", + "kind": "native", + "template": "hl.dsp.group.prev()", + "usage": "hl.dsp.group.prev()" + }, + { + "id": "hyprland/group.toggle", + "source": "hyprland", + "category": "groups", + "kind": "native", + "template": "hl.dsp.group.toggle()", + "usage": "hl.dsp.group.toggle()" + }, + { + "id": "hyprland/pass", + "source": "hyprland", + "category": "input", + "kind": "native", + "template": "hl.dsp.pass({ window = {{window-selector}} })", + "usage": "hl.dsp.pass(…)" + }, + { + "id": "hyprland/release_input_capture", + "source": "hyprland", + "category": "input", + "kind": "native", + "template": "hl.dsp.release_input_capture()", + "usage": "hl.dsp.release_input_capture()" + }, + { + "id": "hyprland/send_key_state", + "source": "hyprland", + "category": "input", + "kind": "native", + "template": "hl.dsp.send_key_state({ mods = {{modifiers}}, key = {{key}}, state = {{state}} })", + "usage": "hl.dsp.send_key_state(…)" + }, + { + "id": "hyprland/send_shortcut", + "source": "hyprland", + "category": "input", + "kind": "native", + "template": "hl.dsp.send_shortcut({ mods = {{modifiers}}, key = {{key}} })", + "usage": "hl.dsp.send_shortcut(…)" + }, + { + "id": "hyprland/layout", + "source": "hyprland", + "category": "layout", + "kind": "native", + "template": "hl.dsp.layout({{message}})", + "usage": "hl.dsp.layout(…)" + }, + { + "id": "hyprland/event", + "source": "hyprland", + "category": "system", + "kind": "native", + "template": "hl.dsp.event({{event}})", + "usage": "hl.dsp.event(…)" + }, + { + "id": "hyprland/exit", + "source": "hyprland", + "category": "system", + "kind": "native", + "template": "hl.dsp.exit()", + "usage": "hl.dsp.exit()" + }, + { + "id": "hyprland/force_idle", + "source": "hyprland", + "category": "system", + "kind": "native", + "template": "hl.dsp.force_idle({{seconds}})", + "usage": "hl.dsp.force_idle(…)" + }, + { + "id": "hyprland/global", + "source": "hyprland", + "category": "system", + "kind": "native", + "template": "hl.dsp.global({{global-shortcut}})", + "usage": "hl.dsp.global(…)" + }, + { + "id": "hyprland/no_op", + "source": "hyprland", + "category": "system", + "kind": "native", + "template": "hl.dsp.no_op()", + "usage": "hl.dsp.no_op()" + }, + { + "id": "hyprland/submap", + "source": "hyprland", + "category": "system", + "kind": "native", + "template": "hl.dsp.submap({{name}})", + "usage": "hl.dsp.submap(…)" + }, + { + "id": "hyprland/window.alter_zorder", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.alter_zorder({ mode = {{mode}} })", + "usage": "hl.dsp.window.alter_zorder(…)" + }, + { + "id": "hyprland/window.bring_to_top", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.bring_to_top()", + "usage": "hl.dsp.window.bring_to_top()" + }, + { + "id": "hyprland/window.center", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.center()", + "usage": "hl.dsp.window.center()" + }, + { + "id": "hyprland/window.clear_tags", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.clear_tags()", + "usage": "hl.dsp.window.clear_tags()" + }, + { + "id": "hyprland/window.close", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.close()", + "usage": "hl.dsp.window.close()" + }, + { + "id": "hyprland/window.cycle_next", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.cycle_next()", + "usage": "hl.dsp.window.cycle_next()" + }, + { + "id": "hyprland/window.deny_from_group", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.deny_from_group()", + "usage": "hl.dsp.window.deny_from_group()" + }, + { + "id": "hyprland/window.drag", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.drag()", + "usage": "hl.dsp.window.drag()" + }, + { + "id": "hyprland/window.float", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.float()", + "usage": "hl.dsp.window.float()" + }, + { + "id": "hyprland/window.fullscreen", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.fullscreen()", + "usage": "hl.dsp.window.fullscreen()" + }, + { + "id": "hyprland/window.fullscreen_state", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.fullscreen_state({ internal = {{internal-state}}, client = {{client-state}} })", + "usage": "hl.dsp.window.fullscreen_state(…)" + }, + { + "id": "hyprland/window.kill", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.kill()", + "usage": "hl.dsp.window.kill()" + }, + { + "id": "hyprland/window.move", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.move({ direction = {{direction}} })", + "usage": "hl.dsp.window.move(…)" + }, + { + "id": "hyprland/window.pin", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.pin()", + "usage": "hl.dsp.window.pin()" + }, + { + "id": "hyprland/window.pseudo", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.pseudo()", + "usage": "hl.dsp.window.pseudo()" + }, + { + "id": "hyprland/window.resize", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.resize()", + "usage": "hl.dsp.window.resize()" + }, + { + "id": "hyprland/window.set_prop", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.set_prop({ prop = {{property}}, value = {{value}} })", + "usage": "hl.dsp.window.set_prop(…)" + }, + { + "id": "hyprland/window.signal", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.signal({ signal = {{signal}} })", + "usage": "hl.dsp.window.signal(…)" + }, + { + "id": "hyprland/window.swap", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.swap({ direction = {{direction}} })", + "usage": "hl.dsp.window.swap(…)" + }, + { + "id": "hyprland/window.tag", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.tag({ tag = {{tag}} })", + "usage": "hl.dsp.window.tag(…)" + }, + { + "id": "hyprland/window.toggle_swallow", + "source": "hyprland", + "category": "windows", + "kind": "native", + "template": "hl.dsp.window.toggle_swallow()", + "usage": "hl.dsp.window.toggle_swallow()" + }, + { + "id": "hyprland/workspace.change_id", + "source": "hyprland", + "category": "workspaces", + "kind": "native", + "template": "hl.dsp.workspace.change_id({ workspace = {{workspace}}, id = {{id}} })", + "usage": "hl.dsp.workspace.change_id(…)" + }, + { + "id": "hyprland/workspace.move", + "source": "hyprland", + "category": "workspaces", + "kind": "native", + "template": "hl.dsp.workspace.move({ workspace = {{workspace}}, monitor = {{monitor}} })", + "usage": "hl.dsp.workspace.move(…)" + }, + { + "id": "hyprland/workspace.rename", + "source": "hyprland", + "category": "workspaces", + "kind": "native", + "template": "hl.dsp.workspace.rename({ workspace = {{workspace}}, name = {{name}} })", + "usage": "hl.dsp.workspace.rename(…)" + }, + { + "id": "hyprland/workspace.swap_monitors", + "source": "hyprland", + "category": "workspaces", + "kind": "native", + "template": "hl.dsp.workspace.swap_monitors({ monitor1 = {{monitor-1}}, monitor2 = {{monitor-2}} })", + "usage": "hl.dsp.workspace.swap_monitors(…)" + }, + { + "id": "hyprland/workspace.toggle_special", + "source": "hyprland", + "category": "workspaces", + "kind": "native", + "template": "hl.dsp.workspace.toggle_special({{name}})", + "usage": "hl.dsp.workspace.toggle_special(…)" + }, + { + "id": "mangowc/spawn", + "source": "mangowc", + "category": "applications", + "kind": "native", + "template": "spawn,{{parameters}}", + "usage": "spawn cmd" + }, + { + "id": "mangowc/spawn_on_empty", + "source": "mangowc", + "category": "applications", + "kind": "native", + "template": "spawn_on_empty,{{parameters}}", + "usage": "spawn_on_empty cmd,tagnumber" + }, + { + "id": "mangowc/spawn_shell", + "source": "mangowc", + "category": "applications", + "kind": "native", + "template": "spawn_shell,{{parameters}}", + "usage": "spawn_shell cmd" + }, + { + "id": "mangowc/dwindle_split_horizontal", + "source": "mangowc", + "category": "layout", + "kind": "native", + "template": "dwindle_split_horizontal", + "usage": "dwindle_split_horizontal" + }, + { + "id": "mangowc/dwindle_split_vertical", + "source": "mangowc", + "category": "layout", + "kind": "native", + "template": "dwindle_split_vertical", + "usage": "dwindle_split_vertical" + }, + { + "id": "mangowc/dwindle_toggle_split_direction", + "source": "mangowc", + "category": "layout", + "kind": "native", + "template": "dwindle_toggle_split_direction", + "usage": "dwindle_toggle_split_direction" + }, + { + "id": "mangowc/incgaps", + "source": "mangowc", + "category": "layout", + "kind": "native", + "template": "incgaps,{{parameters}}", + "usage": "incgaps +/-value" + }, + { + "id": "mangowc/incnmaster", + "source": "mangowc", + "category": "layout", + "kind": "native", + "template": "incnmaster,{{parameters}}", + "usage": "incnmaster +1/-1" + }, + { + "id": "mangowc/scroller_stack", + "source": "mangowc", + "category": "layout", + "kind": "native", + "template": "scroller_stack,{{parameters}}", + "usage": "scroller_stack left/right/up/down" + }, + { + "id": "mangowc/set_proportion", + "source": "mangowc", + "category": "layout", + "kind": "native", + "template": "set_proportion,{{parameters}}", + "usage": "set_proportion float" + }, + { + "id": "mangowc/setlayout", + "source": "mangowc", + "category": "layout", + "kind": "native", + "template": "setlayout,{{parameters}}", + "usage": "setlayout name" + }, + { + "id": "mangowc/setmfact", + "source": "mangowc", + "category": "layout", + "kind": "native", + "template": "setmfact,{{parameters}}", + "usage": "setmfact +0.05" + }, + { + "id": "mangowc/switch_layout", + "source": "mangowc", + "category": "layout", + "kind": "native", + "template": "switch_layout", + "usage": "switch_layout" + }, + { + "id": "mangowc/switch_proportion_preset", + "source": "mangowc", + "category": "layout", + "kind": "native", + "template": "switch_proportion_preset", + "usage": "switch_proportion_preset" + }, + { + "id": "mangowc/togglegaps", + "source": "mangowc", + "category": "layout", + "kind": "native", + "template": "togglegaps", + "usage": "togglegaps" + }, + { + "id": "mangowc/chvt", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "chvt", + "usage": "chvt" + }, + { + "id": "mangowc/create_virtual_output", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "create_virtual_output", + "usage": "create_virtual_output" + }, + { + "id": "mangowc/destroy_all_virtual_output", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "destroy_all_virtual_output", + "usage": "destroy_all_virtual_output" + }, + { + "id": "mangowc/disable_monitor", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "disable_monitor,{{parameters}}", + "usage": "disable_monitor monitor_spec" + }, + { + "id": "mangowc/enable_monitor", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "enable_monitor,{{parameters}}", + "usage": "enable_monitor monitor_spec" + }, + { + "id": "mangowc/load_config_file", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "load_config_file,{{parameters}}", + "usage": "load_config_file file path" + }, + { + "id": "mangowc/moveresize", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "moveresize", + "usage": "moveresize" + }, + { + "id": "mangowc/quit", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "quit", + "usage": "quit" + }, + { + "id": "mangowc/reload_config", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "reload_config", + "usage": "reload_config" + }, + { + "id": "mangowc/setkeymode", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "setkeymode,{{parameters}}", + "usage": "setkeymode mode" + }, + { + "id": "mangowc/setoption", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "setoption,{{parameters}}", + "usage": "setoption key,value" + }, + { + "id": "mangowc/sleep_monitor", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "sleep_monitor,{{parameters}}", + "usage": "sleep_monitor monitor_spec" + }, + { + "id": "mangowc/sleep_toggle_monitor", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "sleep_toggle_monitor,{{parameters}}", + "usage": "sleep_toggle_monitor monitor_spec" + }, + { + "id": "mangowc/switch_keyboard_layout", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "switch_keyboard_layout", + "usage": "switch_keyboard_layout [index]" + }, + { + "id": "mangowc/toggle_monitor", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "toggle_monitor,{{parameters}}", + "usage": "toggle_monitor monitor_spec" + }, + { + "id": "mangowc/toggle_trackpad_enable", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "toggle_trackpad_enable", + "usage": "toggle_trackpad_enable" + }, + { + "id": "mangowc/togglejump", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "togglejump", + "usage": "togglejump" + }, + { + "id": "mangowc/toggleoverlay", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "toggleoverlay", + "usage": "toggleoverlay" + }, + { + "id": "mangowc/toggleoverview", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "toggleoverview", + "usage": "toggleoverview" + }, + { + "id": "mangowc/wakeup_monitor", + "source": "mangowc", + "category": "system", + "kind": "native", + "template": "wakeup_monitor,{{parameters}}", + "usage": "wakeup_monitor monitor_spec" + }, + { + "id": "mangowc/centerwin", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "centerwin", + "usage": "centerwin" + }, + { + "id": "mangowc/exchange_client", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "exchange_client,{{parameters}}", + "usage": "exchange_client left/right/up/down" + }, + { + "id": "mangowc/exchange_stack_client", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "exchange_stack_client,{{parameters}}", + "usage": "exchange_stack_client next/prev" + }, + { + "id": "mangowc/focusdir", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "focusdir,{{parameters}}", + "usage": "focusdir left/right/up/down" + }, + { + "id": "mangowc/focusid", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "focusid", + "usage": "focusid" + }, + { + "id": "mangowc/focuslast", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "focuslast", + "usage": "focuslast" + }, + { + "id": "mangowc/focusstack", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "focusstack,{{parameters}}", + "usage": "focusstack next/prev" + }, + { + "id": "mangowc/groupfocus", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "groupfocus,{{parameters}}", + "usage": "groupfocus prev/next" + }, + { + "id": "mangowc/groupjoin", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "groupjoin,{{parameters}}", + "usage": "groupjoin left/right/up/down" + }, + { + "id": "mangowc/groupleave", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "groupleave", + "usage": "groupleave" + }, + { + "id": "mangowc/killclient", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "killclient,{{parameters}}", + "usage": "killclient force" + }, + { + "id": "mangowc/minimized", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "minimized", + "usage": "minimized" + }, + { + "id": "mangowc/movewin", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "movewin,{{parameters}}", + "usage": "movewin (x,y)" + }, + { + "id": "mangowc/resizewin", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "resizewin,{{parameters}}", + "usage": "resizewin (width,height)" + }, + { + "id": "mangowc/restore_minimized", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "restore_minimized", + "usage": "restore_minimized" + }, + { + "id": "mangowc/smartmovewin", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "smartmovewin,{{parameters}}", + "usage": "smartmovewin left/right/up/down" + }, + { + "id": "mangowc/smartresizewin", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "smartresizewin,{{parameters}}", + "usage": "smartresizewin left/right/up/down" + }, + { + "id": "mangowc/toggle_all_floating", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "toggle_all_floating", + "usage": "toggle_all_floating" + }, + { + "id": "mangowc/toggle_named_scratchpad", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "toggle_named_scratchpad,{{parameters}}", + "usage": "toggle_named_scratchpad appid,title,cmd" + }, + { + "id": "mangowc/toggle_render_border", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "toggle_render_border", + "usage": "toggle_render_border" + }, + { + "id": "mangowc/toggle_scratchpad", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "toggle_scratchpad", + "usage": "toggle_scratchpad" + }, + { + "id": "mangowc/togglefakefullscreen", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "togglefakefullscreen", + "usage": "togglefakefullscreen" + }, + { + "id": "mangowc/togglefloating", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "togglefloating", + "usage": "togglefloating" + }, + { + "id": "mangowc/togglefullscreen", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "togglefullscreen", + "usage": "togglefullscreen" + }, + { + "id": "mangowc/toggleglobal", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "toggleglobal", + "usage": "toggleglobal" + }, + { + "id": "mangowc/togglemaximizescreen", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "togglemaximizescreen", + "usage": "togglemaximizescreen" + }, + { + "id": "mangowc/zoom", + "source": "mangowc", + "category": "windows", + "kind": "native", + "template": "zoom", + "usage": "zoom" + }, + { + "id": "mangowc/comboview", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "comboview,{{parameters}}", + "usage": "comboview 1-9" + }, + { + "id": "mangowc/focusmon", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "focusmon,{{parameters}}", + "usage": "focusmon left/right/up/down/monitor_spec" + }, + { + "id": "mangowc/tag", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "tag,{{parameters}}", + "usage": "tag 1-9 [,synctag]" + }, + { + "id": "mangowc/tagcrossmon", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "tagcrossmon,{{parameters}}", + "usage": "tagcrossmon tag,monitor_spec" + }, + { + "id": "mangowc/tagmon", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "tagmon,{{parameters}}", + "usage": "tagmon left/right/up/down/monitor_spec,[keeptag]" + }, + { + "id": "mangowc/tagsilent", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "tagsilent,{{parameters}}", + "usage": "tagsilent 1-9" + }, + { + "id": "mangowc/tagtoleft", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "tagtoleft", + "usage": "tagtoleft [synctag]" + }, + { + "id": "mangowc/tagtoright", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "tagtoright", + "usage": "tagtoright [synctag]" + }, + { + "id": "mangowc/toggletag", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "toggletag,{{parameters}}", + "usage": "toggletag 0-9" + }, + { + "id": "mangowc/toggleview", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "toggleview,{{parameters}}", + "usage": "toggleview 1-9" + }, + { + "id": "mangowc/view", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "view,{{parameters}}", + "usage": "view -1/0/1-9` or `mask [,synctag]" + }, + { + "id": "mangowc/viewcrossmon", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "viewcrossmon,{{parameters}}", + "usage": "viewcrossmon tag,monitor_spec" + }, + { + "id": "mangowc/viewtoleft", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "viewtoleft", + "usage": "viewtoleft [synctag]" + }, + { + "id": "mangowc/viewtoleft_have_client", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "viewtoleft_have_client", + "usage": "viewtoleft_have_client [synctag]" + }, + { + "id": "mangowc/viewtoright", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "viewtoright", + "usage": "viewtoright [synctag]" + }, + { + "id": "mangowc/viewtoright_have_client", + "source": "mangowc", + "category": "workspaces", + "kind": "native", + "template": "viewtoright_have_client", + "usage": "viewtoright_have_client [synctag]" + }, + { + "id": "niri/spawn", + "source": "niri", + "category": "applications", + "kind": "native", + "template": "spawn {{arguments}}", + "usage": "spawn " + }, + { + "id": "niri/spawn-sh", + "source": "niri", + "category": "applications", + "kind": "native", + "template": "spawn-sh {{command}}", + "usage": "spawn-sh " + }, + { + "id": "niri/clear-dynamic-cast-target", + "source": "niri", + "category": "casting", + "kind": "native", + "template": "clear-dynamic-cast-target", + "usage": "clear-dynamic-cast-target" + }, + { + "id": "niri/focus-column", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-column {{argument}}", + "usage": "focus-column " + }, + { + "id": "niri/focus-column-first", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-column-first", + "usage": "focus-column-first" + }, + { + "id": "niri/focus-column-last", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-column-last", + "usage": "focus-column-last" + }, + { + "id": "niri/focus-column-left", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-column-left", + "usage": "focus-column-left" + }, + { + "id": "niri/focus-column-left-or-last", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-column-left-or-last", + "usage": "focus-column-left-or-last" + }, + { + "id": "niri/focus-column-or-monitor-left", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-column-or-monitor-left", + "usage": "focus-column-or-monitor-left" + }, + { + "id": "niri/focus-column-or-monitor-right", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-column-or-monitor-right", + "usage": "focus-column-or-monitor-right" + }, + { + "id": "niri/focus-column-right", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-column-right", + "usage": "focus-column-right" + }, + { + "id": "niri/focus-column-right-or-first", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-column-right-or-first", + "usage": "focus-column-right-or-first" + }, + { + "id": "niri/focus-floating", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-floating", + "usage": "focus-floating" + }, + { + "id": "niri/focus-monitor", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-monitor {{argument}}", + "usage": "focus-monitor " + }, + { + "id": "niri/focus-monitor-down", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-monitor-down", + "usage": "focus-monitor-down" + }, + { + "id": "niri/focus-monitor-left", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-monitor-left", + "usage": "focus-monitor-left" + }, + { + "id": "niri/focus-monitor-next", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-monitor-next", + "usage": "focus-monitor-next" + }, + { + "id": "niri/focus-monitor-previous", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-monitor-previous", + "usage": "focus-monitor-previous" + }, + { + "id": "niri/focus-monitor-right", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-monitor-right", + "usage": "focus-monitor-right" + }, + { + "id": "niri/focus-monitor-up", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-monitor-up", + "usage": "focus-monitor-up" + }, + { + "id": "niri/focus-tiling", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-tiling", + "usage": "focus-tiling" + }, + { + "id": "niri/focus-window-bottom", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-bottom", + "usage": "focus-window-bottom" + }, + { + "id": "niri/focus-window-down", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-down", + "usage": "focus-window-down" + }, + { + "id": "niri/focus-window-down-or-column-left", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-down-or-column-left", + "usage": "focus-window-down-or-column-left" + }, + { + "id": "niri/focus-window-down-or-column-right", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-down-or-column-right", + "usage": "focus-window-down-or-column-right" + }, + { + "id": "niri/focus-window-down-or-top", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-down-or-top", + "usage": "focus-window-down-or-top" + }, + { + "id": "niri/focus-window-in-column", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-in-column {{argument}}", + "usage": "focus-window-in-column " + }, + { + "id": "niri/focus-window-or-monitor-down", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-or-monitor-down", + "usage": "focus-window-or-monitor-down" + }, + { + "id": "niri/focus-window-or-monitor-up", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-or-monitor-up", + "usage": "focus-window-or-monitor-up" + }, + { + "id": "niri/focus-window-or-workspace-down", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-or-workspace-down", + "usage": "focus-window-or-workspace-down" + }, + { + "id": "niri/focus-window-or-workspace-up", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-or-workspace-up", + "usage": "focus-window-or-workspace-up" + }, + { + "id": "niri/focus-window-previous", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-previous", + "usage": "focus-window-previous" + }, + { + "id": "niri/focus-window-top", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-top", + "usage": "focus-window-top" + }, + { + "id": "niri/focus-window-up", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-up", + "usage": "focus-window-up" + }, + { + "id": "niri/focus-window-up-or-bottom", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-up-or-bottom", + "usage": "focus-window-up-or-bottom" + }, + { + "id": "niri/focus-window-up-or-column-left", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-up-or-column-left", + "usage": "focus-window-up-or-column-left" + }, + { + "id": "niri/focus-window-up-or-column-right", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-window-up-or-column-right", + "usage": "focus-window-up-or-column-right" + }, + { + "id": "niri/focus-workspace", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-workspace {{workspace}}", + "usage": "focus-workspace " + }, + { + "id": "niri/focus-workspace-down", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-workspace-down", + "usage": "focus-workspace-down" + }, + { + "id": "niri/focus-workspace-previous", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-workspace-previous", + "usage": "focus-workspace-previous" + }, + { + "id": "niri/focus-workspace-up", + "source": "niri", + "category": "focus", + "kind": "native", + "template": "focus-workspace-up", + "usage": "focus-workspace-up" + }, + { + "id": "niri/toggle-keyboard-shortcuts-inhibit", + "source": "niri", + "category": "input", + "kind": "native", + "template": "toggle-keyboard-shortcuts-inhibit", + "usage": "toggle-keyboard-shortcuts-inhibit" + }, + { + "id": "niri/close-overview", + "source": "niri", + "category": "layout", + "kind": "native", + "template": "close-overview", + "usage": "close-overview" + }, + { + "id": "niri/open-overview", + "source": "niri", + "category": "layout", + "kind": "native", + "template": "open-overview", + "usage": "open-overview" + }, + { + "id": "niri/switch-layout", + "source": "niri", + "category": "layout", + "kind": "native", + "template": "switch-layout {{layout}}", + "usage": "switch-layout " + }, + { + "id": "niri/toggle-overview", + "source": "niri", + "category": "layout", + "kind": "native", + "template": "toggle-overview", + "usage": "toggle-overview" + }, + { + "id": "niri/power-off-monitors", + "source": "niri", + "category": "monitors", + "kind": "native", + "template": "power-off-monitors", + "usage": "power-off-monitors" + }, + { + "id": "niri/power-on-monitors", + "source": "niri", + "category": "monitors", + "kind": "native", + "template": "power-on-monitors", + "usage": "power-on-monitors" + }, + { + "id": "niri/set-dynamic-cast-monitor", + "source": "niri", + "category": "monitors", + "kind": "native", + "template": "set-dynamic-cast-monitor", + "usage": "set-dynamic-cast-monitor" + }, + { + "id": "niri/quit", + "source": "niri", + "category": "power", + "kind": "native", + "template": "quit", + "usage": "quit" + }, + { + "id": "niri/suspend", + "source": "niri", + "category": "power", + "kind": "native", + "template": "suspend", + "usage": "suspend" + }, + { + "id": "niri/do-screen-transition", + "source": "niri", + "category": "screenshots", + "kind": "native", + "template": "do-screen-transition", + "usage": "do-screen-transition" + }, + { + "id": "niri/screenshot", + "source": "niri", + "category": "screenshots", + "kind": "native", + "template": "screenshot", + "usage": "screenshot" + }, + { + "id": "niri/screenshot-screen", + "source": "niri", + "category": "screenshots", + "kind": "native", + "template": "screenshot-screen", + "usage": "screenshot-screen" + }, + { + "id": "niri/screenshot-window", + "source": "niri", + "category": "screenshots", + "kind": "native", + "template": "screenshot-window", + "usage": "screenshot-window" + }, + { + "id": "niri/debug-toggle-damage", + "source": "niri", + "category": "system", + "kind": "native", + "template": "debug-toggle-damage", + "usage": "debug-toggle-damage" + }, + { + "id": "niri/debug-toggle-opaque-regions", + "source": "niri", + "category": "system", + "kind": "native", + "template": "debug-toggle-opaque-regions", + "usage": "debug-toggle-opaque-regions" + }, + { + "id": "niri/show-hotkey-overlay", + "source": "niri", + "category": "system", + "kind": "native", + "template": "show-hotkey-overlay", + "usage": "show-hotkey-overlay" + }, + { + "id": "niri/toggle-debug-tint", + "source": "niri", + "category": "system", + "kind": "native", + "template": "toggle-debug-tint", + "usage": "toggle-debug-tint" + }, + { + "id": "niri/center-column", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "center-column", + "usage": "center-column" + }, + { + "id": "niri/center-visible-columns", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "center-visible-columns", + "usage": "center-visible-columns" + }, + { + "id": "niri/center-window", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "center-window", + "usage": "center-window" + }, + { + "id": "niri/close-window", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "close-window", + "usage": "close-window" + }, + { + "id": "niri/consume-or-expel-window-left", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "consume-or-expel-window-left", + "usage": "consume-or-expel-window-left" + }, + { + "id": "niri/consume-or-expel-window-right", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "consume-or-expel-window-right", + "usage": "consume-or-expel-window-right" + }, + { + "id": "niri/consume-window-into-column", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "consume-window-into-column", + "usage": "consume-window-into-column" + }, + { + "id": "niri/expand-column-to-available-width", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "expand-column-to-available-width", + "usage": "expand-column-to-available-width" + }, + { + "id": "niri/expel-window-from-column", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "expel-window-from-column", + "usage": "expel-window-from-column" + }, + { + "id": "niri/fullscreen-window", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "fullscreen-window", + "usage": "fullscreen-window" + }, + { + "id": "niri/maximize-column", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "maximize-column", + "usage": "maximize-column" + }, + { + "id": "niri/maximize-window-to-edges", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "maximize-window-to-edges", + "usage": "maximize-window-to-edges" + }, + { + "id": "niri/move-column-left", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-left", + "usage": "move-column-left" + }, + { + "id": "niri/move-column-left-or-to-monitor-left", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-left-or-to-monitor-left", + "usage": "move-column-left-or-to-monitor-left" + }, + { + "id": "niri/move-column-right", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-right", + "usage": "move-column-right" + }, + { + "id": "niri/move-column-right-or-to-monitor-right", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-right-or-to-monitor-right", + "usage": "move-column-right-or-to-monitor-right" + }, + { + "id": "niri/move-column-to-first", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-to-first", + "usage": "move-column-to-first" + }, + { + "id": "niri/move-column-to-index", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-to-index {{argument}}", + "usage": "move-column-to-index " + }, + { + "id": "niri/move-column-to-last", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-to-last", + "usage": "move-column-to-last" + }, + { + "id": "niri/move-column-to-monitor", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-to-monitor {{argument}}", + "usage": "move-column-to-monitor " + }, + { + "id": "niri/move-column-to-monitor-down", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-to-monitor-down", + "usage": "move-column-to-monitor-down" + }, + { + "id": "niri/move-column-to-monitor-left", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-to-monitor-left", + "usage": "move-column-to-monitor-left" + }, + { + "id": "niri/move-column-to-monitor-next", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-to-monitor-next", + "usage": "move-column-to-monitor-next" + }, + { + "id": "niri/move-column-to-monitor-previous", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-to-monitor-previous", + "usage": "move-column-to-monitor-previous" + }, + { + "id": "niri/move-column-to-monitor-right", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-to-monitor-right", + "usage": "move-column-to-monitor-right" + }, + { + "id": "niri/move-column-to-monitor-up", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-to-monitor-up", + "usage": "move-column-to-monitor-up" + }, + { + "id": "niri/move-column-to-workspace", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-to-workspace {{workspace}}", + "usage": "move-column-to-workspace " + }, + { + "id": "niri/move-column-to-workspace-down", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-to-workspace-down", + "usage": "move-column-to-workspace-down" + }, + { + "id": "niri/move-column-to-workspace-up", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-column-to-workspace-up", + "usage": "move-column-to-workspace-up" + }, + { + "id": "niri/move-window-down", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-down", + "usage": "move-window-down" + }, + { + "id": "niri/move-window-down-or-to-workspace-down", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-down-or-to-workspace-down", + "usage": "move-window-down-or-to-workspace-down" + }, + { + "id": "niri/move-window-to-floating", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-to-floating", + "usage": "move-window-to-floating" + }, + { + "id": "niri/move-window-to-monitor", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-to-monitor {{argument}}", + "usage": "move-window-to-monitor " + }, + { + "id": "niri/move-window-to-monitor-down", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-to-monitor-down", + "usage": "move-window-to-monitor-down" + }, + { + "id": "niri/move-window-to-monitor-left", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-to-monitor-left", + "usage": "move-window-to-monitor-left" + }, + { + "id": "niri/move-window-to-monitor-next", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-to-monitor-next", + "usage": "move-window-to-monitor-next" + }, + { + "id": "niri/move-window-to-monitor-previous", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-to-monitor-previous", + "usage": "move-window-to-monitor-previous" + }, + { + "id": "niri/move-window-to-monitor-right", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-to-monitor-right", + "usage": "move-window-to-monitor-right" + }, + { + "id": "niri/move-window-to-monitor-up", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-to-monitor-up", + "usage": "move-window-to-monitor-up" + }, + { + "id": "niri/move-window-to-tiling", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-to-tiling", + "usage": "move-window-to-tiling" + }, + { + "id": "niri/move-window-to-workspace", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-to-workspace {{workspace}}", + "usage": "move-window-to-workspace " + }, + { + "id": "niri/move-window-to-workspace-down", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-to-workspace-down", + "usage": "move-window-to-workspace-down" + }, + { + "id": "niri/move-window-to-workspace-up", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-to-workspace-up", + "usage": "move-window-to-workspace-up" + }, + { + "id": "niri/move-window-up", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-up", + "usage": "move-window-up" + }, + { + "id": "niri/move-window-up-or-to-workspace-up", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "move-window-up-or-to-workspace-up", + "usage": "move-window-up-or-to-workspace-up" + }, + { + "id": "niri/reset-window-height", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "reset-window-height", + "usage": "reset-window-height" + }, + { + "id": "niri/set-column-display", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "set-column-display {{argument}}", + "usage": "set-column-display " + }, + { + "id": "niri/set-column-width", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "set-column-width {{size}}", + "usage": "set-column-width " + }, + { + "id": "niri/set-dynamic-cast-window", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "set-dynamic-cast-window", + "usage": "set-dynamic-cast-window" + }, + { + "id": "niri/set-window-height", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "set-window-height {{size}}", + "usage": "set-window-height " + }, + { + "id": "niri/set-window-width", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "set-window-width {{size}}", + "usage": "set-window-width " + }, + { + "id": "niri/swap-window-left", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "swap-window-left", + "usage": "swap-window-left" + }, + { + "id": "niri/swap-window-right", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "swap-window-right", + "usage": "swap-window-right" + }, + { + "id": "niri/switch-focus-between-floating-and-tiling", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "switch-focus-between-floating-and-tiling", + "usage": "switch-focus-between-floating-and-tiling" + }, + { + "id": "niri/switch-preset-column-width", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "switch-preset-column-width", + "usage": "switch-preset-column-width" + }, + { + "id": "niri/switch-preset-column-width-back", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "switch-preset-column-width-back", + "usage": "switch-preset-column-width-back" + }, + { + "id": "niri/switch-preset-window-height", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "switch-preset-window-height", + "usage": "switch-preset-window-height" + }, + { + "id": "niri/switch-preset-window-height-back", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "switch-preset-window-height-back", + "usage": "switch-preset-window-height-back" + }, + { + "id": "niri/switch-preset-window-width", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "switch-preset-window-width", + "usage": "switch-preset-window-width" + }, + { + "id": "niri/switch-preset-window-width-back", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "switch-preset-window-width-back", + "usage": "switch-preset-window-width-back" + }, + { + "id": "niri/toggle-column-tabbed-display", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "toggle-column-tabbed-display", + "usage": "toggle-column-tabbed-display" + }, + { + "id": "niri/toggle-window-floating", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "toggle-window-floating", + "usage": "toggle-window-floating" + }, + { + "id": "niri/toggle-window-rule-opacity", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "toggle-window-rule-opacity", + "usage": "toggle-window-rule-opacity" + }, + { + "id": "niri/toggle-windowed-fullscreen", + "source": "niri", + "category": "windows", + "kind": "native", + "template": "toggle-windowed-fullscreen", + "usage": "toggle-windowed-fullscreen" + }, + { + "id": "niri/move-workspace-down", + "source": "niri", + "category": "workspaces", + "kind": "native", + "template": "move-workspace-down", + "usage": "move-workspace-down" + }, + { + "id": "niri/move-workspace-to-index", + "source": "niri", + "category": "workspaces", + "kind": "native", + "template": "move-workspace-to-index {{argument}}", + "usage": "move-workspace-to-index " + }, + { + "id": "niri/move-workspace-to-monitor", + "source": "niri", + "category": "workspaces", + "kind": "native", + "template": "move-workspace-to-monitor {{argument}}", + "usage": "move-workspace-to-monitor " + }, + { + "id": "niri/move-workspace-to-monitor-down", + "source": "niri", + "category": "workspaces", + "kind": "native", + "template": "move-workspace-to-monitor-down", + "usage": "move-workspace-to-monitor-down" + }, + { + "id": "niri/move-workspace-to-monitor-left", + "source": "niri", + "category": "workspaces", + "kind": "native", + "template": "move-workspace-to-monitor-left", + "usage": "move-workspace-to-monitor-left" + }, + { + "id": "niri/move-workspace-to-monitor-next", + "source": "niri", + "category": "workspaces", + "kind": "native", + "template": "move-workspace-to-monitor-next", + "usage": "move-workspace-to-monitor-next" + }, + { + "id": "niri/move-workspace-to-monitor-previous", + "source": "niri", + "category": "workspaces", + "kind": "native", + "template": "move-workspace-to-monitor-previous", + "usage": "move-workspace-to-monitor-previous" + }, + { + "id": "niri/move-workspace-to-monitor-right", + "source": "niri", + "category": "workspaces", + "kind": "native", + "template": "move-workspace-to-monitor-right", + "usage": "move-workspace-to-monitor-right" + }, + { + "id": "niri/move-workspace-to-monitor-up", + "source": "niri", + "category": "workspaces", + "kind": "native", + "template": "move-workspace-to-monitor-up", + "usage": "move-workspace-to-monitor-up" + }, + { + "id": "niri/move-workspace-up", + "source": "niri", + "category": "workspaces", + "kind": "native", + "template": "move-workspace-up", + "usage": "move-workspace-up" + }, + { + "id": "niri/set-workspace-name", + "source": "niri", + "category": "workspaces", + "kind": "native", + "template": "set-workspace-name {{argument}}", + "usage": "set-workspace-name " + }, + { + "id": "niri/unset-workspace-name", + "source": "niri", + "category": "workspaces", + "kind": "native", + "template": "unset-workspace-name", + "usage": "unset-workspace-name" + }, + { + "id": "noctalia/color-scheme-get", + "source": "noctalia", + "category": "appearance", + "kind": "shell", + "template": "noctalia msg color-scheme-get", + "usage": "color-scheme-get" + }, + { + "id": "noctalia/color-scheme-set", + "source": "noctalia", + "category": "appearance", + "kind": "shell", + "template": "noctalia msg color-scheme-set {{source}} {{name}}", + "usage": "color-scheme-set " + }, + { + "id": "noctalia/templates-apply", + "source": "noctalia", + "category": "appearance", + "kind": "shell", + "template": "noctalia msg templates-apply", + "usage": "templates-apply" + }, + { + "id": "noctalia/theme-mode-get", + "source": "noctalia", + "category": "appearance", + "kind": "shell", + "template": "noctalia msg theme-mode-get", + "usage": "theme-mode-get" + }, + { + "id": "noctalia/theme-mode-set", + "source": "noctalia", + "category": "appearance", + "kind": "shell", + "template": "noctalia msg theme-mode-set {{dark-light-auto}}", + "usage": "theme-mode-set " + }, + { + "id": "noctalia/theme-mode-toggle", + "source": "noctalia", + "category": "appearance", + "kind": "shell", + "template": "noctalia msg theme-mode-toggle", + "usage": "theme-mode-toggle" + }, + { + "id": "noctalia/wallpaper-get", + "source": "noctalia", + "category": "appearance", + "kind": "shell", + "template": "noctalia msg wallpaper-get {{connector}}", + "usage": "wallpaper-get []" + }, + { + "id": "noctalia/wallpaper-next", + "source": "noctalia", + "category": "appearance", + "kind": "shell", + "template": "noctalia msg wallpaper-next {{connector}}", + "usage": "wallpaper-next []" + }, + { + "id": "noctalia/wallpaper-previous", + "source": "noctalia", + "category": "appearance", + "kind": "shell", + "template": "noctalia msg wallpaper-previous {{connector}}", + "usage": "wallpaper-previous []" + }, + { + "id": "noctalia/wallpaper-random", + "source": "noctalia", + "category": "appearance", + "kind": "shell", + "template": "noctalia msg wallpaper-random {{connector}}", + "usage": "wallpaper-random []" + }, + { + "id": "noctalia/wallpaper-set", + "source": "noctalia", + "category": "appearance", + "kind": "shell", + "template": "noctalia msg wallpaper-set {{connector}} {{path}}", + "usage": "wallpaper-set [] " + }, + { + "id": "noctalia/effects-profile-set", + "source": "noctalia", + "category": "audio", + "kind": "shell", + "template": "noctalia msg effects-profile-set {{output-input}} {{profile}}", + "usage": "effects-profile-set " + }, + { + "id": "noctalia/mic-mute", + "source": "noctalia", + "category": "audio", + "kind": "shell", + "template": "noctalia msg mic-mute", + "usage": "mic-mute" + }, + { + "id": "noctalia/mic-volume-down", + "source": "noctalia", + "category": "audio", + "kind": "shell", + "template": "noctalia msg mic-volume-down", + "usage": "mic-volume-down [step]" + }, + { + "id": "noctalia/mic-volume-set", + "source": "noctalia", + "category": "audio", + "kind": "shell", + "template": "noctalia msg mic-volume-set {{value}}", + "usage": "mic-volume-set " + }, + { + "id": "noctalia/mic-volume-up", + "source": "noctalia", + "category": "audio", + "kind": "shell", + "template": "noctalia msg mic-volume-up", + "usage": "mic-volume-up [step]" + }, + { + "id": "noctalia/volume-down", + "source": "noctalia", + "category": "audio", + "kind": "shell", + "template": "noctalia msg volume-down", + "usage": "volume-down [step]" + }, + { + "id": "noctalia/volume-mute", + "source": "noctalia", + "category": "audio", + "kind": "shell", + "template": "noctalia msg volume-mute", + "usage": "volume-mute" + }, + { + "id": "noctalia/volume-set", + "source": "noctalia", + "category": "audio", + "kind": "shell", + "template": "noctalia msg volume-set {{value}}", + "usage": "volume-set " + }, + { + "id": "noctalia/volume-up", + "source": "noctalia", + "category": "audio", + "kind": "shell", + "template": "noctalia msg volume-up", + "usage": "volume-up [step]" + }, + { + "id": "noctalia/bar-auto-hide-set", + "source": "noctalia", + "category": "bars", + "kind": "shell", + "template": "noctalia msg bar-auto-hide-set {{on-off-true-false-1-0}}", + "usage": "bar-auto-hide-set [bar-name] [monitor-selector]" + }, + { + "id": "noctalia/bar-hide", + "source": "noctalia", + "category": "bars", + "kind": "shell", + "template": "noctalia msg bar-hide", + "usage": "bar-hide [bar-name] [monitor-selector]" + }, + { + "id": "noctalia/bar-layer-set", + "source": "noctalia", + "category": "bars", + "kind": "shell", + "template": "noctalia msg bar-layer-set {{top-overlay}}", + "usage": "bar-layer-set [bar-name] [monitor-selector]" + }, + { + "id": "noctalia/bar-reserve-toggle", + "source": "noctalia", + "category": "bars", + "kind": "shell", + "template": "noctalia msg bar-reserve-toggle", + "usage": "bar-reserve-toggle [bar-name] [monitor-selector]" + }, + { + "id": "noctalia/bar-show", + "source": "noctalia", + "category": "bars", + "kind": "shell", + "template": "noctalia msg bar-show", + "usage": "bar-show [bar-name] [monitor-selector]" + }, + { + "id": "noctalia/bar-toggle", + "source": "noctalia", + "category": "bars", + "kind": "shell", + "template": "noctalia msg bar-toggle", + "usage": "bar-toggle [bar-name] [monitor-selector]" + }, + { + "id": "noctalia/clipboard-clear", + "source": "noctalia", + "category": "clipboard", + "kind": "shell", + "template": "noctalia msg clipboard-clear", + "usage": "clipboard-clear" + }, + { + "id": "noctalia/bluetooth-disable", + "source": "noctalia", + "category": "connectivity", + "kind": "shell", + "template": "noctalia msg bluetooth-disable", + "usage": "bluetooth-disable" + }, + { + "id": "noctalia/bluetooth-enable", + "source": "noctalia", + "category": "connectivity", + "kind": "shell", + "template": "noctalia msg bluetooth-enable", + "usage": "bluetooth-enable" + }, + { + "id": "noctalia/bluetooth-status", + "source": "noctalia", + "category": "connectivity", + "kind": "shell", + "template": "noctalia msg bluetooth-status", + "usage": "bluetooth-status" + }, + { + "id": "noctalia/bluetooth-toggle", + "source": "noctalia", + "category": "connectivity", + "kind": "shell", + "template": "noctalia msg bluetooth-toggle", + "usage": "bluetooth-toggle" + }, + { + "id": "noctalia/wifi-disable", + "source": "noctalia", + "category": "connectivity", + "kind": "shell", + "template": "noctalia msg wifi-disable", + "usage": "wifi-disable" + }, + { + "id": "noctalia/wifi-enable", + "source": "noctalia", + "category": "connectivity", + "kind": "shell", + "template": "noctalia msg wifi-enable", + "usage": "wifi-enable" + }, + { + "id": "noctalia/wifi-status", + "source": "noctalia", + "category": "connectivity", + "kind": "shell", + "template": "noctalia msg wifi-status", + "usage": "wifi-status" + }, + { + "id": "noctalia/wifi-toggle", + "source": "noctalia", + "category": "connectivity", + "kind": "shell", + "template": "noctalia msg wifi-toggle", + "usage": "wifi-toggle" + }, + { + "id": "noctalia/brightness-down", + "source": "noctalia", + "category": "display", + "kind": "shell", + "template": "noctalia msg brightness-down", + "usage": "brightness-down [current|*|all|monitor-selector] [step]" + }, + { + "id": "noctalia/brightness-list-backlight-devices", + "source": "noctalia", + "category": "display", + "kind": "shell", + "template": "noctalia msg brightness-list-backlight-devices", + "usage": "brightness-list-backlight-devices" + }, + { + "id": "noctalia/brightness-osd", + "source": "noctalia", + "category": "display", + "kind": "shell", + "template": "noctalia msg brightness-osd {{value}}", + "usage": "brightness-osd " + }, + { + "id": "noctalia/brightness-set", + "source": "noctalia", + "category": "display", + "kind": "shell", + "template": "noctalia msg brightness-set {{value}}", + "usage": "brightness-set | brightness-set " + }, + { + "id": "noctalia/brightness-up", + "source": "noctalia", + "category": "display", + "kind": "shell", + "template": "noctalia msg brightness-up", + "usage": "brightness-up [current|*|all|monitor-selector] [step]" + }, + { + "id": "noctalia/dpms-off", + "source": "noctalia", + "category": "display", + "kind": "shell", + "template": "noctalia msg dpms-off", + "usage": "dpms-off" + }, + { + "id": "noctalia/dpms-on", + "source": "noctalia", + "category": "display", + "kind": "shell", + "template": "noctalia msg dpms-on", + "usage": "dpms-on" + }, + { + "id": "noctalia/keyboard-backlight-down", + "source": "noctalia", + "category": "display", + "kind": "shell", + "template": "noctalia msg keyboard-backlight-down", + "usage": "keyboard-backlight-down" + }, + { + "id": "noctalia/keyboard-backlight-osd", + "source": "noctalia", + "category": "display", + "kind": "shell", + "template": "noctalia msg keyboard-backlight-osd {{value}}", + "usage": "keyboard-backlight-osd " + }, + { + "id": "noctalia/keyboard-backlight-set", + "source": "noctalia", + "category": "display", + "kind": "shell", + "template": "noctalia msg keyboard-backlight-set {{value}}", + "usage": "keyboard-backlight-set " + }, + { + "id": "noctalia/keyboard-backlight-toggle", + "source": "noctalia", + "category": "display", + "kind": "shell", + "template": "noctalia msg keyboard-backlight-toggle", + "usage": "keyboard-backlight-toggle" + }, + { + "id": "noctalia/keyboard-backlight-up", + "source": "noctalia", + "category": "display", + "kind": "shell", + "template": "noctalia msg keyboard-backlight-up", + "usage": "keyboard-backlight-up" + }, + { + "id": "noctalia/media", + "source": "noctalia", + "category": "media", + "kind": "shell", + "template": "noctalia msg media {{next-previous-toggle-play-pause-stop-next-player-previous-player}}", + "usage": "media " + }, + { + "id": "noctalia/notification-clear-active", + "source": "noctalia", + "category": "notifications", + "kind": "shell", + "template": "noctalia msg notification-clear-active", + "usage": "notification-clear-active" + }, + { + "id": "noctalia/notification-clear-history", + "source": "noctalia", + "category": "notifications", + "kind": "shell", + "template": "noctalia msg notification-clear-history", + "usage": "notification-clear-history" + }, + { + "id": "noctalia/notification-dnd-set", + "source": "noctalia", + "category": "notifications", + "kind": "shell", + "template": "noctalia msg notification-dnd-set {{on-off-true-false-1-0}}", + "usage": "notification-dnd-set " + }, + { + "id": "noctalia/notification-dnd-status", + "source": "noctalia", + "category": "notifications", + "kind": "shell", + "template": "noctalia msg notification-dnd-status", + "usage": "notification-dnd-status" + }, + { + "id": "noctalia/notification-dnd-toggle", + "source": "noctalia", + "category": "notifications", + "kind": "shell", + "template": "noctalia msg notification-dnd-toggle", + "usage": "notification-dnd-toggle" + }, + { + "id": "noctalia/notification-invoke-latest", + "source": "noctalia", + "category": "notifications", + "kind": "shell", + "template": "noctalia msg notification-invoke-latest", + "usage": "notification-invoke-latest" + }, + { + "id": "noctalia/notification-show", + "source": "noctalia", + "category": "notifications", + "kind": "shell", + "template": "noctalia msg notification-show {{summary-body-json}}", + "usage": "notification-show " + }, + { + "id": "noctalia/dock-hide", + "source": "noctalia", + "category": "panels", + "kind": "shell", + "template": "noctalia msg dock-hide", + "usage": "dock-hide" + }, + { + "id": "noctalia/dock-reload", + "source": "noctalia", + "category": "panels", + "kind": "shell", + "template": "noctalia msg dock-reload", + "usage": "dock-reload" + }, + { + "id": "noctalia/dock-show", + "source": "noctalia", + "category": "panels", + "kind": "shell", + "template": "noctalia msg dock-show", + "usage": "dock-show" + }, + { + "id": "noctalia/dock-toggle", + "source": "noctalia", + "category": "panels", + "kind": "shell", + "template": "noctalia msg dock-toggle", + "usage": "dock-toggle" + }, + { + "id": "noctalia/panel-close", + "source": "noctalia", + "category": "panels", + "kind": "shell", + "template": "noctalia msg panel-close", + "usage": "panel-close [id]" + }, + { + "id": "noctalia/panel-open", + "source": "noctalia", + "category": "panels", + "kind": "shell", + "template": "noctalia msg panel-open {{id}}", + "usage": "panel-open [context]" + }, + { + "id": "noctalia/panel-toggle", + "source": "noctalia", + "category": "panels", + "kind": "shell", + "template": "noctalia msg panel-toggle {{id}}", + "usage": "panel-toggle [context]" + }, + { + "id": "noctalia/settings-close", + "source": "noctalia", + "category": "panels", + "kind": "shell", + "template": "noctalia msg settings-close", + "usage": "settings-close" + }, + { + "id": "noctalia/settings-open", + "source": "noctalia", + "category": "panels", + "kind": "shell", + "template": "noctalia msg settings-open", + "usage": "settings-open [context]" + }, + { + "id": "noctalia/settings-toggle", + "source": "noctalia", + "category": "panels", + "kind": "shell", + "template": "noctalia msg settings-toggle", + "usage": "settings-toggle [context]" + }, + { + "id": "noctalia/window-switcher", + "source": "noctalia", + "category": "panels", + "kind": "shell", + "template": "noctalia msg window-switcher", + "usage": "window-switcher [close]" + }, + { + "id": "noctalia/plugin", + "source": "noctalia", + "category": "plugins", + "kind": "shell", + "template": "noctalia msg plugin {{author-plugin-entry}} {{target-bar-name}} {{event}}", + "usage": "plugin [payload]" + }, + { + "id": "noctalia/plugins", + "source": "noctalia", + "category": "plugins", + "kind": "shell", + "template": "noctalia msg plugins {{list-enable-disable-update-source}}", + "usage": "plugins ..." + }, + { + "id": "noctalia/caffeine-disable", + "source": "noctalia", + "category": "power", + "kind": "shell", + "template": "noctalia msg caffeine-disable", + "usage": "caffeine-disable" + }, + { + "id": "noctalia/caffeine-enable", + "source": "noctalia", + "category": "power", + "kind": "shell", + "template": "noctalia msg caffeine-enable", + "usage": "caffeine-enable" + }, + { + "id": "noctalia/caffeine-toggle", + "source": "noctalia", + "category": "power", + "kind": "shell", + "template": "noctalia msg caffeine-toggle", + "usage": "caffeine-toggle" + }, + { + "id": "noctalia/nightlight-disable", + "source": "noctalia", + "category": "power", + "kind": "shell", + "template": "noctalia msg nightlight-disable", + "usage": "nightlight-disable" + }, + { + "id": "noctalia/nightlight-enable", + "source": "noctalia", + "category": "power", + "kind": "shell", + "template": "noctalia msg nightlight-enable", + "usage": "nightlight-enable" + }, + { + "id": "noctalia/nightlight-force-toggle", + "source": "noctalia", + "category": "power", + "kind": "shell", + "template": "noctalia msg nightlight-force-toggle", + "usage": "nightlight-force-toggle" + }, + { + "id": "noctalia/nightlight-toggle", + "source": "noctalia", + "category": "power", + "kind": "shell", + "template": "noctalia msg nightlight-toggle", + "usage": "nightlight-toggle" + }, + { + "id": "noctalia/power-cycle", + "source": "noctalia", + "category": "power", + "kind": "shell", + "template": "noctalia msg power-cycle", + "usage": "power-cycle" + }, + { + "id": "noctalia/power-set", + "source": "noctalia", + "category": "power", + "kind": "shell", + "template": "noctalia msg power-set {{profile}}", + "usage": "power-set " + }, + { + "id": "noctalia/screenshot-fullscreen", + "source": "noctalia", + "category": "screenshots", + "kind": "shell", + "template": "noctalia msg screenshot-fullscreen", + "usage": "screenshot-fullscreen [pick|monitor|all]" + }, + { + "id": "noctalia/screenshot-region", + "source": "noctalia", + "category": "screenshots", + "kind": "shell", + "template": "noctalia msg screenshot-region", + "usage": "screenshot-region" + }, + { + "id": "noctalia/session", + "source": "noctalia", + "category": "session", + "kind": "shell", + "template": "noctalia msg session {{lock-suspend-lock-and-suspend-logout-reboot-shutdown}}", + "usage": "session " + }, + { + "id": "noctalia/config-reload", + "source": "noctalia", + "category": "system", + "kind": "shell", + "template": "noctalia msg config-reload", + "usage": "config-reload" + }, + { + "id": "noctalia/log-level-set", + "source": "noctalia", + "category": "system", + "kind": "shell", + "template": "noctalia msg log-level-set {{debug-info-warn-error}}", + "usage": "log-level-set " + }, + { + "id": "noctalia/log-level-status", + "source": "noctalia", + "category": "system", + "kind": "shell", + "template": "noctalia msg log-level-status", + "usage": "log-level-status" + }, + { + "id": "noctalia/status", + "source": "noctalia", + "category": "system", + "kind": "shell", + "template": "noctalia msg status", + "usage": "status" + }, + { + "id": "noctalia/desktop-widgets-edit", + "source": "noctalia", + "category": "widgets", + "kind": "shell", + "template": "noctalia msg desktop-widgets-edit", + "usage": "desktop-widgets-edit" + }, + { + "id": "noctalia/desktop-widgets-exit", + "source": "noctalia", + "category": "widgets", + "kind": "shell", + "template": "noctalia msg desktop-widgets-exit", + "usage": "desktop-widgets-exit" + }, + { + "id": "noctalia/desktop-widgets-hide", + "source": "noctalia", + "category": "widgets", + "kind": "shell", + "template": "noctalia msg desktop-widgets-hide", + "usage": "desktop-widgets-hide" + }, + { + "id": "noctalia/desktop-widgets-show", + "source": "noctalia", + "category": "widgets", + "kind": "shell", + "template": "noctalia msg desktop-widgets-show", + "usage": "desktop-widgets-show" + }, + { + "id": "noctalia/desktop-widgets-toggle", + "source": "noctalia", + "category": "widgets", + "kind": "shell", + "template": "noctalia msg desktop-widgets-toggle", + "usage": "desktop-widgets-toggle" + }, + { + "id": "noctalia/desktop-widgets-toggle-edit", + "source": "noctalia", + "category": "widgets", + "kind": "shell", + "template": "noctalia msg desktop-widgets-toggle-edit", + "usage": "desktop-widgets-toggle-edit" + }, + { + "id": "noctalia/lockscreen-widgets-edit", + "source": "noctalia", + "category": "widgets", + "kind": "shell", + "template": "noctalia msg lockscreen-widgets-edit", + "usage": "lockscreen-widgets-edit" + }, + { + "id": "noctalia/lockscreen-widgets-exit", + "source": "noctalia", + "category": "widgets", + "kind": "shell", + "template": "noctalia msg lockscreen-widgets-exit", + "usage": "lockscreen-widgets-exit" + }, + { + "id": "noctalia/lockscreen-widgets-toggle-edit", + "source": "noctalia", + "category": "widgets", + "kind": "shell", + "template": "noctalia msg lockscreen-widgets-toggle-edit", + "usage": "lockscreen-widgets-toggle-edit" + }, + { + "id": "noctalia/workspace-alert-add", + "source": "noctalia", + "category": "workspaces", + "kind": "shell", + "template": "noctalia msg workspace-alert-add {{workspace}}", + "usage": "workspace-alert-add " + }, + { + "id": "noctalia/workspace-alert-add-window", + "source": "noctalia", + "category": "workspaces", + "kind": "shell", + "template": "noctalia msg workspace-alert-add-window {{window-id}}", + "usage": "workspace-alert-add-window " + }, + { + "id": "noctalia/workspace-alert-clear", + "source": "noctalia", + "category": "workspaces", + "kind": "shell", + "template": "noctalia msg workspace-alert-clear {{workspace}}", + "usage": "workspace-alert-clear " + }, + { + "id": "noctalia/workspace-alert-clear-all", + "source": "noctalia", + "category": "workspaces", + "kind": "shell", + "template": "noctalia msg workspace-alert-clear-all", + "usage": "workspace-alert-clear-all" + }, + { + "id": "noctalia/workspace-alert-status", + "source": "noctalia", + "category": "workspaces", + "kind": "shell", + "template": "noctalia msg workspace-alert-status", + "usage": "workspace-alert-status" + } + ] +} diff --git a/keymap/examples/hyprland.lua b/keymap/examples/hyprland.lua new file mode 100644 index 0000000..6a924ee --- /dev/null +++ b/keymap/examples/hyprland.lua @@ -0,0 +1,56 @@ +-- Keymap example for Hyprland's native Lua configuration API. +-- This file is documentation and test data; it is not loaded automatically. + +-- 1. Applications +hl.bind("SUPER + RETURN", hl.dsp.exec_cmd("foot"), { description = "Open terminal" }) +hl.bind("SUPER + B", hl.dsp.exec_cmd("firefox"), { description = "Open browser" }) +hl.bind("SUPER + E", hl.dsp.exec_cmd("xdg-open ."), { description = "Open files" }) +hl.bind("SUPER + SPACE", hl.dsp.exec_cmd("noctalia msg panel-toggle launcher"), { description = "Open launcher" }) +hl.bind("SUPER + M", hl.dsp.exec_cmd("thunderbird"), { description = "Open mail" }) +hl.bind("SUPER + A", hl.dsp.exec_cmd("gnome-calculator"), { description = "Open calculator" }) + +-- 2. Windows +hl.bind("SUPER + Q", hl.dsp.window.close(), { description = "Close window" }) +hl.bind("SUPER + F", hl.dsp.window.fullscreen({ mode = "fullscreen", action = "toggle" }), { description = "Toggle fullscreen" }) +hl.bind("SUPER + SHIFT + F", hl.dsp.window.float({ action = "toggle" }), { description = "Toggle floating" }) +hl.bind("SUPER + LEFT", hl.dsp.focus({ direction = "left" }), { description = "Focus left" }) +hl.bind("SUPER + RIGHT", hl.dsp.focus({ direction = "right" }), { description = "Focus right" }) +hl.bind("SUPER + UP", hl.dsp.focus({ direction = "up" }), { description = "Focus up" }) +hl.bind("SUPER + DOWN", hl.dsp.focus({ direction = "down" }), { description = "Focus down" }) +hl.bind("SUPER + SHIFT + LEFT", hl.dsp.window.move({ direction = "left" }), { description = "Move window left" }) +hl.bind("SUPER + SHIFT + RIGHT", hl.dsp.window.move({ direction = "right" }), { description = "Move window right" }) +hl.bind("SUPER + TAB", hl.dsp.window.cycle_next(), { description = "Focus next window" }) + +-- 3. Workspaces +hl.bind("SUPER + 1", hl.dsp.focus({ workspace = 1 }), { description = "Workspace 1" }) +hl.bind("SUPER + 2", hl.dsp.focus({ workspace = 2 }), { description = "Workspace 2" }) +hl.bind("SUPER + 3", hl.dsp.focus({ workspace = 3 }), { description = "Workspace 3" }) +hl.bind("SUPER + 4", hl.dsp.focus({ workspace = 4 }), { description = "Workspace 4" }) +hl.bind("SUPER + SHIFT + 1", hl.dsp.window.move({ workspace = 1, follow = false }), { description = "Move to workspace 1" }) +hl.bind("SUPER + SHIFT + 2", hl.dsp.window.move({ workspace = 2, follow = false }), { description = "Move to workspace 2" }) +hl.bind("SUPER + mouse_down", hl.dsp.focus({ workspace = "e+1" }), { description = "Next workspace" }) +hl.bind("SUPER + mouse_up", hl.dsp.focus({ workspace = "e-1" }), { description = "Previous workspace" }) + +-- 4. Screenshots +hl.bind("Print", hl.dsp.exec_cmd("grimblast copy output"), { description = "Capture monitor" }) +hl.bind("SHIFT + Print", hl.dsp.exec_cmd("grimblast copy area"), { description = "Capture region" }) +hl.bind("CTRL + Print", hl.dsp.exec_cmd("grimblast copy active"), { description = "Capture window" }) + +-- 5. Noctalia +hl.bind("SUPER + K", hl.dsp.exec_cmd("noctalia msg panel-toggle blackbartblues/keymap:panel"), { description = "Open Keymap" }) +hl.bind("SUPER + V", hl.dsp.exec_cmd("noctalia msg panel-toggle clipboard"), { description = "Clipboard history" }) +hl.bind("SUPER + N", hl.dsp.exec_cmd("noctalia msg panel-toggle notifications"), { description = "Notifications" }) +hl.bind("SUPER + P", hl.dsp.exec_cmd("noctalia msg panel-toggle session-menu"), { description = "Session menu" }) + +-- 6. Media +hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true, description = "Play or pause" }) +hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true, description = "Next track" }) +hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true, description = "Previous track" }) +hl.bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true, description = "Mute audio" }) +hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"), { locked = true, repeating = true, description = "Volume up" }) +hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true, repeating = true, description = "Volume down" }) + +-- 7. Utilities +hl.bind("SUPER + C + V", hl.dsp.exec_cmd("wl-paste | wl-copy"), { release = true, description = "Normalize clipboard" }) +hl.bind("SUPER + SHIFT + C", hl.dsp.exec_cmd("hyprpicker -a"), { description = "Pick a color" }) +hl.bind("SUPER + L", hl.dsp.exec_cmd("noctalia msg session lock"), { description = "Lock screen" }) diff --git a/keymap/examples/mangowc.conf b/keymap/examples/mangowc.conf new file mode 100644 index 0000000..e00f874 --- /dev/null +++ b/keymap/examples/mangowc.conf @@ -0,0 +1,56 @@ +# Keymap example for MangoWC. +# This file is documentation and test data; it is not loaded automatically. + +# Applications +bind=SUPER,Return,spawn_shell,foot #"Open terminal" +bind=SUPER,B,spawn_shell,firefox #"Open browser" +bind=SUPER,E,spawn_shell,xdg-open . #"Open files" +bind=SUPER,Space,spawn_shell,noctalia msg panel-toggle launcher #"Open launcher" +bind=SUPER,M,spawn_shell,thunderbird #"Open mail" +bind=SUPER,A,spawn_shell,gnome-calculator #"Open calculator" + +# Windows +bind=SUPER,Q,killclient, #"Close window" +bind=SUPER,F,togglefullscreen, #"Toggle fullscreen" +bind=SUPER+SHIFT,F,togglefloating, #"Toggle floating" +bind=SUPER,Left,focusdir,left #"Focus left" +bind=SUPER,Right,focusdir,right #"Focus right" +bind=SUPER,Up,focusdir,up #"Focus up" +bind=SUPER,Down,focusdir,down #"Focus down" +bind=SUPER+SHIFT,Left,smartmovewin,left #"Move window left" +bind=SUPER+SHIFT,Right,smartmovewin,right #"Move window right" +bind=SUPER,Tab,focusstack,next #"Focus next window" + +# Workspaces +bind=SUPER,1,view,1 #"Workspace 1" +bind=SUPER,2,view,2 #"Workspace 2" +bind=SUPER,3,view,3 #"Workspace 3" +bind=SUPER,4,view,4 #"Workspace 4" +bind=SUPER+SHIFT,1,tag,1 #"Move to workspace 1" +bind=SUPER+SHIFT,2,tag,2 #"Move to workspace 2" +axisbind=SUPER,DOWN,viewtoleft,0 #"Next workspace" +axisbind=SUPER,UP,viewtoright,0 #"Previous workspace" + +# Screenshots +bind=NONE,Print,spawn_shell,grim ~/Pictures/screenshot.png #"Capture monitor" +bind=SHIFT,Print,spawn_shell,grim -g "$(slurp)" ~/Pictures/region.png #"Capture region" +bind=CTRL,Print,spawn_shell,grim -g "$(slurp -w)" ~/Pictures/window.png #"Capture window" + +# Noctalia +bind=SUPER,K,spawn_shell,noctalia msg panel-toggle blackbartblues/keymap:panel #"Open Keymap" +bind=SUPER,V,spawn_shell,noctalia msg panel-toggle clipboard #"Clipboard history" +bind=SUPER,N,spawn_shell,noctalia msg panel-toggle notifications #"Notifications" +bind=SUPER,P,spawn_shell,noctalia msg panel-toggle session-menu #"Session menu" + +# Media +bindl=NONE,XF86AudioPlay,spawn_shell,playerctl play-pause #"Play or pause" +bindl=NONE,XF86AudioNext,spawn_shell,playerctl next #"Next track" +bindl=NONE,XF86AudioPrev,spawn_shell,playerctl previous #"Previous track" +bindl=NONE,XF86AudioMute,spawn_shell,wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle #"Mute audio" +bindl=NONE,XF86AudioRaiseVolume,spawn_shell,wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ #"Volume up" +bindl=NONE,XF86AudioLowerVolume,spawn_shell,wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- #"Volume down" + +# Utilities +bindr=SUPER+SHIFT,V,spawn_shell,wl-paste | wl-copy #"Normalize clipboard" +bind=SUPER+SHIFT,C,spawn_shell,hyprpicker -a #"Pick a color" +bind=SUPER,L,spawn_shell,noctalia msg session lock #"Lock screen" diff --git a/keymap/examples/niri.kdl b/keymap/examples/niri.kdl new file mode 100644 index 0000000..bb602f0 --- /dev/null +++ b/keymap/examples/niri.kdl @@ -0,0 +1,57 @@ +// Keymap example for Niri. +// This file is documentation and test data; it is not loaded automatically. + +binds { + // #"Applications" + Mod+Return hotkey-overlay-title="Open terminal" { spawn-sh "foot"; } + Mod+B hotkey-overlay-title="Open browser" { spawn-sh "firefox"; } + Mod+E hotkey-overlay-title="Open files" { spawn-sh "xdg-open ."; } + Mod+Space hotkey-overlay-title="Open launcher" { spawn-sh "noctalia msg panel-toggle launcher"; } + Mod+M hotkey-overlay-title="Open mail" { spawn-sh "thunderbird"; } + Mod+A hotkey-overlay-title="Open calculator" { spawn-sh "gnome-calculator"; } + + // #"Windows" + Mod+Q hotkey-overlay-title="Close window" { close-window; } + Mod+F hotkey-overlay-title="Toggle fullscreen" { fullscreen-window; } + Mod+Shift+F hotkey-overlay-title="Toggle floating" { toggle-window-floating; } + Mod+Left hotkey-overlay-title="Focus left" { focus-column-left; } + Mod+Right hotkey-overlay-title="Focus right" { focus-column-right; } + Mod+Up hotkey-overlay-title="Focus up" { focus-window-up; } + Mod+Down hotkey-overlay-title="Focus down" { focus-window-down; } + Mod+Shift+Left hotkey-overlay-title="Move window left" { move-column-left; } + Mod+Shift+Right hotkey-overlay-title="Move window right" { move-column-right; } + Mod+Tab hotkey-overlay-title="Focus next window" { focus-window-or-workspace-down; } + + // #"Workspaces" + Mod+1 hotkey-overlay-title="Workspace 1" { focus-workspace 1; } + Mod+2 hotkey-overlay-title="Workspace 2" { focus-workspace 2; } + Mod+3 hotkey-overlay-title="Workspace 3" { focus-workspace 3; } + Mod+4 hotkey-overlay-title="Workspace 4" { focus-workspace 4; } + Mod+Shift+1 hotkey-overlay-title="Move to workspace 1" { move-column-to-workspace 1; } + Mod+Shift+2 hotkey-overlay-title="Move to workspace 2" { move-column-to-workspace 2; } + Mod+WheelScrollDown hotkey-overlay-title="Next workspace" { focus-workspace-down; } + Mod+WheelScrollUp hotkey-overlay-title="Previous workspace" { focus-workspace-up; } + + // #"Screenshots" + Print hotkey-overlay-title="Capture monitor" { screenshot-screen; } + Shift+Print hotkey-overlay-title="Capture region" { screenshot; } + Ctrl+Print hotkey-overlay-title="Capture window" { screenshot-window; } + + // #"Noctalia" + Mod+K hotkey-overlay-title="Open Keymap" { spawn-sh "noctalia msg panel-toggle blackbartblues/keymap:panel"; } + Mod+V hotkey-overlay-title="Clipboard history" { spawn-sh "noctalia msg panel-toggle clipboard"; } + Mod+N hotkey-overlay-title="Notifications" { spawn-sh "noctalia msg panel-toggle notifications"; } + Mod+P hotkey-overlay-title="Session menu" { spawn-sh "noctalia msg panel-toggle session-menu"; } + + // #"Media" + XF86AudioPlay hotkey-overlay-title="Play or pause" { spawn-sh "playerctl play-pause"; } + XF86AudioNext hotkey-overlay-title="Next track" { spawn-sh "playerctl next"; } + XF86AudioPrev hotkey-overlay-title="Previous track" { spawn-sh "playerctl previous"; } + XF86AudioMute hotkey-overlay-title="Mute audio" { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; } + XF86AudioRaiseVolume hotkey-overlay-title="Volume up" { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"; } + XF86AudioLowerVolume hotkey-overlay-title="Volume down" { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"; } + + // #"Utilities" + Mod+Shift+C hotkey-overlay-title="Pick a color" { spawn-sh "hyprpicker -a"; } + Mod+L hotkey-overlay-title="Lock screen" { spawn-sh "noctalia msg session lock"; } +} diff --git a/keymap/mangowc_service.luau b/keymap/mangowc_service.luau new file mode 100644 index 0000000..867619b --- /dev/null +++ b/keymap/mangowc_service.luau @@ -0,0 +1,967 @@ +--!nonstrict +-- Keymap service for MangoWC / mangowm. +-- +-- Mango's IPC does not expose the loaded keybinding registry. The recursively +-- included configuration tree is therefore the authoritative available source. + +local SNAPSHOT_KEY = "keymap.snapshot" +local REFRESH_REQUEST_KEY = "keymap.refresh_request" +local DEFAULT_CONFIG = "~/.config/mango/config.conf" +local SYSTEM_CONFIG = "/etc/mango/config.conf" +local MAX_FILES = 64 +local MAX_SOURCE_BYTES = 512 * 1024 +local MAX_HIDDEN_BYTES = 2 * 1024 * 1024 + +local refreshing = false +local refreshQueued = false + +local function config(key, fallback) + local value = noctalia.getConfig(key) + if value == nil then + return fallback + end + return value +end + +local function trim(value) + if type(value) ~= "string" then + return "" + end + return value:match("^%s*(.-)%s*$") or "" +end + +local function envSet(name) + local value = noctalia.getenv(name) + return type(value) == "string" and value ~= "" +end + +local function environment(name) + local value = noctalia.getenv(name) + return type(value) == "string" and value or "" +end + +local function desktopHas(value, wanted) + local normalized = tostring(value or ""):lower() + return normalized:find(wanted, 1, true) ~= nil +end + +-- Shared detection order used by all compositor-specific services. +local function detectedCompositor() + if envSet("NIRI_SOCKET") then + return "niri" + end + if envSet("HYPRLAND_INSTANCE_SIGNATURE") then + return "hyprland" + end + if envSet("MANGO_INSTANCE_SIGNATURE") then + return "mangowc" + end + + local current = noctalia.getenv("XDG_CURRENT_DESKTOP") + local session = noctalia.getenv("XDG_SESSION_DESKTOP") + local desktopSession = noctalia.getenv("DESKTOP_SESSION") + if desktopHas(current, "niri") or desktopHas(session, "niri") or desktopHas(desktopSession, "niri") then + return "niri" + end + if desktopHas(current, "hyprland") or desktopHas(session, "hyprland") or desktopHas(desktopSession, "hyprland") then + return "hyprland" + end + if desktopHas(current, "mango") or desktopHas(session, "mango") + or desktopHas(desktopSession, "mango") or desktopHas(current, "mangowc") + or desktopHas(session, "mangowc") or desktopHas(desktopSession, "mangowc") then + return "mangowc" + end + return "" +end + +local function selectedCompositor() + local selected = tostring(config("compositor", "auto")):lower() + if selected == "hyprland" or selected == "niri" or selected == "mangowc" then + return selected + end + return detectedCompositor() +end + +local function isActive() + return selectedCompositor() == "mangowc" +end + +local function dirname(path) + local directory = path:match("^(.*)/[^/]*$") + return directory ~= nil and directory ~= "" and directory or "." +end + +local function normalizePath(path) + local absolute = path:sub(1, 1) == "/" + local parts = {} + for part in path:gmatch("[^/]+") do + if part == ".." then + if #parts > 0 and parts[#parts] ~= ".." then + table.remove(parts) + elseif not absolute then + parts[#parts + 1] = part + end + elseif part ~= "." and part ~= "" then + parts[#parts + 1] = part + end + end + local normalized = table.concat(parts, "/") + if absolute then + return "/" .. normalized + end + return normalized ~= "" and normalized or "." +end + +local function expandPath(path) + local expanded = noctalia.expandPath(path) + if type(expanded) == "string" and expanded ~= "" then + return normalizePath(expanded) + end + return normalizePath(path) +end + +local function configPath() + local configured = trim(config("mangowc_config", DEFAULT_CONFIG)) + if configured == "" then + configured = DEFAULT_CONFIG + end + local path = expandPath(configured) + if noctalia.fileExists(path) then return path end + + local xdg = environment("XDG_CONFIG_HOME") + local mangoDir = normalizePath((xdg ~= "" and xdg or (environment("HOME") .. "/.config")) .. "/mango") + local candidates = { normalizePath(mangoDir .. "/config.conf"), SYSTEM_CONFIG } + for _, candidate in ipairs(candidates) do + if noctalia.fileExists(candidate) then return candidate end + end + + local entries = noctalia.listDir(mangoDir) + if type(entries) ~= "table" then return path end + table.sort(entries) + local bestPath, bestScore = nil, -1 + for _, name in ipairs(entries) do + if type(name) == "string" and name:match("%.conf$") and name ~= "keymap.conf" + and not name:lower():find("backup", 1, true) then + local candidate = normalizePath(mangoDir .. "/" .. name) + local source = noctalia.readFile(candidate) + if type(source) == "string" and #source <= MAX_SOURCE_BYTES then + local _, bindCount = source:gsub("bind[%w%-]*%s*=", "") + local _, sourceCount = source:gsub("source[%w%-]*%s*=", "") + local score = bindCount * 10 + sourceCount * 2 + if name:lower():find("bind", 1, true) then score = score + 5 end + if score > bestScore then bestPath, bestScore = candidate, score end + end + end + end + return bestScore > 0 and bestPath or path +end + +local function stripOuterQuotes(value) + local text = trim(value) + local quote = text:sub(1, 1) + if #text >= 2 and (quote == '"' or quote == "'") and text:sub(-1) == quote then + return text:sub(2, -2) + end + return text +end + +local function resolveSource(rootDirectory, source) + local path = stripOuterQuotes(source) + if path == "" then + return nil + end + if path:find("*", 1, true) or path:find("?", 1, true) or path:find("[", 1, true) then + return nil, "mangowc_source_glob_not_supported:" .. path + end + path = noctalia.expandPath(path) or path + if path:sub(1, 1) == "/" then + return normalizePath(path) + end + path = path:gsub("^%./", "") + return normalizePath(rootDirectory .. "/" .. path) +end + +local function slug(value) + local id = value:lower():gsub("[^%a%d]+", "-"):gsub("^-+", ""):gsub("-+$", "") + return id ~= "" and id or "category" +end + +local function titleCase(value) + local text = tostring(value or ""):gsub("_", " "):gsub("%-", " ") + text = text:gsub("(%l)(%u)", "%1 %2") + return (text:gsub("%f[%a]%a", string.upper)) +end + +local function splitCsv(value) + local parts = {} + local start = 1 + while true do + local comma = value:find(",", start, true) + if comma == nil then + parts[#parts + 1] = trim(value:sub(start)) + break + end + parts[#parts + 1] = trim(value:sub(start, comma - 1)) + start = comma + 1 + end + return parts +end + +local function joinTail(parts, first) + local output = {} + for index = first, #parts do + output[#output + 1] = parts[index] + end + return trim(table.concat(output, ",")) +end + +-- Locate an ordinary inline comment while preserving the plugin convention +-- #"description" and quoted hashes in shell commands. +local function findUnquotedComment(line) + local inSingle = false + local inDouble = false + local escaped = false + for index = 1, #line do + local char = line:sub(index, index) + if escaped then + escaped = false + elseif char == "\\" and (inSingle or inDouble) then + escaped = true + elseif char == "'" and not inDouble then + inSingle = not inSingle + elseif char == '"' and not inSingle then + inDouble = not inDouble + elseif char == "#" and not inSingle and not inDouble and line:sub(index + 1, index + 1) ~= '"' then + return index + end + end + return nil +end + +local function extractDescription(line) + local description = line:match('#"(.*)"%s*$') + local marker = line:match('()#".*"%s*$') + if description == nil or marker == nil then + return line, nil + end + description = description:gsub('\\"', '"'):gsub("\\\\", "\\") + return trim(line:sub(1, marker - 1)), description +end + +local function extractCategory(line) + local rest = trim(line) + if rest:sub(1, 1) ~= "#" then + return nil + end + local managed = trim(rest:match("^#%s*Keymap category:%s*(.+)$") or "") + if managed ~= "" then return managed end + rest = trim(rest:gsub("^#+", "", 1)) + if rest == "" or #rest > 100 or rest:match("^[%w%-]+%s*=") then + return nil + end + if rest:match("^[─━═=%-_*#/\\%s]+$") or rest:match("^[%(%[]") then + return nil + end + if rest:match("^%-%>") or rest:match("^=%>") then + return nil + end + local firstWord = rest:match("^(%a+)") + if firstWord ~= nil then + local upper = firstWord:upper() + if upper == "TODO" or upper == "FIXME" or upper == "NOTE" or upper == "HACK" + or upper == "XXX" or upper == "BUG" or upper == "WIP" then + return nil + end + end + rest = trim(rest:gsub("[─━═=%-_*][─━═=%-_*][─━═=%-_*]+%s*$", "")) + if rest == "" then + return nil + end + return trim(rest:match("^%d+%.%s*(.+)$") or rest) +end + +local KEY_NAMES = { + RETURN = "Enter", ESCAPE = "Esc", SPACE = "Space", PRINT = "PrtSc", + PRIOR = "PgUp", NEXT = "PgDn", EQUAL = "=", MINUS = "-", PLUS = "+", + COMMA = ",", PERIOD = ".", SEMICOLON = ";", APOSTROPHE = "'", GRAVE = "`", + SLASH = "/", BACKSLASH = "\\", BRACKETLEFT = "[", BRACKETRIGHT = "]", + XF86AUDIORAISEVOLUME = "Vol Up", XF86AUDIOLOWERVOLUME = "Vol Down", + XF86AUDIOMUTE = "Mute", XF86AUDIOMICMUTE = "Mic Mute", XF86AUDIOPLAY = "Play", + XF86AUDIOPAUSE = "Pause", XF86AUDIONEXT = "Next", XF86AUDIOPREV = "Prev", + XF86AUDIOSTOP = "Stop", XF86MONBRIGHTNESSUP = "Bright Up", + XF86MONBRIGHTNESSDOWN = "Bright Down", +} + +local AXIS_NAMES = { + UP = "Scroll Up", DOWN = "Scroll Down", LEFT = "Scroll Left", RIGHT = "Scroll Right", +} + +local BUTTON_NAMES = { + BTN_LEFT = "Left Click", BTN_RIGHT = "Right Click", BTN_MIDDLE = "Middle Click", + BTN_SIDE = "Mouse Side", BTN_EXTRA = "Mouse Extra", BTN_FORWARD = "Mouse Forward", + BTN_BACK = "Mouse Back", BTN_TASK = "Mouse Task", ["CODE:272"] = "Left Click", + ["CODE:273"] = "Right Click", ["CODE:274"] = "Middle Click", +} + +local MODIFIER_NAMES = { + SUPER = "Super", SUPER_L = "Super", SUPER_R = "Super", LOGO = "Super", + CTRL = "Ctrl", CONTROL = "Ctrl", CTRL_L = "Ctrl", CTRL_R = "Ctrl", + SHIFT = "Shift", SHIFT_L = "Shift", SHIFT_R = "Shift", + ALT = "Alt", ALT_L = "Alt", ALT_R = "Alt", MOD1 = "Alt", + HYPER = "Hyper", HYPER_L = "Hyper", HYPER_R = "Hyper", +} + +local MODIFIER_CODES = { + ["37"] = "Ctrl", ["50"] = "Shift", ["62"] = "Shift", ["64"] = "Alt", + ["105"] = "Ctrl", ["108"] = "Alt", ["133"] = "Super", ["134"] = "Super", +} + +local function parseModifiers(value) + local modifiers = {} + local seen = {} + for token in (value .. "+"):gmatch("(.-)%+") do + local upper = trim(token):upper() + local name = MODIFIER_NAMES[upper] + if name == nil then + name = MODIFIER_CODES[upper:match("^CODE:(%d+)$") or ""] + end + if name ~= nil and not seen[name] then + seen[name] = true + modifiers[#modifiers + 1] = name + elseif upper ~= "" and upper ~= "NONE" and name == nil then + local code = upper:match("^CODE:(%d+)$") + local rawName = code ~= nil and ("Code " .. code) or titleCase(upper:lower()) + if not seen[rawName] then + seen[rawName] = true + modifiers[#modifiers + 1] = rawName + end + end + end + return modifiers +end + +local function formatKey(value) + local key = trim(value) + return KEY_NAMES[key:upper()] or key +end + +local NO_ARG_ACTIONS = { + killclient = "actions.mangowc.killclient", togglefullscreen = "actions.mangowc.togglefullscreen", + togglefakefullscreen = "actions.mangowc.togglefakefullscreen", togglemaximizescreen = "actions.mangowc.togglemaximizescreen", + togglefloating = "actions.mangowc.togglefloating", toggle_all_floating = "actions.mangowc.toggle_all_floating", + toggleglobal = "actions.mangowc.toggleglobal", toggleoverview = "actions.mangowc.toggleoverview", + togglejump = "actions.mangowc.togglejump", toggleoverlay = "actions.mangowc.toggleoverlay", + toggle_scratchpad = "actions.mangowc.toggle_scratchpad", minimized = "actions.mangowc.minimized", + restore_minimized = "actions.mangowc.restore_minimized", reload_config = "actions.mangowc.reload_config", + quit = "actions.mangowc.quit", switch_proportion_preset = "actions.mangowc.switch_proportion_preset", + switch_keyboard_layout = "actions.mangowc.switch_keyboard_layout", zoom = "actions.mangowc.zoom", + restart = "actions.mangowc.restart", incnmaster = "actions.mangowc.incnmaster", + switch_layout = "actions.mangowc.switch_layout", togglegaps = "actions.mangowc.togglegaps", + dwindle_toggle_split_direction = "actions.mangowc.dwindle_toggle_split_direction", +} + +local DIRECTION_ACTIONS = { + focusdir = "actions.mangowc.focusdir", exchange_client = "actions.mangowc.exchange_client", + focusmon = "actions.mangowc.focusmon", tagmon = "actions.mangowc.tagmon", + groupjoin = "actions.mangowc.groupjoin", groupfocus = "actions.mangowc.groupfocus", + smartmovewin = "actions.mangowc.smartmovewin", smartresizewin = "actions.mangowc.smartresizewin", + scroller_stack = "actions.mangowc.scroller_stack", +} + +local function formatAction(action, args) + local normalized = trim(action):lower() + local argument = trim(args) + if NO_ARG_ACTIONS[normalized] ~= nil and (argument == "" or argument == "0") then + return noctalia.tr(NO_ARG_ACTIONS[normalized]) + end + if DIRECTION_ACTIONS[normalized] ~= nil then + local label = noctalia.tr(DIRECTION_ACTIONS[normalized]) + return argument ~= "" and noctalia.tr("actions.with_argument", { action = label, argument = titleCase(argument) }) or label + end + if normalized == "spawn" or normalized == "spawn_shell" or normalized == "spawn_on_empty" then + return argument ~= "" and noctalia.tr("actions.run", { command = argument }) or noctalia.tr("actions.run_command") + end + if normalized == "view" then + local tag = argument:match("^([^,]+)") or argument + return tag ~= "" and noctalia.tr("actions.view_tag_number", { tag = tag }) or noctalia.tr("actions.view_tag") + end + if normalized == "tag" or normalized == "tagsilent" then + local tag = argument:match("^([^,]+)") or argument + return tag ~= "" and noctalia.tr("actions.move_to_tag_number", { tag = tag }) or noctalia.tr("actions.move_to_tag") + end + if normalized == "setlayout" then + return argument ~= "" and noctalia.tr("actions.layout_value", { layout = argument }) or noctalia.tr("actions.set_layout") + end + if normalized == "setkeymode" then + return argument ~= "" and noctalia.tr("actions.key_mode_value", { mode = argument }) or noctalia.tr("actions.set_key_mode") + end + return noctalia.tr("actions.native", { + action = normalized .. (argument ~= "" and (" " .. argument) or ""), + }) +end + +local function parseFlags(suffix) + local flags = {} + for index = 1, #suffix do + local flag = suffix:sub(index, index):lower() + if flag == "l" then flags.locked = true + elseif flag == "s" then flags.keysym = true + elseif flag == "r" then flags.release = true + elseif flag == "p" then flags.pass = true end + end + return flags +end + +local function addCategory(context, name) + local category = context.byCategory[name] + if category ~= nil then + return category + end + local baseId = slug(name) + local id = baseId + local suffix = 2 + while context.categoryIds[id] do + id = baseId .. "-" .. tostring(suffix) + suffix = suffix + 1 + end + context.categoryIds[id] = true + category = { id = id, name = name, binds = {} } + context.byCategory[name] = category + context.categories[#context.categories + 1] = category + return category +end + +local fingerprint + +local function stableBindId(context, fields) + local identity = table.concat(fields, "|") + local count = (context.bindIds[identity] or 0) + 1 + context.bindIds[identity] = count + return "mango:" .. fingerprint(identity) .. (count > 1 and (":" .. tostring(count)) or "") +end + +-- FNV-1a over the exact source line. The editor recalculates this before a +-- write so a bind is never replaced after the file changed behind its back. +-- Multiplication is split into 16-bit words to retain exact 32-bit arithmetic +-- in runtimes where Lua numbers are doubles. +local function xorLowByte(value, byte) + local low = value % 256 + local result = 0 + local place = 1 + for _ = 1, 8 do + if low % 2 ~= byte % 2 then + result = result + place + end + low = math.floor(low / 2) + byte = math.floor(byte / 2) + place = place * 2 + end + return value - (value % 256) + result +end + +fingerprint = function(rawSnippet) + local hash = 2166136261 + for index = 1, #rawSnippet do + hash = xorLowByte(hash, rawSnippet:byte(index)) + local low = hash % 65536 + local high = math.floor(hash / 65536) + local lowProduct = low * 403 + local resultLow = lowProduct % 65536 + local resultHigh = ( + math.floor(lowProduct / 65536) + low * 256 + high * 403 + ) % 65536 + hash = resultHigh * 65536 + resultLow + end + return string.format("%08x", hash) +end + +local function sourceLines(source) + local lines = {} + for line in (source .. "\n"):gmatch("([^\n]*)\n") do lines[#lines + 1] = line end + return lines +end + +local function hexDecode(value) + if value == "" or #value % 2 ~= 0 or value:find("[^0-9a-f]") ~= nil then return nil end + local output = {} + for index = 1, #value, 2 do output[#output + 1] = string.char(tonumber(value:sub(index, index + 1), 16)) end + return table.concat(output) +end + +local function hiddenBlockAt(lines, startLine) + local namespace = "^([\t ]*)# Keymap hidden " + if lines[startLine]:match(namespace) == nil then return nil, startLine, false end + local indent, blockId, originalFingerprint = lines[startLine]:match( + namespace .. "v1 begin ([0-9a-f]+) ([0-9a-f]+)$" + ) + if indent == nil or #blockId ~= 8 or #originalFingerprint ~= 8 then return nil, startLine, true end + local marker = indent .. "# Keymap hidden v1" + local escaped = marker:gsub("(%W)", "%%%1") + local encoded, cursor = {}, startLine + 1 + while cursor <= #lines do + local chunk = lines[cursor]:match("^" .. escaped .. " data ([0-9a-f]+)$") + if chunk ~= nil then + if #chunk > 96 or #chunk % 2 ~= 0 then return nil, cursor, true end + encoded[#encoded + 1] = chunk + cursor = cursor + 1 + else + local endId = lines[cursor]:match("^" .. escaped .. " end ([0-9a-f]+)$") + if endId == nil then return nil, math.max(startLine, cursor - 1), true end + local original = hexDecode(table.concat(encoded)) + if #encoded == 0 or endId ~= blockId or original == nil or original == "" + or #original > MAX_HIDDEN_BYTES or fingerprint(original) ~= originalFingerprint + or fingerprint("MangoWC\0" .. original) ~= blockId then return nil, cursor, true end + return { block_id = blockId, original = original, original_fingerprint = originalFingerprint, + raw_snippet = table.concat(lines, "\n", startLine, cursor) }, cursor, true + end + end + return nil, #lines, true +end + +local function editCapabilities(kind, action) + local isKeyboard = kind == "bind" + return { + combo = isKeyboard, + category = isKeyboard, + description = isKeyboard, + command = isKeyboard and trim(action):lower() == "spawn_shell", + activation = isKeyboard, + } +end + +local function disabledEditCapabilities() + return { + combo = false, category = false, description = false, + command = false, activation = false, + } +end + +local function addBind( + context, categoryName, kind, suffix, parts, description, + source, lineNumber, rawSnippet, startLine +) + local modifiers = {} + local key = "" + local action = "" + local args = "" + if kind == "bind" then + if #parts < 3 then return false end + modifiers, key, action, args = parseModifiers(parts[1]), formatKey(parts[2]), parts[3], joinTail(parts, 4) + elseif kind == "axisbind" then + if #parts < 3 then return false end + modifiers = parseModifiers(parts[1]) + key, action, args = AXIS_NAMES[parts[2]:upper()] or titleCase(parts[2]), parts[3], joinTail(parts, 4) + elseif kind == "mousebind" then + if #parts < 3 then return false end + modifiers = parseModifiers(parts[1]) + key, action, args = BUTTON_NAMES[parts[2]:upper()] or titleCase(parts[2]), parts[3], joinTail(parts, 4) + elseif kind == "gesturebind" then + if #parts < 4 then return false end + modifiers = parseModifiers(parts[1]) + key, action, args = tostring(parts[3]) .. "-finger " .. titleCase(parts[2]), parts[4], joinTail(parts, 5) + elseif kind == "switchbind" then + if #parts < 2 then return false end + local state = parts[1]:lower() + key = state == "fold" and "Lid Closed" or (state == "unfold" and "Lid Open" or titleCase(parts[1])) + action, args = parts[2], joinTail(parts, 3) + else + return false + end + + local fields = { context.keymode, kind, table.concat(modifiers, "+"), key, suffix, action, args } + local category = addCategory(context, categoryName) + local activation = parseFlags(suffix).release == true and "release" or "press" + category.binds[#category.binds + 1] = { + id = stableBindId(context, fields), modifiers = modifiers, key = key, + description = description or formatAction(action, args), dispatcher = trim(action), + command = args, action = formatAction(action, args), + mode = context.keymode, kind = kind, flags = parseFlags(suffix), activation = activation, + source = source, line = startLine or lineNumber, + start_line = startLine or lineNumber, end_line = lineNumber, + raw_snippet = rawSnippet, fingerprint = fingerprint(rawSnippet), + managed = source:match("([^/]+)$") == "keymap.conf", + capabilities = editCapabilities(kind, action), + _rawKey = kind == "bind" and trim(parts[2]) or key, + } + context.total = context.total + 1 + return true +end + +local function hiddenTarget(block, path, startLine, endLine, inheritedCategory, keymode) + local bindLine = "" + for raw in (block.original .. "\n"):gmatch("([^\n]*)\n") do + if trim(raw):match("^bind[lsrp]*%s*=") or trim(raw):match("^axisbind%s*=") + or trim(raw):match("^mousebind%s*=") or trim(raw):match("^gesturebind%s*=") + or trim(raw):match("^switchbind%s*=") then bindLine = raw end + end + local effective, description = extractDescription(trim(bindLine)) + local directive, value = effective:match("^([%w%-]+)%s*=%s*(.*)$") + directive = tostring(directive or ""):lower() + local suffix = directive:match("^bind([lsrp]*)$") + local kind = suffix ~= nil and "bind" or directive + if kind ~= "bind" and kind ~= "axisbind" and kind ~= "mousebind" + and kind ~= "gesturebind" and kind ~= "switchbind" then kind, suffix = "bind", "" end + local markedCategory = block.original:match( + "^%s*#%s*Keymap bind%-category:%s*([^\n]-)%s*\n" + ) + local temporary = { + defaultCategory = inheritedCategory or "", filesRead = 0, files = {}, visited = {}, warnings = {}, + categories = {}, byCategory = {}, categoryIds = {}, bindIds = {}, total = 0, + keymode = keymode or "default", + } + addBind(temporary, markedCategory or inheritedCategory or temporary.defaultCategory, kind, suffix or "", + splitCsv(value or ""), description, path, endLine, block.original, startLine) + local bind = temporary.categories[1] and temporary.categories[1].binds[1] or {} + bind.hidden = true + bind.id = "hidden:mango:" .. fingerprint(path .. "\0" .. tostring(startLine) .. "\0" .. block.block_id) + bind.source, bind.line, bind.start_line, bind.end_line = path, startLine, startLine, endLine + bind.raw_snippet, bind.fingerprint = block.raw_snippet, fingerprint(block.raw_snippet) + bind.original_fingerprint = block.original_fingerprint + bind.category = markedCategory or inheritedCategory or temporary.defaultCategory + bind.capabilities = { restore = true, delete = true } + return bind +end + +local function parseFile(context, path, optional, depth) + if context.filesRead >= MAX_FILES then + context.warnings[#context.warnings + 1] = "mangowc_file_limit_reached" + return + end + if context.visited[path] then + return + end + context.visited[path] = true + local source = noctalia.readFile(path) + if type(source) ~= "string" then + if not optional then + local prefix = depth == 0 and "mangowc_config_unreadable:" or "mangowc_required_source_unreadable:" + context.warnings[#context.warnings + 1] = prefix .. path + if depth > 0 then + context.fatalError = "mangowc_required_source_missing" + end + end + return + end + context.filesRead = context.filesRead + 1 + context.files[#context.files + 1] = path + if #source > MAX_SOURCE_BYTES then + source = source:sub(1, MAX_SOURCE_BYTES) + context.warnings[#context.warnings + 1] = "mangowc_source_truncated:" .. path + end + + local currentCategory = nil + local pendingBindCategory = nil + local pendingMarkerLine = nil + local pendingMarkerRaw = nil + local lineNumber = 0 + local hiddenLines = sourceLines(source) + local hiddenCategory = nil + local hiddenKeymode = context.keymode + local hiddenCursor = 1 + local hiddenConsumed = {} + while hiddenCursor <= #hiddenLines do + local block, blockEnd, candidate = hiddenBlockAt(hiddenLines, hiddenCursor) + if candidate then + for consumed = hiddenCursor, blockEnd do hiddenConsumed[consumed] = true end + if block == nil then + context.warnings[#context.warnings + 1] = "hidden_block_invalid:" .. path .. ":" .. tostring(hiddenCursor) + else + context.hidden[#context.hidden + 1] = hiddenTarget( + block, path, hiddenCursor, blockEnd, + hiddenCategory or context.defaultCategory, hiddenKeymode + ) + end + hiddenCursor = blockEnd + 1 + else + local raw = hiddenLines[hiddenCursor] + local category = extractCategory(raw) + if category ~= nil then hiddenCategory = category end + local effective = trim(raw:sub(1, (findUnquotedComment(raw) or (#raw + 1)) - 1)) + local directive, value = effective:match("^([%w%-]+)%s*=%s*(.*)$") + if directive ~= nil and directive:lower() == "keymode" then + hiddenKeymode = trim(value) ~= "" and trim(value) or "default" + end + hiddenCursor = hiddenCursor + 1 + end + end + for rawLine in (source .. "\n"):gmatch("([^\n]*)\n") do + lineNumber = lineNumber + 1 + if not hiddenConsumed[lineNumber] then + local commentAt = findUnquotedComment(rawLine) + local effective = trim(commentAt ~= nil and rawLine:sub(1, commentAt - 1) or rawLine) + if effective == "" then + local bindCategory = rawLine:match( + "^%s*#%s*Keymap bind%-category:%s*(.-)%s*$" + ) + if bindCategory ~= nil and bindCategory ~= "" then + pendingBindCategory = bindCategory + pendingMarkerLine = lineNumber + pendingMarkerRaw = rawLine + else + local category = extractCategory(rawLine) + if category ~= nil then currentCategory = category end + end + else + local cleanLine, description = extractDescription(effective) + local directive, value = cleanLine:match("^([%w%-]+)%s*=%s*(.*)$") + if directive ~= nil then + directive = directive:lower() + if directive == "source" or directive == "source-optional" then + local included, warning = resolveSource(dirname(path), value) + if warning ~= nil then + context.warnings[#context.warnings + 1] = warning + elseif included ~= nil then + parseFile(context, included, directive == "source-optional", depth + 1) + end + elseif directive == "keymode" then + context.keymode = trim(value) ~= "" and trim(value) or "default" + else + local suffix = directive:match("^bind([lsrp]*)$") + local kind = suffix ~= nil and "bind" or nil + if kind == nil and (directive == "axisbind" or directive == "mousebind" + or directive == "gesturebind" or directive == "switchbind") then + kind, suffix = directive, "" + end + if kind ~= nil then + local rawSnippet = pendingMarkerRaw ~= nil + and (pendingMarkerRaw .. "\n" .. rawLine) or rawLine + if not addBind( + context, pendingBindCategory or currentCategory or context.defaultCategory, + kind, suffix, splitCsv(value), description, path, lineNumber, + rawSnippet, pendingMarkerLine + ) then + context.warnings[#context.warnings + 1] = "mangowc_invalid_bind:" + .. path .. ":" .. tostring(lineNumber) + end + end + end + end + pendingBindCategory = nil + pendingMarkerLine = nil + pendingMarkerRaw = nil + end + end + end +end + +local function cleanBind(bind) + return { + id = bind.id, modifiers = bind.modifiers, key = bind.key, + description = bind.description, dispatcher = bind.dispatcher, + mode = bind.mode, kind = bind.kind, flags = bind.flags, + activation = type(bind.flags) == "table" and bind.flags.release == true and "release" or "press", + command = bind.command, action = bind.action, managed = bind.managed == true, + source = bind.source, line = bind.line, + start_line = bind.start_line, end_line = bind.end_line, + raw_snippet = bind.raw_snippet, fingerprint = bind.fingerprint, + capabilities = bind.capabilities, + } +end + +local function descriptionTemplate(description) + local template, replacements = description:gsub("%f[%d]%d+%f[%D]", "%%N%%") + return template, replacements > 0 +end + +local function mergedBind(run) + local first = run[1].bind + local last = run[#run].bind + local range = tostring(run[1].number) .. "-" .. tostring(run[#run].number) + local startLine = first.start_line or first.line + local endLine = first.end_line or first.line + for _, item in ipairs(run) do + local itemStart = item.bind.start_line or item.bind.line + local itemEnd = item.bind.end_line or item.bind.line + if itemStart ~= nil and (startLine == nil or itemStart < startLine) then startLine = itemStart end + if itemEnd ~= nil and (endLine == nil or itemEnd > endLine) then endLine = itemEnd end + end + return { + id = "range:" .. first.id .. ":" .. last.id, + modifiers = first.modifiers, + key = range, + description = first.description:gsub("%f[%d]%d+%f[%D]", range, 1), + dispatcher = first.dispatcher, + mode = first.mode, + kind = first.kind, + flags = first.flags, + activation = type(first.flags) == "table" and first.flags.release == true and "release" or "press", + command = first.command, managed = first.managed == true, + source = first.source, + line = first.line, + start_line = startLine, end_line = endLine, + raw_snippet = "", fingerprint = "", + -- A displayed numeric range can represent interleaved, non-contiguous + -- source lines, so it deliberately cannot be edited as a single bind. + capabilities = disabledEditCapabilities(), + } +end + +-- Merge interleaved numeric runs (for example view 1, tag 1, view 2, +-- tag 2...). Mode is part of the signature so keymodes never bleed together. +local function mergeSequential(binds) + if #binds < 3 then + local output = {} + for _, bind in ipairs(binds) do output[#output + 1] = cleanBind(bind) end + return output + end + + local groups = {} + for index, bind in ipairs(binds) do + local rawKey = tostring(bind._rawKey or "") + local number = rawKey:match("^%d+$") and tonumber(rawKey) or nil + local template, hasNumber = descriptionTemplate(bind.description) + if number ~= nil and hasNumber then + local signature = table.concat({ + tostring(bind.mode or "default"), + table.concat(bind.modifiers or {}, "+"), + tostring(bind.dispatcher or ""), + template, + }, "|") + groups[signature] = groups[signature] or {} + groups[signature][#groups[signature] + 1] = { index = index, number = number, bind = bind } + end + end + + local replacements = {} + local skipped = {} + for _, group in pairs(groups) do + table.sort(group, function(a, b) + return a.number == b.number and a.index < b.index or a.number < b.number + end) + local runStart = 1 + for cursor = 2, #group + 1 do + local continues = cursor <= #group and group[cursor].number == group[cursor - 1].number + 1 + if not continues then + if cursor - runStart >= 3 then + local run = {} + local insertionIndex = group[runStart].index + for itemIndex = runStart, cursor - 1 do + local item = group[itemIndex] + run[#run + 1] = item + if item.index < insertionIndex then insertionIndex = item.index end + end + replacements[insertionIndex] = mergedBind(run) + for _, item in ipairs(run) do + if item.index ~= insertionIndex then skipped[item.index] = true end + end + end + runStart = cursor + end + end + end + + local output = {} + for index, bind in ipairs(binds) do + if replacements[index] ~= nil then + output[#output + 1] = replacements[index] + elseif not skipped[index] then + output[#output + 1] = cleanBind(bind) + end + end + return output +end + +local function parseConfig(source) + local context = { + defaultCategory = noctalia.tr("category.other"), + filesRead = 0, files = {}, visited = {}, warnings = {}, categories = {}, + byCategory = {}, categoryIds = {}, bindIds = {}, total = 0, hidden = {}, + keymode = "default", fatalError = nil, + } + parseFile(context, source, false, 0) + if config("merge_sequential", true) == true then + for _, category in ipairs(context.categories) do + category.binds = mergeSequential(category.binds) + end + else + for _, category in ipairs(context.categories) do + local clean = {} + for _, bind in ipairs(category.binds) do clean[#clean + 1] = cleanBind(bind) end + category.binds = clean + end + end + return context +end + +local function snapshot(status, source, errorCode, categories, total, warnings, updatedAt, hidden) + return { + status = status, error = errorCode or "", compositor = "MangoWC", source = source, + updated_at = updatedAt or "", total = total or 0, categories = categories or {}, + warnings = warnings or {}, + hidden = hidden or {}, + } +end + +local function publishError(source, errorCode, warnings) + noctalia.state.set( + SNAPSHOT_KEY, + snapshot("error", source, errorCode, {}, 0, warnings, os.date("%H:%M:%S")) + ) +end + +local function finishRefresh() + refreshing = false + if refreshQueued then + refreshQueued = false + refresh() + end +end + +function refresh() + if not isActive() then + return + end + if refreshing then + refreshQueued = true + return + end + refreshing = true + + local source = configPath() + local previous = noctalia.state.get(SNAPSHOT_KEY) + local sameCompositor = type(previous) == "table" and previous.compositor == "MangoWC" + local previousCategories = sameCompositor and previous.categories or {} + local previousTotal = sameCompositor and previous.total or 0 + local previousUpdatedAt = sameCompositor and previous.updated_at or "" + local previousHidden = sameCompositor and previous.hidden or {} + noctalia.state.set( + SNAPSHOT_KEY, + snapshot("loading", source, "", previousCategories, previousTotal, {}, previousUpdatedAt, previousHidden) + ) + + local parsed = parseConfig(source) + if #parsed.files == 0 then + publishError(source, "mangowc_config_unreadable", parsed.warnings) + finishRefresh() + return + end + if parsed.fatalError ~= nil then + publishError(source, parsed.fatalError, parsed.warnings) + finishRefresh() + return + end + if parsed.total == 0 and #parsed.hidden == 0 then + publishError(source, "mangowc_no_binds", parsed.warnings) + finishRefresh() + return + end + noctalia.state.set( + SNAPSHOT_KEY, + snapshot("ready", source, "", parsed.categories, parsed.total, parsed.warnings, os.date("%H:%M:%S"), parsed.hidden) + ) + finishRefresh() +end + +function onIpc(event, _payload) + if event == "refresh" then refresh() end +end + +function onConfigChanged() + refresh() +end + +noctalia.state.watch(REFRESH_REQUEST_KEY, function(_request) + refresh() +end) + +refresh() diff --git a/keymap/niri_service.luau b/keymap/niri_service.luau new file mode 100644 index 0000000..1c860d9 --- /dev/null +++ b/keymap/niri_service.luau @@ -0,0 +1,959 @@ +--!nonstrict +-- Niri data service for Keymap. +-- Niri IPC does not expose keybindings, so this service reads the active KDL +-- config and positional include files. It deliberately performs no subprocesses. + +local SNAPSHOT_KEY = "keymap.snapshot" +local REFRESH_REQUEST_KEY = "keymap.refresh_request" +local MAX_FILES = 64 +local MAX_SOURCE_BYTES = 512 * 1024 +local MAX_HIDDEN_BYTES = 2 * 1024 * 1024 + +local refreshing = false +local refreshQueued = false + +local function config(key, fallback) + local value = noctalia.getConfig(key) + if value == nil then + return fallback + end + return value +end + +local function trim(value) + if type(value) ~= "string" then + return "" + end + return value:match("^%s*(.-)%s*$") or "" +end + +local function env(name) + return trim(noctalia.getenv(name)) +end + +-- Shared compositor priority used by all three parser services. +local function detectedCompositor() + local selected = trim(config("compositor", "auto")):lower() + if selected ~= "" and selected ~= "auto" then + if selected == "mango" then + return "mangowc" + end + return selected + end + if env("NIRI_SOCKET") ~= "" then + return "niri" + end + if env("HYPRLAND_INSTANCE_SIGNATURE") ~= "" then + return "hyprland" + end + if env("MANGO_INSTANCE_SIGNATURE") ~= "" then + return "mangowc" + end + local desktop = (env("XDG_CURRENT_DESKTOP") .. ":" .. env("XDG_SESSION_DESKTOP") + .. ":" .. env("DESKTOP_SESSION")):lower() + if desktop:find("niri", 1, true) ~= nil then + return "niri" + end + if desktop:find("hyprland", 1, true) ~= nil then + return "hyprland" + end + if desktop:find("mangowc", 1, true) ~= nil or desktop:find("mango", 1, true) ~= nil then + return "mangowc" + end + return "unknown" +end + +local function normalizePath(path) + local absolute = path:sub(1, 1) == "/" + local parts = {} + for part in path:gmatch("[^/]+") do + if part == ".." then + if #parts > 0 and parts[#parts] ~= ".." then + table.remove(parts) + elseif not absolute then + parts[#parts + 1] = part + end + elseif part ~= "." and part ~= "" then + parts[#parts + 1] = part + end + end + local normalized = table.concat(parts, "/") + if absolute then + return "/" .. normalized + end + return normalized ~= "" and normalized or "." +end + +local function dirname(path) + local directory = path:match("^(.*)/[^/]*$") + return directory ~= nil and directory ~= "" and directory or "." +end + +local function expandHome(path) + if path == "~" then + return env("HOME") + end + if path:sub(1, 2) == "~/" then + return env("HOME") .. path:sub(2) + end + return path +end + +local function sourcePath() + local configured = trim(config("niri_config", "")) + if configured == "" then + configured = "~/.config/niri/config.kdl" + end + local expanded = normalizePath(expandHome(configured)) + if noctalia.fileExists(expanded) then return expanded end + + local xdg = env("XDG_CONFIG_HOME") + local niriDir = normalizePath((xdg ~= "" and xdg or (env("HOME") .. "/.config")) .. "/niri") + local candidates = { + normalizePath(expandHome(env("NIRI_CONFIG"))), + normalizePath(niriDir .. "/config.kdl"), + } + for _, path in ipairs(candidates) do + if path ~= "." and noctalia.fileExists(path) then return path end + end + + local entries = noctalia.listDir(niriDir) + if type(entries) ~= "table" then return expanded end + table.sort(entries) + local bestPath, bestScore = nil, -1 + for _, name in ipairs(entries) do + if type(name) == "string" and name:match("%.kdl$") and name ~= "keymap.kdl" + and not name:lower():find("backup", 1, true) then + local path = normalizePath(niriDir .. "/" .. name) + local source = noctalia.readFile(path) + if type(source) == "string" and #source <= MAX_SOURCE_BYTES then + local _, bindBlocks = source:gsub("%f[%a]binds%s*{", "") + local _, includes = source:gsub("%f[%a]include%s+", "") + local score = bindBlocks * 20 + includes * 2 + if name:lower():find("bind", 1, true) then score = score + 5 end + if score > bestScore then bestPath, bestScore = path, score end + end + end + end + return bestScore > 0 and bestPath or expanded +end + +local function resolveInclude(currentFile, path) + local expanded = expandHome(path) + if expanded:sub(1, 1) == "/" then + return normalizePath(expanded) + end + return normalizePath(dirname(currentFile) .. "/" .. expanded) +end + +local ESCAPES = { + n = "\n", + r = "\r", + t = "\t", + ["\\"] = "\\", + ['"'] = '"', + ["'"] = "'", +} + +local function quotedLiteral(text, startAt) + local start = startAt or 1 + while start <= #text and text:sub(start, start):match("%s") do + start = start + 1 + end + local quote = text:sub(start, start) + if quote ~= '"' and quote ~= "'" then + return nil, nil + end + local output = {} + local escaped = false + for index = start + 1, #text do + local char = text:sub(index, index) + if escaped then + output[#output + 1] = ESCAPES[char] or char + escaped = false + elseif char == "\\" then + escaped = true + elseif char == quote then + return table.concat(output), index + 1 + else + output[#output + 1] = char + end + end + return nil, nil +end + +-- Returns executable code and a trailing // comment. Comment markers inside +-- strings are preserved. KDL block comments remain stateful across lines. +local function stripComments(line, inBlockComment) + local code = {} + local comment = nil + local quote = nil + local escaped = false + local index = 1 + while index <= #line do + local char = line:sub(index, index) + local pair = line:sub(index, index + 1) + if inBlockComment then + if pair == "*/" then + inBlockComment = false + index = index + 2 + else + index = index + 1 + end + elseif quote ~= nil then + code[#code + 1] = char + if escaped then + escaped = false + elseif char == "\\" then + escaped = true + elseif char == quote then + quote = nil + end + index = index + 1 + elseif char == '"' or char == "'" then + quote = char + code[#code + 1] = char + index = index + 1 + elseif pair == "//" then + comment = line:sub(index + 2) + break + elseif pair == "/*" then + inBlockComment = true + index = index + 2 + else + code[#code + 1] = char + index = index + 1 + end + end + return table.concat(code), comment, inBlockComment +end + +local function braceDelta(text) + local delta = 0 + local quote = nil + local escaped = false + for index = 1, #text do + local char = text:sub(index, index) + if quote ~= nil then + if escaped then + escaped = false + elseif char == "\\" then + escaped = true + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + elseif char == "{" then + delta = delta + 1 + elseif char == "}" then + delta = delta - 1 + end + end + return delta +end + +local function firstOpenBrace(text) + local quote = nil + local escaped = false + for index = 1, #text do + local char = text:sub(index, index) + if quote ~= nil then + if escaped then + escaped = false + elseif char == "\\" then + escaped = true + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + elseif char == "{" then + return index + end + end + return nil +end + +local function contentBeforeOuterClose(text) + local depth = 1 + local quote = nil + local escaped = false + for index = 1, #text do + local char = text:sub(index, index) + if quote ~= nil then + if escaped then + escaped = false + elseif char == "\\" then + escaped = true + elseif char == quote then + quote = nil + end + elseif char == '"' or char == "'" then + quote = char + elseif char == "{" then + depth = depth + 1 + elseif char == "}" then + depth = depth - 1 + if depth == 0 then + return text:sub(1, index - 1) + end + end + end + return text +end + +local function includeNode(code) + local rest = code:match("^%s*include%s+(.+)$") + if rest == nil then + return nil, false + end + local optional = rest:match("%f[%w]optional%s*=%s*true%f[^%w]") ~= nil + local quoteAt = rest:find('"', 1, true) or rest:find("'", 1, true) + if quoteAt == nil then + return nil, optional + end + return quotedLiteral(rest, quoteAt), optional +end + +local MODIFIER_ALIASES = { + Control = "Ctrl", Ctrl = "Ctrl", Win = "Super", Super = "Super", + Mod = "Mod", Alt = "Alt", Shift = "Shift", + ISO_Level3_Shift = "Mod5", Mod5 = "Mod5", + ISO_Level5_Shift = "ISO_Level5_Shift", +} + +local KEY_NAMES = { + RETURN = "Enter", SPACE = "Space", ESCAPE = "Esc", PRINT = "PrtSc", + PRIOR = "PgUp", NEXT = "PgDn", + WHEELSCROLLUP = "Scroll Up", WHEELSCROLLDOWN = "Scroll Down", + WHEELSCROLLLEFT = "Scroll Left", WHEELSCROLLRIGHT = "Scroll Right", + TOUCHPADSCROLLUP = "Touchpad Up", TOUCHPADSCROLLDOWN = "Touchpad Down", + TOUCHPADSCROLLLEFT = "Touchpad Left", TOUCHPADSCROLLRIGHT = "Touchpad Right", + MOUSELEFT = "Left Click", MOUSERIGHT = "Right Click", MOUSEMIDDLE = "Middle Click", + MOUSEFORWARD = "Mouse Forward", MOUSEBACK = "Mouse Back", + XF86AUDIORAISEVOLUME = "Vol Up", XF86AUDIOLOWERVOLUME = "Vol Down", + XF86AUDIOMUTE = "Mute", XF86AUDIOMICMUTE = "Mic Mute", + XF86AUDIOPLAY = "Play", XF86AUDIOPAUSE = "Pause", XF86AUDIONEXT = "Next", + XF86AUDIOPREV = "Prev", XF86AUDIOSTOP = "Stop", + XF86MONBRIGHTNESSUP = "Bright Up", XF86MONBRIGHTNESSDOWN = "Bright Down", +} + +local function splitCombo(combo) + local modifiers = {} + local mainKey = "" + local signature = {} + for part in combo:gmatch("[^+]+") do + local item = trim(part) + local modifier = MODIFIER_ALIASES[item] + if modifier ~= nil then + modifiers[#modifiers + 1] = modifier + signature[#signature + 1] = modifier:lower() + elseif item ~= "" then + mainKey = item + signature[#signature + 1] = item:lower() + end + end + return modifiers, KEY_NAMES[mainKey:upper()] or mainKey, mainKey, table.concat(signature, "+") +end + +local function bindHeader(prefix) + local header = trim(prefix) + if header:sub(1, 1) == '"' or header:sub(1, 1) == "'" then + local combo, nextAt = quotedLiteral(header, 1) + return combo, combo ~= nil and trim(header:sub(nextAt)) or nil + end + local combo, attributes = header:match("^(%S+)%s*(.-)%s*$") + return combo, attributes +end + +local function titleAttribute(attributes) + local _, valueAt = (attributes or ""):find("hotkey%-overlay%-title%s*=%s*") + if valueAt == nil then + return nil + end + local suffix = attributes:sub(valueAt + 1) + if suffix:match("^%s*null%f[^%w_]") then + return nil + end + local value = quotedLiteral(suffix, 1) + if value == nil then + return nil + end + value = value:gsub("<[^>]*>", "") + value = value:gsub("&", "&"):gsub("<", "<"):gsub(">", ">") + return trim(value) +end + +local ACTION_CATEGORIES = { + spawn = "category.niri.applications", ["spawn-sh"] = "category.niri.applications", + ["focus-column"] = "category.niri.column_navigation", ["focus-window"] = "category.niri.window_focus", + ["focus-workspace"] = "category.niri.workspace_navigation", + ["move-column-to-workspace"] = "category.niri.workspace_management", + ["move-window-to-workspace"] = "category.niri.workspace_management", + ["move-column"] = "category.niri.move_columns", ["move-window"] = "category.niri.move_windows", + ["consume-window"] = "category.niri.window_management", ["expel-window"] = "category.niri.window_management", + ["close-window"] = "category.niri.window_management", ["fullscreen-window"] = "category.niri.window_management", + ["maximize-column"] = "category.niri.column_management", ["set-column-width"] = "category.niri.column_width", + ["switch-preset-column-width"] = "category.niri.column_width", ["reset-window-height"] = "category.niri.window_size", + screenshot = "category.niri.screenshots", ["power-off-monitors"] = "category.niri.power", + ["power-on-monitors"] = "category.niri.power", quit = "category.niri.system", + ["toggle-animation"] = "category.niri.animations", +} + +local function actionVerb(action) + return action:match("^%s*([%w_-]+)") or "" +end + +local function actionDescription(action, verb) + if verb == "spawn" or verb == "spawn-sh" then + local command = quotedLiteral(action:sub(#verb + 1), 1) + if command ~= nil and command ~= "" then + return noctalia.tr("actions.run", { command = command }) + end + end + return noctalia.tr("actions.native", { action = trim(action:gsub(";%s*$", "")) }) +end + +local function actionCategory(action, verb) + local lower = action:lower() + if lower:find("noctalia", 1, true) ~= nil + and (lower:find(" msg ", 1, true) ~= nil or lower:find(" ipc ", 1, true) ~= nil) + then + return noctalia.tr("category.noctalia") + end + local best = nil + local bestLength = -1 + for prefix, category in pairs(ACTION_CATEGORIES) do + if verb:sub(1, #prefix) == prefix and #prefix > bestLength then + best = category + bestLength = #prefix + end + end + return noctalia.tr(best or "category.other") +end + +local function slug(value) + local id = value:lower():gsub("[^%a%d]+", "-"):gsub("^-+", ""):gsub("-+$", "") + return id ~= "" and id or "category" +end + +local function xorByte(left, right) + local result = 0 + local place = 1 + for _ = 1, 8 do + local leftBit = left % 2 + local rightBit = right % 2 + if leftBit ~= rightBit then result = result + place end + left = math.floor(left / 2) + right = math.floor(right / 2) + place = place * 2 + end + return result +end + +-- FNV-1a 32-bit without bit libraries. Splitting the prime (0x01000193) into +-- 2^24 + 403 keeps every intermediate below the exact integer limit of a +-- Lua/Luau number. +local function stableFingerprint(value) + local hash = 2166136261 + for index = 1, #value do + local low = hash % 256 + hash = hash - low + xorByte(low, value:byte(index)) + hash = (hash * 403 + (hash % 256) * 16777216) % 4294967296 + end + return string.format("%08x", hash) +end + +local function sourceLines(source) + local lines = {} + for line in (source .. "\n"):gmatch("([^\n]*)\n") do lines[#lines + 1] = line end + return lines +end + +local function hexDecode(value) + if value == "" or #value % 2 ~= 0 or value:find("[^0-9a-f]") ~= nil then return nil end + local output = {} + for index = 1, #value, 2 do output[#output + 1] = string.char(tonumber(value:sub(index, index + 1), 16)) end + return table.concat(output) +end + +local function hiddenBlockAt(lines, startLine) + local namespace = "^([\t ]*)// Keymap hidden " + if lines[startLine]:match(namespace) == nil then return nil, startLine, false end + local indent, blockId, originalFingerprint = lines[startLine]:match( + namespace .. "v1 begin ([0-9a-f]+) ([0-9a-f]+)$" + ) + if indent == nil or #blockId ~= 8 or #originalFingerprint ~= 8 then return nil, startLine, true end + local marker = indent .. "// Keymap hidden v1" + local escaped = marker:gsub("(%W)", "%%%1") + local encoded, cursor = {}, startLine + 1 + while cursor <= #lines do + local chunk = lines[cursor]:match("^" .. escaped .. " data ([0-9a-f]+)$") + if chunk ~= nil then + if #chunk > 96 or #chunk % 2 ~= 0 then return nil, cursor, true end + encoded[#encoded + 1] = chunk + cursor = cursor + 1 + else + local endId = lines[cursor]:match("^" .. escaped .. " end ([0-9a-f]+)$") + if endId == nil then return nil, math.max(startLine, cursor - 1), true end + local original = hexDecode(table.concat(encoded)) + if #encoded == 0 or endId ~= blockId or original == nil or original == "" + or #original > MAX_HIDDEN_BYTES or stableFingerprint(original) ~= originalFingerprint + or stableFingerprint("Niri\0" .. original) ~= blockId then return nil, cursor, true end + return { block_id = blockId, original = original, original_fingerprint = originalFingerprint, + raw_snippet = table.concat(lines, "\n", startLine, cursor) }, cursor, true + end + end + return nil, #lines, true +end + +local function hiddenTarget(block, path, startLine, endLine, inheritedCategory) + local original = block.original + local markedCategory = original:match( + "^%s*//%s*Keymap bind%-category:%s*([^\n]-)%s*\n" + ) + local codeLines, inBlock = {}, false + for raw in (original .. "\n"):gmatch("([^\n]*)\n") do + local code + code, _, inBlock = stripComments(raw, inBlock) + if trim(code) ~= "" then codeLines[#codeLines + 1] = code end + end + local code = table.concat(codeLines, "\n") + local openAt = firstOpenBrace(code) + local combo, attributes, action = "", "", "" + if openAt ~= nil then + combo, attributes = bindHeader(code:sub(1, openAt - 1)) + action = contentBeforeOuterClose(code:sub(openAt + 1)) + end + combo, attributes = combo or "", attributes or "" + local modifiers, key, _, signature = splitCombo(combo) + local normalizedAction = trim(action):gsub(";%s*$", "") + local verb = actionVerb(normalizedAction) + local description = titleAttribute(attributes) + if description == nil or description == "" then description = actionDescription(normalizedAction, verb) end + local command = verb == "spawn-sh" and (quotedLiteral(normalizedAction:sub(#verb + 1), 1) or "") or "" + return { + hidden = true, id = "hidden:niri:" .. stableFingerprint(path .. "\0" .. tostring(startLine) .. "\0" .. block.block_id), + source = path, line = startLine, start_line = startLine, end_line = endLine, + raw_snippet = block.raw_snippet, fingerprint = stableFingerprint(block.raw_snippet), + original_fingerprint = block.original_fingerprint, modifiers = modifiers, key = key, + description = description, dispatcher = verb, command = command, action = normalizedAction, + activation = "press", category = markedCategory or inheritedCategory or actionCategory(normalizedAction, verb), + capabilities = { restore = true, delete = true }, _signature = signature, + } +end + +local function cleanBind(bind) + return { + id = bind.id, modifiers = bind.modifiers, key = bind.key, + description = bind.description, dispatcher = bind.dispatcher, + activation = "press", command = bind.command, action = bind.action, + source = bind.source, start_line = bind.start_line, end_line = bind.end_line, + raw_snippet = bind.raw_snippet, fingerprint = bind.fingerprint, + managed = bind.managed == true, capabilities = bind.capabilities, + } +end + +local function mergeSequential(binds) + local groups = {} + for index, bind in ipairs(binds) do + local number = bind._rawKey:match("^%d+$") and tonumber(bind._rawKey) or nil + local template, replacements = bind.description:gsub("%f[%d]%d+%f[%D]", "%%N%%") + if number ~= nil and replacements > 0 then + local signature = table.concat(bind.modifiers, "+") .. "|" .. bind.dispatcher .. "|" .. template + groups[signature] = groups[signature] or {} + groups[signature][#groups[signature] + 1] = { index = index, number = number, bind = bind } + end + end + local replacements = {} + local skipped = {} + for _, group in pairs(groups) do + table.sort(group, function(a, b) + return a.number == b.number and a.index < b.index or a.number < b.number + end) + local runStart = 1 + for cursor = 2, #group + 1 do + local continues = cursor <= #group and group[cursor].number == group[cursor - 1].number + 1 + if not continues then + if cursor - runStart >= 3 then + local first = group[runStart] + local last = group[cursor - 1] + local range = tostring(first.number) .. "-" .. tostring(last.number) + local merged = cleanBind(first.bind) + merged.id = "range:" .. first.bind.id .. ":" .. last.bind.id + merged.key = range + merged.description = first.bind.description:gsub("%f[%d]%d+%f[%D]", range, 1) + replacements[first.index] = merged + for item = runStart + 1, cursor - 1 do + skipped[group[item].index] = true + end + end + runStart = cursor + end + end + end + local output = {} + for index, bind in ipairs(binds) do + -- An editable range cannot safely carry the provenance of one real bind. + -- Preserve every effective record individually once provenance is present. + if bind.fingerprint ~= nil then + output[#output + 1] = cleanBind(bind) + elseif replacements[index] ~= nil then + output[#output + 1] = replacements[index] + elseif not skipped[index] then + output[#output + 1] = cleanBind(bind) + end + end + return output +end + +local function buildCategories(records) + local shouldMerge = config("merge_sequential", true) == true + local byName = {} + local order = {} + local seen = {} + local total = 0 + for _, bind in ipairs(records) do + if not bind._overridden then + if not seen[bind._category] then + seen[bind._category] = true + order[#order + 1] = bind._category + end + byName[bind._category] = byName[bind._category] or {} + byName[bind._category][#byName[bind._category] + 1] = bind + total = total + 1 + end + end + local categories = {} + local usedIds = {} + for _, name in ipairs(order) do + local baseId = slug(name) + local id = baseId + local suffix = 2 + while usedIds[id] do + id = baseId .. "-" .. tostring(suffix) + suffix = suffix + 1 + end + usedIds[id] = true + local binds = byName[name] + local output = {} + if shouldMerge then + output = mergeSequential(binds) + else + for _, bind in ipairs(binds) do + output[#output + 1] = cleanBind(bind) + end + end + categories[#categories + 1] = { id = id, name = name, binds = output } + end + return categories, total +end + +local function parseBind(combo, attributes, action, category, records, activeByCombo, provenance) + local modifiers, key, rawKey, signature = splitCombo(combo) + local normalizedAction = trim(action):gsub(";%s*$", "") + local verb = actionVerb(normalizedAction) + if rawKey == "" or verb == "" then + return + end + local previous = activeByCombo[signature] + if previous ~= nil then + previous._overridden = true + end + local description = titleAttribute(attributes) + if description == nil or description == "" then + description = actionDescription(normalizedAction, verb) + end + local command = "" + if verb == "spawn-sh" then + command = quotedLiteral(normalizedAction:sub(#verb + 1), 1) or "" + end + local rawSnippet = provenance.raw_snippet or "" + local bind = { + id = "niri:" .. stableFingerprint(signature .. "\0" .. verb .. "\0" .. normalizedAction), + modifiers = modifiers, key = key, description = description, dispatcher = verb, + activation = "press", command = command, action = normalizedAction, + source = provenance.source, start_line = provenance.start_line, + end_line = provenance.end_line, raw_snippet = rawSnippet, + fingerprint = stableFingerprint(rawSnippet), + managed = provenance.source:match("([^/]+)$") == "keymap.kdl", + capabilities = { + combo = true, category = true, description = true, + command = verb == "spawn-sh", activation = false, + }, + _rawKey = rawKey, _category = category or actionCategory(normalizedAction, verb), + } + records[#records + 1] = bind + activeByCombo[signature] = bind +end + +local function parseConfig(root) + local records = {} + local hidden = {} + local activeByCombo = {} + local warnings = {} + local warningSeen = {} + local visiting = {} + local filesRead = 0 + local fatalError = nil + + local function warn(value) + if not warningSeen[value] then + warningSeen[value] = true + warnings[#warnings + 1] = value + end + end + + local parseFile + parseFile = function(path, optional) + if visiting[path] then + warn("niri_include_cycle:" .. path) + return + end + if filesRead >= MAX_FILES then + warn("niri_file_limit_reached") + return + end + local source = noctalia.readFile(path) + if type(source) ~= "string" then + warn((optional and "niri_optional_include_missing:" or "niri_include_unreadable:") .. path) + if not optional then + fatalError = "niri_required_include_missing" + end + return + end + filesRead = filesRead + 1 + visiting[path] = true + if #source > MAX_SOURCE_BYTES then + source = source:sub(1, MAX_SOURCE_BYTES) + warn("niri_source_truncated:" .. path) + end + + local inBlockComment = false + local topDepth = 0 + local inBinds = false + local bindsDepth = 0 + local currentCategory = nil + local pendingBindCategory = nil + local pendingMarkerLine = nil + local pendingMarkerRaw = nil + local currentBind = nil + local slashdashDepth = nil + local lineNumber = 0 + local hiddenLines = sourceLines(source) + local hiddenCategory = nil + local hiddenCursor = 1 + while hiddenCursor <= #hiddenLines do + local block, blockEnd, candidate = hiddenBlockAt(hiddenLines, hiddenCursor) + if candidate then + if block == nil then + warn("hidden_block_invalid:" .. path .. ":" .. tostring(hiddenCursor)) + else + hidden[#hidden + 1] = hiddenTarget(block, path, hiddenCursor, blockEnd, hiddenCategory) + end + hiddenCursor = blockEnd + 1 + else + local _, comment = stripComments(hiddenLines[hiddenCursor], false) + local label = type(comment) == "string" and comment:match('^%s*#?%s*"([^\"]+)"%s*$') or nil + if label ~= nil and label ~= "" then hiddenCategory = label end + hiddenCursor = hiddenCursor + 1 + end + end + + local function finishBind() + local action = contentBeforeOuterClose(table.concat(currentBind.parts, "\n")) + parseBind( + currentBind.combo, currentBind.attributes, action, currentBind.category, + records, activeByCombo, + { + source = path, start_line = currentBind.start_line, + end_line = lineNumber, raw_snippet = table.concat(currentBind.raw_lines, "\n"), + } + ) + currentBind = nil + end + + for rawLine in (source .. "\n"):gmatch("([^\n]*)\n") do + lineNumber = lineNumber + 1 + local code, comment + code, comment, inBlockComment = stripComments(rawLine, inBlockComment) + local stripped = trim(code) + if slashdashDepth ~= nil then + slashdashDepth = slashdashDepth + braceDelta(code) + if slashdashDepth <= 0 then slashdashDepth = nil end + elseif stripped:sub(1, 2) == "/-" then + local delta = braceDelta(stripped:sub(3)) + if delta > 0 then slashdashDepth = delta end + elseif inBinds then + local delta = braceDelta(code) + if currentBind ~= nil then + currentBind.parts[#currentBind.parts + 1] = code + currentBind.raw_lines[#currentBind.raw_lines + 1] = rawLine + currentBind.depth = currentBind.depth + delta + bindsDepth = bindsDepth + delta + if currentBind.depth <= 0 then finishBind() end + else + local bindCategory = type(comment) == "string" and comment:match( + "^%s*Keymap bind%-category:%s*(.-)%s*$" + ) or nil + if bindCategory ~= nil and bindCategory ~= "" then + pendingBindCategory = bindCategory + pendingMarkerLine = lineNumber + pendingMarkerRaw = rawLine + else + local label = type(comment) == "string" and comment:match('^%s*#?%s*"([^\"]+)"%s*$') or nil + if label ~= nil and label ~= "" then currentCategory = label end + end + if stripped ~= "" and not stripped:match("^}") then + local openAt = firstOpenBrace(code) + if openAt ~= nil then + local combo, attributes = bindHeader(code:sub(1, openAt - 1)) + if combo ~= nil then + local actionStart = code:sub(openAt + 1) + currentBind = { + combo = combo, attributes = attributes, + category = pendingBindCategory or currentCategory, + parts = { actionStart }, depth = 1 + braceDelta(actionStart), + start_line = pendingMarkerLine or lineNumber, + raw_lines = pendingMarkerRaw ~= nil and { pendingMarkerRaw, rawLine } or { rawLine }, + } + pendingBindCategory = nil + pendingMarkerLine = nil + pendingMarkerRaw = nil + if currentBind.depth <= 0 then finishBind() end + else + pendingBindCategory = nil + pendingMarkerLine = nil + pendingMarkerRaw = nil + end + end + end + bindsDepth = bindsDepth + delta + end + if bindsDepth <= 0 then + inBinds = false + bindsDepth = 0 + currentCategory = nil + pendingBindCategory = nil + pendingMarkerLine = nil + pendingMarkerRaw = nil + currentBind = nil + end + else + if topDepth == 0 then + local includePath, optionalInclude = includeNode(code) + if includePath ~= nil then + parseFile(resolveInclude(path, includePath), optionalInclude) + end + if code:match("^%s*binds%s*{") then + inBinds = true + bindsDepth = braceDelta(code) + currentCategory = nil + end + end + if not inBinds then topDepth = topDepth + braceDelta(code) end + end + end + visiting[path] = nil + end + + parseFile(root, false) + local categories, total = buildCategories(records) + return categories, total, hidden, warnings, fatalError +end + +local function snapshot(status, source, errorCode, categories, total, warnings, updatedAt, hidden) + return { + status = status, + error = errorCode or "", + compositor = "Niri", + source = source, + updated_at = updatedAt or "", + total = total or 0, + categories = categories or {}, + warnings = warnings or {}, + hidden = hidden or {}, + } +end + +local function finishRefresh() + refreshing = false + if refreshQueued then + refreshQueued = false + refresh() + end +end + +function refresh() + -- Each compositor service receives the same events. Only the selected or + -- auto-detected service may publish, preventing snapshot races. + if detectedCompositor() ~= "niri" then + return + end + if refreshing then + refreshQueued = true + return + end + refreshing = true + local source = sourcePath() + local previous = noctalia.state.get(SNAPSHOT_KEY) + local keepPrevious = type(previous) == "table" and previous.compositor == "Niri" + local previousCategories = keepPrevious and previous.categories or {} + local previousTotal = keepPrevious and previous.total or 0 + local previousUpdatedAt = keepPrevious and previous.updated_at or "" + local previousHidden = keepPrevious and previous.hidden or {} + noctalia.state.set( + SNAPSHOT_KEY, + snapshot("loading", source, "", previousCategories, previousTotal, {}, previousUpdatedAt, previousHidden) + ) + + if type(noctalia.readFile(source)) ~= "string" then + noctalia.state.set( + SNAPSHOT_KEY, + snapshot("error", source, "niri_config_unreadable", {}, 0, {}, os.date("%H:%M:%S")) + ) + finishRefresh() + return + end + + local categories, total, hidden, warnings, fatalError = parseConfig(source) + if fatalError ~= nil then + noctalia.state.set( + SNAPSHOT_KEY, + snapshot("error", source, fatalError, {}, 0, warnings, os.date("%H:%M:%S")) + ) + elseif total == 0 and #hidden == 0 then + noctalia.state.set( + SNAPSHOT_KEY, + snapshot("error", source, "niri_no_binds", {}, 0, warnings, os.date("%H:%M:%S")) + ) + else + noctalia.state.set( + SNAPSHOT_KEY, + snapshot("ready", source, "", categories, total, warnings, os.date("%H:%M:%S"), hidden) + ) + end + finishRefresh() +end + +function onIpc(event, _payload) + if event == "refresh" then + refresh() + end +end + +function onConfigChanged() + refresh() +end + +-- Manual lifecycle: refreshes are driven by initial load, config changes, IPC, +-- and the shared refresh request. The host's periodic update hook is a no-op. +function update() +end + +noctalia.state.watch(REFRESH_REQUEST_KEY, function(_request) + refresh() +end) + +refresh() diff --git a/keymap/panel.luau b/keymap/panel.luau new file mode 100644 index 0000000..837264e --- /dev/null +++ b/keymap/panel.luau @@ -0,0 +1,3281 @@ +--!nonstrict + +-- Service/UI state contract +-- ------------------------- +-- `keymap.snapshot` is replaced atomically by the service: +-- { +-- status = "idle" | "loading" | "ready" | "error", +-- error = string, +-- compositor = "Hyprland", +-- source = string?, +-- updated_at = string?, +-- total = number, +-- warnings = { string }, +-- categories = { +-- { +-- id = string, +-- name = string, +-- binds = { +-- { +-- id = string, +-- modifiers = { "SUPER", "CTRL", ... }, +-- key = string, +-- description = string, +-- dispatcher = string?, +-- activation = "press" | "release", +-- }, +-- }, +-- }, +-- }, +-- } +-- The panel increments `keymap.refresh_request` to request a new +-- snapshot. It never mutates the service-owned snapshot. + +local SNAPSHOT_KEY = "keymap.snapshot" +local REFRESH_KEY = "keymap.refresh_request" +local CREATE_REQUEST_KEY = "keymap.create_request" +local CREATE_RESULT_KEY = "keymap.create_result" +local UPDATE_REQUEST_KEY = "keymap.update_request" +local UPDATE_RESULT_KEY = "keymap.update_result" + +local EMPTY_SNAPSHOT = { + status = "idle", + error = "", + compositor = "", + total = 0, + categories = {}, + hidden = {}, +} + +local snapshot = EMPTY_SNAPSHOT +local searchQuery = "" +local searchRevision = 0 +local viewMode = "keyboard" +local keyboardLayoutId = "" +local selectedKeyboardKey = nil +local activeModifiers = { SUPER = false, CTRL = false, SHIFT = false, ALT = false } +local creatorOpen = false +local creatorKeys = {} +local creatorKeysText = "" +local creatorActivation = "press" +local creatorCommand = "" +local creatorCommandKind = "shell" +local creatorLibraryEntryId = "" +local creatorDescription = "" +local creatorCategoryIndex = 0 +local creatorNewCategory = "" +local creatorCategoryNamesFrozen = {} +local creatorContextCompositor = "" +local creatorContextSource = "" +local creatorRevision = 0 +local creatorRequestId = "" +local creatorRequestCounter = 0 +local creatorBusy = false +local creatorError = "" +local commandLibraryOpen = false +local commandLibraryQuery = "" +local commandLibrarySourceIndex = 0 +local commandLibraryCategoryIndex = 0 +local commandLibraryReadinessIndex = 0 +local commandLibraryRevision = 0 +local formMode = "create" +local editorMode = false +local editingBindId = "" +local editingCategory = "" +local editingCapabilities = {} +local editingAction = "" +local editingRequestId = "" +local editingOperation = "update" +local editingSnapshotBefore = nil +local deleteConfirmBindId = "" +local hiddenDeleteConfirmBindId = "" +local renamingCategoryId = "" +local renamingCategoryOriginal = "" +local renamingCategoryValue = "" +local renamingCategoryRevision = 0 +local render +local selectKeyboardKey +local env = getfenv() +local dynamicCallbackNames = {} +local renderCallbackNames = nil + +local COMMAND_LIBRARY = { entries = {} } +do + local encoded = noctalia.readFile("command_library.json") + if type(encoded) == "string" and type(noctalia.json) == "table" + and type(noctalia.json.decode) == "function" then + local ok, decoded = pcall(noctalia.json.decode, encoded) + if ok and type(decoded) == "table" and decoded.schema == 1 + and type(decoded.entries) == "table" then + COMMAND_LIBRARY = decoded + end + end +end + +local function tr(key, args) + return noctalia.tr(key, args) +end + +local function cfg(key) + return noctalia.getConfig(key) +end + +local function asString(value, fallback) + if type(value) == "string" and value ~= "" then + return value + end + return fallback or "" +end + +local function asArray(value) + if type(value) == "table" then + return value + end + return {} +end + +local function registerDynamicCallback(name, callback) + env[name] = callback + if renderCallbackNames ~= nil then + renderCallbackNames[name] = true + end + return name +end + +local function finishDynamicCallbackRender() + local previous = dynamicCallbackNames + dynamicCallbackNames = renderCallbackNames or {} + renderCallbackNames = nil + for name, _ in pairs(previous) do + if dynamicCallbackNames[name] ~= true then + env[name] = nil + end + end +end + +local function shellQuote(value) + return "'" .. asString(value):gsub("'", "'\\''") .. "'" +end + +local function sourceDirectory() + local source = asString(snapshot.source) + if source == "" then return "" end + local expanded = noctalia.expandPath(source) + local directory = expanded:match("^(.*)/[^/]*$") + if directory == nil then return "." end + if directory == "" then return "/" end + return directory +end + +local function normalized(value) + return string.lower(noctalia.string.trim(asString(value))) +end + +local MODIFIER_ORDER = { "SUPER", "CTRL", "SHIFT", "ALT" } +local MODIFIER_ALIASES = { + super = "SUPER", meta = "SUPER", win = "SUPER", logo = "SUPER", mod = "SUPER", mod4 = "SUPER", + ctrl = "CTRL", control = "CTRL", + shift = "SHIFT", + alt = "ALT", option = "ALT", mod1 = "ALT", +} + +local KEY_ALIASES = { + escape = "esc", esc = "esc", + ["return"] = "enter", enter = "enter", kp_enter = "numenter", kpenter = "numenter", + space = "space", spacebar = "space", + print = "prtsc", printscreen = "prtsc", sys_req = "prtsc", prtsc = "prtsc", + scroll_lock = "scrolllock", scrolllock = "scrolllock", + pause = "pause", ["break"] = "pause", + insert = "insert", ins = "insert", delete = "delete", del = "delete", + home = "home", ["end"] = "end", prior = "pgup", page_up = "pgup", pageup = "pgup", pgup = "pgup", + next = "pgdn", page_down = "pgdn", pagedown = "pgdn", pgdn = "pgdn", + left = "left", arrowleft = "left", right = "right", arrowright = "right", + up = "up", arrowup = "up", down = "down", arrowdown = "down", + grave = "grave", asciitilde = "grave", minus = "minus", underscore = "minus", + equal = "equal", plus = "equal", + bracketleft = "bracketleft", braceleft = "bracketleft", + bracketright = "bracketright", braceright = "bracketright", + backslash = "backslash", bar = "backslash", + semicolon = "semicolon", colon = "semicolon", apostrophe = "apostrophe", quotedbl = "apostrophe", + comma = "comma", less = "comma", period = "period", greater = "period", slash = "slash", question = "slash", + num_lock = "numlock", numlock = "numlock", kp_divide = "numdivide", kpdivide = "numdivide", + kp_multiply = "nummultiply", kpmultiply = "nummultiply", kp_subtract = "numminus", kpsubtract = "numminus", + kp_add = "numplus", kpadd = "numplus", kp_decimal = "numdecimal", kpdecimal = "numdecimal", + num_plus = "numplus", ["num_+"] = "numplus", num_minus = "numminus", ["num_-"] = "numminus", + num_multiply = "nummultiply", ["num_*"] = "nummultiply", + num_divide = "numdivide", ["num_/"] = "numdivide", + kp_insert = "num0", kpinsert = "num0", kp_end = "num1", kpend = "num1", + kp_down = "num2", kpdown = "num2", kp_next = "num3", kpnext = "num3", + kp_left = "num4", kpleft = "num4", kp_begin = "num5", kpbegin = "num5", + kp_right = "num6", kpright = "num6", kp_home = "num7", kphome = "num7", + kp_up = "num8", kpup = "num8", kp_prior = "num9", kpprior = "num9", + kp_delete = "numdecimal", kpdelete = "numdecimal", +} + +local SYMBOL_ALIASES = { + ["~"] = "grave", ["!"] = "1", ["@"] = "2", ["#"] = "3", + ["$"] = "4", ["%"] = "5", ["^"] = "6", ["&"] = "7", ["*"] = "8", + ["("] = "9", [")"] = "0", ["-"] = "minus", ["_"] = "minus", ["="] = "equal", ["+"] = "equal", + ["["] = "bracketleft", ["{"] = "bracketleft", ["]"] = "bracketright", ["}"] = "bracketright", + ["\\"] = "backslash", ["|"] = "backslash", [";"] = "semicolon", [":"] = "semicolon", + ["'"] = "apostrophe", ["\""] = "apostrophe", [","] = "comma", ["<"] = "comma", + ["."] = "period", [">"] = "period", ["/"] = "slash", ["?"] = "slash", +} +SYMBOL_ALIASES[string.char(96)] = "grave" + +local function canonicalModifier(value) + return MODIFIER_ALIASES[normalized(value)] or string.upper(asString(value)) +end + +local function canonicalKey(value) + local raw = normalized(value) + if SYMBOL_ALIASES[raw] ~= nil then + return SYMBOL_ALIASES[raw] + end + local key = raw:gsub("%s+", "_"):gsub("%-", "_") + local rangeStart, rangeEnd = key:match("^(%d)_(%d)$") + if rangeStart ~= nil then + return rangeStart .. "-" .. rangeEnd + end + if KEY_ALIASES[key] ~= nil then + return KEY_ALIASES[key] + end + local numpad = key:match("^kp_?(%d)$") or key:match("^num_?(%d)$") + if numpad ~= nil then + return "num" .. numpad + end + if key:match("^f%d%d?$") or key:match("^[a-z]$") or key:match("^%d$") then + return key + end + return key +end + +local function modifierSignature(modifiers) + local enabled = {} + local extras = {} + for _, modifier in ipairs(asArray(modifiers)) do + local canonical = canonicalModifier(modifier) + if activeModifiers[canonical] ~= nil then + enabled[canonical] = true + else + extras[#extras + 1] = canonical + end + end + local ordered = {} + for _, modifier in ipairs(MODIFIER_ORDER) do + if enabled[modifier] then + ordered[#ordered + 1] = modifier + end + end + table.sort(extras) + for _, modifier in ipairs(extras) do + ordered[#ordered + 1] = modifier + end + return table.concat(ordered, "+") +end + +local function activeModifierSignature() + local ordered = {} + for _, modifier in ipairs(MODIFIER_ORDER) do + if activeModifiers[modifier] then + ordered[#ordered + 1] = modifier + end + end + return table.concat(ordered, "+") +end + +local function expandedKeys(value) + local canonical = canonicalKey(value) + local first, last = canonical:match("^(%d)%-(%d)$") + if first ~= nil and tonumber(first) <= tonumber(last) then + local result = {} + for number = tonumber(first), tonumber(last) do + result[#result + 1] = tostring(number) + end + return result + end + return { canonical } +end + +local function keyboardIndex() + local exact = {} + local any = {} + for _, category in ipairs(asArray(snapshot.categories)) do + for _, bind in ipairs(asArray(category.binds)) do + local signature = modifierSignature(bind.modifiers) + local indexedKeys = type(bind.keys) == "table" and bind.keys or expandedKeys(bind.key) + for _, rawKey in ipairs(indexedKeys) do + local key = canonicalKey(rawKey) + local entry = { + bind = bind, + category = asString(category.name, tr("panel.uncategorized")), + } + local chord = signature .. "|" .. key + exact[chord] = exact[chord] or {} + exact[chord][#exact[chord] + 1] = entry + any[key] = any[key] or {} + any[key][#any[key] + 1] = entry + end + end + end + return { exact = exact, any = any } +end + +local function keyCallback(id, code) + local name = "onKeyboardKey_" .. id + return registerDynamicCallback(name, function() + selectKeyboardKey(code) + end) +end + +local function contains(haystack, needle) + return string.find(normalized(haystack), needle, 1, true) ~= nil +end + +local function bindMatches(bind, needle) + if contains(bind.description, needle) or contains(bind.key, needle) or contains(bind.dispatcher, needle) + or contains(bind.action, needle) or contains(bind.mode, needle) then + return true + end + for _, modifier in ipairs(asArray(bind.modifiers)) do + if contains(modifier, needle) then + return true + end + end + return false +end + +local function filteredCategories() + local result = {} + local needle = normalized(searchQuery) + + for _, category in ipairs(asArray(snapshot.categories)) do + local binds = asArray(category.binds) + if needle == "" or contains(category.name, needle) then + result[#result + 1] = { + id = asString(category.id, asString(category.name)), + name = asString(category.name, tr("panel.uncategorized")), + binds = binds, + } + else + local matching = {} + for _, bind in ipairs(binds) do + if bindMatches(bind, needle) then + matching[#matching + 1] = bind + end + end + if #matching > 0 then + result[#result + 1] = { + id = asString(category.id, asString(category.name)), + name = asString(category.name, tr("panel.uncategorized")), + binds = matching, + } + end + end + end + + return result +end + +local function filteredHidden() + local result = {} + local needle = normalized(searchQuery) + for _, bind in ipairs(asArray(snapshot.hidden)) do + if needle == "" or bindMatches(bind, needle) or contains(bind.category, needle) then + result[#result + 1] = bind + end + end + return result +end + +local function categoryWeight(category) + return #asArray(category.binds) + 2 +end + +-- Split categories into contiguous, approximately equal columns. Contiguous +-- partitions preserve the reading order from the Lua configuration. +local function partitionCategories(categories, requestedColumns) + if #categories == 0 then + return {} + end + + local columnCount = math.max(1, math.min(requestedColumns, #categories)) + local columns = {} + local index = 1 + local remainingWeight = 0 + for _, category in ipairs(categories) do + remainingWeight += categoryWeight(category) + end + + for columnIndex = 1, columnCount do + local remainingColumns = columnCount - columnIndex + local target = remainingWeight / (remainingColumns + 1) + local column = {} + local weight = 0 + local lastAvailableIndex = #categories - remainingColumns + + while index <= lastAvailableIndex do + local nextCategory = categories[index] + local nextWeight = categoryWeight(nextCategory) + if #column > 0 and math.abs(weight - target) <= math.abs(weight + nextWeight - target) then + break + end + column[#column + 1] = nextCategory + weight += nextWeight + index += 1 + end + + columns[#columns + 1] = column + remainingWeight -= weight + end + + return columns +end + +local function cardFill() + local color = asString(cfg("card_color"), "surface_variant") + local opacity = math.max(0, math.min(100, tonumber(cfg("card_opacity")) or 35)) / 100 + if string.sub(color, 1, 1) == "#" then + local alpha = string.format("%02X", math.floor(opacity * 255 + 0.5)) + if #color == 7 then + return color .. alpha + elseif #color == 9 then + return string.sub(color, 1, 7) .. alpha + end + return color + end + return color .. "/" .. string.format("%.2f", opacity) +end + +local function modifierStyle(token) + local name = normalized(token) + if name == "super" or name == "meta" or name == "win" or name == "logo" then + return cfg("super_color"), cfg("super_text_color") + elseif name == "ctrl" or name == "control" then + return cfg("ctrl_color"), cfg("ctrl_text_color") + elseif name == "shift" then + return cfg("shift_color"), cfg("shift_text_color") + elseif name == "alt" or name == "mod1" or name == "option" then + return cfg("alt_color"), cfg("alt_text_color") + end + return cfg("key_color"), cfg("key_text_color") +end + +-- BEGIN KEYBOARD LAYOUT DATA +local KEYBOARD_100 = { + -- Function row: 16 keys. + { + { id = "escape", label = "Esc", code = "Esc", units = 1 }, + { spacer = 1 }, + { id = "f1", label = "F1", code = "F1", units = 1 }, + { id = "f2", label = "F2", code = "F2", units = 1 }, + { id = "f3", label = "F3", code = "F3", units = 1 }, + { id = "f4", label = "F4", code = "F4", units = 1 }, + { spacer = 0.5 }, + { id = "f5", label = "F5", code = "F5", units = 1 }, + { id = "f6", label = "F6", code = "F6", units = 1 }, + { id = "f7", label = "F7", code = "F7", units = 1 }, + { id = "f8", label = "F8", code = "F8", units = 1 }, + { spacer = 0.5 }, + { id = "f9", label = "F9", code = "F9", units = 1 }, + { id = "f10", label = "F10", code = "F10", units = 1 }, + { id = "f11", label = "F11", code = "F11", units = 1 }, + { id = "f12", label = "F12", code = "F12", units = 1 }, + { spacer = 0.5 }, + { id = "print_screen", label = "Prt", code = "PrtSc", units = 1 }, + { id = "scroll_lock", label = "Scr", code = "Scroll Lock", units = 1 }, + { id = "pause", label = "Pau", code = "Pause", units = 1 }, + { spacer = 4.5 }, + }, + + -- Number row: 21 keys. + { + { id = "grave", label = "`", code = "`", units = 1 }, + { id = "digit_1", label = "1", code = "1", units = 1 }, + { id = "digit_2", label = "2", code = "2", units = 1 }, + { id = "digit_3", label = "3", code = "3", units = 1 }, + { id = "digit_4", label = "4", code = "4", units = 1 }, + { id = "digit_5", label = "5", code = "5", units = 1 }, + { id = "digit_6", label = "6", code = "6", units = 1 }, + { id = "digit_7", label = "7", code = "7", units = 1 }, + { id = "digit_8", label = "8", code = "8", units = 1 }, + { id = "digit_9", label = "9", code = "9", units = 1 }, + { id = "digit_0", label = "0", code = "0", units = 1 }, + { id = "minus", label = "-", code = "-", units = 1 }, + { id = "equal", label = "=", code = "=", units = 1 }, + { id = "backspace", label = "Back", code = "Backspace", units = 2 }, + { spacer = 0.5 }, + { id = "insert", label = "Ins", code = "Insert", units = 1 }, + { id = "home", label = "Home", code = "Home", units = 1 }, + { id = "page_up", label = "PgU", code = "PgUp", units = 1 }, + { spacer = 0.5 }, + { id = "num_lock", label = "Num", code = "Num Lock", units = 1 }, + { id = "kp_divide", label = "/", code = "KP_Divide", units = 1 }, + { id = "kp_multiply", label = "*", code = "KP_Multiply", units = 1 }, + { id = "kp_subtract", label = "-", code = "KP_Subtract", units = 1 }, + }, + + -- QWERTY row: 21 keys. + { + { id = "tab", label = "Tab", code = "Tab", units = 1.5 }, + { id = "key_q", label = "Q", code = "Q", units = 1 }, + { id = "key_w", label = "W", code = "W", units = 1 }, + { id = "key_e", label = "E", code = "E", units = 1 }, + { id = "key_r", label = "R", code = "R", units = 1 }, + { id = "key_t", label = "T", code = "T", units = 1 }, + { id = "key_y", label = "Y", code = "Y", units = 1 }, + { id = "key_u", label = "U", code = "U", units = 1 }, + { id = "key_i", label = "I", code = "I", units = 1 }, + { id = "key_o", label = "O", code = "O", units = 1 }, + { id = "key_p", label = "P", code = "P", units = 1 }, + { id = "bracket_left", label = "[", code = "[", units = 1 }, + { id = "bracket_right", label = "]", code = "]", units = 1 }, + { id = "backslash", label = "\\", code = "\\", units = 1.5 }, + { spacer = 0.5 }, + { id = "delete", label = "Del", code = "Delete", units = 1 }, + { id = "end", label = "End", code = "End", units = 1 }, + { id = "page_down", label = "PgD", code = "PgDn", units = 1 }, + { spacer = 0.5 }, + { id = "kp_7", label = "7", code = "KP_7", units = 1 }, + { id = "kp_8", label = "8", code = "KP_8", units = 1 }, + { id = "kp_9", label = "9", code = "KP_9", units = 1 }, + { id = "kp_add", label = "+", code = "KP_Add", units = 1 }, + }, + + -- Home row: 16 keys. The final numpad spacer continues KP Add from above. + { + { id = "caps_lock", label = "Caps", code = "Caps Lock", units = 1.75, modifier = true }, + { id = "key_a", label = "A", code = "A", units = 1 }, + { id = "key_s", label = "S", code = "S", units = 1 }, + { id = "key_d", label = "D", code = "D", units = 1 }, + { id = "key_f", label = "F", code = "F", units = 1 }, + { id = "key_g", label = "G", code = "G", units = 1 }, + { id = "key_h", label = "H", code = "H", units = 1 }, + { id = "key_j", label = "J", code = "J", units = 1 }, + { id = "key_k", label = "K", code = "K", units = 1 }, + { id = "key_l", label = "L", code = "L", units = 1 }, + { id = "semicolon", label = ";", code = ";", units = 1 }, + { id = "apostrophe", label = "'", code = "'", units = 1 }, + { id = "enter", label = "Enter", code = "Enter", units = 2.25 }, + { spacer = 4 }, + { id = "kp_4", label = "4", code = "KP_4", units = 1 }, + { id = "kp_5", label = "5", code = "KP_5", units = 1 }, + { id = "kp_6", label = "6", code = "KP_6", units = 1 }, + { id = "kp_add_lower", label = "+", code = "KP_Add", units = 1 }, + }, + + -- Shift row: 17 keys. + { + { id = "shift_left", label = "Shift", code = "Shift", units = 2.25, modifier = true }, + { id = "key_z", label = "Z", code = "Z", units = 1 }, + { id = "key_x", label = "X", code = "X", units = 1 }, + { id = "key_c", label = "C", code = "C", units = 1 }, + { id = "key_v", label = "V", code = "V", units = 1 }, + { id = "key_b", label = "B", code = "B", units = 1 }, + { id = "key_n", label = "N", code = "N", units = 1 }, + { id = "key_m", label = "M", code = "M", units = 1 }, + { id = "comma", label = ",", code = ",", units = 1 }, + { id = "period", label = ".", code = ".", units = 1 }, + { id = "slash", label = "/", code = "/", units = 1 }, + { id = "shift_right", label = "Shift", code = "Shift", units = 2.75, modifier = true }, + { spacer = 1.5 }, + { id = "arrow_up", label = "↑", code = "Up", units = 1 }, + { spacer = 1 }, + { spacer = 0.5 }, + { id = "kp_1", label = "1", code = "KP_1", units = 1 }, + { id = "kp_2", label = "2", code = "KP_2", units = 1 }, + { id = "kp_3", label = "3", code = "KP_3", units = 1 }, + { id = "kp_enter", label = "Ent", code = "KP_Enter", units = 1 }, + }, + + -- Bottom row: 13 keys. The final numpad spacer continues KP Enter from above. + { + { id = "ctrl_left", label = "Ctrl", code = "Ctrl", units = 1.25, modifier = true }, + { id = "super_left", label = "Win", code = "Super", units = 1.25, modifier = true }, + { id = "alt_left", label = "Alt", code = "Alt", units = 1.25, modifier = true }, + { id = "space", label = "Space", code = "Space", units = 6.25 }, + { id = "alt_right", label = "Alt", code = "Alt", units = 1.25, modifier = true }, + { id = "super_right", label = "Win", code = "Super", units = 1.25, modifier = true }, + { id = "menu", label = "Menu", code = "Menu", units = 1.25 }, + { id = "ctrl_right", label = "Ctrl", code = "Ctrl", units = 1.25, modifier = true }, + { spacer = 0.5 }, + { id = "arrow_left", label = "←", code = "Left", units = 1 }, + { id = "arrow_down", label = "↓", code = "Down", units = 1 }, + { id = "arrow_right", label = "→", code = "Right", units = 1 }, + { spacer = 0.5 }, + { id = "kp_0", label = "0", code = "KP_0", units = 2 }, + { id = "kp_decimal", label = ".", code = "KP_Decimal", units = 1 }, + { id = "kp_enter_lower", label = "Ent", code = "KP_Enter", units = 1 }, + }, +} + +local function keyboardRowSlice(source, firstIndex, lastIndex) + local row = {} + for index = firstIndex, lastIndex do row[#row + 1] = source[index] end + return row +end + +local function keyboardAppend(row, spec) + row[#row + 1] = spec + return row +end + +local function keyboardAppendSlice(row, source, firstIndex, lastIndex) + for index = firstIndex, lastIndex do row[#row + 1] = source[index] end + return row +end + +local function keyboardSizedKey(spec, units) + return { + id = spec.id, label = spec.label, code = spec.code, units = units, + modifier = spec.modifier, bindable = spec.bindable, + } +end + +-- 96% / 1800-compact: the navigation block is folded into the main cluster, +-- while the complete numpad remains directly beside it. +local KEYBOARD_96_FUNCTION = { + KEYBOARD_100[1][1], + KEYBOARD_100[1][3], KEYBOARD_100[1][4], KEYBOARD_100[1][5], KEYBOARD_100[1][6], + KEYBOARD_100[1][8], KEYBOARD_100[1][9], KEYBOARD_100[1][10], KEYBOARD_100[1][11], + KEYBOARD_100[1][13], KEYBOARD_100[1][14], KEYBOARD_100[1][15], KEYBOARD_100[1][16], + KEYBOARD_100[3][16], KEYBOARD_100[2][17], KEYBOARD_100[3][17], + KEYBOARD_100[2][18], KEYBOARD_100[3][18], KEYBOARD_100[1][18], +} +local KEYBOARD_96_SHIFT = keyboardRowSlice(KEYBOARD_100[5], 1, 11) +keyboardAppend(KEYBOARD_96_SHIFT, keyboardSizedKey(KEYBOARD_100[5][12], 1.75)) +keyboardAppend(KEYBOARD_96_SHIFT, KEYBOARD_100[5][14]) +keyboardAppendSlice(KEYBOARD_96_SHIFT, KEYBOARD_100[5], 17, 20) +local KEYBOARD_96_BOTTOM = { + KEYBOARD_100[6][1], KEYBOARD_100[6][2], KEYBOARD_100[6][3], KEYBOARD_100[6][4], + keyboardSizedKey(KEYBOARD_100[6][5], 1), + keyboardSizedKey(KEYBOARD_100[6][6], 1), + keyboardSizedKey(KEYBOARD_100[6][8], 1), + KEYBOARD_100[6][10], KEYBOARD_100[6][11], KEYBOARD_100[6][12], + keyboardSizedKey(KEYBOARD_100[6][14], 1), KEYBOARD_100[6][15], KEYBOARD_100[6][16], +} +local KEYBOARD_96 = { + KEYBOARD_96_FUNCTION, + keyboardAppendSlice(keyboardRowSlice(KEYBOARD_100[2], 1, 14), KEYBOARD_100[2], 20, 23), + keyboardAppendSlice(keyboardRowSlice(KEYBOARD_100[3], 1, 14), KEYBOARD_100[3], 20, 23), + keyboardAppendSlice(keyboardRowSlice(KEYBOARD_100[4], 1, 13), KEYBOARD_100[4], 15, 18), + KEYBOARD_96_SHIFT, + KEYBOARD_96_BOTTOM, +} + +-- 80% / tenkeyless: full function and navigation clusters without a numpad. +local KEYBOARD_80 = { + keyboardRowSlice(KEYBOARD_100[1], 1, 20), + keyboardRowSlice(KEYBOARD_100[2], 1, 18), + keyboardRowSlice(KEYBOARD_100[3], 1, 18), + keyboardAppend(keyboardRowSlice(KEYBOARD_100[4], 1, 13), { spacer = 3.5 }), + keyboardRowSlice(KEYBOARD_100[5], 1, 15), + keyboardRowSlice(KEYBOARD_100[6], 1, 12), +} + +local KEYBOARD_COMPACT_BOTTOM = { + KEYBOARD_100[6][1], + KEYBOARD_100[6][2], + KEYBOARD_100[6][3], + KEYBOARD_100[6][4], + keyboardSizedKey(KEYBOARD_100[6][5], 1), + keyboardSizedKey(KEYBOARD_100[6][6], 1), + { id = "ctrl_right", label = "Ctrl", code = "Ctrl", units = 1, modifier = true }, + KEYBOARD_100[6][10], + KEYBOARD_100[6][11], + KEYBOARD_100[6][12], +} + +local function compactKeyboardBody() + local shiftRow = keyboardRowSlice(KEYBOARD_100[5], 1, 11) + keyboardAppend(shiftRow, keyboardSizedKey(KEYBOARD_100[5][12], 1.75)) + keyboardAppend(shiftRow, KEYBOARD_100[5][14]) + keyboardAppend(shiftRow, KEYBOARD_100[3][17]) + return { + keyboardAppend(keyboardRowSlice(KEYBOARD_100[2], 1, 14), KEYBOARD_100[2][17]), + keyboardAppend(keyboardRowSlice(KEYBOARD_100[3], 1, 14), KEYBOARD_100[2][18]), + keyboardAppend(keyboardRowSlice(KEYBOARD_100[4], 1, 13), KEYBOARD_100[3][18]), + shiftRow, + keyboardRowSlice(KEYBOARD_COMPACT_BOTTOM, 1, #KEYBOARD_COMPACT_BOTTOM), + } +end + +-- 75%: compact navigation and arrows plus a dedicated function row. +local KEYBOARD_75_FUNCTION = { + KEYBOARD_100[1][1], + KEYBOARD_100[1][3], KEYBOARD_100[1][4], KEYBOARD_100[1][5], KEYBOARD_100[1][6], + KEYBOARD_100[1][8], KEYBOARD_100[1][9], KEYBOARD_100[1][10], KEYBOARD_100[1][11], + KEYBOARD_100[1][13], KEYBOARD_100[1][14], KEYBOARD_100[1][15], KEYBOARD_100[1][16], + KEYBOARD_100[1][18], KEYBOARD_100[1][20], KEYBOARD_100[3][16], +} +local KEYBOARD_75 = { KEYBOARD_75_FUNCTION } +for _, row in ipairs(compactKeyboardBody()) do KEYBOARD_75[#KEYBOARD_75 + 1] = row end + +-- 65%: the same compact navigation column and arrows, without function keys. +local KEYBOARD_65_NUMBER = { KEYBOARD_100[1][1] } +keyboardAppendSlice(KEYBOARD_65_NUMBER, KEYBOARD_100[2], 2, 14) +keyboardAppend(KEYBOARD_65_NUMBER, KEYBOARD_100[2][17]) +local KEYBOARD_65 = compactKeyboardBody() +KEYBOARD_65[1] = KEYBOARD_65_NUMBER +KEYBOARD_65[5] = { + KEYBOARD_100[6][1], KEYBOARD_100[6][2], KEYBOARD_100[6][3], KEYBOARD_100[6][4], + keyboardSizedKey(KEYBOARD_100[6][5], 1), + { id = "fn", label = "Fn", units = 1, bindable = false }, + { id = "ctrl_right", label = "Ctrl", code = "Ctrl", units = 1, modifier = true }, + KEYBOARD_100[6][10], KEYBOARD_100[6][11], KEYBOARD_100[6][12], +} + +-- 60%: the ANSI alphanumeric block only. +local KEYBOARD_60 = { + keyboardRowSlice(KEYBOARD_100[2], 1, 14), + keyboardRowSlice(KEYBOARD_100[3], 1, 14), + keyboardRowSlice(KEYBOARD_100[4], 1, 13), + keyboardRowSlice(KEYBOARD_100[5], 1, 12), + keyboardRowSlice(KEYBOARD_100[6], 1, 8), +} + +local KEYBOARD_LAYOUT_ORDER = { "100", "96", "80", "75", "65", "60" } +local KEYBOARD_LAYOUTS = { + ["100"] = { id = "100", rows = KEYBOARD_100, rowUnits = 23, labelKey = "p100", + features = { functionRow = true, numpad = true, navCluster = true, arrows = true } }, + ["96"] = { id = "96", rows = KEYBOARD_96, rowUnits = 19, labelKey = "p96", + features = { functionRow = true, numpad = true, navCluster = false, arrows = true } }, + ["80"] = { id = "80", rows = KEYBOARD_80, rowUnits = 18.5, labelKey = "p80", + features = { functionRow = true, numpad = false, navCluster = true, arrows = true } }, + ["75"] = { id = "75", rows = KEYBOARD_75, rowUnits = 16, labelKey = "p75", + features = { functionRow = true, numpad = false, navCluster = false, arrows = true } }, + ["65"] = { id = "65", rows = KEYBOARD_65, rowUnits = 16, labelKey = "p65", + features = { functionRow = false, numpad = false, navCluster = false, arrows = true } }, + ["60"] = { id = "60", rows = KEYBOARD_60, rowUnits = 15, labelKey = "p60", + features = { functionRow = false, numpad = false, navCluster = false, arrows = false } }, +} +-- END KEYBOARD LAYOUT DATA + +-- One unit plus one gap is 48 px, divisible by four. Quarter-unit keys can +-- therefore be positioned without fractional rounding drift. +local KEYBOARD_UNIT = 44 +local KEYBOARD_GAP = 4 +local KEYBOARD_KEY_HEIGHT = 42 +local KEYBOARD_CODE_SET = {} +local KEYBOARD_LABELS = {} +for _, layoutId in ipairs(KEYBOARD_LAYOUT_ORDER) do + local layout = KEYBOARD_LAYOUTS[layoutId] + layout.codeSet = {} + for _, keyboardRow in ipairs(layout.rows) do + for _, keySpec in ipairs(keyboardRow) do + if keySpec.bindable ~= false and keySpec.code ~= nil then + local code = canonicalKey(keySpec.code) + KEYBOARD_CODE_SET[code] = true + layout.codeSet[code] = true + KEYBOARD_LABELS[code] = asString(keySpec.label, keySpec.code) + end + end + end +end + +local function activeKeyboardLayoutId() + local layoutId = keyboardLayoutId ~= "" and keyboardLayoutId or asString(cfg("keyboard_layout"), "100") + if KEYBOARD_LAYOUTS[layoutId] == nil then return "100" end + return layoutId +end + +local function activeKeyboardLayout() + return KEYBOARD_LAYOUTS[activeKeyboardLayoutId()] +end + +local function keyboardLayoutSelectedIndex() + local activeId = activeKeyboardLayoutId() + for index, layoutId in ipairs(KEYBOARD_LAYOUT_ORDER) do + if layoutId == activeId then return index - 1 end + end + return 0 +end + +local function keyboardLayoutOptions() + local options = {} + for _, layoutId in ipairs(KEYBOARD_LAYOUT_ORDER) do + options[#options + 1] = tr("panel.keyboard.layouts." .. KEYBOARD_LAYOUTS[layoutId].labelKey) + end + return options +end + +-- Config files use XKB names, while the visual keyboard uses compact labels. +-- These names are accepted by Hyprland Lua, Niri KDL, and MangoWC. +local CONFIG_KEY_NAMES = { + esc = "Escape", prtsc = "Print", scrolllock = "Scroll_Lock", pause = "Pause", + grave = "grave", minus = "minus", equal = "equal", backspace = "BackSpace", + tab = "Tab", bracketleft = "bracketleft", bracketright = "bracketright", + backslash = "backslash", caps_lock = "Caps_Lock", semicolon = "semicolon", + apostrophe = "apostrophe", enter = "Return", comma = "comma", period = "period", + slash = "slash", space = "space", menu = "Menu", + insert = "Insert", home = "Home", pgup = "Prior", delete = "Delete", ["end"] = "End", + pgdn = "Next", left = "Left", right = "Right", up = "Up", down = "Down", + numlock = "Num_Lock", numdivide = "KP_Divide", nummultiply = "KP_Multiply", + numminus = "KP_Subtract", numplus = "KP_Add", numenter = "KP_Enter", + numdecimal = "KP_Decimal", + num0 = "KP_0", num1 = "KP_1", num2 = "KP_2", num3 = "KP_3", num4 = "KP_4", + num5 = "KP_5", num6 = "KP_6", num7 = "KP_7", num8 = "KP_8", num9 = "KP_9", +} + +local function creatorHasKey(code) + for _, selectedCode in ipairs(creatorKeys) do + if selectedCode == code then + return true + end + end + return false +end + +local function configKeyName(code) + local mapped = CONFIG_KEY_NAMES[code] + if mapped ~= nil then + return mapped + end + if code:match("^[a-z]$") then + return string.upper(code) + end + if code:match("^f%d%d?$") then + return string.upper(code) + end + return code +end + +-- A multi-unit key replaces the gaps that would exist between unit-sized +-- keys. Using the same pitch for keys and spacers keeps every 23-unit row at +-- exactly the same physical width, regardless of its number of children. +local function keyboardUnitsWidth(units) + local value = tonumber(units) or 1 + return math.max(1, math.floor(value * KEYBOARD_UNIT + (value - 1) * KEYBOARD_GAP + 0.5)) +end + +local function colorWithOpacity(value, fallback, opacity) + local color = asString(value, fallback) + if string.sub(color, 1, 1) == "#" then + local alpha = string.format("%02X", math.floor(math.max(0, math.min(1, opacity)) * 255 + 0.5)) + if #color == 7 then + return color .. alpha + elseif #color == 9 then + return string.sub(color, 1, 7) .. alpha + end + return color + end + return color .. "/" .. string.format("%.2f", opacity) +end + +local function keyboardKeyNode(spec, index) + if spec.spacer ~= nil then + -- `ui.spacer` is flexible: inside a fixed-width row it absorbs remaining + -- space and moves every key that follows it. Keyboard gaps must be rigid, + -- otherwise the function/navigation blocks and the inverted-T arrows no + -- longer share the same unit grid. + return ui.spacer({ width = keyboardUnitsWidth(spec.spacer), flexGrow = 0 }) + end + + if spec.bindable == false or spec.code == nil then + local width = keyboardUnitsWidth(spec.units or 1) + return ui.row({ + key = "keyboard-" .. spec.id, + width = width, + height = KEYBOARD_KEY_HEIGHT, + fill = colorWithOpacity(cfg("key_color"), "surface_variant", 0.28), + border = "outline", + borderWidth = 1, + radius = 7, + align = "center", + justify = "center", + }, { + ui.label({ + text = asString(spec.label, ""), color = "on_surface_variant", + fontSize = 8, textAlign = "center", maxLines = 1, + }), + }) + end + + local code = canonicalKey(spec.code) + local signature = activeModifierSignature() + local exactEntries = asArray(index.exact[signature .. "|" .. code]) + local anyEntries = asArray(index.any[code]) + local physicalModifier = MODIFIER_ALIASES[normalized(spec.code)] + local modifierActive = physicalModifier ~= nil and activeModifiers[physicalModifier] == true + local selected = creatorOpen and creatorHasKey(code) or selectedKeyboardKey == code + + local fill + local border + local borderWidth = 1 + if modifierActive then + local modifierFill = modifierStyle(physicalModifier) + fill = asString(modifierFill, "primary") + border = asString(modifierFill, "primary") + borderWidth = 2 + elseif selected then + fill = colorWithOpacity("secondary", "secondary", 0.24) + border = "secondary" + borderWidth = 3 + elseif #exactEntries > 0 then + fill = colorWithOpacity(cfg("category_color"), "primary", 0.30) + border = asString(cfg("category_color"), "primary") + borderWidth = 2 + elseif #anyEntries > 0 then + fill = colorWithOpacity(cfg("category_color"), "primary", 0.10) + border = asString(cfg("category_color"), "primary") + else + fill = colorWithOpacity(cfg("key_color"), "surface_variant", 0.42) + border = "outline" + end + + local displayText = asString(spec.label, "?") + if #exactEntries > 0 then + displayText = displayText .. "\n" .. tostring(#exactEntries) + elseif #anyEntries > 0 then + displayText = displayText .. "\n•" + end + + local width = keyboardUnitsWidth(spec.units or 1) + return ui.row({ + key = "keyboard-" .. spec.id, + width = width, + height = KEYBOARD_KEY_HEIGHT, + fill = fill, + border = border, + borderWidth = borderWidth, + radius = 7, + paddingH = 4, + paddingV = 3, + align = "center", + justify = "center", + }, { + ui.button({ + text = displayText, + width = math.max(18, width - 6), + height = KEYBOARD_KEY_HEIGHT - 6, + fontSize = #asString(spec.label) > 4 and 7 or 8, + variant = "ghost", + selected = selected or modifierActive, + contentAlign = "center", + controlSize = "sm", + tooltip = #exactEntries > 0 and tr("panel.keyboard.occupied") or ( + #anyEntries > 0 and tr("panel.keyboard.other_layer") or tr("panel.keyboard.free") + ), + onClick = keyCallback(spec.id, spec.code), + }), + }) +end + +local function keyboardLegendItem(fill, border, label) + return ui.row({ gap = 5, align = "center" }, { + ui.box({ width = 12, height = 12, radius = 3, fill = fill, border = border, borderWidth = 1 }), + ui.label({ text = label, color = "on_surface_variant", fontSize = 10, maxLines = 1 }), + }) +end + +local function keyboardLayerToolbar() + local buttons = { + ui.label({ text = tr("panel.keyboard.layer"), color = "on_surface_variant", fontSize = 11 }), + } + local callbacks = { + SUPER = "onToggleSuper", CTRL = "onToggleCtrl", SHIFT = "onToggleShift", ALT = "onToggleAlt", + } + local labels = { + SUPER = "Super", CTRL = "Ctrl", SHIFT = "Shift", ALT = "Alt", + } + for _, modifier in ipairs(MODIFIER_ORDER) do + buttons[#buttons + 1] = ui.button({ + text = labels[modifier], + selected = activeModifiers[modifier], + variant = activeModifiers[modifier] and "primary" or "ghost", + controlSize = "sm", + onClick = callbacks[modifier], + }) + end + buttons[#buttons + 1] = ui.button({ + glyph = "restore", + variant = "ghost", + controlSize = "sm", + tooltip = tr("panel.keyboard.clear_modifiers"), + onClick = "onClearModifiers", + }) + buttons[#buttons + 1] = ui.spacer({ flexGrow = 1 }) + buttons[#buttons + 1] = keyboardLegendItem( + colorWithOpacity(cfg("key_color"), "surface_variant", 0.42), "outline", tr("panel.keyboard.free") + ) + buttons[#buttons + 1] = keyboardLegendItem( + colorWithOpacity(cfg("category_color"), "primary", 0.30), + asString(cfg("category_color"), "primary"), + tr("panel.keyboard.occupied") + ) + buttons[#buttons + 1] = keyboardLegendItem( + colorWithOpacity(cfg("category_color"), "primary", 0.10), + asString(cfg("category_color"), "primary"), + tr("panel.keyboard.other_layer") + ) + return ui.row({ gap = 8, align = "center" }, buttons) +end + +local function selectedKeyDetails(index) + if selectedKeyboardKey == nil then + return ui.row({ + minHeight = 62, + paddingH = 12, + paddingV = 9, + radius = 10, + fill = colorWithOpacity(cfg("card_color"), "surface_variant", 0.25), + border = "outline", + borderWidth = 1, + align = "center", + justify = "center", + }, { + ui.label({ + text = tr("panel.keyboard.select_hint"), + color = "on_surface_variant", + fontSize = 11, + textAlign = "center", + maxLines = 2, + }), + }) + end + + local signature = activeModifierSignature() + local entries = asArray(index.exact[signature .. "|" .. selectedKeyboardKey]) + local chordParts = {} + for _, modifier in ipairs(MODIFIER_ORDER) do + if activeModifiers[modifier] then + chordParts[#chordParts + 1] = modifier == "CTRL" and "Ctrl" or ( + modifier == "SUPER" and "Super" or (modifier == "SHIFT" and "Shift" or "Alt") + ) + end + end + chordParts[#chordParts + 1] = KEYBOARD_LABELS[selectedKeyboardKey] or selectedKeyboardKey + local chord = table.concat(chordParts, " + ") + local children = { + ui.row({ gap = 8, align = "center" }, { + ui.label({ text = chord, color = "on_surface", fontSize = 13, fontWeight = "bold", flexGrow = 1 }), + ui.label({ + text = #entries > 0 and tr("panel.keyboard.occupied") or tr("panel.keyboard.free"), + color = #entries > 0 and asString(cfg("category_color"), "primary") or "on_surface_variant", + fontSize = 11, + fontWeight = "bold", + }), + }), + } + if #entries == 0 then + children[#children + 1] = ui.label({ + text = tr("panel.keyboard.free_hint"), + color = "on_surface_variant", + fontSize = 11, + maxLines = 2, + }) + else + for entryIndex, entry in ipairs(entries) do + if entryIndex > 5 then + children[#children + 1] = ui.label({ + text = tr("panel.keyboard.more_actions", { count = #entries - 5 }), + color = "on_surface_variant", + fontSize = 10, + }) + break + end + local bind = entry.bind + local meta = entry.category + local mode = asString(bind.mode) + if mode ~= "" and normalized(mode) ~= "default" then + meta = meta .. " · " .. mode + end + children[#children + 1] = ui.row({ + key = "keyboard-detail-" .. asString(bind.id, tostring(entryIndex)), + gap = 8, + paddingH = 8, + paddingV = 4, + radius = 7, + fill = "surface/0.45", + align = "center", + }, { + ui.label({ + text = asString(bind.description, tr("panel.no_description")), + color = asString(cfg("description_color"), "on_surface"), + fontSize = 11, + flexGrow = 1, + maxLines = 2, + }), + ui.label({ text = meta, color = "on_surface_variant", fontSize = 9, maxLines = 1 }), + }) + end + end + return ui.column({ + paddingH = 12, + paddingV = 9, + gap = 5, + radius = 10, + fill = colorWithOpacity(cfg("card_color"), "surface_variant", 0.25), + border = "outline", + borderWidth = 1, + }, children) +end + +local function creatorCategoryNames() + if creatorOpen and #creatorCategoryNamesFrozen > 0 then + return creatorCategoryNamesFrozen + end + local names = {} + local seen = {} + for _, category in ipairs(asArray(snapshot.categories)) do + local name = noctalia.string.trim(asString(category.name)) + if name ~= "" and not seen[name] then + seen[name] = true + names[#names + 1] = name + end + end + return names +end + +local function creatorCategory() + local names = creatorCategoryNames() + if creatorCategoryIndex >= #names then + return noctalia.string.trim(creatorNewCategory) + end + return names[creatorCategoryIndex + 1] or "" +end + +local function creatorChord() + local parts = {} + for _, modifier in ipairs(MODIFIER_ORDER) do + if activeModifiers[modifier] then + parts[#parts + 1] = modifier == "SUPER" and "Super" or ( + modifier == "CTRL" and "Ctrl" or (modifier == "SHIFT" and "Shift" or "Alt") + ) + end + end + for _, code in ipairs(creatorKeys) do + parts[#parts + 1] = KEYBOARD_LABELS[code] or configKeyName(code) + end + return table.concat(parts, " + ") +end + +local function creatorKeysDisplay() + local labels = {} + for _, code in ipairs(creatorKeys) do + labels[#labels + 1] = KEYBOARD_LABELS[code] or configKeyName(code) + end + return table.concat(labels, ", ") +end + +local function creatorConflict(index) + local signature = activeModifierSignature() + for _, category in ipairs(asArray(snapshot.categories)) do + for _, bind in ipairs(asArray(category.binds)) do + if not (formMode == "edit" and asString(bind.id) == editingBindId) + and modifierSignature(bind.modifiers) == signature + and asString(bind.activation, "press") == creatorActivation then + local rawKeys = type(bind.keys) == "table" and bind.keys or expandedKeys(bind.key) + if #rawKeys == #creatorKeys then + local matches = true + for index, rawKey in ipairs(rawKeys) do + if canonicalKey(rawKey) ~= creatorKeys[index] then matches = false break end + end + if matches then + return { bind = bind, category = asString(category.name, tr("panel.uncategorized")) } + end + end + end + end + end + return nil +end + +local function creatorErrorText() + if creatorError == "" then + return "" + end + local key = "panel.creator.errors." .. creatorError + local translated = tr(key) + return translated ~= key and translated or creatorError +end + +local function creatorCanSave(index) + local compositor = normalized(snapshot.compositor) + if snapshot.status ~= "ready" or snapshot.compositor ~= creatorContextCompositor + or snapshot.source ~= creatorContextSource or creatorBusy or #creatorKeys == 0 + or (editingCapabilities.command ~= false and noctalia.string.trim(creatorCommand) == "") + or creatorCommand:find("{{", 1, true) ~= nil + or noctalia.string.trim(creatorDescription) == "" or creatorCategory() == "" then + return false + end + if compositor ~= "hyprland" and #creatorKeys ~= 1 then + return false + end + if compositor == "niri" and creatorActivation == "release" then + return false + end + return creatorConflict(index) == nil +end + +local function commandLibraryCompositorSource() + local compositor = normalized(snapshot.compositor) + if compositor == "hyprland" or compositor == "niri" or compositor == "mangowc" then + return compositor + end + return "" +end + +local function commandLibrarySourceLabel(source) + local key = "panel.command_library.sources." .. source + local translated = tr(key) + return translated ~= key and translated or source +end + +local function commandLibraryCategoryLabel(category) + local key = "panel.command_library.categories." .. category + local translated = tr(key) + return translated ~= key and translated or category +end + +local function commandLibrarySourceIds() + local result = { "", "noctalia" } + local compositorSource = commandLibraryCompositorSource() + if formMode ~= "edit" and compositorSource ~= "" then result[#result + 1] = compositorSource end + return result +end + +local function commandLibraryEntryAllowed(entry) + if type(entry) ~= "table" then return false end + if entry.source == "noctalia" then return entry.kind == "shell" end + return formMode ~= "edit" and entry.kind == "native" + and entry.source == commandLibraryCompositorSource() +end + +local function commandLibraryCategoryIds() + local sourceIds = commandLibrarySourceIds() + local selectedSource = sourceIds[commandLibrarySourceIndex + 1] or "" + local seen = {} + for _, entry in ipairs(asArray(COMMAND_LIBRARY.entries)) do + if commandLibraryEntryAllowed(entry) and type(entry.category) == "string" + and (selectedSource == "" or entry.source == selectedSource) then + seen[entry.category] = true + end + end + local result = {} + for category, _ in pairs(seen) do result[#result + 1] = category end + table.sort(result, function(left, right) + return commandLibraryCategoryLabel(left):lower() < commandLibraryCategoryLabel(right):lower() + end) + return result +end + +local function commandLibraryFilteredEntries() + local sourceIds = commandLibrarySourceIds() + local selectedSource = sourceIds[commandLibrarySourceIndex + 1] or "" + local categoryIds = commandLibraryCategoryIds() + local selectedCategory = categoryIds[commandLibraryCategoryIndex] or "" + local query = normalized(commandLibraryQuery) + local result = {} + for _, entry in ipairs(asArray(COMMAND_LIBRARY.entries)) do + if commandLibraryEntryAllowed(entry) + and (selectedSource == "" or entry.source == selectedSource) + and (selectedCategory == "" or entry.category == selectedCategory) then + local needsInput = asString(entry.template):find("{{", 1, true) ~= nil + local readinessMatches = commandLibraryReadinessIndex == 0 + or (commandLibraryReadinessIndex == 1 and not needsInput) + or (commandLibraryReadinessIndex == 2 and needsInput) + local haystack = normalized(table.concat({ + asString(entry.id), asString(entry.template), asString(entry.usage), + commandLibrarySourceLabel(asString(entry.source)), + commandLibraryCategoryLabel(asString(entry.category)), + }, " ")) + if readinessMatches and (query == "" or haystack:find(query, 1, true) ~= nil) then + result[#result + 1] = entry + end + end + end + return result +end + +local function commandLibraryNode() + if not commandLibraryOpen then return ui.row({ visible = false }, {}) end + local sourceIds = commandLibrarySourceIds() + if commandLibrarySourceIndex >= #sourceIds then commandLibrarySourceIndex = 0 end + local sourceOptions = { tr("panel.command_library.all_sources") } + for index = 2, #sourceIds do + sourceOptions[#sourceOptions + 1] = commandLibrarySourceLabel(sourceIds[index]) + end + local categoryIds = commandLibraryCategoryIds() + if commandLibraryCategoryIndex > #categoryIds then commandLibraryCategoryIndex = 0 end + local categoryOptions = { tr("panel.command_library.all_categories") } + for _, category in ipairs(categoryIds) do + categoryOptions[#categoryOptions + 1] = commandLibraryCategoryLabel(category) + end + local matches = commandLibraryFilteredEntries() + local resultNodes = {} + local visibleCount = math.min(#matches, 6) + for index = 1, visibleCount do + local entry = matches[index] + local callbackName = "onCommandLibraryUse:" .. asString(entry.id) + registerDynamicCallback(callbackName, function() + creatorCommand = asString(entry.template) + creatorCommandKind = asString(entry.kind, "shell") + creatorLibraryEntryId = asString(entry.id) + commandLibraryOpen = false + creatorError = "" + creatorRevision += 1 + render() + end) + resultNodes[#resultNodes + 1] = ui.row({ gap = 6, align = "center" }, { + ui.glyph({ + name = entry.kind == "native" and "binary-tree" or "terminal-2", + size = 15, color = entry.kind == "native" and "primary" or "on_surface_variant", + }), + ui.column({ gap = 1, flexGrow = 1 }, { + ui.label({ text = asString(entry.usage, asString(entry.id)), fontSize = 10, maxLines = 1 }), + ui.label({ + text = commandLibrarySourceLabel(asString(entry.source)) .. " · " + .. commandLibraryCategoryLabel(asString(entry.category)), + color = "on_surface_variant", fontSize = 9, maxLines = 1, + }), + }), + asString(entry.template):find("{{", 1, true) ~= nil and ui.label({ + text = tr("panel.command_library.requires_input"), color = "tertiary", fontSize = 9, + }) or ui.label({ text = tr("panel.command_library.ready"), color = "secondary", fontSize = 9 }), + ui.button({ + text = tr("panel.command_library.use"), variant = "ghost", controlSize = "sm", + onClick = callbackName, + }), + }) + end + if #resultNodes == 0 then + resultNodes[1] = ui.label({ + text = tr("panel.command_library.no_results"), color = "on_surface_variant", fontSize = 10, + }) + end + return ui.column({ + gap = 6, paddingH = 8, paddingV = 8, radius = 8, + fill = colorWithOpacity(cfg("card_color"), "surface", 0.35), + border = "outline", borderWidth = 1, + }, { + ui.row({ gap = 6, align = "center" }, { + ui.glyph({ name = "books", size = 16, color = "primary" }), + ui.label({ text = tr("panel.command_library.title"), fontSize = 11, fontWeight = "bold", flexGrow = 1 }), + ui.button({ + text = tr("panel.command_library.custom_command"), variant = "ghost", controlSize = "sm", + onClick = "onCommandLibraryUseCustom", + }), + }), + ui.input({ + key = "command-library-search-" .. tostring(commandLibraryRevision), + value = commandLibraryQuery, placeholder = tr("panel.command_library.search_placeholder"), + controlSize = "sm", onChange = "onCommandLibraryQueryChanged", + }), + ui.row({ gap = 6, align = "center" }, { + ui.select({ + options = sourceOptions, selectedIndex = commandLibrarySourceIndex, + flexGrow = 1, controlSize = "sm", onChange = "onCommandLibrarySourceChanged", + }), + ui.select({ + options = categoryOptions, selectedIndex = commandLibraryCategoryIndex, + flexGrow = 1, controlSize = "sm", onChange = "onCommandLibraryCategoryChanged", + }), + ui.select({ + options = { + tr("panel.command_library.all_readiness"), tr("panel.command_library.ready"), + tr("panel.command_library.requires_input"), + }, + selectedIndex = commandLibraryReadinessIndex, flexGrow = 1, controlSize = "sm", + onChange = "onCommandLibraryReadinessChanged", + }), + }), + ui.label({ + text = tr("panel.command_library.showing", { shown = visibleCount, total = #matches }), + color = "on_surface_variant", fontSize = 9, + }), + ui.column({ gap = 3 }, resultNodes), + }) +end + +local function creatorForm(index) + local names = creatorCategoryNames() + local options = {} + for _, name in ipairs(names) do + options[#options + 1] = name + end + options[#options + 1] = tr("panel.creator.new_category") + if creatorCategoryIndex > #names then + creatorCategoryIndex = #names + end + local isEditing = formMode == "edit" + local showManualCombination = isEditing or viewMode == "list" + local usesNewCategory = creatorCategoryIndex >= #names + local compositor = normalized(snapshot.compositor) + local releaseSupported = compositor ~= "niri" and (not isEditing or editingCapabilities.activation == true) + local conflict = creatorConflict(index) + local chord = creatorChord() + local statusText = creatorErrorText() + if conflict ~= nil and statusText == "" then + statusText = tr("panel.creator.errors.conflict", { + description = asString(conflict.bind.description, tr("panel.no_description")), + }) + end + + local hintKey = isEditing and "panel.editor.hint" or (viewMode == "list" and "panel.creator.list_hint" or ( + compositor == "hyprland" and "panel.creator.hyprland_hint" or ( + compositor == "niri" and "panel.creator.niri_hint" or "panel.creator.mangowc_hint" + ))) + + local categoryControl = ui.select({ + key = "creator-category-" .. tostring(creatorRevision), options = options, + selectedIndex = creatorCategoryIndex, width = 250, controlSize = "sm", + onChange = "onCreatorCategoryChanged", + }) + + return ui.column({ + paddingH = 12, + paddingV = 10, + gap = 8, + radius = 10, + fill = colorWithOpacity(cfg("card_color"), "surface_variant", 0.25), + border = "primary", + borderWidth = 1, + }, { + ui.row({ gap = 8, align = "center" }, { + ui.glyph({ name = isEditing and "pencil" or "pencil-plus", size = 18, color = "primary" }), + ui.label({ + text = tr(isEditing and "panel.editor.title" or "panel.creator.title"), + fontSize = 13, fontWeight = "bold", flexGrow = 1, + }), + ui.label({ + text = chord ~= "" and chord or tr("panel.creator.no_combination"), + color = chord ~= "" and "primary" or "on_surface_variant", + fontSize = 11, + fontWeight = "bold", + maxLines = 1, + }), + }), + ui.label({ text = tr(hintKey), color = "on_surface_variant", fontSize = 10, maxLines = 2 }), + showManualCombination and ui.row({ gap = 6, align = "center" }, { + ui.label({ text = tr("panel.editor.combination"), color = "on_surface_variant", fontSize = 11 }), + ui.button({ + text = "Super", selected = activeModifiers.SUPER, + variant = activeModifiers.SUPER and "primary" or "ghost", controlSize = "sm", + onClick = "onToggleSuper", + }), + ui.button({ + text = "Ctrl", selected = activeModifiers.CTRL, + variant = activeModifiers.CTRL and "primary" or "ghost", controlSize = "sm", + onClick = "onToggleCtrl", + }), + ui.button({ + text = "Shift", selected = activeModifiers.SHIFT, + variant = activeModifiers.SHIFT and "primary" or "ghost", controlSize = "sm", + onClick = "onToggleShift", + }), + ui.button({ + text = "Alt", selected = activeModifiers.ALT, + variant = activeModifiers.ALT and "primary" or "ghost", controlSize = "sm", + onClick = "onToggleAlt", + }), + ui.input({ + key = "editor-keys-" .. tostring(creatorRevision), value = creatorKeysText, + placeholder = tr("panel.editor.keys_placeholder"), flexGrow = 1, controlSize = "sm", + onChange = "onEditorKeysChanged", + }), + }) or ui.row({ visible = false }, {}), + ui.row({ gap = 8, align = "center" }, { + ui.label({ text = tr("panel.creator.trigger"), color = "on_surface_variant", fontSize = 11 }), + ui.button({ + text = tr("panel.creator.press"), selected = creatorActivation == "press", + variant = creatorActivation == "press" and "primary" or "ghost", controlSize = "sm", + onClick = "onCreatorPress", + }), + ui.button({ + text = tr("panel.creator.release"), selected = creatorActivation == "release", + variant = creatorActivation == "release" and "primary" or "ghost", controlSize = "sm", + enabled = releaseSupported, tooltip = releaseSupported and "" or tr("panel.creator.release_unavailable"), + onClick = "onCreatorRelease", + }), + ui.spacer({ flexGrow = 1 }), + ui.label({ text = tr("panel.creator.category"), color = "on_surface_variant", fontSize = 11 }), + categoryControl, + ui.input({ + key = "creator-new-category-" .. tostring(creatorRevision), value = creatorNewCategory, + placeholder = tr("panel.creator.category_placeholder"), width = 220, controlSize = "sm", + visible = usesNewCategory, onChange = "onCreatorNewCategoryChanged", + }), + }), + ui.row({ gap = 8, align = "center" }, { + ui.input({ + key = "creator-description-" .. tostring(creatorRevision), value = creatorDescription, + placeholder = tr("panel.creator.description_placeholder"), flexGrow = 1, controlSize = "sm", + onChange = "onCreatorDescriptionChanged", + }), + ui.input({ + key = "creator-command-" .. tostring(creatorRevision), + value = isEditing and editingCapabilities.command ~= true and editingAction or creatorCommand, + placeholder = isEditing and editingCapabilities.command ~= true + and tr("panel.editor.action_placeholder") or tr("panel.creator.command_placeholder"), + flexGrow = 2, controlSize = "sm", + enabled = not isEditing or editingCapabilities.command == true, + onChange = "onCreatorCommandChanged", + }), + ui.button({ + text = tr("panel.command_library.open"), glyph = "books", + variant = commandLibraryOpen and "primary" or "ghost", controlSize = "sm", + enabled = not isEditing or editingCapabilities.command == true, + tooltip = tr("panel.command_library.open_hint"), + onClick = "onCommandLibraryToggle", + }), + }), + (not isEditing or editingCapabilities.command == true) and ui.row({ gap = 6, align = "center" }, { + ui.label({ + text = tr(creatorCommandKind == "native" + and "panel.command_library.native_action" or "panel.command_library.custom_command"), + color = creatorCommandKind == "native" and "primary" or "on_surface_variant", fontSize = 9, + }), + creatorCommand:find("{{", 1, true) ~= nil and ui.label({ + text = tr("panel.command_library.placeholder_hint"), color = "tertiary", fontSize = 9, + maxLines = 2, flexGrow = 1, + }) or ui.spacer({ flexGrow = 1 }), + }) or ui.row({ visible = false }, {}), + commandLibraryNode(), + ui.row({ gap = 8, align = "center" }, { + ui.label({ + text = statusText, + color = (conflict ~= nil or creatorError ~= "") and "error" or "on_surface_variant", + fontSize = 10, maxLines = 2, flexGrow = 1, + }), + ui.button({ text = tr("panel.creator.cancel"), variant = "ghost", controlSize = "sm", onClick = "onCreatorCancel" }), + ui.button({ + text = creatorBusy and tr("panel.creator.saving") + or tr(isEditing and "panel.editor.save" or "panel.creator.save"), + glyph = "device-floppy", variant = "primary", controlSize = "sm", + enabled = creatorCanSave(index), onClick = "onCreatorSave", + }), + }), + }) +end + +local function keyboardBody() + local index = keyboardIndex() + local layout = activeKeyboardLayout() + local signature = activeModifierSignature() + local occupied = 0 + for code, _ in pairs(layout.codeSet) do + if #asArray(index.exact[signature .. "|" .. code]) > 0 then + occupied = occupied + 1 + end + end + + local unmapped = 0 + for key, entries in pairs(index.any) do + if not layout.codeSet[key] then + unmapped = unmapped + #entries + end + end + + local keyboardRows = {} + for rowIndex, row in ipairs(layout.rows) do + local keys = {} + for _, spec in ipairs(row) do + keys[#keys + 1] = keyboardKeyNode(spec, index) + end + keyboardRows[#keyboardRows + 1] = ui.row({ + key = "keyboard-row-" .. layout.id .. "-" .. tostring(rowIndex), + width = keyboardUnitsWidth(layout.rowUnits), + gap = KEYBOARD_GAP, + align = "center", + justify = "start", + }, keys) + end + + local layer = signature ~= "" and signature:gsub("%+", " + ") or tr("panel.keyboard.no_modifiers") + local summary = tr("panel.keyboard.summary", { layer = layer, count = occupied }) + if unmapped > 0 then + summary = summary .. " · " .. tr("panel.keyboard.outside", { count = unmapped }) + end + + return ui.scroll({ flexGrow = 1 }, { + ui.column({ gap = 9, align = "stretch" }, { + keyboardLayerToolbar(), + ui.label({ text = summary, color = "on_surface_variant", fontSize = 10, textAlign = "center" }), + ui.column({ + gap = KEYBOARD_GAP, + padding = 10, + radius = 13, + fill = colorWithOpacity(cfg("card_color"), "surface_variant", 0.18), + border = "outline", + borderWidth = 1, + align = "center", + }, keyboardRows), + creatorOpen and creatorForm(index) or selectedKeyDetails(index), + }), + }) +end + +selectKeyboardKey = function(code) + local modifier = MODIFIER_ALIASES[normalized(code)] + if modifier ~= nil and activeModifiers[modifier] ~= nil then + activeModifiers[modifier] = not activeModifiers[modifier] + selectedKeyboardKey = nil + else + local canonical = canonicalKey(code) + selectedKeyboardKey = canonical + if creatorOpen then + if creatorHasKey(canonical) then + local remaining = {} + for _, selectedCode in ipairs(creatorKeys) do + if selectedCode ~= canonical then + remaining[#remaining + 1] = selectedCode + end + end + creatorKeys = remaining + elseif normalized(snapshot.compositor) == "hyprland" then + if #creatorKeys < 4 then + creatorKeys[#creatorKeys + 1] = canonical + end + else + creatorKeys = { canonical } + end + creatorKeysText = creatorKeysDisplay() + creatorError = "" + end + end + render() +end + + +local function keyPill(token, isKey) + local fill + local textColor + if isKey then + fill = cfg("key_color") + textColor = cfg("key_text_color") + else + fill, textColor = modifierStyle(token) + end + return ui.row({ + fill = asString(fill, "surface"), + border = "outline", + borderWidth = 1, + radius = 7, + paddingH = 7, + paddingV = 2, + align = "center", + justify = "center", + }, { + ui.label({ + text = asString(token, "?"), + color = asString(textColor, "on_surface"), + fontSize = 11, + fontWeight = "bold", + maxLines = 1, + }), + }) +end + +local function keyWidth(columnCount) + if columnCount <= 1 then + return 330 + elseif columnCount == 2 then + return 245 + elseif columnCount == 3 then + return 210 + end + return 180 +end + +local DND_BIND_TYPE = "keybind-category" + +local function bindHasWritableProvenance(bind) + return not asString(bind.id):match("^range:") + and asString(bind.source) ~= "" and tonumber(bind.start_line) ~= nil + and tonumber(bind.end_line) ~= nil and asString(bind.raw_snippet) ~= "" + and asString(bind.fingerprint) ~= "" +end + +local function bindEditable(bind) + local capabilities = type(bind.capabilities) == "table" and bind.capabilities or {} + return capabilities.combo == true and capabilities.description == true + and bindHasWritableProvenance(bind) +end + +local function bindCategoryMovable(bind) + local capabilities = type(bind.capabilities) == "table" and bind.capabilities or {} + local bindId = asString(bind.id) + return capabilities.category == true and bindHasWritableProvenance(bind) + and bindId ~= "" and #bindId <= 256 +end + +local function categoryCanAcceptDrop(categoryName) + local name = noctalia.string.trim(asString(categoryName)) + return name ~= "" and #name <= 80 and not name:find("%c") + and not name:find('"', 1, true) + and not name:find("Keymap managed", 1, true) +end + +local function categoryRenameable(category) + local binds = asArray(type(category) == "table" and category.binds or nil) + if asString(type(category) == "table" and category.id or "") == "" or #binds == 0 then return false end + for _, bind in ipairs(binds) do + local firstLine = tonumber(bind.start_line) + local lastLine = tonumber(bind.end_line) + if not bindCategoryMovable(bind) or bind.hidden == true + or asString(bind.source):sub(1, 1) ~= "/" + or firstLine == nil or lastLine == nil or firstLine % 1 ~= 0 or lastLine % 1 ~= 0 + or firstLine < 1 or lastLine < firstLine then return false end + end + return true +end + +local function findSnapshotCategory(categoryId) + for _, category in ipairs(asArray(snapshot.categories)) do + if asString(category.id) == categoryId then return category end + end + return nil +end + +local function categoryNameExists(name, excludedId) + local wanted = normalized(name) + if wanted == "" then return false end + for _, category in ipairs(asArray(snapshot.categories)) do + if asString(category.id) ~= excludedId and normalized(category.name) == wanted then return true end + end + return false +end + +local function optimisticRenameCategory(categoryId, oldName, newName) + local categories = {} + local renamed = false + for _, category in ipairs(asArray(snapshot.categories)) do + local nextCategory = {} + for key, value in pairs(category) do nextCategory[key] = value end + if asString(category.id) == categoryId and asString(category.name) == oldName then + nextCategory.name = newName + local binds = {} + for _, bind in ipairs(asArray(category.binds)) do + local nextBind = {} + for key, value in pairs(bind) do nextBind[key] = value end + if nextBind.category ~= nil then nextBind.category = newName end + binds[#binds + 1] = nextBind + end + nextCategory.binds = binds + renamed = true + end + categories[#categories + 1] = nextCategory + end + if not renamed then return false end + local nextSnapshot = {} + for key, value in pairs(snapshot) do nextSnapshot[key] = value end + nextSnapshot.categories = categories + snapshot = nextSnapshot + return true +end + +local function findSnapshotBind(bindId) + for _, category in ipairs(asArray(snapshot.categories)) do + for _, bind in ipairs(asArray(category.binds)) do + if asString(bind.id) == bindId then + return bind, asString(category.name, tr("panel.uncategorized")) + end + end + end + return nil, "" +end + +local function findHiddenBind(bindId) + for _, bind in ipairs(asArray(snapshot.hidden)) do + if bind.hidden == true and asString(bind.id) == bindId then return bind end + end + return nil +end + +local function snapshotHasCategory(categoryName) + for _, category in ipairs(asArray(snapshot.categories)) do + if asString(category.name, tr("panel.uncategorized")) == categoryName then + return true + end + end + return false +end + +local function optimisticPlaceBind(bindId, targetCategory, anchorId, placement) + local moved = nil + local categories = {} + for _, category in ipairs(asArray(snapshot.categories)) do + local nextCategory = {} + for key, value in pairs(category) do nextCategory[key] = value end + local binds = {} + for _, bind in ipairs(asArray(category.binds)) do + if asString(bind.id) == bindId then moved = bind else binds[#binds + 1] = bind end + end + nextCategory.binds = binds + categories[#categories + 1] = nextCategory + end + if moved == nil then return false end + + local inserted = false + for _, category in ipairs(categories) do + if asString(category.name, tr("panel.uncategorized")) == targetCategory then + local binds = category.binds + if asString(anchorId) == "" then + binds[#binds + 1] = moved + inserted = true + else + for index, bind in ipairs(binds) do + if asString(bind.id) == anchorId then + table.insert(binds, placement == "after" and index + 1 or index, moved) + inserted = true + break + end + end + end + break + end + end + if not inserted then return false end + + local nextSnapshot = {} + for key, value in pairs(snapshot) do nextSnapshot[key] = value end + nextSnapshot.categories = categories + snapshot = nextSnapshot + return true +end + +-- Logical pixels: the declarative UI scales explicit dimensions with the +-- shell UI scale. Keep row actions no taller than the compact key pills so +-- enabling edit mode does not change the height of every keybind row. +local BIND_ROW_ACTION_SIZE = 18 +local BIND_ROW_ACTION_GLYPH_SIZE = 12 + +local function beginBindOperation(bind, operation) + if creatorBusy or not bindEditable(bind) then return end + local bindId = asString(bind.id) + if operation == "delete" and deleteConfirmBindId ~= bindId then + deleteConfirmBindId = bindId + render() + return + end + deleteConfirmBindId = "" + creatorRequestCounter += 1 + local requestId = tostring(os.time()) .. "-action-" .. tostring(creatorRequestCounter) + formMode = "edit" + editingOperation = operation + editingBindId = bindId + editingRequestId = requestId + creatorContextCompositor = asString(snapshot.compositor) + creatorContextSource = asString(snapshot.source) + creatorBusy = true + creatorError = "" + creatorOpen = false + noctalia.state.set(UPDATE_REQUEST_KEY, { + request_id = requestId, + target_id = bindId, + operation = operation, + compositor = creatorContextCompositor, + source = creatorContextSource, + }) + render() +end + +local function beginHiddenOperation(bind, operation) + if creatorBusy or type(bind) ~= "table" or bind.hidden ~= true + or not bindHasWritableProvenance(bind) then return end + local capabilities = type(bind.capabilities) == "table" and bind.capabilities or {} + if operation == "restore" and capabilities.restore ~= true then return end + if operation == "delete" and capabilities.delete ~= true then return end + + local bindId = asString(bind.id) + if operation == "delete" and hiddenDeleteConfirmBindId ~= bindId then + hiddenDeleteConfirmBindId = bindId + render() + return + end + hiddenDeleteConfirmBindId = "" + creatorRequestCounter += 1 + local requestId = tostring(os.time()) .. "-hidden-" .. tostring(creatorRequestCounter) + formMode = "edit" + editingOperation = operation + editingBindId = bindId + editingRequestId = requestId + creatorContextCompositor = asString(snapshot.compositor) + creatorContextSource = asString(snapshot.source) + creatorBusy = true + creatorError = "" + creatorOpen = false + noctalia.state.set(UPDATE_REQUEST_KEY, { + request_id = requestId, + target_id = bindId, + operation = operation, + hidden = true, + compositor = creatorContextCompositor, + source = creatorContextSource, + }) + render() +end + +local function clearCategoryRename() + renamingCategoryId = "" + renamingCategoryOriginal = "" + renamingCategoryValue = "" + creatorError = "" +end + +local function openCategoryRename(category) + if creatorBusy or not editorMode or not categoryRenameable(category) then return end + creatorOpen = false + formMode = "edit" + editingBindId = "" + editingOperation = "update" + deleteConfirmBindId = "" + hiddenDeleteConfirmBindId = "" + renamingCategoryId = asString(category.id) + renamingCategoryOriginal = asString(category.name, tr("panel.uncategorized")) + renamingCategoryValue = renamingCategoryOriginal + renamingCategoryRevision += 1 + creatorError = "" + render() +end + +local function openBindEditor(bind, categoryName) + if creatorBusy or not bindEditable(bind) then return end + clearCategoryRename() + formMode = "edit" + editingOperation = "update" + deleteConfirmBindId = "" + editingBindId = asString(bind.id) + editingCategory = categoryName + editingCapabilities = type(bind.capabilities) == "table" and bind.capabilities or {} + editingAction = asString(bind.action, asString(bind.dispatcher)) + creatorCategoryNamesFrozen = {} + local categoryNames = creatorCategoryNames() + creatorCategoryNamesFrozen = categoryNames + creatorCategoryIndex = 0 + for index, name in ipairs(categoryNames) do + if name == categoryName then creatorCategoryIndex = index - 1 break end + end + creatorNewCategory = "" + creatorKeys = {} + local rawKeys = type(bind.keys) == "table" and bind.keys or expandedKeys(bind.key) + for _, rawKey in ipairs(rawKeys) do creatorKeys[#creatorKeys + 1] = canonicalKey(rawKey) end + creatorKeysText = creatorKeysDisplay() + for _, modifier in ipairs(MODIFIER_ORDER) do activeModifiers[modifier] = false end + for _, rawModifier in ipairs(asArray(bind.modifiers)) do + local modifier = canonicalModifier(rawModifier) + if activeModifiers[modifier] ~= nil then activeModifiers[modifier] = true end + end + creatorActivation = asString(bind.activation, bind.release == true and "release" or "press") + creatorCommand = asString(bind.command) + creatorCommandKind = "shell" + creatorLibraryEntryId = "" + creatorDescription = asString(bind.description) + creatorContextCompositor = asString(snapshot.compositor) + creatorContextSource = asString(snapshot.source) + creatorError = "" + creatorBusy = false + creatorOpen = true + commandLibraryOpen = false + commandLibraryQuery = "" + commandLibrarySourceIndex = 0 + commandLibraryCategoryIndex = 0 + commandLibraryReadinessIndex = 0 + commandLibraryRevision += 1 + selectedKeyboardKey = creatorKeys[1] + creatorRevision += 1 + viewMode = "list" + render() +end + +local function registerCategoryRenameCallback(categoryId) + local name = "onCategoryRename:" .. categoryId + return registerDynamicCallback(name, function() + local category = findSnapshotCategory(categoryId) + if category ~= nil then openCategoryRename(category) end + end) +end + +local function registerBindCallback(bindId, operation) + local name = "onBindAction:" .. operation .. ":" .. bindId + return registerDynamicCallback(name, function() + local bind, categoryName = findSnapshotBind(bindId) + if bind == nil then return end + if operation == "edit" then + openBindEditor(bind, categoryName) + else + beginBindOperation(bind, operation) + end + end) +end + +local function registerHiddenCallback(bindId, operation) + local name = "onHiddenBindAction:" .. operation .. ":" .. bindId + return registerDynamicCallback(name, function() + local bind = findHiddenBind(bindId) + if bind ~= nil then beginHiddenOperation(bind, operation) end + end) +end + +local function editBindActions(bind, categoryName) + if not editorMode then return {} end + local actions = {} + if bindCategoryMovable(bind) then + actions[#actions + 1] = ui.dragSource({ + key = "drag-bind-" .. asString(bind.id), + dragType = DND_BIND_TYPE, + payload = asString(bind.id), + previewAncestor = 1, + liftFromLayout = true, + enabled = not creatorBusy and not creatorOpen and renamingCategoryId == "", + tooltip = tr("panel.editor.drag"), + width = BIND_ROW_ACTION_SIZE, + height = BIND_ROW_ACTION_SIZE, + radius = 4, + align = "center", + justify = "center", + }, { + ui.glyph({ + name = "menu-2", size = BIND_ROW_ACTION_GLYPH_SIZE, + color = "on_surface_variant", + }), + }) + end + if not bindEditable(bind) then + actions[#actions + 1] = ui.button({ + glyph = "lock", variant = "ghost", + width = BIND_ROW_ACTION_SIZE, height = BIND_ROW_ACTION_SIZE, + glyphSize = BIND_ROW_ACTION_GLYPH_SIZE, enabled = false, + tooltip = tr("panel.editor.read_only"), + }) + return actions + end + local bindId = asString(bind.id) + local editCallback = registerBindCallback(bindId, "edit") + local hideCallback = registerBindCallback(bindId, "hide") + local deleteCallback = registerBindCallback(bindId, "delete") + + actions[#actions + 1] = ui.button({ + glyph = "pencil", variant = "ghost", + width = BIND_ROW_ACTION_SIZE, height = BIND_ROW_ACTION_SIZE, + glyphSize = BIND_ROW_ACTION_GLYPH_SIZE, + tooltip = tr("panel.editor.edit"), onClick = editCallback, + }) + actions[#actions + 1] = ui.button({ + glyph = "eye-off", variant = "ghost", + width = BIND_ROW_ACTION_SIZE, height = BIND_ROW_ACTION_SIZE, + glyphSize = BIND_ROW_ACTION_GLYPH_SIZE, + tooltip = tr("panel.editor.hide"), onClick = hideCallback, + }) + + if deleteConfirmBindId == asString(bind.id) then + local cancelCallback = "onCancelDeleteBind" + actions[#actions + 1] = ui.button({ + glyph = "x", variant = "ghost", + width = BIND_ROW_ACTION_SIZE, height = BIND_ROW_ACTION_SIZE, + glyphSize = BIND_ROW_ACTION_GLYPH_SIZE, + tooltip = tr("panel.editor.cancel_delete"), onClick = cancelCallback, + }) + actions[#actions + 1] = ui.button({ + glyph = "trash", variant = "destructive", + width = BIND_ROW_ACTION_SIZE, height = BIND_ROW_ACTION_SIZE, + glyphSize = BIND_ROW_ACTION_GLYPH_SIZE, + tooltip = tr("panel.editor.confirm_delete"), onClick = deleteCallback, + }) + else + actions[#actions + 1] = ui.button({ + glyph = "trash", variant = "ghost", + width = BIND_ROW_ACTION_SIZE, height = BIND_ROW_ACTION_SIZE, + glyphSize = BIND_ROW_ACTION_GLYPH_SIZE, + tooltip = tr("panel.editor.delete"), onClick = deleteCallback, + }) + end + return actions +end + +local function bindRow(bind, categoryName, columnCount, rowIndex) + local width = keyWidth(columnCount) + local keys = {} + local tokens = {} + local estimatedWidth = 0 + for _, modifier in ipairs(asArray(bind.modifiers)) do + tokens[#tokens + 1] = asString(modifier, "?") + estimatedWidth = estimatedWidth + #asString(modifier, "?") * 7 + 18 + keys[#keys + 1] = keyPill(modifier, false) + end + local mainKey = asString(bind.key, "?") + tokens[#tokens + 1] = mainKey + estimatedWidth = estimatedWidth + #mainKey * 7 + 18 + math.max(0, #tokens - 1) * 4 + keys[#keys + 1] = keyPill(mainKey, true) + + local keyArea + if estimatedWidth > width then + keyArea = ui.row({ + width = width, + fill = asString(cfg("key_color"), "surface"), + border = "outline", + borderWidth = 1, + radius = 7, + paddingH = 6, + paddingV = 2, + align = "center", + justify = "center", + }, { + ui.label({ + text = table.concat(tokens, " + "), + color = asString(cfg("key_text_color"), "on_surface"), + fontSize = 10, + fontWeight = "bold", + textAlign = "center", + maxWidth = width - 12, + maxLines = 2, + }), + }) + else + keyArea = ui.row({ width = width, gap = 4, align = "center" }, keys) + end + + local description = asString(bind.description, tr("panel.no_description")) + local details = {} + local mode = asString(bind.mode) + if mode ~= "" and normalized(mode) ~= "default" then + details[#details + 1] = ui.label({ + text = mode, + color = "secondary", + fontSize = 10, + fontWeight = "bold", + maxLines = 1, + }) + end + details[#details + 1] = ui.label({ + text = description, + color = asString(cfg("description_color"), "on_surface"), + fontSize = 12, + maxLines = 2, + flexGrow = 1, + }) + + local children = { + keyArea, + ui.row({ gap = 6, align = "center", flexGrow = 1 }, details), + } + for _, action in ipairs(editBindActions(bind, categoryName)) do + children[#children + 1] = action + end + + return ui.row({ + key = "bind-" .. asString(bind.id, tostring(rowIndex)), + gap = 10, + paddingH = 6, + paddingV = 4, + radius = 8, + fill = "surface/0.42", + align = "center", + }, children) +end + +local function reorderInsertionZone(bind, placement) + if not editorMode or not bindCategoryMovable(bind) then return nil end + return ui.dropZone({ + key = "reorder-" .. placement .. "-" .. asString(bind.id), + accepts = { DND_BIND_TYPE }, + value = placement .. "|" .. asString(bind.id), + onDrop = "onBindReordered", + height = 3, + radius = 8, + expandOnDrag = true, + hitSlop = 64, + enabled = not creatorBusy and not creatorOpen and renamingCategoryId == "", + }, {}) +end + +local function hiddenBindRow(bind, columnCount, rowIndex) + local keys = {} + for _, modifier in ipairs(asArray(bind.modifiers)) do keys[#keys + 1] = keyPill(modifier, false) end + local rawKeys = type(bind.keys) == "table" and bind.keys or { asString(bind.key, "?") } + for _, key in ipairs(rawKeys) do keys[#keys + 1] = keyPill(key, true) end + + local bindId = asString(bind.id) + local restoreCallback = registerHiddenCallback(bindId, "restore") + local deleteCallback = registerHiddenCallback(bindId, "delete") + local actions = { + ui.button({ + glyph = "restore", variant = "ghost", + width = BIND_ROW_ACTION_SIZE, height = BIND_ROW_ACTION_SIZE, + glyphSize = BIND_ROW_ACTION_GLYPH_SIZE, + tooltip = tr("panel.editor.restore"), onClick = restoreCallback, + }), + } + if hiddenDeleteConfirmBindId == bindId then + actions[#actions + 1] = ui.button({ + glyph = "x", variant = "ghost", + width = BIND_ROW_ACTION_SIZE, height = BIND_ROW_ACTION_SIZE, + glyphSize = BIND_ROW_ACTION_GLYPH_SIZE, + tooltip = tr("panel.editor.cancel_delete"), onClick = "onCancelDeleteHiddenBind", + }) + actions[#actions + 1] = ui.button({ + glyph = "trash", variant = "destructive", + width = BIND_ROW_ACTION_SIZE, height = BIND_ROW_ACTION_SIZE, + glyphSize = BIND_ROW_ACTION_GLYPH_SIZE, + tooltip = tr("panel.editor.confirm_delete_hidden"), onClick = deleteCallback, + }) + else + actions[#actions + 1] = ui.button({ + glyph = "trash", variant = "ghost", + width = BIND_ROW_ACTION_SIZE, height = BIND_ROW_ACTION_SIZE, + glyphSize = BIND_ROW_ACTION_GLYPH_SIZE, + tooltip = tr("panel.editor.delete_hidden"), onClick = deleteCallback, + }) + end + + local details = { + ui.label({ + text = asString(bind.description, tr("panel.no_description")), + color = asString(cfg("description_color"), "on_surface"), + fontSize = 12, maxLines = 2, flexGrow = 1, + }), + } + if asString(bind.category) ~= "" then + details[#details + 1] = ui.label({ + text = asString(bind.category), color = "secondary", fontSize = 10, + fontWeight = "bold", maxLines = 1, + }) + end + + local children = { + ui.row({ width = keyWidth(columnCount), gap = 4, align = "center" }, keys), + ui.row({ gap = 6, align = "center", flexGrow = 1 }, details), + } + for _, action in ipairs(actions) do children[#children + 1] = action end + return ui.row({ + key = "hidden-bind-" .. bindId .. "-" .. tostring(rowIndex), + gap = 10, paddingH = 6, paddingV = 4, radius = 8, + fill = "surface/0.42", align = "center", + }, children) +end + +local function hiddenSection(hidden, columnCount) + if not editorMode or #hidden == 0 then return nil end + local children = { + ui.row({ gap = 8, align = "center" }, { + ui.glyph({ name = "eye-off", size = 16, color = "secondary" }), + ui.label({ + text = tr("panel.editor.hidden_title"), color = "secondary", + fontSize = 14, fontWeight = "bold", flexGrow = 1, + }), + ui.label({ + text = noctalia.trp("panel.editor.hidden_count", #hidden), + color = "on_surface_variant", fontSize = 11, + }), + }), + ui.label({ + text = tr("panel.editor.hidden_hint"), color = "on_surface_variant", + fontSize = 11, maxLines = 2, + }), + } + for index, bind in ipairs(hidden) do + children[#children + 1] = hiddenBindRow(bind, columnCount, index) + end + return ui.column({ + key = "hidden-shortcuts", gap = 3, padding = 8, radius = 12, + fill = cardFill(), border = "secondary", borderWidth = 1, + }, children) +end + +local function categoryCard(category, columnCount) + local categoryName = asString(category.name, tr("panel.uncategorized")) + local categoryId = asString(category.id, categoryName) + local isRenaming = editorMode and renamingCategoryId == categoryId + local canRename = categoryRenameable(category) + local header + if isRenaming then + local nextName = noctalia.string.trim(renamingCategoryValue) + local canSave = not creatorBusy and categoryCanAcceptDrop(nextName) + and nextName ~= categoryName and not categoryNameExists(nextName, categoryId) + header = ui.row({ gap = 6, align = "center" }, { + ui.glyph({ name = "pencil", size = 15, color = "primary" }), + ui.input({ + key = "category-name-" .. tostring(renamingCategoryRevision), + value = renamingCategoryValue, + placeholder = tr("panel.editor.category_name_placeholder"), + flexGrow = 1, + controlSize = "sm", + enabled = not creatorBusy, + focus = true, + onChange = "onCategoryRenameChanged", + onSubmit = "onCategoryRenameSave", + }), + ui.button({ + glyph = "x", variant = "ghost", controlSize = "sm", + tooltip = tr("panel.editor.category_rename_cancel"), + enabled = not creatorBusy, onClick = "onCategoryRenameCancel", + }), + ui.button({ + glyph = "device-floppy", variant = "primary", controlSize = "sm", + tooltip = tr("panel.editor.category_rename_save"), + enabled = canSave, onClick = "onCategoryRenameSave", + }), + }) + else + local headerChildren = { + ui.label({ + text = categoryName, + color = asString(cfg("category_color"), "primary"), + fontSize = 14, + fontWeight = "bold", + maxLines = 1, + flexGrow = 1, + }), + ui.label({ + text = noctalia.trp("panel.category_count", #asArray(category.binds)), + color = "on_surface_variant", + fontSize = 11, + }), + } + if editorMode then + local renameProps = { + glyph = "pencil", variant = "ghost", + width = BIND_ROW_ACTION_SIZE, height = BIND_ROW_ACTION_SIZE, + glyphSize = BIND_ROW_ACTION_GLYPH_SIZE, + enabled = canRename and not creatorBusy and not creatorOpen and renamingCategoryId == "", + tooltip = canRename and tr("panel.editor.category_rename") + or tr("panel.editor.category_read_only"), + } + if canRename then renameProps.onClick = registerCategoryRenameCallback(categoryId) end + headerChildren[#headerChildren + 1] = ui.button(renameProps) + end + header = ui.row({ gap = 8, align = "center" }, headerChildren) + end + local children = { header } + if isRenaming and creatorError ~= "" then + children[#children + 1] = ui.label({ + text = creatorErrorText(), color = "error", fontSize = 10, maxLines = 2, + }) + end + + local categoryBinds = asArray(category.binds) + for index, bind in ipairs(categoryBinds) do + local insertion = reorderInsertionZone(bind, "before") + if insertion ~= nil then children[#children + 1] = insertion end + children[#children + 1] = bindRow( + bind, categoryName, columnCount, index + ) + end + if #categoryBinds > 0 then + local insertion = reorderInsertionZone(categoryBinds[#categoryBinds], "after") + if insertion ~= nil then children[#children + 1] = insertion end + end + local props = { + key = "category-" .. categoryId, + gap = editorMode and 0 or 3, + padding = 8, + radius = 12, + fill = cardFill(), + border = "outline", + borderWidth = 1, + } + if editorMode and categoryCanAcceptDrop(categoryName) then + props.accepts = { DND_BIND_TYPE } + props.value = categoryName + props.onDrop = "onBindDropped" + props.enabled = not creatorBusy and not creatorOpen and renamingCategoryId == "" + return ui.dropZone(props, children) + end + return ui.column(props, children) +end + +local function readyBody(categories, requestedColumns) + local hidden = filteredHidden() + if #categories == 0 and (not editorMode or #hidden == 0) then + local searching = normalized(searchQuery) ~= "" + local message = searching and tr("panel.no_results") or tr("panel.empty") + local hint = searching and tr("panel.no_results_hint") or tr("panel.empty_hint") + local children = { + ui.glyph({ name = "keyboard-off", size = 44, color = "on_surface_variant" }), + ui.label({ text = message, fontSize = 16, fontWeight = "bold", color = "on_surface" }), + ui.label({ text = hint, color = "on_surface_variant", textAlign = "center", maxWidth = 560, maxLines = 3 }), + } + if not searching then + children[#children + 1] = ui.button({ + text = tr("panel.open_settings_action"), glyph = "settings", variant = "ghost", + onClick = "onOpenSettingsClicked", + }) + end + return ui.column({ flexGrow = 1, align = "center", justify = "center", gap = 10 }, children) + end + + local columns = partitionCategories(categories, requestedColumns) + local columnNodes = {} + for columnIndex, column in ipairs(columns) do + local cards = {} + for _, category in ipairs(column) do + cards[#cards + 1] = categoryCard(category, #columns) + end + columnNodes[#columnNodes + 1] = ui.column({ + key = "column-" .. tostring(columnIndex), + gap = 12, + flexGrow = 1, + align = "stretch", + }, cards) + end + + local content = {} + local hiddenNode = hiddenSection(hidden, math.max(1, #columns)) + if hiddenNode ~= nil then content[#content + 1] = hiddenNode end + if #columnNodes > 0 then + content[#content + 1] = ui.row({ gap = 12, align = "start" }, columnNodes) + end + return ui.scroll({ flexGrow = 1, gap = 0 }, { + ui.column({ gap = 12, align = "stretch" }, content), + }) +end + +local function statusBody(status, errorText) + local isError = status == "error" + local errorKey = "panel.errors." .. asString(errorText, "unknown") + local translatedError = tr(errorKey) + if translatedError == errorKey then + translatedError = tr("panel.unknown_error") + end + return ui.column({ flexGrow = 1, align = "center", justify = "center", gap = 12 }, { + ui.glyph({ + name = isError and "alert-circle" or "refresh", + size = 44, + color = isError and "error" or "primary", + }), + ui.label({ + text = isError and tr("panel.load_failed") or tr("panel.loading"), + fontSize = 16, + fontWeight = "bold", + color = isError and "error" or "on_surface", + }), + ui.label({ + text = isError and translatedError or tr("panel.loading_hint"), + color = "on_surface_variant", + textAlign = "center", + maxWidth = 680, + maxLines = 4, + }), + ui.label({ + text = tr("panel.path_hint"), + color = "on_surface_variant", + textAlign = "center", + maxWidth = 680, + maxLines = 3, + visible = isError, + }), + ui.button({ + text = tr("panel.retry"), + glyph = "refresh", + variant = "primary", + visible = isError, + onClick = "onRefreshClicked", + }), + ui.button({ + text = tr("panel.open_settings_action"), + glyph = "settings", + variant = "ghost", + visible = isError, + onClick = "onOpenSettingsClicked", + }), + }) +end + +local function countVisibleBinds(categories) + local count = 0 + for _, category in ipairs(categories) do + count += #asArray(category.binds) + end + return count +end + +local function header() + local compositor = asString(snapshot.compositor, tr("panel.detecting")) + local total = tonumber(snapshot.total) or countVisibleBinds(asArray(snapshot.categories)) + local subtitle = compositor .. " · " .. noctalia.trp("panel.bind_count", total) + if asString(snapshot.updated_at) ~= "" then + subtitle ..= " · " .. tr("panel.updated", { time = snapshot.updated_at }) + end + + return ui.row({ gap = 10, align = "center" }, { + ui.glyph({ name = "keyboard", size = 26, color = "primary" }), + ui.column({ gap = 2, flexGrow = 1 }, { + ui.label({ text = tr("title"), fontSize = 18, fontWeight = "bold", color = "on_surface" }), + ui.label({ text = subtitle, fontSize = 11, color = "on_surface_variant", maxLines = 1 }), + }), + ui.button({ + glyph = "keyboard", + variant = viewMode == "keyboard" and "primary" or "ghost", + selected = viewMode == "keyboard", + tooltip = tr("panel.keyboard.view"), + onClick = "onShowKeyboard", + }), + ui.button({ + glyph = "list", + variant = viewMode == "list" and "primary" or "ghost", + selected = viewMode == "list", + tooltip = tr("panel.list_view"), + onClick = "onShowList", + }), + ui.label({ + text = tr("panel.keyboard.layout"), + color = "on_surface_variant", + fontSize = 10, + visible = editorMode, + }), + ui.select({ + key = "keyboard-layout-selector", + options = keyboardLayoutOptions(), + selectedIndex = keyboardLayoutSelectedIndex(), + width = 120, + controlSize = "sm", + visible = editorMode, + enabled = not creatorBusy, + onChange = "onKeyboardLayoutChanged", + }), + ui.spacer({ width = 8, flexGrow = 0 }), + ui.button({ + text = tr("panel.creator.new_shortcut"), + glyph = "pencil-plus", + variant = creatorOpen and formMode == "create" and "primary" or "ghost", + selected = creatorOpen and formMode == "create", + enabled = snapshot.status == "ready", + tooltip = tr("panel.creator.open"), + onClick = "onCreatorToggle", + }), + ui.button({ + text = tr("panel.editor.open"), + glyph = "edit", + variant = editorMode and "primary" or "ghost", + selected = editorMode, + enabled = snapshot.status == "ready", + tooltip = tr("panel.editor.open_hint"), + onClick = "onEditorToggle", + }), + ui.button({ + glyph = "refresh", + variant = "ghost", + tooltip = tr("panel.refresh"), + enabled = snapshot.status ~= "loading", + onClick = "onRefreshClicked", + }), + ui.button({ + glyph = "folder-open", + variant = "ghost", + tooltip = tr("panel.open_config_folder"), + enabled = sourceDirectory() ~= "" and not creatorBusy, + onClick = "onOpenConfigFolderClicked", + }), + ui.button({ + glyph = "settings", + variant = "ghost", + tooltip = tr("panel.open_settings"), + enabled = not creatorBusy, + onClick = "onOpenSettingsClicked", + }), + ui.button({ glyph = "close", variant = "ghost", tooltip = tr("panel.close"), onClick = "onCloseClicked" }), + }) +end + +local function searchBar(categories) + local children = { + ui.input({ + key = "search-" .. tostring(searchRevision), + value = searchQuery, + placeholder = tr("panel.search_placeholder"), + focus = true, + flexGrow = 1, + onChange = "onSearchChanged", + onSubmit = "onSearchChanged", + }), + } + + if searchQuery ~= "" then + children[#children + 1] = ui.label({ + text = noctalia.trp("panel.search_results", countVisibleBinds(categories)), + color = "on_surface_variant", + fontSize = 11, + }) + children[#children + 1] = ui.button({ + glyph = "x", + variant = "ghost", + tooltip = tr("panel.clear_search"), + onClick = "onClearSearch", + }) + end + + return ui.row({ gap = 8, align = "center" }, children) +end + +local function warningsBanner() + local warnings = asArray(snapshot.warnings) + if #warnings == 0 then + return nil + end + return ui.row({ + gap = 8, + paddingH = 10, + paddingV = 7, + radius = 9, + fill = "warning/0.12", + border = "warning", + borderWidth = 1, + align = "center", + }, { + ui.glyph({ name = "alert-triangle", size = 17, color = "tertiary" }), + ui.label({ + text = noctalia.trp("panel.warning_count", #warnings), + color = "on_surface", + fontSize = 11, + maxLines = 2, + flexGrow = 1, + }), + }) +end + +render = function() + renderCallbackNames = {} + local status = asString(snapshot.status, "idle") + local categories = filteredCategories() + local requestedColumns = math.max(1, math.min(4, math.floor(tonumber(cfg("columns")) or 3))) + local body + if status == "error" then + body = statusBody(status, snapshot.error) + elseif status == "loading" or status == "idle" then + body = statusBody(status, "") + elseif viewMode == "keyboard" then + body = keyboardBody() + else + body = readyBody(categories, requestedColumns) + end + + local children = { + header(), + ui.separator({ orientation = "horizontal", color = "outline", opacity = 0.65 }), + } + if viewMode == "list" then + children[#children + 1] = searchBar(categories) + end + if viewMode == "list" and creatorOpen and status == "ready" then + children[#children + 1] = creatorForm(keyboardIndex()) + end + local warning = warningsBanner() + if warning ~= nil and status == "ready" then + children[#children + 1] = warning + end + children[#children + 1] = body + panel.render(ui.column({ flexGrow = 1, gap = 12, align = "stretch" }, children)) + finishDynamicCallbackRender() +end + +local function requestRefresh() + local current = tonumber(noctalia.state.get(REFRESH_KEY)) or 0 + noctalia.state.set(REFRESH_KEY, current + 1) +end + +noctalia.state.watch(SNAPSHOT_KEY, function(value) + if type(value) == "table" and value.status == "loading" and snapshot.status == "ready" then + return + end + if type(value) == "table" then + snapshot = value + else + snapshot = EMPTY_SNAPSHOT + end + render() +end) + +noctalia.state.watch(CREATE_RESULT_KEY, function(result) + if formMode ~= "create" or type(result) ~= "table" + or tostring(result.request_id or "") ~= creatorRequestId or not creatorBusy then + return + end + creatorBusy = false + if result.ok == true then + local chord = creatorChord() + creatorOpen = false + creatorKeys = {} + selectedKeyboardKey = nil + noctalia.notify(tr("title"), tr("panel.creator.saved", { chord = chord })) + else + creatorError = asString(result.error, "write_failed") + noctalia.notifyError(tr("title"), creatorErrorText()) + end + render() +end) + +noctalia.state.watch(UPDATE_RESULT_KEY, function(result) + if formMode ~= "edit" or type(result) ~= "table" + or tostring(result.request_id or "") ~= editingRequestId or not creatorBusy then + return + end + creatorBusy = false + if result.ok == true then + editingSnapshotBefore = nil + creatorOpen = false + editingBindId = "" + selectedKeyboardKey = nil + viewMode = "list" + local resultKey = editingOperation == "hide" and "hidden" + or (editingOperation == "restore" and "restored" + or (editingOperation == "delete" and "deleted" + or (editingOperation == "move" and "moved" + or (editingOperation == "reorder" and "reordered" + or (editingOperation == "rename_category" and "category_renamed" or "updated"))))) + noctalia.notify(tr("title"), tr("panel.editor." .. resultKey)) + if editingOperation == "rename_category" then clearCategoryRename() end + else + if editingSnapshotBefore ~= nil then snapshot = editingSnapshotBefore end + editingSnapshotBefore = nil + creatorError = asString(result.error, "write_failed") + noctalia.notifyError(tr("title"), creatorErrorText()) + end + editingOperation = "update" + deleteConfirmBindId = "" + hiddenDeleteConfirmBindId = "" + render() +end) + +function onOpen(_context) + local configuredLayout = asString(cfg("keyboard_layout"), "100") + keyboardLayoutId = KEYBOARD_LAYOUTS[configuredLayout] ~= nil and configuredLayout or "100" + local current = noctalia.state.get(SNAPSHOT_KEY) + snapshot = type(current) == "table" and current or EMPTY_SNAPSHOT + searchQuery = "" + searchRevision += 1 + if creatorBusy then + creatorOpen = editingOperation == "update" + viewMode = formMode == "edit" and "list" or "keyboard" + else + creatorOpen = false + creatorError = "" + editorMode = false + formMode = "create" + editingBindId = "" + editingOperation = "update" + deleteConfirmBindId = "" + hiddenDeleteConfirmBindId = "" + clearCategoryRename() + end + if snapshot.status == nil or snapshot.status == "idle" then + requestRefresh() + end + render() +end + +function onConfigChanged() + render() +end + +function update() + render() +end + +function onSearchChanged(value) + searchQuery = asString(value) + render() +end + +function onClearSearch() + searchQuery = "" + searchRevision += 1 + render() +end + +function onShowKeyboard() + viewMode = "keyboard" + render() +end + +function onShowList() + if creatorBusy then return end + viewMode = "list" + creatorOpen = false + render() +end + +function onKeyboardLayoutChanged(index, _label) + if creatorBusy then return end + local selectedIndex = math.floor(tonumber(index) or -1) + 1 + local layoutId = KEYBOARD_LAYOUT_ORDER[selectedIndex] + if layoutId == nil or KEYBOARD_LAYOUTS[layoutId] == nil then return end + keyboardLayoutId = layoutId + render() +end + +local function resetCreator() + formMode = "create" + editingBindId = "" + editingCategory = "" + editingCapabilities = {} + editingAction = "" + creatorKeys = viewMode == "keyboard" and selectedKeyboardKey ~= nil and { selectedKeyboardKey } or {} + creatorKeysText = creatorKeysDisplay() + creatorActivation = "press" + creatorCommand = "" + creatorCommandKind = "shell" + creatorLibraryEntryId = "" + creatorDescription = "" + creatorCategoryIndex = 0 + creatorNewCategory = "" + creatorCategoryNamesFrozen = creatorCategoryNames() + creatorContextCompositor = asString(snapshot.compositor) + creatorContextSource = asString(snapshot.source) + creatorBusy = false + creatorError = "" + commandLibraryOpen = false + commandLibraryQuery = "" + commandLibrarySourceIndex = 0 + commandLibraryCategoryIndex = 0 + commandLibraryReadinessIndex = 0 + commandLibraryRevision += 1 + creatorRevision += 1 +end + +function onCreatorToggle() + if creatorBusy then return end + clearCategoryRename() + if creatorOpen and formMode == "create" then + creatorOpen = false + else + editorMode = false + resetCreator() + creatorOpen = true + end + render() +end + +function onCreatorCancel() + if creatorBusy then return end + creatorOpen = false + creatorBusy = false + creatorError = "" + commandLibraryOpen = false + if formMode == "edit" then viewMode = "list" end + render() +end + +function onEditorToggle() + if creatorBusy then return end + if editorMode then + editorMode = false + creatorOpen = false + formMode = "create" + editingBindId = "" + deleteConfirmBindId = "" + hiddenDeleteConfirmBindId = "" + clearCategoryRename() + else + editorMode = true + creatorOpen = false + formMode = "edit" + editingBindId = "" + deleteConfirmBindId = "" + hiddenDeleteConfirmBindId = "" + clearCategoryRename() + viewMode = "list" + end + render() +end + +function onCategoryRenameChanged(value) + if creatorBusy or renamingCategoryId == "" then return end + renamingCategoryValue = asString(value) + creatorError = "" + render() +end + +function onCategoryRenameCancel() + if creatorBusy then return end + clearCategoryRename() + render() +end + +function onCategoryRenameSave() + if creatorBusy or renamingCategoryId == "" or snapshot.status ~= "ready" then return end + local category = findSnapshotCategory(renamingCategoryId) + local nextName = noctalia.string.trim(renamingCategoryValue) + if snapshot.compositor == "" or snapshot.source == "" then + creatorError = "stale_context" + elseif category == nil or asString(category.name) ~= renamingCategoryOriginal then + creatorError = "category_changed" + elseif not categoryRenameable(category) then + creatorError = "category_not_editable" + elseif nextName == "" then + creatorError = "category_required" + elseif not categoryCanAcceptDrop(nextName) then + creatorError = "category_invalid" + elseif categoryNameExists(nextName, renamingCategoryId) then + creatorError = "category_exists" + elseif nextName == renamingCategoryOriginal then + clearCategoryRename() + else + creatorRequestCounter += 1 + local requestId = tostring(os.time()) .. "-rename-category-" .. tostring(creatorRequestCounter) + formMode = "edit" + editingOperation = "rename_category" + editingBindId = "" + editingRequestId = requestId + creatorContextCompositor = asString(snapshot.compositor) + creatorContextSource = asString(snapshot.source) + creatorBusy = true + creatorError = "" + creatorOpen = false + editingSnapshotBefore = snapshot + if not optimisticRenameCategory( + renamingCategoryId, renamingCategoryOriginal, nextName + ) then + editingSnapshotBefore = nil + creatorBusy = false + creatorError = "category_changed" + else + noctalia.state.set(UPDATE_REQUEST_KEY, { + request_id = requestId, + operation = "rename_category", + compositor = creatorContextCompositor, + source = creatorContextSource, + category_id = renamingCategoryId, + old_category = renamingCategoryOriginal, + new_category = nextName, + }) + end + end + render() +end + +function onCreatorPress() + creatorActivation = "press" + creatorError = "" + render() +end + +function onCreatorRelease() + if normalized(snapshot.compositor) ~= "niri" then + creatorActivation = "release" + creatorError = "" + render() + end +end + +function onCreatorCategoryChanged(index, _text) + creatorCategoryIndex = tonumber(index) or 0 + creatorError = "" + render() +end + +function onCreatorNewCategoryChanged(value) + creatorNewCategory = asString(value) + creatorError = "" + render() +end + +function onCreatorDescriptionChanged(value) + creatorDescription = asString(value) + creatorError = "" + render() +end + +function onCommandLibraryToggle() + if formMode == "edit" and editingCapabilities.command ~= true then return end + commandLibraryOpen = not commandLibraryOpen + creatorError = "" + render() +end + +function onCommandLibraryQueryChanged(value) + commandLibraryQuery = asString(value) + render() +end + +function onCommandLibrarySourceChanged(index, _text) + commandLibrarySourceIndex = math.max(0, math.floor(tonumber(index) or 0)) + commandLibraryCategoryIndex = 0 + render() +end + +function onCommandLibraryCategoryChanged(index, _text) + commandLibraryCategoryIndex = math.max(0, math.floor(tonumber(index) or 0)) + render() +end + +function onCommandLibraryReadinessChanged(index, _text) + commandLibraryReadinessIndex = math.max(0, math.min(2, math.floor(tonumber(index) or 0))) + render() +end + +function onCommandLibraryUseCustom() + if creatorCommandKind == "native" then + creatorCommand = "" + creatorRevision += 1 + end + creatorCommandKind = "shell" + creatorLibraryEntryId = "" + commandLibraryOpen = false + creatorError = "" + render() +end + +function onCreatorCommandChanged(value) + creatorCommand = asString(value) + creatorError = "" + render() +end + +function onEditorKeysChanged(value) + creatorKeysText = asString(value) + local keys = {} + local seen = {} + for part in (creatorKeysText .. ","):gmatch("(.-),") do + local key = canonicalKey(noctalia.string.trim(part)) + if key ~= "" and not seen[key] then + seen[key] = true + keys[#keys + 1] = key + end + end + creatorKeys = keys + selectedKeyboardKey = keys[1] + creatorError = "" + render() +end + +function onCreatorSave() + if creatorBusy then + return + end + local index = keyboardIndex() + local compositor = normalized(snapshot.compositor) + local category = creatorCategory() + if snapshot.status ~= "ready" or snapshot.compositor ~= creatorContextCompositor + or snapshot.source ~= creatorContextSource then + creatorError = "stale_context" + elseif #creatorKeys == 0 then + creatorError = "combination_required" + elseif (formMode ~= "edit" or editingCapabilities.command == true) + and noctalia.string.trim(creatorCommand) == "" then + creatorError = "command_required" + elseif creatorCommand:find("{{", 1, true) ~= nil then + creatorError = "library_arguments_required" + elseif noctalia.string.trim(creatorDescription) == "" then + creatorError = "description_required" + elseif category == "" then + creatorError = "category_required" + elseif compositor ~= "hyprland" and #creatorKeys ~= 1 then + creatorError = "single_key_only" + elseif compositor == "niri" and creatorActivation == "release" then + creatorError = "release_unsupported" + elseif creatorConflict(index) ~= nil then + creatorError = "conflict_blocked" + else + local keys = {} + for _, code in ipairs(creatorKeys) do + keys[#keys + 1] = configKeyName(code) + end + local modifiers = {} + for _, modifier in ipairs(MODIFIER_ORDER) do + if activeModifiers[modifier] then + modifiers[#modifiers + 1] = modifier + end + end + creatorRequestCounter += 1 + local requestId = tostring(os.time()) .. "-" .. tostring(creatorRequestCounter) + creatorBusy = true + creatorError = "" + local request = { + request_id = requestId, + compositor = creatorContextCompositor, + source = creatorContextSource, + modifiers = modifiers, + keys = keys, + activation = creatorActivation, + command = noctalia.string.trim(creatorCommand), + command_kind = creatorCommandKind, + library_entry_id = creatorLibraryEntryId, + description = noctalia.string.trim(creatorDescription), + category = category, + } + if formMode == "edit" then + editingOperation = "update" + editingRequestId = requestId + request.target_id = editingBindId + noctalia.state.set(UPDATE_REQUEST_KEY, request) + else + creatorRequestId = requestId + noctalia.state.set(CREATE_REQUEST_KEY, request) + end + end + render() +end + +local function toggleModifier(modifier) + activeModifiers[modifier] = not activeModifiers[modifier] + selectedKeyboardKey = nil + render() +end + +function onToggleSuper() toggleModifier("SUPER") end +function onToggleCtrl() toggleModifier("CTRL") end +function onToggleShift() toggleModifier("SHIFT") end +function onToggleAlt() toggleModifier("ALT") end + +function onClearModifiers() + for _, modifier in ipairs(MODIFIER_ORDER) do + activeModifiers[modifier] = false + end + selectedKeyboardKey = nil + render() +end + +function onCancelDeleteBind() + deleteConfirmBindId = "" + render() +end + +function onCancelDeleteHiddenBind() + hiddenDeleteConfirmBindId = "" + render() +end + +function onBindDropped(bindId, targetCategory) + if not editorMode or creatorBusy or snapshot.status ~= "ready" then return end + local targetName = noctalia.string.trim(asString(targetCategory)) + if not categoryCanAcceptDrop(targetName) or not snapshotHasCategory(targetName) then return end + local bind, currentCategory = findSnapshotBind(asString(bindId)) + if bind == nil or not bindCategoryMovable(bind) or currentCategory == targetName then return end + + creatorRequestCounter += 1 + local requestId = tostring(os.time()) .. "-move-" .. tostring(creatorRequestCounter) + formMode = "edit" + editingOperation = "move" + editingBindId = asString(bind.id) + editingRequestId = requestId + creatorContextCompositor = asString(snapshot.compositor) + creatorContextSource = asString(snapshot.source) + creatorBusy = true + creatorError = "" + creatorOpen = false + deleteConfirmBindId = "" + editingSnapshotBefore = snapshot + if not optimisticPlaceBind(editingBindId, targetName, "", "after") then + editingSnapshotBefore = nil + creatorBusy = false + return + end + noctalia.state.set(UPDATE_REQUEST_KEY, { + request_id = requestId, + target_id = editingBindId, + operation = "move", + category = targetName, + compositor = creatorContextCompositor, + source = creatorContextSource, + }) + render() +end + +function onBindReordered(bindId, targetValue) + if not editorMode or creatorBusy or snapshot.status ~= "ready" then return end + local placement, anchorId = asString(targetValue):match("^(%a+)|(.+)$") + if (placement ~= "before" and placement ~= "after") or asString(anchorId) == "" then return end + local bind, bindCategory = findSnapshotBind(asString(bindId)) + local anchor, anchorCategory = findSnapshotBind(asString(anchorId)) + if bind == nil or anchor == nil or asString(bind.id) == asString(anchor.id) + or not bindCategoryMovable(bind) or not bindCategoryMovable(anchor) then return end + if asString(bind.source) ~= asString(anchor.source) then + if bindCategory ~= anchorCategory then onBindDropped(bindId, anchorCategory) end + return + end + + creatorRequestCounter += 1 + local requestId = tostring(os.time()) .. "-reorder-" .. tostring(creatorRequestCounter) + formMode = "edit" + editingOperation = "reorder" + editingBindId = asString(bind.id) + editingRequestId = requestId + creatorContextCompositor = asString(snapshot.compositor) + creatorContextSource = asString(snapshot.source) + creatorBusy = true + creatorError = "" + creatorOpen = false + deleteConfirmBindId = "" + hiddenDeleteConfirmBindId = "" + editingSnapshotBefore = snapshot + if not optimisticPlaceBind(editingBindId, anchorCategory, asString(anchor.id), placement) then + editingSnapshotBefore = nil + creatorBusy = false + return + end + noctalia.state.set(UPDATE_REQUEST_KEY, { + request_id = requestId, + target_id = editingBindId, + anchor_id = asString(anchor.id), + operation = "reorder", + placement = placement, + compositor = creatorContextCompositor, + source = creatorContextSource, + }) + render() +end + +function onIpc(event, payload) + if event == "view-keyboard" then + onShowKeyboard() + elseif event == "view-list" then + onShowList() + elseif event == "keyboard-layout" and KEYBOARD_LAYOUTS[asString(payload)] ~= nil then + keyboardLayoutId = asString(payload) + render() + elseif event == "keyboard-key" and asString(payload) ~= "" then + selectKeyboardKey(payload) + elseif event == "clear-modifiers" then + onClearModifiers() + elseif event == "creator-open" and not creatorOpen then + onCreatorToggle() + elseif event == "creator-cancel" and creatorOpen then + onCreatorCancel() + elseif event == "editor-open" and not editorMode then + onEditorToggle() + elseif event == "editor-bind" and asString(payload) ~= "" then + for _, category in ipairs(asArray(snapshot.categories)) do + for _, bind in ipairs(asArray(category.binds)) do + if asString(bind.id) == asString(payload) then + editorMode = true + openBindEditor(bind, asString(category.name, tr("panel.uncategorized"))) + return + end + end + end + end +end + +function onRefreshClicked() + requestRefresh() +end + +function onOpenConfigFolderClicked() + if creatorBusy then return end + local directory = sourceDirectory() + if directory == "" then + noctalia.notifyError(tr("title"), tr("panel.config_folder_unavailable")) + return + end + local info = noctalia.fileInfo(directory) + if type(info) ~= "table" or info.isDir ~= true then + noctalia.notifyError(tr("title"), tr("panel.config_folder_unavailable")) + return + end + if not noctalia.commandExists("xdg-open") then + noctalia.notifyError(tr("title"), tr("panel.xdg_open_unavailable")) + return + end + local started = noctalia.runAsync("xdg-open " .. shellQuote(directory) .. " >/dev/null 2>&1", function(result) + if result.timedOut == true or tonumber(result.exitCode) ~= 0 then + noctalia.notifyError(tr("title"), tr("panel.config_folder_failed")) + end + end) + if not started then + noctalia.notifyError(tr("title"), tr("panel.config_folder_failed")) + end +end + +function onOpenSettingsClicked() + if creatorBusy then return end + local started = noctalia.runAsync("noctalia msg settings-open plugins") + if not started then + noctalia.notifyError(tr("title"), tr("panel.settings_failed")) + end +end + +function onCloseClicked() + if creatorBusy then return end + panel.close() +end diff --git a/keymap/plugin.toml b/keymap/plugin.toml new file mode 100644 index 0000000..aad8572 --- /dev/null +++ b/keymap/plugin.toml @@ -0,0 +1,229 @@ +id = "blackbartblues/keymap" +name = "Keymap" +version = "1.3.0" +plugin_api = 5 +author = "blackbartblues" +license = "MIT" +dependencies = ["hyprctl", "Hyprland", "niri", "mango", "mmsg", "xdg-open"] +tags = ["bar", "panel", "service", "hyprland", "niri", "mangowc", "productivity", "utility"] +icon = "keyboard" +description = "View, create, organize, and edit Hyprland, Niri, and MangoWC keybindings." + +[[setting]] +key = "compositor" +type = "select" +label_key = "settings.compositor.label" +description_key = "settings.compositor.description" +options = [ + { value = "auto", label_key = "settings.compositor.options.auto" }, + { value = "hyprland", label_key = "settings.compositor.options.hyprland" }, + { value = "niri", label_key = "settings.compositor.options.niri" }, + { value = "mangowc", label_key = "settings.compositor.options.mangowc" }, +] +default = "auto" + +[[setting]] +key = "hyprland_config" +type = "file" +label_key = "settings.hyprland_config.label" +description_key = "settings.hyprland_config.description" +default = "~/.config/hypr/hyprland.lua" +visible_when = { key = "compositor", values = ["auto", "hyprland"] } + +[[setting]] +key = "niri_config" +type = "file" +label_key = "settings.niri_config.label" +description_key = "settings.niri_config.description" +default = "~/.config/niri/config.kdl" +visible_when = { key = "compositor", values = ["auto", "niri"] } + +[[setting]] +key = "mangowc_config" +type = "file" +label_key = "settings.mangowc_config.label" +description_key = "settings.mangowc_config.description" +default = "~/.config/mango/config.conf" +visible_when = { key = "compositor", values = ["auto", "mangowc"] } + +[[setting]] +key = "merge_sequential" +type = "bool" +label_key = "settings.merge_sequential.label" +description_key = "settings.merge_sequential.description" +default = true + +[[setting]] +key = "show_undescribed" +type = "bool" +label_key = "settings.show_undescribed.label" +description_key = "settings.show_undescribed.description" +default = true + +[[setting]] +key = "keyboard_layout" +type = "select" +label_key = "settings.keyboard_layout.label" +description_key = "settings.keyboard_layout.description" +options = [ + { value = "100", label_key = "settings.keyboard_layout.options.p100" }, + { value = "96", label_key = "settings.keyboard_layout.options.p96" }, + { value = "80", label_key = "settings.keyboard_layout.options.p80" }, + { value = "75", label_key = "settings.keyboard_layout.options.p75" }, + { value = "65", label_key = "settings.keyboard_layout.options.p65" }, + { value = "60", label_key = "settings.keyboard_layout.options.p60" }, +] +default = "100" + +[[setting]] +key = "columns" +type = "int" +label_key = "settings.columns.label" +description_key = "settings.columns.description" +default = 3 +min = 1 +max = 4 +step = 1 + +[[setting]] +key = "card_color" +type = "color" +label_key = "settings.card_color.label" +description_key = "settings.card_color.description" +default = "surface_variant" + +[[setting]] +key = "card_opacity" +type = "int" +label_key = "settings.card_opacity.label" +description_key = "settings.card_opacity.description" +default = 35 +min = 0 +max = 100 +step = 5 + +[[setting]] +key = "category_color" +type = "color" +label_key = "settings.category_color.label" +description_key = "settings.category_color.description" +default = "primary" + +[[setting]] +key = "description_color" +type = "color" +label_key = "settings.description_color.label" +description_key = "settings.description_color.description" +default = "on_surface" + +[[setting]] +key = "super_color" +type = "color" +label_key = "settings.super_color.label" +description_key = "settings.modifier_color.description" +default = "primary" + +[[setting]] +key = "super_text_color" +type = "color" +label_key = "settings.super_text_color.label" +description_key = "settings.modifier_text_color.description" +default = "on_primary" + +[[setting]] +key = "ctrl_color" +type = "color" +label_key = "settings.ctrl_color.label" +description_key = "settings.modifier_color.description" +default = "secondary" + +[[setting]] +key = "ctrl_text_color" +type = "color" +label_key = "settings.ctrl_text_color.label" +description_key = "settings.modifier_text_color.description" +default = "on_secondary" + +[[setting]] +key = "shift_color" +type = "color" +label_key = "settings.shift_color.label" +description_key = "settings.modifier_color.description" +default = "tertiary" + +[[setting]] +key = "shift_text_color" +type = "color" +label_key = "settings.shift_text_color.label" +description_key = "settings.modifier_text_color.description" +default = "on_tertiary" + +[[setting]] +key = "alt_color" +type = "color" +label_key = "settings.alt_color.label" +description_key = "settings.modifier_color.description" +default = "surface_variant" + +[[setting]] +key = "alt_text_color" +type = "color" +label_key = "settings.alt_text_color.label" +description_key = "settings.modifier_text_color.description" +default = "on_surface_variant" + +[[setting]] +key = "key_color" +type = "color" +label_key = "settings.key_color.label" +description_key = "settings.key_color.description" +default = "surface" + +[[setting]] +key = "key_text_color" +type = "color" +label_key = "settings.key_text_color.label" +description_key = "settings.key_text_color.description" +default = "on_surface" + +[[widget]] +id = "widget" +entry = "widget.luau" + + [[widget.setting]] + key = "glyph" + type = "glyph" + label_key = "settings.widget.glyph.label" + description_key = "settings.widget.glyph.description" + default = "keyboard" + + [[widget.setting]] + key = "show_label" + type = "bool" + label_key = "settings.widget.show_label.label" + description_key = "settings.widget.show_label.description" + default = false + +[[panel]] +id = "panel" +entry = "panel.luau" +width = 1400 +height = 850 +placement = "floating" +position = "center" + +[[service]] +id = "service" +entry = "service.luau" + +[[service]] +id = "niri-service" +entry = "niri_service.luau" + +[[service]] +id = "mangowc-service" +entry = "mangowc_service.luau" + +[[service]] +id = "writer-service" +entry = "writer_service.luau" diff --git a/keymap/screenshots/command-library.webp b/keymap/screenshots/command-library.webp new file mode 100644 index 0000000000000000000000000000000000000000..08cb72c3609379c7809ed69d6dd7681456960df4 GIT binary patch literal 47838 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 PWK9;Qv`n9(C%^yz6)l%= literal 0 HcmV?d00001 diff --git a/keymap/screenshots/editor.webp b/keymap/screenshots/editor.webp new file mode 100644 index 0000000000000000000000000000000000000000..467b844fe6a15cd5b255411fcfc7af8dbb47b50a GIT binary patch literal 70100 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%0A#Tq(XLddCkFY$!JzYiZfyl=2hMEjJ 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