listless

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

commit 509e82ff4e5729f2fef2cfcd2da0422e0c0632a4
parent f3c807105ab2dba85d4e532e0b22a4a9be870adc
Author: Michael Camilleri <[email protected]>
Date:   Sat, 18 Apr 2026 07:26:44 +0900

Add further keyboard shortcuts

On Magic Keyboards for iPads, there are no Page-Up/Page-Down/Home/End
keys. This commit adds alernative keys to support those input methods.

Co-Authored-By: Claude Opus 4.7 <[email protected]>

Diffstat:
MListless.xcodeproj/xcshareddata/xcschemes/Listless macOS.xcscheme | 22+++++++---------------
MListless.xcodeproj/xcshareddata/xcschemes/Listless watchOS.xcscheme | 25++++++++-----------------
MListlessiOS/Helpers/AppCommands.swift | 12++++++++++++
MListlessiOS/Helpers/KeyCommandBridge.swift | 21+++++++++++++++++++++
MListlessiOS/ListlessiOSApp.swift | 35+++++++++++++++++++++++++++++++++++
MListlessiOS/Views/ItemListView.swift | 4++++
6 files changed, 87 insertions(+), 32 deletions(-)

diff --git a/Listless.xcodeproj/xcshareddata/xcschemes/Listless macOS.xcscheme b/Listless.xcodeproj/xcshareddata/xcschemes/Listless macOS.xcscheme @@ -4,8 +4,7 @@ version = "1.7"> <BuildAction parallelizeBuildables = "YES" - buildImplicitDependencies = "YES" - runPostActionsOnFailure = "NO"> + buildImplicitDependencies = "YES"> <PreActions> <ExecutionAction ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction"> @@ -16,7 +15,7 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB" - BuildableName = "Listless macOS.app" + BuildableName = "Listless.app" BlueprintName = "Listless macOS" ReferencedContainer = "container:Listless.xcodeproj"> </BuildableReference> @@ -34,7 +33,7 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB" - BuildableName = "Listless macOS.app" + BuildableName = "Listless.app" BlueprintName = "Listless macOS" ReferencedContainer = "container:Listless.xcodeproj"> </BuildableReference> @@ -45,13 +44,12 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - onlyGenerateCoverageForSpecifiedTargets = "NO"> + shouldUseLaunchSchemeArgsEnv = "YES"> <MacroExpansion> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB" - BuildableName = "Listless macOS.app" + BuildableName = "Listless.app" BlueprintName = "Listless macOS" ReferencedContainer = "container:Listless.xcodeproj"> </BuildableReference> @@ -82,8 +80,6 @@ </BuildableReference> </TestableReference> </Testables> - <CommandLineArguments> - </CommandLineArguments> </TestAction> <LaunchAction buildConfiguration = "Debug" @@ -100,13 +96,11 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB" - BuildableName = "Listless macOS.app" + BuildableName = "Listless.app" BlueprintName = "Listless macOS" ReferencedContainer = "container:Listless.xcodeproj"> </BuildableReference> </BuildableProductRunnable> - <CommandLineArguments> - </CommandLineArguments> </LaunchAction> <ProfileAction buildConfiguration = "Release" @@ -119,13 +113,11 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB" - BuildableName = "Listless macOS.app" + BuildableName = "Listless.app" BlueprintName = "Listless macOS" ReferencedContainer = "container:Listless.xcodeproj"> </BuildableReference> </BuildableProductRunnable> - <CommandLineArguments> - </CommandLineArguments> </ProfileAction> <AnalyzeAction buildConfiguration = "Debug"> diff --git a/Listless.xcodeproj/xcshareddata/xcschemes/Listless watchOS.xcscheme b/Listless.xcodeproj/xcshareddata/xcschemes/Listless watchOS.xcscheme @@ -4,8 +4,7 @@ version = "1.7"> <BuildAction parallelizeBuildables = "YES" - buildImplicitDependencies = "YES" - runPostActionsOnFailure = "NO"> + buildImplicitDependencies = "YES"> <PreActions> <ExecutionAction ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction"> @@ -16,7 +15,7 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "9BDC1B2175AB9CE26790448D" - BuildableName = "Listless watchOS.app" + BuildableName = "Listless.app" BlueprintName = "Listless watchOS" ReferencedContainer = "container:Listless.xcodeproj"> </BuildableReference> @@ -34,7 +33,7 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "9BDC1B2175AB9CE26790448D" - BuildableName = "Listless watchOS.app" + BuildableName = "Listless.app" BlueprintName = "Listless watchOS" ReferencedContainer = "container:Listless.xcodeproj"> </BuildableReference> @@ -45,21 +44,19 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - onlyGenerateCoverageForSpecifiedTargets = "NO"> + shouldUseLaunchSchemeArgsEnv = "YES"> <MacroExpansion> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "9BDC1B2175AB9CE26790448D" - BuildableName = "Listless watchOS.app" + BuildableName = "Listless.app" BlueprintName = "Listless watchOS" ReferencedContainer = "container:Listless.xcodeproj"> </BuildableReference> </MacroExpansion> <Testables> <TestableReference - skipped = "NO" - parallelizable = "NO"> + skipped = "NO"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "FAC694EFB69F0D9F12557137" @@ -69,8 +66,6 @@ </BuildableReference> </TestableReference> </Testables> - <CommandLineArguments> - </CommandLineArguments> </TestAction> <LaunchAction buildConfiguration = "Debug" @@ -87,13 +82,11 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "9BDC1B2175AB9CE26790448D" - BuildableName = "Listless watchOS.app" + BuildableName = "Listless.app" BlueprintName = "Listless watchOS" ReferencedContainer = "container:Listless.xcodeproj"> </BuildableReference> </BuildableProductRunnable> - <CommandLineArguments> - </CommandLineArguments> </LaunchAction> <ProfileAction buildConfiguration = "Release" @@ -106,13 +99,11 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "9BDC1B2175AB9CE26790448D" - BuildableName = "Listless watchOS.app" + BuildableName = "Listless.app" BlueprintName = "Listless watchOS" ReferencedContainer = "container:Listless.xcodeproj"> </BuildableReference> </BuildableProductRunnable> - <CommandLineArguments> - </CommandLineArguments> </ProfileAction> <AnalyzeAction buildConfiguration = "Debug"> diff --git a/ListlessiOS/Helpers/AppCommands.swift b/ListlessiOS/Helpers/AppCommands.swift @@ -17,6 +17,10 @@ final class IOSMenuCoordinator { var moveUp: (() -> Void)? var moveDown: (() -> Void)? var markCompleted: (() -> Void)? + var navigatePageUp: (() -> Void)? + var navigatePageDown: (() -> Void)? + var navigateToFirst: (() -> Void)? + var navigateToLast: (() -> Void)? // Enabled state — read by KeyCaptureView in validate(_:). var canDelete = false @@ -38,6 +42,10 @@ enum IOSMenuSelectors { static let moveUp = #selector(IOSMenuActions.handleMoveUp) static let moveDown = #selector(IOSMenuActions.handleMoveDown) static let markCompleted = #selector(IOSMenuActions.handleMarkCompleted) + static let navigatePageUp = #selector(IOSMenuActions.handleNavigatePageUp) + static let navigatePageDown = #selector(IOSMenuActions.handleNavigatePageDown) + static let navigateToFirst = #selector(IOSMenuActions.handleNavigateToFirst) + static let navigateToLast = #selector(IOSMenuActions.handleNavigateToLast) } /// Protocol declaring the `@objc` action methods so selectors can be @@ -48,4 +56,8 @@ enum IOSMenuSelectors { func handleMoveUp() func handleMoveDown() func handleMarkCompleted() + func handleNavigatePageUp() + func handleNavigatePageDown() + func handleNavigateToFirst() + func handleNavigateToLast() } diff --git a/ListlessiOS/Helpers/KeyCommandBridge.swift b/ListlessiOS/Helpers/KeyCommandBridge.swift @@ -144,6 +144,22 @@ struct KeyCommandBridge: UIViewRepresentable { IOSMenuCoordinator.shared.markCompleted?() } + @objc func handleNavigatePageUp() { + IOSMenuCoordinator.shared.navigatePageUp?() + } + + @objc func handleNavigatePageDown() { + IOSMenuCoordinator.shared.navigatePageDown?() + } + + @objc func handleNavigateToFirst() { + IOSMenuCoordinator.shared.navigateToFirst?() + } + + @objc func handleNavigateToLast() { + IOSMenuCoordinator.shared.navigateToLast?() + } + // MARK: - Menu validation override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { @@ -158,6 +174,11 @@ struct KeyCommandBridge: UIViewRepresentable { return isActive && IOSMenuCoordinator.shared.canMoveDown case IOSMenuSelectors.markCompleted: return isActive && IOSMenuCoordinator.shared.canMarkCompleted + case IOSMenuSelectors.navigatePageUp, + IOSMenuSelectors.navigatePageDown, + IOSMenuSelectors.navigateToFirst, + IOSMenuSelectors.navigateToLast: + return isActive default: return super.canPerformAction(action, withSender: sender) } diff --git a/ListlessiOS/ListlessiOSApp.swift b/ListlessiOS/ListlessiOSApp.swift @@ -69,6 +69,41 @@ class IOSAppDelegate: UIResponder, UIApplicationDelegate { ]), atEndOfMenu: .edit ) + + // Edit menu — Page Up (⌥↑), Page Down (⌥↓), + // Jump to Top (⌘⌥↑), Jump to Bottom (⌘⌥↓). + // Provides Magic Keyboard users an alternative to the absent + // Page/Home/End keys. + let pageUp = UIKeyCommand( + title: "Page Up", + action: IOSMenuSelectors.navigatePageUp, + input: UIKeyCommand.inputUpArrow, + modifierFlags: .alternate + ) + let pageDown = UIKeyCommand( + title: "Page Down", + action: IOSMenuSelectors.navigatePageDown, + input: UIKeyCommand.inputDownArrow, + modifierFlags: .alternate + ) + let jumpToTop = UIKeyCommand( + title: "Jump to Top", + action: IOSMenuSelectors.navigateToFirst, + input: UIKeyCommand.inputUpArrow, + modifierFlags: [.command, .alternate] + ) + let jumpToBottom = UIKeyCommand( + title: "Jump to Bottom", + action: IOSMenuSelectors.navigateToLast, + input: UIKeyCommand.inputDownArrow, + modifierFlags: [.command, .alternate] + ) + builder.insertChild( + UIMenu(title: "", options: .displayInline, children: [ + pageUp, pageDown, jumpToTop, jumpToBottom, + ]), + atEndOfMenu: .edit + ) } } diff --git a/ListlessiOS/Views/ItemListView.swift b/ListlessiOS/Views/ItemListView.swift @@ -139,6 +139,10 @@ struct ItemListView: View, ItemListViewProtocol { coord.moveUp = { moveSelectedItemUp() } coord.moveDown = { moveSelectedItemDown() } coord.markCompleted = { markSelectedItemCompleted() } + coord.navigatePageUp = { _ = navigatePageUp() } + coord.navigatePageDown = { _ = navigatePageDown() } + coord.navigateToFirst = { _ = navigateToFirst() } + coord.navigateToLast = { _ = navigateToLast() } let inNavMode = focusedField == .scrollView coord.canDelete = fState.selectedItemID != nil && inNavMode coord.canMoveUp = canMoveSelectionUp