Add a keybindings manager widget
This commit is contained in:
1 parent
2df1454219
commit
010ab5fd4c
15 files changed
+926
-1
No files matched your search
@@ -114,6 +114,7 @@ blob-docs-keybinds
|
|||||||
| `Super + Ctrl + Q` | Quick settings |
|
| `Super + Ctrl + Q` | Quick settings |
|
||||||
| `Super + Ctrl + M` | System monitor |
|
| `Super + Ctrl + M` | System monitor |
|
||||||
| `Super + Alt + W` | Wallpaper picker |
|
| `Super + Alt + W` | Wallpaper picker |
|
||||||
|
| `Super + Shift + K` | Manage keybindings |
|
||||||
| `Super + Ctrl + L` | Lock |
|
| `Super + Ctrl + L` | Lock |
|
||||||
|
|
||||||
The rest are in [keybinds.md](docs/keybinds.md), or run `blob menu keybindings`.
|
The rest are in [keybinds.md](docs/keybinds.md), or run `blob menu keybindings`.
|
||||||
Executable
+37
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# blob:summary=List, add, reset and disable Hyprland keybindings
|
||||||
|
# blob:args=list | add <keys> <description> <command> [replaced-keys] | reset <keys> | disable <keys>
|
||||||
|
# blob:examples=blob keybinds add "SUPER + SHIFT + B" "Browser" "blob-launch-browser"
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
blob_path="${BLOB_PATH:-$HOME/.local/share/blob}"
|
||||||
|
manager="$blob_path/default/keybinds/manager.lua"
|
||||||
|
action="${1:-list}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: blob-keybinds list | add <keys> <description> <command> [replaced-keys] | reset <keys> | disable <keys>" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! command -v lua &>/dev/null; then
|
||||||
|
echo "blob-keybinds needs lua. Install it with: sudo pacman -S lua" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case $action in
|
||||||
|
list) (( $# <= 1 )) || usage ;;
|
||||||
|
add) (( $# == 4 || $# == 5 )) || usage ;;
|
||||||
|
reset | disable) (( $# == 2 )) || usage ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
BLOB_PATH="$blob_path" \
|
||||||
|
BLOB_KEYBINDS_CONFIG="$HOME/.config/hypr/hyprland.lua" \
|
||||||
|
BLOB_KEYBINDS_USER_FILE="$HOME/.config/hypr/bindings.lua" \
|
||||||
|
lua "$manager" "$@"
|
||||||
|
|
||||||
|
if [[ $action != list ]]; then
|
||||||
|
hyprctl reload >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
"setup.monitors": {"icon":"","label":"Monitors","action":"blob-launch-config-editor \"$HOME/.config/hypr/monitors.lua\""},
|
"setup.monitors": {"icon":"","label":"Monitors","action":"blob-launch-config-editor \"$HOME/.config/hypr/monitors.lua\""},
|
||||||
"setup.keybindings": {"icon":"","label":"Keybindings","when":"[[ -f ~/.config/hypr/bindings.lua ]]","action":"blob-launch-config-editor \"$HOME/.config/hypr/bindings.lua\""},
|
"setup.keybindings": {"icon":"","label":"Keybindings","action":"blob-shell shell toggle blob.keybinds"},
|
||||||
"setup.input": {"icon":"","label":"Input","when":"[[ -f ~/.config/hypr/input.lua ]]","action":"blob-launch-config-editor \"$HOME/.config/hypr/input.lua\""},
|
"setup.input": {"icon":"","label":"Input","when":"[[ -f ~/.config/hypr/input.lua ]]","action":"blob-launch-config-editor \"$HOME/.config/hypr/input.lua\""},
|
||||||
"setup.network": {"icon":"","label":"Network","aliases":["network"]},
|
"setup.network": {"icon":"","label":"Network","aliases":["network"]},
|
||||||
"setup.network.dns": {"icon":"","label":"DNS","aliases":["dns"]},
|
"setup.network.dns": {"icon":"","label":"DNS","aliases":["dns"]},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ o.bind("SUPER + SHIFT + code:201", "Blob menu", "blob-menu toggle root")
|
|||||||
o.bind("SUPER + ESCAPE", "System menu", "blob-menu toggle system")
|
o.bind("SUPER + ESCAPE", "System menu", "blob-menu toggle system")
|
||||||
o.bind("XF86PowerOff", "Power menu", "blob-menu toggle system", { locked = true })
|
o.bind("XF86PowerOff", "Power menu", "blob-menu toggle system", { locked = true })
|
||||||
o.bind("SUPER + K", "Keybindings", "blob-menu-keybindings")
|
o.bind("SUPER + K", "Keybindings", "blob-menu-keybindings")
|
||||||
|
o.bind("SUPER + SHIFT + K", "Manage keybindings", "blob-shell shell toggle blob.keybinds")
|
||||||
|
|
||||||
o.bind_toggle("SUPER + SHIFT + SPACE", "Toggle top bar", "bar")
|
o.bind_toggle("SUPER + SHIFT + SPACE", "Toggle top bar", "bar")
|
||||||
o.bind("SUPER + CTRL + SPACE", "Background switcher", "blob-menu toggle background")
|
o.bind("SUPER + CTRL + SPACE", "Background switcher", "blob-menu toggle background")
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
local action = arg[1] or "list"
|
||||||
|
local config_path = os.getenv("BLOB_KEYBINDS_CONFIG")
|
||||||
|
local user_file = os.getenv("BLOB_KEYBINDS_USER_FILE")
|
||||||
|
local personal_prefix = (os.getenv("HOME") or "") .. "/.config/hypr/"
|
||||||
|
|
||||||
|
local modifier_names = { SUPER = true, SHIFT = true, CTRL = true, ALT = true }
|
||||||
|
|
||||||
|
local function fail(message)
|
||||||
|
io.stderr:write(message, "\n")
|
||||||
|
os.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function trim(value)
|
||||||
|
local trimmed = value:gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
return trimmed
|
||||||
|
end
|
||||||
|
|
||||||
|
local function canonical_keys(keys)
|
||||||
|
local modifiers, key = {}, ""
|
||||||
|
for part in tostring(keys):gmatch("[^+]+") do
|
||||||
|
local token = trim(part):upper()
|
||||||
|
if token == "CONTROL" then token = "CTRL" end
|
||||||
|
if modifier_names[token] then
|
||||||
|
modifiers[#modifiers + 1] = token
|
||||||
|
elseif token ~= "" then
|
||||||
|
key = token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(modifiers)
|
||||||
|
modifiers[#modifiers + 1] = key
|
||||||
|
return table.concat(modifiers, "+"), key
|
||||||
|
end
|
||||||
|
|
||||||
|
local bindings = {}
|
||||||
|
|
||||||
|
local function caller_source()
|
||||||
|
for level = 2, 12 do
|
||||||
|
local info = debug.getinfo(level, "S")
|
||||||
|
if not info then break end
|
||||||
|
local source = info.source or ""
|
||||||
|
if source:sub(1, 1) == "@" and not source:match("helpers%.lua$") then
|
||||||
|
return source:sub(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local function dispatcher_proxy(path)
|
||||||
|
return setmetatable({ path = path }, {
|
||||||
|
__index = function(self, key)
|
||||||
|
return dispatcher_proxy(self.path .. "." .. tostring(key))
|
||||||
|
end,
|
||||||
|
__call = function(self, first)
|
||||||
|
if self.path == "hl.dsp.exec_cmd" and type(first) == "string" then
|
||||||
|
return { blob_dispatcher = true, kind = "exec", text = first }
|
||||||
|
end
|
||||||
|
return { blob_dispatcher = true, kind = "dispatcher", text = (self.path:gsub("^hl%.dsp%.", "")) }
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function describe_dispatcher(dispatcher)
|
||||||
|
if type(dispatcher) == "table" and dispatcher.blob_dispatcher then
|
||||||
|
return dispatcher.kind, dispatcher.text
|
||||||
|
elseif type(dispatcher) == "string" then
|
||||||
|
return "exec", dispatcher
|
||||||
|
end
|
||||||
|
return "dispatcher", "Lua function"
|
||||||
|
end
|
||||||
|
|
||||||
|
local noop
|
||||||
|
noop = setmetatable({}, {
|
||||||
|
__index = function() return noop end,
|
||||||
|
__call = function() return noop end,
|
||||||
|
})
|
||||||
|
|
||||||
|
hl = setmetatable({
|
||||||
|
dsp = dispatcher_proxy("hl.dsp"),
|
||||||
|
bind = function(keys, dispatcher, options)
|
||||||
|
local opts = options or {}
|
||||||
|
local kind, text = describe_dispatcher(dispatcher)
|
||||||
|
local source = caller_source()
|
||||||
|
bindings[#bindings + 1] = {
|
||||||
|
keys = tostring(keys),
|
||||||
|
canonical = canonical_keys(keys),
|
||||||
|
description = opts.description or "",
|
||||||
|
kind = kind,
|
||||||
|
action = text,
|
||||||
|
origin = source:sub(1, #personal_prefix) == personal_prefix and "personal" or "default",
|
||||||
|
state = "active",
|
||||||
|
}
|
||||||
|
return noop
|
||||||
|
end,
|
||||||
|
unbind = function(keys)
|
||||||
|
local canonical = canonical_keys(keys)
|
||||||
|
for _, binding in ipairs(bindings) do
|
||||||
|
if binding.canonical == canonical and binding.state == "active" then
|
||||||
|
binding.state = "disabled"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return noop
|
||||||
|
end,
|
||||||
|
get_config = function() return nil end,
|
||||||
|
}, {
|
||||||
|
__index = function() return noop end,
|
||||||
|
})
|
||||||
|
|
||||||
|
local function load_bindings()
|
||||||
|
local config_file = config_path and io.open(config_path, "r")
|
||||||
|
if not config_file then
|
||||||
|
fail("Hyprland config not found: " .. tostring(config_path))
|
||||||
|
end
|
||||||
|
config_file:close()
|
||||||
|
local ok, message = pcall(dofile, config_path)
|
||||||
|
if not ok then
|
||||||
|
fail("Could not read the Hyprland config: " .. tostring(message))
|
||||||
|
end
|
||||||
|
|
||||||
|
local personal_active = {}
|
||||||
|
for _, binding in ipairs(bindings) do
|
||||||
|
if binding.origin == "personal" and binding.state == "active" then
|
||||||
|
personal_active[binding.canonical] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, binding in ipairs(bindings) do
|
||||||
|
if binding.origin == "default" and binding.state == "disabled" and personal_active[binding.canonical] then
|
||||||
|
binding.state = "overridden"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clean(value)
|
||||||
|
return (tostring(value):gsub("[\t\n]", " "))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function print_bindings()
|
||||||
|
for _, binding in ipairs(bindings) do
|
||||||
|
if binding.description ~= "" then
|
||||||
|
print(table.concat({
|
||||||
|
binding.origin, binding.state, clean(binding.keys),
|
||||||
|
clean(binding.description), binding.kind, clean(binding.action),
|
||||||
|
}, "\t"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read_lines(path)
|
||||||
|
local lines = {}
|
||||||
|
local file = io.open(path, "r")
|
||||||
|
if not file then return lines end
|
||||||
|
for line in file:lines() do lines[#lines + 1] = line end
|
||||||
|
file:close()
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
local function write_lines(path, lines)
|
||||||
|
local temporary = path .. ".tmp"
|
||||||
|
local file = io.open(temporary, "w") or fail("Cannot write " .. temporary)
|
||||||
|
file:write(table.concat(lines, "\n"), "\n")
|
||||||
|
file:close()
|
||||||
|
local ok, message = os.rename(temporary, path)
|
||||||
|
if not ok then fail("Cannot replace " .. path .. ": " .. tostring(message)) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bind_line_keys(line)
|
||||||
|
return line:match('^%s*o%.bind_toggle%(%s*"(.-)"')
|
||||||
|
or line:match('^%s*o%.bind%(%s*"(.-)"')
|
||||||
|
or line:match('^%s*hl%.bind%(%s*"(.-)"')
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unbind_line_keys(line)
|
||||||
|
return line:match('^%s*hl%.unbind%(%s*"(.-)"%s*%)')
|
||||||
|
end
|
||||||
|
|
||||||
|
local function without_lines(lines, canonical, drop_binds, drop_unbinds)
|
||||||
|
local kept, removed = {}, 0
|
||||||
|
for _, line in ipairs(lines) do
|
||||||
|
local bind_keys = bind_line_keys(line)
|
||||||
|
local unbind_keys = unbind_line_keys(line)
|
||||||
|
if drop_binds and bind_keys and canonical_keys(bind_keys) == canonical then
|
||||||
|
removed = removed + 1
|
||||||
|
elseif drop_unbinds and unbind_keys and canonical_keys(unbind_keys) == canonical then
|
||||||
|
removed = removed + 1
|
||||||
|
else
|
||||||
|
kept[#kept + 1] = line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return kept, removed
|
||||||
|
end
|
||||||
|
|
||||||
|
local function has_unbind(lines, canonical)
|
||||||
|
for _, line in ipairs(lines) do
|
||||||
|
local keys = unbind_line_keys(line)
|
||||||
|
if keys and canonical_keys(keys) == canonical then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function has_active_default(canonical)
|
||||||
|
for _, binding in ipairs(bindings) do
|
||||||
|
if binding.origin == "default" and binding.canonical == canonical and binding.state == "active" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function require_key(keys)
|
||||||
|
local canonical, key = canonical_keys(keys)
|
||||||
|
if key == "" then fail("Keys need a key besides the modifiers, such as SUPER + SHIFT + K") end
|
||||||
|
return canonical
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reject_newlines(...)
|
||||||
|
for index = 1, select("#", ...) do
|
||||||
|
if tostring(select(index, ...)):find("\n") then fail("Values must be a single line") end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function add_binding(keys, description, command, replaced_keys)
|
||||||
|
reject_newlines(keys, description, command)
|
||||||
|
if trim(description) == "" or trim(command) == "" then fail("A binding needs a description and a command") end
|
||||||
|
local canonical = require_key(keys)
|
||||||
|
local lines = read_lines(user_file)
|
||||||
|
|
||||||
|
if replaced_keys and replaced_keys ~= "" then
|
||||||
|
lines = without_lines(lines, canonical_keys(replaced_keys), true, true)
|
||||||
|
end
|
||||||
|
lines = without_lines(lines, canonical, true, false)
|
||||||
|
|
||||||
|
if has_active_default(canonical) and not has_unbind(lines, canonical) then
|
||||||
|
lines[#lines + 1] = string.format("hl.unbind(%q)", trim(keys))
|
||||||
|
end
|
||||||
|
lines[#lines + 1] = string.format("o.bind(%q, %q, %q)", trim(keys), trim(description), trim(command))
|
||||||
|
write_lines(user_file, lines)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reset_binding(keys)
|
||||||
|
local lines, removed = without_lines(read_lines(user_file), require_key(keys), true, true)
|
||||||
|
if removed == 0 then fail("No personal binding or disabled default for " .. keys) end
|
||||||
|
write_lines(user_file, lines)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function disable_binding(keys)
|
||||||
|
local canonical = require_key(keys)
|
||||||
|
if not has_active_default(canonical) then fail("No active default binding for " .. keys) end
|
||||||
|
local lines = read_lines(user_file)
|
||||||
|
lines[#lines + 1] = string.format("hl.unbind(%q)", trim(keys))
|
||||||
|
write_lines(user_file, lines)
|
||||||
|
end
|
||||||
|
|
||||||
|
load_bindings()
|
||||||
|
|
||||||
|
if action == "list" then
|
||||||
|
print_bindings()
|
||||||
|
elseif action == "add" then
|
||||||
|
add_binding(arg[2] or "", arg[3] or "", arg[4] or "", arg[5])
|
||||||
|
elseif action == "reset" then
|
||||||
|
reset_binding(arg[2] or "")
|
||||||
|
elseif action == "disable" then
|
||||||
|
disable_binding(arg[2] or "")
|
||||||
|
else
|
||||||
|
fail("Unknown action: " .. action)
|
||||||
|
end
|
||||||
@@ -269,6 +269,12 @@ of the `blob` listing.
|
|||||||
| `blob-install-gaming-xbox-controllers` | Install support for using Xbox controllers with Steam/RetroArch/etc. | - |
|
| `blob-install-gaming-xbox-controllers` | Install support for using Xbox controllers with Steam/RetroArch/etc. | - |
|
||||||
| `blob-install-launch` | Install a packaged app and launch it once it finishes | <display-name> <packages> <desktop-id> |
|
| `blob-install-launch` | Install a packaged app and launch it once it finishes | <display-name> <packages> <desktop-id> |
|
||||||
|
|
||||||
|
## keybinds
|
||||||
|
|
||||||
|
| Command | Does | Arguments |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `blob-keybinds` | List, add, reset and disable Hyprland keybindings | list \| add <keys> <description> <command> [replaced-keys] \| reset <keys> \| disable <keys> |
|
||||||
|
|
||||||
## launch
|
## launch
|
||||||
|
|
||||||
| Command | Does | Arguments |
|
| Command | Does | Arguments |
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ Personal overrides in `hypr/bindings.lua` win over the defaults, and
|
|||||||
| `SUPER + SHIFT + comma` | Dismiss all notifications |
|
| `SUPER + SHIFT + comma` | Dismiss all notifications |
|
||||||
| `SUPER + SHIFT + CTRL + R` | Clear reminders |
|
| `SUPER + SHIFT + CTRL + R` | Clear reminders |
|
||||||
| `SUPER + SHIFT + CTRL + SPACE` | Theme menu |
|
| `SUPER + SHIFT + CTRL + SPACE` | Theme menu |
|
||||||
|
| `SUPER + SHIFT + K` | Manage keybindings |
|
||||||
| `SUPER + SHIFT + SPACE` | Toggle top bar |
|
| `SUPER + SHIFT + SPACE` | Toggle top bar |
|
||||||
| `SUPER + SPACE` | Blob menu |
|
| `SUPER + SPACE` | Blob menu |
|
||||||
| `XF86PowerOff` | Power menu |
|
| `XF86PowerOff` | Power menu |
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ falling back to `accent` for themes that define neither. `color0` is
|
|||||||
| Quick settings | left-click the bar clock, or `Super + Ctrl + Q` |
|
| Quick settings | left-click the bar clock, or `Super + Ctrl + Q` |
|
||||||
| Notification centre | left-click the bar bell; right-click silences notifications, and the bell is crossed out while they are |
|
| Notification centre | left-click the bar bell; right-click silences notifications, and the bell is crossed out while they are |
|
||||||
| System monitor | `Super + Ctrl + M` |
|
| System monitor | `Super + Ctrl + M` |
|
||||||
|
| Keybindings | `Super + Shift + K`, or Setup > Keybindings in the Blob menu |
|
||||||
|
|
||||||
Both new bar click actions are already in `shell.json`. The system monitor had no
|
Both new bar click actions are already in `shell.json`. The system monitor had no
|
||||||
bar entry point in the AGS setup either, so the keybinding is how it is reached.
|
bar entry point in the AGS setup either, so the keybinding is how it is reached.
|
||||||
@@ -63,6 +64,31 @@ removes it through `blob-shell notifications remove <file>`, the bell button
|
|||||||
toggles silencing, and the trash button clears the whole history with
|
toggles silencing, and the trash button clears the whole history with
|
||||||
`blob-shell notifications clear`.
|
`blob-shell notifications clear`.
|
||||||
|
|
||||||
|
## Keybindings
|
||||||
|
|
||||||
|
`blob.keybinds` lists every binding Hyprland loads, your own first, with a
|
||||||
|
search field and filters for all, mine, and disabled or replaced. The list comes
|
||||||
|
from `blob keybinds list`, which evaluates the real Lua config with a stubbed
|
||||||
|
`hl`, so bindings made in loops or with Hyprland dispatchers show up too, each
|
||||||
|
marked by the file it came from.
|
||||||
|
|
||||||
|
| Button | On | Does |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| + | header | add a binding |
|
||||||
|
| pencil | your binding | edit it; changing the keys replaces the old line |
|
||||||
|
| pencil | a default | replace it with your own command on the same keys |
|
||||||
|
| trash | your binding | remove it, bringing back any default it replaced |
|
||||||
|
| ban | a default | disable it with `hl.unbind` |
|
||||||
|
| undo | a disabled or replaced default | restore it |
|
||||||
|
|
||||||
|
The editor warns when the keys are already taken and says whether saving will
|
||||||
|
replace your binding or disable a default. Every change is a line added to or
|
||||||
|
removed from `~/.config/hypr/bindings.lua`, which is written through a temporary
|
||||||
|
file, and Hyprland reloads straight after. Default bindings that call a Hyprland
|
||||||
|
dispatcher rather than a command open the editor with an empty command, since
|
||||||
|
only commands can be written from the widget. It needs `lua`, which is in the
|
||||||
|
package list.
|
||||||
|
|
||||||
## Deliberately not ported
|
## Deliberately not ported
|
||||||
|
|
||||||
The tiles still poll shell commands the way the GTK widgets did - `pamixer` for
|
The tiles still poll shell commands the way the GTK widgets did - `pamixer` for
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ lazygit
|
|||||||
btop
|
btop
|
||||||
fastfetch
|
fastfetch
|
||||||
inxi
|
inxi
|
||||||
|
lua
|
||||||
dua-cli
|
dua-cli
|
||||||
tldr
|
tldr
|
||||||
whois
|
whois
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var binding: null
|
||||||
|
property var conflictLookup: null
|
||||||
|
|
||||||
|
signal saveRequested(string keys, string description, string command)
|
||||||
|
signal cancelRequested()
|
||||||
|
|
||||||
|
readonly property var conflict: conflictLookup ? conflictLookup(keysField.text) : null
|
||||||
|
readonly property bool canSave: keysField.text.trim().length > 0
|
||||||
|
&& descriptionField.text.trim().length > 0
|
||||||
|
&& commandField.text.trim().length > 0
|
||||||
|
|
||||||
|
function load(source) {
|
||||||
|
root.binding = source
|
||||||
|
keysField.text = source ? source.keys : ""
|
||||||
|
descriptionField.text = source ? source.description : ""
|
||||||
|
commandField.text = source && source.kind === "exec" ? source.action : ""
|
||||||
|
keysField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if (!root.canSave) return
|
||||||
|
root.saveRequested(keysField.text.trim(), descriptionField.text.trim(), commandField.text.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: form.implicitHeight + Style.spaceReal(24)
|
||||||
|
radius: Style.cardRadius
|
||||||
|
color: Util.alpha(Color.foreground, 0.06)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Util.alpha(Color.accent, 0.6)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: form
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.margins: Style.spaceReal(12)
|
||||||
|
spacing: Style.spaceReal(8)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: !root.binding ? "New binding"
|
||||||
|
: (root.binding.origin === "personal" ? "Edit binding" : "Replace default binding")
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Color.foreground
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: keysField
|
||||||
|
width: parent.width
|
||||||
|
placeholderText: "Keys, e.g. SUPER + SHIFT + B"
|
||||||
|
onAccepted: descriptionField.forceActiveFocus()
|
||||||
|
Keys.onEscapePressed: root.cancelRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: descriptionField
|
||||||
|
width: parent.width
|
||||||
|
placeholderText: "Description, e.g. Browser"
|
||||||
|
onAccepted: commandField.forceActiveFocus()
|
||||||
|
Keys.onEscapePressed: root.cancelRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: commandField
|
||||||
|
width: parent.width
|
||||||
|
placeholderText: "Command, e.g. blob-launch-browser"
|
||||||
|
onAccepted: root.save()
|
||||||
|
Keys.onEscapePressed: root.cancelRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.conflict !== null
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: !root.conflict ? ""
|
||||||
|
: (root.conflict.origin === "personal"
|
||||||
|
? "Replaces your binding: " + root.conflict.description
|
||||||
|
: "Disables the default: " + root.conflict.description)
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: Color.urgent
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.caption
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.spaceReal(6)
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
label: "Save"
|
||||||
|
active: root.canSave
|
||||||
|
enabled: root.canSave
|
||||||
|
onActivated: root.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
label: "Cancel"
|
||||||
|
onActivated: root.cancelRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var binding: null
|
||||||
|
|
||||||
|
signal editRequested()
|
||||||
|
signal resetRequested()
|
||||||
|
signal disableRequested()
|
||||||
|
|
||||||
|
readonly property bool personal: binding ? binding.origin === "personal" : false
|
||||||
|
readonly property bool active: binding ? binding.state === "active" : false
|
||||||
|
readonly property string stateText: {
|
||||||
|
if (!binding) return ""
|
||||||
|
if (binding.state === "overridden") return "Replaced by your binding"
|
||||||
|
if (binding.state === "disabled") return "Disabled"
|
||||||
|
return binding.kind === "exec" ? binding.action : "Hyprland: " + binding.action
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: Style.spaceReal(46)
|
||||||
|
radius: Style.cardRadius
|
||||||
|
color: hover.hovered ? Util.alpha(Color.foreground, 0.08) : Util.alpha(Color.background, Style.cardFillAlpha)
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: hover
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: keysChip
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Style.spaceReal(8)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: Style.spaceReal(200)
|
||||||
|
height: Style.spaceReal(26)
|
||||||
|
radius: Style.cardRadius
|
||||||
|
color: root.personal ? Util.alpha(Color.accent, 0.25) : Util.alpha(Color.foreground, 0.1)
|
||||||
|
border.width: 1
|
||||||
|
border.color: root.personal ? Color.accent : Util.alpha(Color.foreground, 0.2)
|
||||||
|
opacity: root.active ? 1 : 0.5
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Style.spaceReal(8)
|
||||||
|
anchors.rightMargin: Style.spaceReal(8)
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
text: root.binding ? root.binding.keys : ""
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
color: Color.foreground
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: keysChip.right
|
||||||
|
anchors.leftMargin: Style.spaceReal(12)
|
||||||
|
anchors.right: buttons.left
|
||||||
|
anchors.rightMargin: Style.spaceReal(8)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Style.spaceReal(2)
|
||||||
|
opacity: root.active ? 1 : 0.6
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.binding ? root.binding.description : ""
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
color: Color.foreground
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.stateText
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
color: root.active ? Util.alpha(Color.foreground, 0.6) : Color.urgent
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.caption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: buttons
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Style.spaceReal(8)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Style.spaceReal(4)
|
||||||
|
|
||||||
|
CardIconButton {
|
||||||
|
visible: root.active
|
||||||
|
icon: ""
|
||||||
|
onActivated: root.editRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
CardIconButton {
|
||||||
|
visible: root.personal
|
||||||
|
icon: ""
|
||||||
|
onActivated: root.resetRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
CardIconButton {
|
||||||
|
visible: !root.personal && root.active
|
||||||
|
icon: ""
|
||||||
|
onActivated: root.disableRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
CardIconButton {
|
||||||
|
visible: !root.personal && !root.active
|
||||||
|
icon: ""
|
||||||
|
onActivated: root.resetRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
import "KeybindsModel.js" as KeybindsModel
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var shell: null
|
||||||
|
property var manifest: null
|
||||||
|
property bool opened: false
|
||||||
|
|
||||||
|
readonly property string pluginId: (manifest && manifest.id) || "blob.keybinds"
|
||||||
|
|
||||||
|
property var bindings: []
|
||||||
|
property string searchText: ""
|
||||||
|
property string filterMode: "all"
|
||||||
|
property string statusText: ""
|
||||||
|
property bool statusIsError: false
|
||||||
|
property bool editorOpen: false
|
||||||
|
|
||||||
|
readonly property var visibleBindings: KeybindsModel.filterBindings(root.bindings, root.searchText, root.filterMode)
|
||||||
|
|
||||||
|
function open(payloadJson) {
|
||||||
|
root.opened = true
|
||||||
|
root.refresh()
|
||||||
|
Qt.callLater(function() { searchField.forceActiveFocus() })
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
root.opened = false
|
||||||
|
root.editorOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
root.close()
|
||||||
|
if (root.shell && typeof root.shell.hide === "function")
|
||||||
|
root.shell.hide(root.pluginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (!listProc.running) listProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function runChange(args, successMessage) {
|
||||||
|
if (changeProc.running) return
|
||||||
|
root.statusText = ""
|
||||||
|
root.statusIsError = false
|
||||||
|
changeProc.successMessage = successMessage
|
||||||
|
changeProc.command = ["blob-keybinds"].concat(args)
|
||||||
|
changeProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditor(binding) {
|
||||||
|
root.editorOpen = true
|
||||||
|
editor.load(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveBinding(keys, description, command) {
|
||||||
|
var args = ["add", keys, description, command]
|
||||||
|
var editing = editor.binding
|
||||||
|
if (editing && editing.origin === "personal" && editing.canonical !== KeybindsModel.canonicalKeys(keys))
|
||||||
|
args.push(editing.keys)
|
||||||
|
root.runChange(args, "Saved " + keys)
|
||||||
|
root.editorOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function conflictFor(keys) {
|
||||||
|
return KeybindsModel.conflictFor(root.bindings, keys, editor.binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: listProc
|
||||||
|
command: ["blob-keybinds", "list"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: root.bindings = KeybindsModel.parseBindings(text)
|
||||||
|
}
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
if (text.trim().length === 0) return
|
||||||
|
root.statusText = text.trim()
|
||||||
|
root.statusIsError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: changeProc
|
||||||
|
property string successMessage: ""
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
if (text.trim().length === 0) return
|
||||||
|
root.statusText = text.trim()
|
||||||
|
root.statusIsError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExited: function(exitCode) {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
root.statusText = changeProc.successMessage
|
||||||
|
root.statusIsError = false
|
||||||
|
} else if (root.statusText.length === 0) {
|
||||||
|
root.statusText = "The change could not be saved"
|
||||||
|
root.statusIsError = true
|
||||||
|
}
|
||||||
|
root.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: panelWindow
|
||||||
|
visible: root.opened
|
||||||
|
anchors { top: true; bottom: true; left: true; right: true }
|
||||||
|
color: "transparent"
|
||||||
|
WlrLayershell.namespace: "blob-keybinds"
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: root.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: card
|
||||||
|
}
|
||||||
|
|
||||||
|
Card {
|
||||||
|
id: card
|
||||||
|
|
||||||
|
readonly property real screenMargin: Style.spaceReal(20)
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
implicitWidth: Math.min(Style.spaceReal(760), panelWindow.width - screenMargin * 2)
|
||||||
|
implicitHeight: Math.min(Style.spaceReal(640), panelWindow.height - screenMargin * 2)
|
||||||
|
fillColor: Util.alpha(Color.background, Style.panelFillAlpha)
|
||||||
|
spacing: Style.spaceReal(10)
|
||||||
|
|
||||||
|
KeybindsHeader {
|
||||||
|
id: header
|
||||||
|
width: parent.width
|
||||||
|
onAddRequested: root.openEditor(null)
|
||||||
|
onCloseRequested: root.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: searchField
|
||||||
|
width: parent.width
|
||||||
|
placeholderText: "Search keys, descriptions or commands"
|
||||||
|
onTextChanged: root.searchText = text
|
||||||
|
Keys.onEscapePressed: root.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
id: filters
|
||||||
|
width: parent.width
|
||||||
|
spacing: Style.spaceReal(4)
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
label: "All"
|
||||||
|
active: root.filterMode === "all"
|
||||||
|
onActivated: root.filterMode = "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
label: "Mine"
|
||||||
|
active: root.filterMode === "personal"
|
||||||
|
onActivated: root.filterMode = "personal"
|
||||||
|
}
|
||||||
|
|
||||||
|
Chip {
|
||||||
|
label: "Disabled or replaced"
|
||||||
|
active: root.filterMode === "changed"
|
||||||
|
onActivated: root.filterMode = "changed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeybindEditor {
|
||||||
|
id: editor
|
||||||
|
width: parent.width
|
||||||
|
visible: root.editorOpen
|
||||||
|
conflictLookup: root.conflictFor
|
||||||
|
onSaveRequested: function(keys, description, command) { root.saveBinding(keys, description, command) }
|
||||||
|
onCancelRequested: root.editorOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: list
|
||||||
|
width: parent.width
|
||||||
|
height: Math.max(Style.spaceReal(80), card.height - card.padding * 2 - y - status.height - card.spacing)
|
||||||
|
clip: true
|
||||||
|
spacing: Style.spaceReal(4)
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
model: root.visibleBindings
|
||||||
|
|
||||||
|
delegate: KeybindRow {
|
||||||
|
required property var modelData
|
||||||
|
width: ListView.view.width
|
||||||
|
binding: modelData
|
||||||
|
onEditRequested: root.openEditor(modelData)
|
||||||
|
onResetRequested: root.runChange(["reset", modelData.keys],
|
||||||
|
modelData.origin === "personal" ? "Removed " + modelData.keys : "Restored " + modelData.keys)
|
||||||
|
onDisableRequested: root.runChange(["disable", modelData.keys], "Disabled " + modelData.keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: status
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: root.statusText.length > 0 ? root.statusText
|
||||||
|
: root.visibleBindings.length + " bindings. Changes go to ~/.config/hypr/bindings.lua and Hyprland reloads."
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
color: root.statusIsError ? Color.urgent : Util.alpha(Color.foreground, 0.6)
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.caption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Ui
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
signal addRequested()
|
||||||
|
signal closeRequested()
|
||||||
|
|
||||||
|
implicitHeight: Math.max(title.implicitHeight, buttons.implicitHeight)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: title
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: buttons.left
|
||||||
|
anchors.rightMargin: Style.spaceReal(8)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Keybindings"
|
||||||
|
textFormat: Text.PlainText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
color: Color.foreground
|
||||||
|
font.family: Style.font.family
|
||||||
|
font.pixelSize: Style.font.body
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: buttons
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Style.spaceReal(4)
|
||||||
|
|
||||||
|
CardIconButton {
|
||||||
|
icon: ""
|
||||||
|
onActivated: root.addRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
CardIconButton {
|
||||||
|
icon: ""
|
||||||
|
onActivated: root.closeRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
.pragma library
|
||||||
|
|
||||||
|
var modifierNames = { SUPER: true, SHIFT: true, CTRL: true, CONTROL: true, ALT: true }
|
||||||
|
|
||||||
|
function canonicalKeys(keys) {
|
||||||
|
var modifiers = []
|
||||||
|
var key = ""
|
||||||
|
var parts = String(keys || "").split("+")
|
||||||
|
for (var i = 0; i < parts.length; i++) {
|
||||||
|
var token = parts[i].trim().toUpperCase()
|
||||||
|
if (token === "CONTROL") token = "CTRL"
|
||||||
|
if (modifierNames[token]) modifiers.push(token)
|
||||||
|
else if (token.length > 0) key = token
|
||||||
|
}
|
||||||
|
modifiers.sort()
|
||||||
|
modifiers.push(key)
|
||||||
|
return modifiers.join("+")
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBindings(raw) {
|
||||||
|
var personal = []
|
||||||
|
var defaults = []
|
||||||
|
var lines = String(raw || "").split("\n")
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
var fields = lines[i].split("\t")
|
||||||
|
if (fields.length < 6) continue
|
||||||
|
var binding = {
|
||||||
|
origin: fields[0],
|
||||||
|
state: fields[1],
|
||||||
|
keys: fields[2],
|
||||||
|
description: fields[3],
|
||||||
|
kind: fields[4],
|
||||||
|
action: fields[5],
|
||||||
|
canonical: canonicalKeys(fields[2])
|
||||||
|
}
|
||||||
|
if (binding.origin === "personal") personal.push(binding)
|
||||||
|
else defaults.push(binding)
|
||||||
|
}
|
||||||
|
return personal.concat(defaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesFilter(binding, filterMode) {
|
||||||
|
if (filterMode === "personal") return binding.origin === "personal"
|
||||||
|
if (filterMode === "changed") return binding.state !== "active"
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesSearch(binding, query) {
|
||||||
|
if (!query) return true
|
||||||
|
return binding.keys.toLowerCase().indexOf(query) !== -1
|
||||||
|
|| binding.description.toLowerCase().indexOf(query) !== -1
|
||||||
|
|| binding.action.toLowerCase().indexOf(query) !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterBindings(bindings, searchText, filterMode) {
|
||||||
|
var query = String(searchText || "").trim().toLowerCase()
|
||||||
|
var result = []
|
||||||
|
for (var i = 0; i < bindings.length; i++) {
|
||||||
|
if (matchesFilter(bindings[i], filterMode) && matchesSearch(bindings[i], query)) result.push(bindings[i])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function conflictFor(bindings, keys, editingBinding) {
|
||||||
|
var canonical = canonicalKeys(keys)
|
||||||
|
if (canonical.charAt(canonical.length - 1) === "+" || canonical === "") return null
|
||||||
|
for (var i = 0; i < bindings.length; i++) {
|
||||||
|
var binding = bindings[i]
|
||||||
|
if (binding.state !== "active" || binding.canonical !== canonical) continue
|
||||||
|
if (editingBinding && editingBinding.origin === "personal" && editingBinding.canonical === canonical) continue
|
||||||
|
return binding
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"name": "Keybindings",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Blob",
|
||||||
|
"description": "Search, add, edit, disable and restore Hyprland keybindings",
|
||||||
|
"id": "blob.keybinds",
|
||||||
|
"kinds": [
|
||||||
|
"overlay"
|
||||||
|
],
|
||||||
|
"keepLoaded": true,
|
||||||
|
"entryPoints": {
|
||||||
|
"overlay": "Keybinds.qml"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user