← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


commit 59fbebbcd921ecfc3180469d2c97c7fda0596761
parent 31bdb1189bf5ddd08ac1d0b4438e27b2c831ced2
Author: Michael Camilleri <[email protected]>
Date:   Sat,  7 Feb 2026 19:04:38 +0900

Improve keyboard shortcuts

Co-Authored-By: Claude 4.5 Sonnet <[email protected]>

Diffstat

MListless/Views/KeyboardNavigationModifier.swift | 64++++++++++++++++++++++++++++++++++++++++++++++------------------
MListless/Views/TaskListView.swift | 32++++++++++++++++++++++++++------

2 files changed, 72 insertions(+), 24 deletions(-)

diff --git a/Listless/Views/KeyboardNavigationModifier.swift b/Listless/Views/KeyboardNavigationModifier.swift

@@ -1,24 +1,52 @@
 import SwiftUI
 
+struct ShortcutKey: Hashable {
+    let key: KeyEquivalent
+    let modifiers: EventModifiers
+
+    init(key: KeyEquivalent, modifiers: EventModifiers = []) {
+        self.key = key
+        self.modifiers = modifiers
+    }
+
+    static func == (lhs: ShortcutKey, rhs: ShortcutKey) -> Bool {
+        lhs.key == rhs.key && lhs.modifiers == rhs.modifiers
+    }
+
+    func hash(into hasher: inout Hasher) {
+        hasher.combine(key)
+        hasher.combine(modifiers.rawValue)
+    }
+}
+
 extension View {
-    func keyboardNavigation(
-        onUpArrow: @escaping () -> KeyPress.Result,
-        onDownArrow: @escaping () -> KeyPress.Result,
-        onSpace: @escaping () -> KeyPress.Result,
-        onReturn: @escaping () -> KeyPress.Result
-    ) -> some View {
-        self
-            .onKeyPress(.upArrow) {
-                onUpArrow()
-            }
-            .onKeyPress(.downArrow) {
-                onDownArrow()
-            }
-            .onKeyPress(.space) {
-                onSpace()
-            }
-            .onKeyPress(.return) {
-                onReturn()
+    func keyboardNavigation(_ bindings: [ShortcutKey: () -> KeyPress.Result]) -> some View {
+        self.onKeyPress { press in
+            let key = normalizeKey(press)
+            let modifiers = normalizeModifiers(press.modifiers)
+            let shortcut = ShortcutKey(key: key, modifiers: modifiers)
+
+            if let action = bindings[shortcut] {
+                return action()
             }
+            return .ignored
+        }
+    }
+
+    private func normalizeKey(_ press: KeyPress) -> KeyEquivalent {
+        // Normalize backspace/delete key
+        if press.characters == "\u{7F}" {
+            return .delete
+        }
+        return press.key
+    }
+
+    private func normalizeModifiers(_ modifiers: EventModifiers) -> EventModifiers {
+        // Filter out system modifiers that come automatically with certain keys
+        // (function keys, numericPad) - only keep user-intentional modifiers
+        var normalized = modifiers
+        normalized.remove(.function)
+        normalized.remove(.numericPad)
+        return normalized
     }
 }

diff --git a/Listless/Views/TaskListView.swift b/Listless/Views/TaskListView.swift

@@ -127,12 +127,13 @@ struct TaskListView: View {
         .focused($focusedField, equals: .scrollView)
         .focusEffectDisabled()
         .accessibilityIdentifier("task-list-scrollview")
-        .keyboardNavigation(
-            onUpArrow: navigateUp,
-            onDownArrow: navigateDown,
-            onSpace: toggleSelectedTask,
-            onReturn: focusSelectedTask
-        )
+        .keyboardNavigation([
+            ShortcutKey(key: .upArrow): navigateUp,
+            ShortcutKey(key: .downArrow): navigateDown,
+            ShortcutKey(key: .space): toggleSelectedTask,
+            ShortcutKey(key: .return): focusSelectedTask,
+            ShortcutKey(key: .delete): deleteSelectedTask,
+        ])
         .onAppear {
             // Set initial focus to enable keyboard navigation
             if focusedField == nil {
@@ -349,6 +350,25 @@ struct TaskListView: View {
         return .handled
     }
 
+    private func deleteSelectedTask() -> KeyPress.Result {
+        print("🗑️ deleteSelectedTask() called, focusedField: \(String(describing: focusedField))")
+        guard focusedField == .scrollView else {
+            print("🗑️ deleteSelectedTask() IGNORED - focus is not .scrollView")
+            return .ignored
+        }
+        guard let currentID = selectedTaskID else {
+            print("🗑️ deleteSelectedTask() no task selected")
+            return .handled
+        }
+        guard let task = allTasksInDisplayOrder.first(where: { $0.id == currentID }) else {
+            print("🗑️ deleteSelectedTask() task not found")
+            return .handled
+        }
+        print("🗑️ deleteSelectedTask() deleting task \(currentID)")
+        deleteTask(task)
+        return .handled
+    }
+
 
     // MARK: - Focus Management