listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

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