listless

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

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:
MListless/Extensions/ItemListView+Logic.swift | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MListless/Helpers/ItemListTypes.swift | 2++
MListlessMac/Views/ItemListView.swift | 4++++
MListlessiOS/Helpers/KeyCommandBridge.swift | 28++++++++++++++++++++++++++++
MListlessiOS/Views/ItemListView.swift | 6+++++-
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 {