1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00

keybinds: fix alt+shift, kdl parsing, allow arguments

This commit is contained in:
bbedward
2025-12-05 12:31:15 -05:00
parent b5378e5d3c
commit 6b1bbca620
8 changed files with 885 additions and 79 deletions

View File

@@ -333,35 +333,6 @@ func (n *NiriProvider) isRecentWindowsAction(action string) bool {
}
}
func (n *NiriProvider) parseSpawnArgs(s string) []string {
var args []string
var current strings.Builder
var inQuote, escaped bool
for _, r := range s {
switch {
case escaped:
current.WriteRune(r)
escaped = false
case r == '\\':
escaped = true
case r == '"':
inQuote = !inQuote
case r == ' ' && !inQuote:
if current.Len() > 0 {
args = append(args, current.String())
current.Reset()
}
default:
current.WriteRune(r)
}
}
if current.Len() > 0 {
args = append(args, current.String())
}
return args
}
func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node {
node := document.NewNode()
node.SetName(bind.Key)
@@ -392,19 +363,48 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node {
action = strings.TrimSpace(action)
node := document.NewNode()
if !strings.HasPrefix(action, "spawn ") {
parts := n.parseActionParts(action)
if len(parts) == 0 {
node.SetName(action)
return node
}
node.SetName("spawn")
args := n.parseSpawnArgs(strings.TrimPrefix(action, "spawn "))
for _, arg := range args {
node.SetName(parts[0])
for _, arg := range parts[1:] {
node.AddArgument(arg, "")
}
return node
}
func (n *NiriProvider) parseActionParts(action string) []string {
var parts []string
var current strings.Builder
var inQuote, escaped bool
for _, r := range action {
switch {
case escaped:
current.WriteRune(r)
escaped = false
case r == '\\':
escaped = true
case r == '"':
inQuote = !inQuote
case r == ' ' && !inQuote:
if current.Len() > 0 {
parts = append(parts, current.String())
current.Reset()
}
default:
current.WriteRune(r)
}
}
if current.Len() > 0 {
parts = append(parts, current.String())
}
return parts
}
func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error {
overridePath := n.GetOverridePath()
content := n.generateBindsContent(binds)
@@ -501,21 +501,46 @@ func (n *NiriProvider) writeBindNode(sb *strings.Builder, bind *overrideBind, in
sb.WriteString(" { ")
if len(node.Children) > 0 {
child := node.Children[0]
sb.WriteString(child.Name.String())
actionName := child.Name.String()
sb.WriteString(actionName)
forceQuote := actionName == "spawn"
for _, arg := range child.Arguments {
sb.WriteString(" ")
n.writeQuotedArg(sb, arg.ValueString())
n.writeArg(sb, arg.ValueString(), forceQuote)
}
}
sb.WriteString("; }\n")
}
func (n *NiriProvider) writeQuotedArg(sb *strings.Builder, val string) {
func (n *NiriProvider) writeArg(sb *strings.Builder, val string, forceQuote bool) {
if !forceQuote && n.isNumericArg(val) {
sb.WriteString(val)
return
}
sb.WriteString("\"")
sb.WriteString(strings.ReplaceAll(val, "\"", "\\\""))
sb.WriteString("\"")
}
func (n *NiriProvider) isNumericArg(val string) bool {
if val == "" {
return false
}
start := 0
if val[0] == '-' || val[0] == '+' {
if len(val) == 1 {
return false
}
start = 1
}
for i := start; i < len(val); i++ {
if val[i] < '0' || val[i] > '9' {
return false
}
}
return true
}
func (n *NiriProvider) validateBindsContent(content string) error {
tmpFile, err := os.CreateTemp("", "dms-binds-*.kdl")
if err != nil {

View File

@@ -496,3 +496,119 @@ func TestNiriParseMultipleArgs(t *testing.T) {
}
}
}
func TestNiriParseNumericWorkspaceBinds(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
Mod+2 hotkey-overlay-title="Focus Workspace 2" { focus-workspace 2; }
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 4 {
t.Errorf("Expected 4 keybinds, got %d", len(result.Section.Keybinds))
}
for _, kb := range result.Section.Keybinds {
switch kb.Key {
case "1":
if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" {
if kb.Action != "focus-workspace" || len(kb.Args) != 1 || kb.Args[0] != "1" {
t.Errorf("Mod+1 action/args mismatch: %+v", kb)
}
if kb.Description != "Focus Workspace 1" {
t.Errorf("Mod+1 description = %q, want 'Focus Workspace 1'", kb.Description)
}
}
case "0":
if kb.Action != "focus-workspace" || len(kb.Args) != 1 || kb.Args[0] != "10" {
t.Errorf("Mod+0 action/args mismatch: %+v", kb)
}
}
}
}
func TestNiriParseQuotedStringArgs(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
Super+Shift+Minus hotkey-overlay-title="Adjust Window Height -10%" { set-window-height "-10%"; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 3 {
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
}
for _, kb := range result.Section.Keybinds {
if kb.Action == "set-column-width" {
if len(kb.Args) != 1 {
t.Errorf("set-column-width should have 1 arg, got %d", len(kb.Args))
continue
}
if kb.Args[0] != "-10%" && kb.Args[0] != "+10%" {
t.Errorf("set-column-width arg = %q, want -10%% or +10%%", kb.Args[0])
}
}
}
}
func TestNiriParseActionWithProperties(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1 focus=false; }
Mod+Shift+2 hotkey-overlay-title="Move to Workspace 2" { move-column-to-workspace 2 focus=false; }
Alt+Tab { next-window scope="output"; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 3 {
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
}
for _, kb := range result.Section.Keybinds {
switch kb.Action {
case "move-column-to-workspace":
if len(kb.Args) != 1 {
t.Errorf("move-column-to-workspace should have 1 arg, got %d", len(kb.Args))
}
case "next-window":
if kb.Key != "Tab" {
t.Errorf("next-window key = %q, want 'Tab'", kb.Key)
}
}
}
}

View File

@@ -397,3 +397,211 @@ recent-windows {
t.Errorf("Expected at least 2 Alt-Tab binds, got %d", len(cheatSheet.Binds["Alt-Tab"]))
}
}
func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
provider := NewNiriProvider("")
tests := []struct {
name string
binds map[string]*overrideBind
expected string
}{
{
name: "workspace with numeric arg",
binds: map[string]*overrideBind{
"Mod+1": {
Key: "Mod+1",
Action: "focus-workspace 1",
Description: "Focus Workspace 1",
},
},
expected: `binds {
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
}
`,
},
{
name: "workspace with large numeric arg",
binds: map[string]*overrideBind{
"Mod+0": {
Key: "Mod+0",
Action: "focus-workspace 10",
Description: "Focus Workspace 10",
},
},
expected: `binds {
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
}
`,
},
{
name: "percentage string arg (should be quoted)",
binds: map[string]*overrideBind{
"Super+Minus": {
Key: "Super+Minus",
Action: `set-column-width "-10%"`,
Description: "Adjust Column Width -10%",
},
},
expected: `binds {
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
}
`,
},
{
name: "positive percentage string arg",
binds: map[string]*overrideBind{
"Super+Equal": {
Key: "Super+Equal",
Action: `set-column-width "+10%"`,
Description: "Adjust Column Width +10%",
},
},
expected: `binds {
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
}
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := provider.generateBindsContent(tt.binds)
if result != tt.expected {
t.Errorf("generateBindsContent() =\n%q\nwant:\n%q", result, tt.expected)
}
})
}
}
func TestNiriGenerateActionWithUnquotedPercentArg(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"Super+Equal": {
Key: "Super+Equal",
Action: "set-window-height +10%",
Description: "Adjust Window Height +10%",
},
}
content := provider.generateBindsContent(binds)
expected := `binds {
Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; }
}
`
if content != expected {
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
}
}
func TestNiriGenerateSpawnWithNumericArgs(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"XF86AudioLowerVolume": {
Key: "XF86AudioLowerVolume",
Action: `spawn "dms" "ipc" "call" "audio" "decrement" "3"`,
Options: map[string]any{"allow-when-locked": true},
},
}
content := provider.generateBindsContent(binds)
expected := `binds {
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
}
`
if content != expected {
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
}
}
func TestNiriGenerateSpawnNumericArgFromCLI(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"XF86AudioLowerVolume": {
Key: "XF86AudioLowerVolume",
Action: "spawn dms ipc call audio decrement 3",
Options: map[string]any{"allow-when-locked": true},
},
}
content := provider.generateBindsContent(binds)
expected := `binds {
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
}
`
if content != expected {
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
}
}
func TestNiriGenerateWorkspaceBindsRoundTrip(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"Mod+1": {
Key: "Mod+1",
Action: "focus-workspace 1",
Description: "Focus Workspace 1",
},
"Mod+2": {
Key: "Mod+2",
Action: "focus-workspace 2",
Description: "Focus Workspace 2",
},
"Mod+Shift+1": {
Key: "Mod+Shift+1",
Action: "move-column-to-workspace 1",
Description: "Move to Workspace 1",
},
"Super+Minus": {
Key: "Super+Minus",
Action: "set-column-width -10%",
Description: "Adjust Column Width -10%",
},
}
content := provider.generateBindsContent(binds)
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write temp file: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("Failed to parse generated content: %v\nContent was:\n%s", err, content)
}
if len(result.Section.Keybinds) != 4 {
t.Errorf("Expected 4 keybinds after round-trip, got %d", len(result.Section.Keybinds))
}
foundFocusWS1 := false
foundMoveWS1 := false
foundSetWidth := false
for _, kb := range result.Section.Keybinds {
switch {
case kb.Action == "focus-workspace" && len(kb.Args) > 0 && kb.Args[0] == "1":
foundFocusWS1 = true
case kb.Action == "move-column-to-workspace" && len(kb.Args) > 0 && kb.Args[0] == "1":
foundMoveWS1 = true
case kb.Action == "set-column-width" && len(kb.Args) > 0 && kb.Args[0] == "-10%":
foundSetWidth = true
}
}
if !foundFocusWS1 {
t.Error("focus-workspace 1 not found after round-trip")
}
if !foundMoveWS1 {
t.Error("move-column-to-workspace 1 not found after round-trip")
}
if !foundSetWidth {
t.Error("set-column-width -10% not found after round-trip")
}
}

View File

@@ -18,6 +18,7 @@ const KEY_MAP = {
96: "grave",
32: "space",
16777225: "Print",
16777226: "Print",
16777220: "Return",
16777221: "Return",
16777217: "Tab",
@@ -93,20 +94,20 @@ function xkbKeyFromQtKey(qk) {
function modsFromEvent(mods) {
var result = [];
if (mods & 0x04000000)
result.push("Ctrl");
if (mods & 0x02000000)
result.push("Shift");
var hasAlt = mods & 0x08000000;
var hasSuper = mods & 0x10000000;
if (hasAlt && hasSuper) {
result.push("Mod");
} else {
if (hasAlt)
result.push("Alt");
if (hasSuper)
result.push("Super");
if (hasAlt)
result.push("Alt");
}
if (mods & 0x04000000)
result.push("Ctrl");
if (mods & 0x02000000)
result.push("Shift");
return result;
}

View File

@@ -34,7 +34,7 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call notepad toggle", label: "Notepad: Toggle" },
{ id: "spawn dms ipc call notepad open", label: "Notepad: Open" },
{ id: "spawn dms ipc call notepad close", label: "Notepad: Close" },
{ id: "spawn dms ipc call dash toggle", label: "Dashboard: Toggle" },
{ id: "spawn dms ipc call dash toggle \"\"", label: "Dashboard: Toggle" },
{ id: "spawn dms ipc call dash open overview", label: "Dashboard: Overview" },
{ id: "spawn dms ipc call dash open media", label: "Dashboard: Media" },
{ id: "spawn dms ipc call dash open weather", label: "Dashboard: Weather" },
@@ -109,9 +109,15 @@ const COMPOSITOR_ACTIONS = {
{ id: "fullscreen-window", label: "Fullscreen" },
{ id: "maximize-column", label: "Maximize Column" },
{ id: "center-column", label: "Center Column" },
{ id: "center-visible-columns", label: "Center Visible Columns" },
{ id: "toggle-window-floating", label: "Toggle Floating" },
{ id: "switch-focus-between-floating-and-tiling", label: "Switch Floating/Tiling Focus" },
{ id: "switch-preset-column-width", label: "Cycle Column Width" },
{ id: "switch-preset-window-height", label: "Cycle Window Height" },
{ id: "set-column-width", label: "Set Column Width" },
{ id: "set-window-height", label: "Set Window Height" },
{ id: "reset-window-height", label: "Reset Window Height" },
{ id: "expand-column-to-available-width", label: "Expand to Available Width" },
{ id: "consume-or-expel-window-left", label: "Consume/Expel Left" },
{ id: "consume-or-expel-window-right", label: "Consume/Expel Right" },
{ id: "toggle-column-tabbed-display", label: "Toggle Tabbed" }
@@ -136,8 +142,10 @@ const COMPOSITOR_ACTIONS = {
{ id: "focus-workspace-down", label: "Focus Workspace Down" },
{ id: "focus-workspace-up", label: "Focus Workspace Up" },
{ id: "focus-workspace-previous", label: "Focus Previous Workspace" },
{ id: "focus-workspace", label: "Focus Workspace (by index)" },
{ id: "move-column-to-workspace-down", label: "Move to Workspace Down" },
{ id: "move-column-to-workspace-up", label: "Move to Workspace Up" },
{ id: "move-column-to-workspace", label: "Move to Workspace (by index)" },
{ id: "move-workspace-down", label: "Move Workspace Down" },
{ id: "move-workspace-up", label: "Move Workspace Up" }
],
@@ -173,6 +181,52 @@ const COMPOSITOR_ACTIONS = {
const CATEGORY_ORDER = ["DMS", "Execute", "Workspace", "Window", "Monitor", "Screenshot", "System", "Overview", "Alt-Tab", "Other"];
const ACTION_ARGS = {
"set-column-width": {
args: [{ name: "value", type: "text", label: "Width", placeholder: "+10%, -10%, 50%" }]
},
"set-window-height": {
args: [{ name: "value", type: "text", label: "Height", placeholder: "+10%, -10%, 50%" }]
},
"focus-workspace": {
args: [{ name: "index", type: "number", label: "Workspace", placeholder: "1, 2, 3..." }]
},
"move-column-to-workspace": {
args: [
{ name: "index", type: "number", label: "Workspace", placeholder: "1, 2, 3..." },
{ name: "focus", type: "bool", label: "Follow focus", default: false }
]
},
"screenshot": {
args: [{ name: "opts", type: "screenshot", label: "Options" }]
},
"screenshot-screen": {
args: [{ name: "opts", type: "screenshot", label: "Options" }]
},
"screenshot-window": {
args: [{ name: "opts", type: "screenshot", label: "Options" }]
}
};
const DMS_ACTION_ARGS = {
"audio increment": {
base: "spawn dms ipc call audio increment",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"audio decrement": {
base: "spawn dms ipc call audio decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"brightness increment": {
base: "spawn dms ipc call brightness increment",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"brightness decrement": {
base: "spawn dms ipc call brightness decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
}
};
function getActionTypes() {
return ACTION_TYPES;
}
@@ -322,3 +376,120 @@ function parseShellCommand(action) {
content = content.slice(1, -1);
return content.replace(/\\"/g, "\"");
}
function getActionArgConfig(action) {
if (!action)
return null;
var baseAction = action.split(" ")[0];
if (ACTION_ARGS[baseAction])
return { type: "compositor", base: baseAction, config: ACTION_ARGS[baseAction] };
for (var key in DMS_ACTION_ARGS) {
if (action.startsWith(DMS_ACTION_ARGS[key].base))
return { type: "dms", base: key, config: DMS_ACTION_ARGS[key] };
}
return null;
}
function parseCompositorActionArgs(action) {
if (!action)
return { base: "", args: {} };
var parts = action.split(" ");
var base = parts[0];
var args = {};
if (!ACTION_ARGS[base])
return { base: action, args: {} };
var argConfig = ACTION_ARGS[base];
var argParts = parts.slice(1);
if (base === "move-column-to-workspace") {
for (var i = 0; i < argParts.length; i++) {
if (argParts[i] === "focus=true" || argParts[i] === "focus=false") {
args.focus = argParts[i] === "focus=true";
} else if (!args.index) {
args.index = argParts[i];
}
}
} else if (base.startsWith("screenshot")) {
args.opts = {};
for (var j = 0; j < argParts.length; j += 2) {
if (j + 1 < argParts.length)
args.opts[argParts[j]] = argParts[j + 1];
}
} else if (argParts.length > 0) {
args.value = argParts.join(" ");
}
return { base: base, args: args };
}
function buildCompositorAction(base, args) {
if (!base)
return "";
var parts = [base];
if (!args || Object.keys(args).length === 0)
return base;
if (base === "move-column-to-workspace") {
if (args.index)
parts.push(args.index);
if (args.focus === true)
parts.push("focus=true");
else if (args.focus === false)
parts.push("focus=false");
} else if (base.startsWith("screenshot") && args.opts) {
for (var key in args.opts) {
if (args.opts[key] !== undefined && args.opts[key] !== "") {
parts.push(key);
parts.push(args.opts[key]);
}
}
} else if (args.value) {
parts.push(args.value);
} else if (args.index) {
parts.push(args.index);
}
return parts.join(" ");
}
function parseDmsActionArgs(action) {
if (!action)
return { base: "", args: {} };
for (var key in DMS_ACTION_ARGS) {
var config = DMS_ACTION_ARGS[key];
if (action.startsWith(config.base)) {
var rest = action.slice(config.base.length).trim();
return { base: key, args: { amount: rest || "" } };
}
}
return { base: action, args: {} };
}
function buildDmsAction(baseKey, args) {
var config = DMS_ACTION_ARGS[baseKey];
if (!config)
return "";
var action = config.base;
if (args && args.amount)
action += " " + args.amount;
return action;
}
function getScreenshotOptions() {
return [
{ id: "write-to-disk", label: "Save to disk", type: "bool" },
{ id: "show-pointer", label: "Show pointer", type: "bool" }
];
}

View File

@@ -89,12 +89,17 @@ Item {
function saveNewBind(bindData) {
KeybindsService.saveBind("", bindData);
showingNewBind = false;
selectedCategory = "";
_editingKey = bindData.key;
expandedKey = bindData.action;
}
function _onSaveSuccess() {
if (showingNewBind) {
showingNewBind = false;
selectedCategory = "";
}
}
function scrollToTop() {
flickable.contentY = 0;
}
@@ -121,6 +126,10 @@ Item {
keybindsTab._savedScrollY = flickable.contentY;
keybindsTab._preserveScroll = true;
}
function onBindSaveCompleted(success) {
if (success)
keybindsTab._onSaveSuccess();
}
function onBindRemoved(key) {
keybindsTab._savedScrollY = flickable.contentY;
keybindsTab._preserveScroll = true;

View File

@@ -55,6 +55,7 @@ Singleton {
signal bindsLoaded
signal bindSaved(string key)
signal bindSaveCompleted(bool success)
signal bindRemoved(string key)
signal dmsBindsFixed
@@ -118,12 +119,14 @@ Singleton {
onExited: exitCode => {
root.saving = false;
if (exitCode === 0) {
root.lastError = "";
root.loadBinds(false);
} else {
if (exitCode !== 0) {
console.error("[KeybindsService] Save failed with code:", exitCode);
root.bindSaveCompleted(false);
return;
}
root.lastError = "";
root.bindSaveCompleted(true);
root.loadBinds(false);
}
}
@@ -141,12 +144,12 @@ Singleton {
}
onExited: exitCode => {
if (exitCode === 0) {
if (exitCode !== 0) {
console.error("[KeybindsService] Remove failed with code:", exitCode);
return;
}
root.lastError = "";
root.loadBinds(false);
} else {
console.error("[KeybindsService] Remove failed with code:", exitCode);
}
}
}
@@ -165,15 +168,15 @@ Singleton {
onExited: exitCode => {
root.fixing = false;
if (exitCode === 0) {
if (exitCode !== 0) {
console.error("[KeybindsService] Fix failed with code:", exitCode);
return;
}
root.lastError = "";
root.dmsBindsIncluded = true;
root.dmsBindsFixed();
ToastService.showSuccess(I18n.tr("Binds include added"), I18n.tr("dms/binds.kdl is now included in config.kdl"), "", "keybinds");
Qt.callLater(root.forceReload);
} else {
console.error("[KeybindsService] Fix failed with code:", exitCode);
}
}
}

View File

@@ -28,6 +28,7 @@ Item {
property bool addingNewKey: false
property bool useCustomCompositor: false
property var _shortcutInhibitor: null
property bool _altShiftGhost: false
readonly property bool _shortcutInhibitorAvailable: {
try {
@@ -61,6 +62,11 @@ Item {
Component.onDestruction: _destroyShortcutInhibitor()
Component.onCompleted: {
if (isNew && isExpanded)
resetEdits();
}
onIsExpandedChanged: {
if (!isExpanded)
return;
@@ -86,7 +92,7 @@ Item {
editDesc = bindData.desc || "";
hasChanges = false;
_actionType = Actions.getActionType(editAction);
useCustomCompositor = _actionType === "compositor" && !Actions.isKnownCompositorAction(editAction);
useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(editAction);
return;
}
}
@@ -105,7 +111,7 @@ Item {
editDesc = bindData.desc || "";
hasChanges = false;
_actionType = Actions.getActionType(editAction);
useCustomCompositor = _actionType === "compositor" && !Actions.isKnownCompositorAction(editAction);
useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(editAction);
}
function startAddingNewKey() {
@@ -590,37 +596,93 @@ Item {
Keys.onPressed: event => {
if (!root.recording)
return;
if (event.key === Qt.Key_Escape) {
root.stopRecording();
event.accepted = true;
return;
}
switch (event.key) {
case Qt.Key_Control:
case Qt.Key_Shift:
case Qt.Key_Alt:
case Qt.Key_Meta:
event.accepted = true;
return;
}
const mods = KeyUtils.modsFromEvent(event.modifiers);
const key = KeyUtils.xkbKeyFromQtKey(event.key);
if (key) {
if (event.key === 0 && (event.modifiers & Qt.AltModifier)) {
root._altShiftGhost = true;
return;
}
let mods = KeyUtils.modsFromEvent(event.modifiers);
let qtKey = event.key;
if (root._altShiftGhost && (event.modifiers & Qt.AltModifier) && !mods.includes("Shift")) {
mods.push("Shift");
}
root._altShiftGhost = false;
if (qtKey === Qt.Key_Backtab) {
qtKey = Qt.Key_Tab;
if (!mods.includes("Shift"))
mods.push("Shift");
}
const key = KeyUtils.xkbKeyFromQtKey(qtKey);
if (!key) {
console.warn("[KeybindItem] Unknown key:", event.key, "mods:", event.modifiers);
return;
}
root.updateEdit({
key: KeyUtils.formatToken(mods, key)
});
root.stopRecording();
event.accepted = true;
}
}
MouseArea {
anchors.fill: parent
enabled: !root.recording
cursorShape: Qt.PointingHandCursor
onClicked: root.startRecording()
hoverEnabled: true
cursorShape: root.recording ? Qt.CrossCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onClicked: {
if (!root.recording)
root.startRecording();
}
onWheel: wheel => {
if (!root.recording)
return;
wheel.accepted = true;
const mods = [];
if (wheel.modifiers & Qt.ControlModifier)
mods.push("Ctrl");
if (wheel.modifiers & Qt.ShiftModifier)
mods.push("Shift");
if (wheel.modifiers & Qt.AltModifier)
mods.push("Alt");
if (wheel.modifiers & Qt.MetaModifier)
mods.push("Super");
let wheelKey = "";
if (wheel.angleDelta.y > 0)
wheelKey = "WheelScrollUp";
else if (wheel.angleDelta.y < 0)
wheelKey = "WheelScrollDown";
else if (wheel.angleDelta.x > 0)
wheelKey = "WheelScrollRight";
else if (wheel.angleDelta.x < 0)
wheelKey = "WheelScrollLeft";
if (!wheelKey)
return;
root.updateEdit({
key: KeyUtils.formatToken(mods, wheelKey)
});
root.stopRecording();
}
}
}
@@ -816,6 +878,69 @@ Item {
}
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingM
property var dmsArgConfig: {
const action = root.editAction;
if (!action)
return null;
if (action.indexOf("audio increment") !== -1 || action.indexOf("audio decrement") !== -1 || action.indexOf("brightness increment") !== -1 || action.indexOf("brightness decrement") !== -1) {
const parts = action.split(" ");
const lastPart = parts[parts.length - 1];
const hasAmount = /^\d+$/.test(lastPart);
return {
hasAmount: hasAmount,
amount: hasAmount ? lastPart : ""
};
}
return null;
}
visible: root._actionType === "dms" && dmsArgConfig !== null
StyledText {
text: I18n.tr("Amount")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
}
DankTextField {
Layout.preferredWidth: 80
Layout.preferredHeight: 40
placeholderText: "5"
text: parent.dmsArgConfig?.amount || ""
onTextChanged: {
if (!parent.dmsArgConfig)
return;
const action = root.editAction;
const parts = action.split(" ");
const lastPart = parts[parts.length - 1];
const hasOldAmount = /^\d+$/.test(lastPart);
if (hasOldAmount)
parts.pop();
if (text && /^\d+$/.test(text))
parts.push(text);
root.updateEdit({
action: parts.join(" ")
});
}
}
StyledText {
text: "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
Item {
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingM
@@ -898,6 +1023,154 @@ Item {
}
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingM
visible: root._actionType === "compositor" && !root.useCustomCompositor && Actions.getActionArgConfig(root.editAction)
property var argConfig: Actions.getActionArgConfig(root.editAction)
property var parsedArgs: Actions.parseCompositorActionArgs(root.editAction)
StyledText {
text: I18n.tr("Options")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingS
DankTextField {
id: argValueField
Layout.fillWidth: true
Layout.preferredHeight: 40
visible: {
const cfg = parent.parent.argConfig;
if (!cfg || !cfg.config || !cfg.config.args)
return false;
const firstArg = cfg.config.args[0];
return firstArg && (firstArg.type === "text" || firstArg.type === "number");
}
placeholderText: {
const cfg = parent.parent.argConfig;
if (!cfg || !cfg.config || !cfg.config.args)
return "";
return cfg.config.args[0]?.placeholder || "";
}
text: parent.parent.parsedArgs?.args?.value || parent.parent.parsedArgs?.args?.index || ""
onTextChanged: {
const cfg = parent.parent.argConfig;
if (!cfg)
return;
const base = parent.parent.parsedArgs?.base || root.editAction.split(" ")[0];
const args = cfg.config.args[0]?.type === "number" ? {
index: text
} : {
value: text
};
root.updateEdit({
action: Actions.buildCompositorAction(base, args)
});
}
}
RowLayout {
visible: {
const cfg = parent.parent.argConfig;
return cfg && cfg.base === "move-column-to-workspace";
}
spacing: Theme.spacingXS
DankToggle {
id: focusToggle
checked: parent.parent.parent.parsedArgs?.args?.focus === true
onCheckedChanged: {
const cfg = parent.parent.parent.argConfig;
if (!cfg)
return;
const parsed = parent.parent.parent.parsedArgs;
const args = {
index: parsed?.args?.index || "",
focus: checked
};
root.updateEdit({
action: Actions.buildCompositorAction("move-column-to-workspace", args)
});
}
}
StyledText {
text: I18n.tr("Follow focus")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
RowLayout {
visible: {
const cfg = parent.parent.argConfig;
return cfg && cfg.base && cfg.base.startsWith("screenshot");
}
spacing: Theme.spacingM
RowLayout {
spacing: Theme.spacingXS
DankToggle {
id: writeToDiskToggle
checked: parent.parent.parent.parent.parsedArgs?.args?.opts?.["write-to-disk"] === "true"
onCheckedChanged: {
const parsed = parent.parent.parent.parent.parsedArgs;
const base = parsed?.base || "screenshot";
const opts = parsed?.args?.opts || {};
opts["write-to-disk"] = checked ? "true" : "";
root.updateEdit({
action: Actions.buildCompositorAction(base, {
opts: opts
})
});
}
}
StyledText {
text: I18n.tr("Save")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
RowLayout {
spacing: Theme.spacingXS
DankToggle {
id: showPointerToggle
checked: parent.parent.parent.parent.parsedArgs?.args?.opts?.["show-pointer"] === "true"
onCheckedChanged: {
const parsed = parent.parent.parent.parent.parsedArgs;
const base = parsed?.base || "screenshot";
const opts = parsed?.args?.opts || {};
opts["show-pointer"] = checked ? "true" : "";
root.updateEdit({
action: Actions.buildCompositorAction(base, {
opts: opts
})
});
}
}
StyledText {
text: I18n.tr("Pointer")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingM