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:
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