← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


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