commit 89f160402415dfa0447ad9d229424c03ce39e184
parent ae7e996e35301e0daf31ec67608f9968f4867bfb
Author: Michael Camilleri <[email protected]>
Date: Sat, 18 Apr 2026 06:19:09 +0900
Add additional keyboard shortcuts
This commit adds support for Home/End/Page-Up/Page-Down. It does this
using the existing keyboard shortcut mechanism.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Diffstat:
5 files changed, 105 insertions(+), 1 deletion(-)
diff --git a/Listless/Extensions/ItemListView+Logic.swift b/Listless/Extensions/ItemListView+Logic.swift
@@ -355,6 +355,72 @@ extension ItemListViewProtocol {
return .handled
}
+ func navigateToFirst() -> KeyPress.Result {
+ guard focusedField == .scrollView else {
+ return .ignored
+ }
+
+ let displayOrder = allItemsInDisplayOrder
+ guard let first = displayOrder.first else {
+ return .handled
+ }
+ fState.selectedItemID = first.id
+ return .handled
+ }
+
+ func navigateToLast() -> KeyPress.Result {
+ guard focusedField == .scrollView else {
+ return .ignored
+ }
+
+ let displayOrder = allItemsInDisplayOrder
+ guard let last = displayOrder.last else {
+ return .handled
+ }
+ fState.selectedItemID = last.id
+ return .handled
+ }
+
+ func navigatePageUp() -> KeyPress.Result {
+ guard focusedField == .scrollView else {
+ return .ignored
+ }
+
+ let displayOrder = allItemsInDisplayOrder
+ guard !displayOrder.isEmpty else { return .handled }
+
+ guard let currentID = fState.selectedItemID,
+ let currentIndex = displayOrder.firstIndex(where: { $0.id == currentID })
+ else {
+ fState.selectedItemID = displayOrder.first?.id
+ return .handled
+ }
+
+ let targetIndex = max(0, currentIndex - pageNavigationSize)
+ fState.selectedItemID = displayOrder[targetIndex].id
+ return .handled
+ }
+
+ func navigatePageDown() -> KeyPress.Result {
+ guard focusedField == .scrollView else {
+ return .ignored
+ }
+
+ let displayOrder = allItemsInDisplayOrder
+ guard !displayOrder.isEmpty else { return .handled }
+
+ guard let currentID = fState.selectedItemID,
+ let currentIndex = displayOrder.firstIndex(where: { $0.id == currentID })
+ else {
+ fState.selectedItemID = displayOrder.first?.id
+ return .handled
+ }
+
+ let targetIndex = min(displayOrder.count - 1, currentIndex + pageNavigationSize)
+ fState.selectedItemID = displayOrder[targetIndex].id
+ return .handled
+ }
+
func navigateUpExtend() -> KeyPress.Result {
guard focusedField == .scrollView else {
return .ignored
diff --git a/Listless/Helpers/ItemListTypes.swift b/Listless/Helpers/ItemListTypes.swift
@@ -5,6 +5,8 @@ enum FocusField: Hashable {
case scrollView
}
+let pageNavigationSize = 10
+
enum DragState: Equatable {
case idle
case dragging(id: UUID, order: [UUID])
diff --git a/ListlessMac/Views/ItemListView.swift b/ListlessMac/Views/ItemListView.swift
@@ -426,6 +426,10 @@ struct ItemListView: View, ItemListViewProtocol {
ShortcutKey(key: .downArrow): navigateDown,
ShortcutKey(key: .upArrow, modifiers: .shift): navigateUpExtend,
ShortcutKey(key: .downArrow, modifiers: .shift): navigateDownExtend,
+ ShortcutKey(key: .home): navigateToFirst,
+ ShortcutKey(key: .end): navigateToLast,
+ ShortcutKey(key: .pageUp): navigatePageUp,
+ ShortcutKey(key: .pageDown): navigatePageDown,
ShortcutKey(key: .return): focusSelectedItem,
])
.onAppear {
diff --git a/ListlessiOS/Helpers/KeyCommandBridge.swift b/ListlessiOS/Helpers/KeyCommandBridge.swift
@@ -18,6 +18,10 @@ struct KeyCommandBridge: UIViewRepresentable {
let onSpace: () -> Void
let onReturn: () -> Void
let onDelete: () -> Void
+ let onHome: () -> Void
+ let onEnd: () -> Void
+ let onPageUp: () -> Void
+ let onPageDown: () -> Void
func makeUIView(context: Context) -> KeyCaptureView {
let view = KeyCaptureView()
@@ -26,6 +30,10 @@ struct KeyCommandBridge: UIViewRepresentable {
view.onSpace = onSpace
view.onReturn = onReturn
view.onDelete = onDelete
+ view.onHome = onHome
+ view.onEnd = onEnd
+ view.onPageUp = onPageUp
+ view.onPageDown = onPageDown
view.isActive = isActive
return view
}
@@ -36,6 +44,10 @@ struct KeyCommandBridge: UIViewRepresentable {
view.onSpace = onSpace
view.onReturn = onReturn
view.onDelete = onDelete
+ view.onHome = onHome
+ view.onEnd = onEnd
+ view.onPageUp = onPageUp
+ view.onPageDown = onPageDown
view.isActive = isActive
if isActive && !view.isFirstResponder {
@@ -53,6 +65,10 @@ struct KeyCommandBridge: UIViewRepresentable {
var onSpace: (() -> Void)?
var onReturn: (() -> Void)?
var onDelete: (() -> Void)?
+ var onHome: (() -> Void)?
+ var onEnd: (() -> Void)?
+ var onPageUp: (() -> Void)?
+ var onPageDown: (() -> Void)?
override var canBecomeFirstResponder: Bool { true }
@@ -66,6 +82,10 @@ struct KeyCommandBridge: UIViewRepresentable {
" ",
"\r",
"\u{8}",
+ UIKeyCommand.inputHome,
+ UIKeyCommand.inputEnd,
+ UIKeyCommand.inputPageUp,
+ UIKeyCommand.inputPageDown,
].map { input in
let cmd = UIKeyCommand(
input: input,
@@ -89,6 +109,14 @@ struct KeyCommandBridge: UIViewRepresentable {
onReturn?()
case "\u{8}":
onDelete?()
+ case UIKeyCommand.inputHome:
+ onHome?()
+ case UIKeyCommand.inputEnd:
+ onEnd?()
+ case UIKeyCommand.inputPageUp:
+ onPageUp?()
+ case UIKeyCommand.inputPageDown:
+ onPageDown?()
default:
break
}
diff --git a/ListlessiOS/Views/ItemListView.swift b/ListlessiOS/Views/ItemListView.swift
@@ -464,7 +464,11 @@ struct ItemListView: View, ItemListViewProtocol {
onDown: { _ = navigateDown() },
onSpace: { _ = toggleSelectedItem() },
onReturn: { _ = focusSelectedItem() },
- onDelete: { _ = deleteSelectedItemWithUndo() }
+ onDelete: { _ = deleteSelectedItemWithUndo() },
+ onHome: { _ = navigateToFirst() },
+ onEnd: { _ = navigateToLast() },
+ onPageUp: { _ = navigatePageUp() },
+ onPageDown: { _ = navigatePageDown() }
)
}
.onAppear {