← All repositories

listless

A simple list app for Apple platforms

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


commit df715444409c0e5546b7ff6b98eb8087c591b511
parent d881c5950e4acb08a8cc7c4d64ec9241b06f2d86
Author: Michael Camilleri <[email protected]>
Date:   Mon, 23 Feb 2026 16:05:01 +0900

Improve macOS menus

Co-Authored-By: Codex GPT 5.3 <[email protected]>

Diffstat

MListless.xcodeproj/project.pbxproj | 6++++++
MListless.xcodeproj/xcshareddata/xcschemes/Listless macOS.xcscheme | 20+++++++-------------
MListless/Extensions/TaskListView+Logic.swift | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
AListlessMac/Helpers/AppCommands.swift | 26++++++++++++++++++++++++++
MListlessMac/ListlessMacApp.swift | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
MListlessMac/Views/TaskListView.swift | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
MListlessMac/Views/TaskRowView.swift | 6+++---

7 files changed, 375 insertions(+), 20 deletions(-)

diff --git a/Listless.xcodeproj/project.pbxproj b/Listless.xcodeproj/project.pbxproj

@@ -42,6 +42,7 @@
 		99977BFA37FBAAA49AF6B71E /* TaskStoreEdgeCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2BBE01D99CDA278BCB9F49 /* TaskStoreEdgeCaseTests.swift */; };
 		99D17075DA3F00F52A18BB4D /* AccentColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DD7EDA74DAAFA27C84CA08 /* AccentColor.swift */; };
 		A0AA8FD4C542E9AEB2437BC2 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14858BDFD1FD5119F1F24A6 /* PersistenceController.swift */; };
+		B7CFDCA5EA48EDE1C768FA21 /* AppCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C79ABB39A40D3E1828716C7 /* AppCommands.swift */; };
 		BA6953E0EFE6F8255F05A3FD /* AccentColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DD7EDA74DAAFA27C84CA08 /* AccentColor.swift */; };
 		C169823665158AA347A63990 /* TaskListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51B8129962E5CC78ECDDC2B /* TaskListView.swift */; };
 		C1FE091454864C4BBBBEB077 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB8A3BEB346267B30B4675F /* TaskItem.swift */; };
@@ -87,6 +88,7 @@
 		3FA75CA18C6270D68A755AEB /* TaskListView+Drag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+Drag.swift"; sourceTree = "<group>"; };
 		4669414A460FC0758D5B49A8 /* KeyboardNavigationModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardNavigationModifier.swift; sourceTree = "<group>"; };
 		466F9B0E407DF1F5B4789531 /* PlatformScrollIndicatorsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformScrollIndicatorsModifier.swift; sourceTree = "<group>"; };
+		4C79ABB39A40D3E1828716C7 /* AppCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCommands.swift; sourceTree = "<group>"; };
 		4FC64B9F9370041BEDBD1E14 /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; };
 		567DBAC2A39FA2760D006AAB /* PullToClear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullToClear.swift; sourceTree = "<group>"; };
 		5A6D74EDDD7E3FC150064FB5 /* TaskListView+PullToClear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+PullToClear.swift"; sourceTree = "<group>"; };
@@ -162,6 +164,7 @@
 		42CC0393E2524624AEC54D03 /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				4C79ABB39A40D3E1828716C7 /* AppCommands.swift */,
 				D640E7D21735C62A30A26DA4 /* ClickableTextField.swift */,
 				9F5D8B5866362D422A2A331C /* HoverCursorModifier.swift */,
 				466F9B0E407DF1F5B4789531 /* PlatformScrollIndicatorsModifier.swift */,
@@ -460,6 +463,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				99D17075DA3F00F52A18BB4D /* AccentColor.swift in Sources */,
+				B7CFDCA5EA48EDE1C768FA21 /* AppCommands.swift in Sources */,
 				DB5FF6C1AA57D4C9BDDD50FD /* ClickableTextField.swift in Sources */,
 				E4BD761E34CBB84CE80F7F49 /* CloudKitErrorClassifier.swift in Sources */,
 				E429067963379F99DD184FED /* CloudKitSyncMonitor.swift in Sources */,
@@ -554,6 +558,7 @@
 					"@executable_path/../Frameworks",
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = net.inqk.listless.mac;
+				PRODUCT_NAME = Listless;
 				SDKROOT = macosx;
 			};
 			name = Debug;
@@ -572,6 +577,7 @@
 					"@executable_path/../Frameworks",
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = net.inqk.listless.mac;
+				PRODUCT_NAME = Listless;
 				SDKROOT = macosx;
 			};
 			name = Release;

diff --git a/Listless.xcodeproj/xcshareddata/xcschemes/Listless macOS.xcscheme b/Listless.xcodeproj/xcshareddata/xcschemes/Listless macOS.xcscheme

@@ -1,11 +1,10 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
    LastUpgradeVersion = "1430"
-   version = "1.7">
+   version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
-      buildImplicitDependencies = "YES"
-      runPostActionsOnFailure = "NO">
+      buildImplicitDependencies = "YES">
       <BuildActionEntries>
          <BuildActionEntry
             buildForTesting = "YES"
@@ -16,7 +15,7 @@
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB"
-               BuildableName = "Listless macOS.app"
+               BuildableName = "Listless.app"
                BlueprintName = "Listless macOS"
                ReferencedContainer = "container:Listless.xcodeproj">
             </BuildableReference>
@@ -27,13 +26,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>
@@ -56,13 +54,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"
@@ -75,13 +71,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/Extensions/TaskListView+Logic.swift b/Listless/Extensions/TaskListView+Logic.swift

@@ -256,6 +256,24 @@ extension TaskListView {
         return .handled
     }
 
+    func createNewTaskFromShortcut() -> KeyPress.Result {
+        createNewTask()
+        focusedField = nil
+        return .handled
+    }
+
+    func moveSelectedTaskUpFromShortcut() -> KeyPress.Result {
+        guard focusedField == .scrollView else { return .ignored }
+        moveSelectedTaskUp()
+        return .handled
+    }
+
+    func moveSelectedTaskDownFromShortcut() -> KeyPress.Result {
+        guard focusedField == .scrollView else { return .ignored }
+        moveSelectedTaskDown()
+        return .handled
+    }
+
     func focusSelectedTask() -> KeyPress.Result {
         guard focusedField == .scrollView else { return .ignored }
         guard let currentID = selectedTaskID else { return .handled }
@@ -286,6 +304,39 @@ extension TaskListView {
         return .handled
     }
 
+    func moveSelectedTaskUp() {
+        guard focusedField == .scrollView else { return }
+        guard let currentID = selectedTaskID else { return }
+        guard let currentIndex = activeTasks.firstIndex(where: { $0.id == currentID }) else { return }
+        guard currentIndex > 0 else { return }
+
+        do {
+            try store.moveTask(taskID: currentID, toIndex: currentIndex - 1)
+        } catch {
+            presentStoreError(error)
+        }
+    }
+
+    func moveSelectedTaskDown() {
+        guard focusedField == .scrollView else { return }
+        guard let currentID = selectedTaskID else { return }
+        guard let currentIndex = activeTasks.firstIndex(where: { $0.id == currentID }) else { return }
+        guard currentIndex < activeTasks.count - 1 else { return }
+
+        do {
+            try store.moveTask(taskID: currentID, toIndex: currentIndex + 1)
+        } catch {
+            presentStoreError(error)
+        }
+    }
+
+    func markSelectedTaskCompleted() {
+        guard focusedField == .scrollView else { return }
+        guard let currentID = selectedTaskID else { return }
+        guard let task = allTasksInDisplayOrder.first(where: { $0.id == currentID }) else { return }
+        toggleCompletion(task)
+    }
+
     // MARK: - Focus Management
 
     func focusTextField(_ taskID: UUID) {

diff --git a/ListlessMac/Helpers/AppCommands.swift b/ListlessMac/Helpers/AppCommands.swift

@@ -0,0 +1,26 @@
+import Foundation
+
+// Bridges SwiftUI view state to AppKit menu items without using SwiftUI's Commands API.
+@MainActor
+final class MenuCoordinator {
+    static let shared = MenuCoordinator()
+    private init() {}
+
+    // Actions — set by TaskListView on each relevant state change.
+    var newTask: (() -> Void)?
+    var deleteSelectedTask: (() -> Void)?
+    var moveSelectedTaskUp: (() -> Void)?
+    var moveSelectedTaskDown: (() -> Void)?
+    var markSelectedTaskCompleted: (() -> Void)?
+    var clearCompletedTasks: (() -> Void)?
+
+    // Enabled state — read by AppDelegate in menuWillOpen and validateMenuItem.
+    var canDeleteSelectedTask = false
+    var canMoveSelectedTaskUp = false
+    var canMoveSelectedTaskDown = false
+    var canMarkSelectedTaskCompleted = false
+    var canClearCompletedTasks = false
+
+    // Dynamic titles — read by AppDelegate in validateMenuItem.
+    var markCompletedTitle: String = "Mark as Completed"
+}

diff --git a/ListlessMac/ListlessMacApp.swift b/ListlessMac/ListlessMacApp.swift

@@ -1,22 +1,224 @@
 import SwiftUI
+import AppKit
+
+private let customMenuTag = 1001
+
+@MainActor
+class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
+    func applicationDidFinishLaunching(_ notification: Notification) {
+        removeFormatMenu()
+
+        // SwiftUI builds menus asynchronously; watch for items appearing.
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(handleMenuDidAddItem(_:)),
+            name: NSMenu.didAddItemNotification,
+            object: nil
+        )
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(handleAppDidBecomeActive(_:)),
+            name: NSApplication.didBecomeActiveNotification,
+            object: nil
+        )
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(handleWindowDidBecomeKey(_:)),
+            name: NSWindow.didBecomeKeyNotification,
+            object: nil
+        )
+
+        // SwiftUI/AppKit can finish menu construction after launch callbacks; apply
+        // our menu patch repeatedly in the first moments to avoid startup races.
+        refreshMenus()
+        let startupDelays: [TimeInterval] = [0.0, 0.05, 0.15, 0.35]
+        for delay in startupDelays {
+            DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
+                self?.refreshMenus()
+            }
+        }
+    }
+
+    deinit {
+        NotificationCenter.default.removeObserver(self)
+    }
+
+    @objc private func handleMenuDidAddItem(_ notification: Notification) {
+        Task { @MainActor [weak self] in
+            self?.removeFormatMenu()
+            self?.setupFileMenuIfNeeded()
+            self?.setupEditMenuIfNeeded()
+        }
+    }
+
+    @objc private func handleAppDidBecomeActive(_ notification: Notification) {
+        refreshMenus()
+    }
+
+    @objc private func handleWindowDidBecomeKey(_ notification: Notification) {
+        refreshMenus()
+    }
+
+    private func refreshMenus() {
+        removeFormatMenu()
+        setupFileMenuIfNeeded()
+        setupEditMenuIfNeeded()
+    }
+
+    // MARK: - Menu Setup
+
+    @MainActor private func removeFormatMenu() {
+        guard let mainMenu = NSApp.mainMenu else { return }
+        mainMenu.items
+            .filter { $0.title == "Format" }
+            .forEach { mainMenu.removeItem($0) }
+    }
+
+    @MainActor private func setupFileMenuIfNeeded() {
+        guard let fileMenu = NSApp.mainMenu?.items.first(where: { $0.title == "File" })?.submenu else { return }
+
+        // Always update the New Window shortcut in-place — runs on every notification
+        // in case SwiftUI re-adds a fresh item. Never remove it; removal causes SwiftUI
+        // to re-add it at the bottom with the original shortcut.
+        if let newWindowItem = fileMenu.items.first(where: { $0.action == NSSelectorFromString("menuAction:") }) {
+            newWindowItem.keyEquivalent = "n"
+            newWindowItem.keyEquivalentModifierMask = [.command, .shift]
+        }
+
+        guard !fileMenu.items.contains(where: { $0.tag == customMenuTag }) else { return }
+
+        // Defer until both Close and Close All are present — the build order of these
+        // items is non-deterministic, so we wait for both before touching anything.
+        guard fileMenu.items.contains(where: { $0.action == NSSelectorFromString("performClose:") }),
+              fileMenu.items.contains(where: { $0.action == NSSelectorFromString("closeAll:") }) else { return }
+
+        guard let newWindowItem = fileMenu.items.first(where: { $0.action == NSSelectorFromString("menuAction:") }) else { return }
+        let insertIndex = fileMenu.index(of: newWindowItem)
+
+        // Insert sep1 and New Task before New Window, then sep2 immediately after it.
+        // We own both separators so the layout is stable regardless of where the
+        // system separator between Close/Close All ends up.
+        let sep1 = NSMenuItem.separator()
+        sep1.tag = customMenuTag
+        fileMenu.insertItem(sep1, at: insertIndex)
+
+        let newTaskItem = NSMenuItem(title: "New Task", action: #selector(handleNewTask), keyEquivalent: "n")
+        newTaskItem.keyEquivalentModifierMask = .command
+        newTaskItem.target = self
+        newTaskItem.tag = customMenuTag
+        fileMenu.insertItem(newTaskItem, at: insertIndex)
+
+        // New Window is now at insertIndex + 2; place sep2 immediately after it.
+        let sep2 = NSMenuItem.separator()
+        sep2.tag = customMenuTag
+        fileMenu.insertItem(sep2, at: insertIndex + 3)
+    }
+
+    @MainActor private func setupEditMenuIfNeeded() {
+        guard let editMenu = NSApp.mainMenu?.items.first(where: { $0.title == "Edit" })?.submenu else { return }
+
+        // NOTE: Any new menu key equivalents added in this file should also be mapped in
+        // TaskListView.keyboardNavigation(...). SwiftUI's onKeyPress layer can intercept
+        // events before AppKit key equivalents, so duplicating shortcut bindings at the
+        // SwiftUI layer keeps shortcut handling reliable.
+
+        // Modify the system "Delete" item in-place so it stays in its expected position
+        // but dispatches our action with our preferred shortcut (⌫). Runs on every
+        // notification in case SwiftUI re-adds a fresh item; left untagged so the guard
+        // below can still fire correctly on first setup.
+        if let systemDelete = editMenu.items.first(where: { $0.action == NSSelectorFromString("delete:") }) {
+            systemDelete.action = #selector(handleDeleteTask)
+            systemDelete.target = self
+            systemDelete.keyEquivalent = "\u{08}"
+            systemDelete.keyEquivalentModifierMask = []
+        }
+
+        guard !editMenu.items.contains(where: { $0.tag == customMenuTag }) else { return }
+
+        func addSep() {
+            let s = NSMenuItem.separator()
+            s.tag = customMenuTag
+            editMenu.addItem(s)
+        }
+
+        func addItem(title: String, action: Selector, key: String, modifiers: NSEvent.ModifierFlags) {
+            let i = NSMenuItem(title: title, action: action, keyEquivalent: key)
+            i.keyEquivalentModifierMask = modifiers
+            i.target = self
+            i.tag = customMenuTag
+            editMenu.addItem(i)
+        }
+
+        addSep()
+        addItem(title: "Move Up",           action: #selector(handleMoveUp),         key: "\u{F700}", modifiers: .command)
+        addItem(title: "Move Down",         action: #selector(handleMoveDown),        key: "\u{F701}", modifiers: .command)
+        addItem(title: "Mark as Completed", action: #selector(handleMarkCompleted),   key: " ",        modifiers: [])
+        addSep()
+        addItem(title: "Clear Completed",   action: #selector(handleClearCompleted),  key: "",         modifiers: [])
+    }
+
+    // MARK: - NSMenuItemValidation
+    // AppKit calls this automatically for each item targeting self, both when the
+    // menu opens and when keyboard shortcuts are evaluated.
+
+    func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
+        let coord = MenuCoordinator.shared
+        switch menuItem.action {
+        case #selector(handleDeleteTask):     return coord.canDeleteSelectedTask
+        case #selector(handleMoveUp):         return coord.canMoveSelectedTaskUp
+        case #selector(handleMoveDown):       return coord.canMoveSelectedTaskDown
+        case #selector(handleMarkCompleted):
+            menuItem.title = coord.markCompletedTitle
+            return coord.canMarkSelectedTaskCompleted
+        case #selector(handleClearCompleted): return coord.canClearCompletedTasks
+        default: return true
+        }
+    }
+
+    // MARK: - Actions
+
+    @objc private func handleNewTask() {
+        MenuCoordinator.shared.newTask?()
+    }
+
+    @objc private func handleDeleteTask() {
+        MenuCoordinator.shared.deleteSelectedTask?()
+    }
+
+    @objc private func handleMoveUp() {
+        MenuCoordinator.shared.moveSelectedTaskUp?()
+    }
+
+    @objc private func handleMoveDown() {
+        MenuCoordinator.shared.moveSelectedTaskDown?()
+    }
+
+    @objc private func handleMarkCompleted() {
+        MenuCoordinator.shared.markSelectedTaskCompleted?()
+    }
+
+    @objc private func handleClearCompleted() {
+        MenuCoordinator.shared.clearCompletedTasks?()
+    }
+}
 
 @main
 struct ListlessMacApp: App {
+    @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
     private let persistenceController: PersistenceController
 
     init() {
-        // Use in-memory storage during UI tests
         let isUITesting = ProcessInfo.processInfo.arguments.contains("UI_TESTING")
         persistenceController = isUITesting ? PersistenceController(inMemory: true) : .shared
     }
 
     var body: some Scene {
-        WindowGroup {
+        WindowGroup(id: "main") {
             TaskListView(
                 store: TaskStore(persistenceController: persistenceController),
                 syncMonitor: persistenceController.syncMonitor
             )
-                .environment(\.managedObjectContext, persistenceController.viewContext)
+            .environment(\.managedObjectContext, persistenceController.viewContext)
         }
         .windowStyle(.hiddenTitleBar)
     }

diff --git a/ListlessMac/Views/TaskListView.swift b/ListlessMac/Views/TaskListView.swift

@@ -32,6 +32,75 @@ struct TaskListView: View {
     @State var pullOffset: CGFloat = 0
 
     var vStackSpacing: CGFloat { 0 }
+    var selectedIndex: Int? {
+        guard let currentID = selectedTaskID else { return nil }
+        return activeTasks.firstIndex(where: { $0.id == currentID })
+    }
+
+    var canDeleteSelectionFromList: Bool {
+        selectedTaskID != nil && focusedField == .scrollView
+    }
+
+    var canMarkSelectionCompleted: Bool {
+        guard focusedField == .scrollView else { return false }
+        guard let currentID = selectedTaskID else { return false }
+        return allTasksInDisplayOrder.contains(where: { $0.id == currentID })
+    }
+
+    var markCompletedMenuTitle: String {
+        guard let currentID = selectedTaskID,
+              let task = allTasksInDisplayOrder.first(where: { $0.id == currentID }),
+              task.isCompleted else {
+            return "Mark as Completed"
+        }
+        return "Mark as Incomplete"
+    }
+
+    var canMoveSelectionUp: Bool {
+        guard focusedField == .scrollView else { return false }
+        guard let index = selectedIndex else { return false }
+        return index > 0
+    }
+
+    var canMoveSelectionDown: Bool {
+        guard focusedField == .scrollView else { return false }
+        guard let index = selectedIndex else { return false }
+        return index < activeTasks.count - 1
+    }
+
+    struct MenuState: Equatable {
+        let selectedTaskID: UUID?
+        let isScrollViewFocused: Bool
+        let activeTaskCount: Int
+        let completedTaskCount: Int
+        let selectedIndex: Int?
+    }
+
+    var menuCoordinatorTrigger: MenuState {
+        MenuState(
+            selectedTaskID: selectedTaskID,
+            isScrollViewFocused: focusedField == .scrollView,
+            activeTaskCount: activeTasks.count,
+            completedTaskCount: completedTasks.count,
+            selectedIndex: selectedIndex
+        )
+    }
+
+    func updateMenuCoordinator() {
+        let coord = MenuCoordinator.shared
+        coord.newTask = { createNewTask(); focusedField = nil }
+        coord.deleteSelectedTask = { _ = deleteSelectedTask() }
+        coord.moveSelectedTaskUp = { moveSelectedTaskUp() }
+        coord.moveSelectedTaskDown = { moveSelectedTaskDown() }
+        coord.markSelectedTaskCompleted = { markSelectedTaskCompleted() }
+        coord.clearCompletedTasks = { clearCompletedTasks() }
+        coord.canDeleteSelectedTask = canDeleteSelectionFromList
+        coord.canMoveSelectedTaskUp = canMoveSelectionUp
+        coord.canMoveSelectedTaskDown = canMoveSelectionDown
+        coord.canMarkSelectedTaskCompleted = canMarkSelectionCompleted
+        coord.markCompletedTitle = markCompletedMenuTitle
+        coord.canClearCompletedTasks = !completedTasks.isEmpty
+    }
 
     init(store: TaskStore, syncMonitor: CloudKitSyncMonitor) {
         self.store = store
@@ -149,16 +218,20 @@ struct TaskListView: View {
         .focusEffectDisabled()
         .accessibilityIdentifier("task-list-scrollview")
         .keyboardNavigation([
+            ShortcutKey(key: "n", modifiers: .command): createNewTaskFromShortcut,
             ShortcutKey(key: .upArrow): navigateUp,
             ShortcutKey(key: .downArrow): navigateDown,
-            ShortcutKey(key: .space): toggleSelectedTask,
+            ShortcutKey(key: .upArrow, modifiers: .command): moveSelectedTaskUpFromShortcut,
+            ShortcutKey(key: .downArrow, modifiers: .command): moveSelectedTaskDownFromShortcut,
             ShortcutKey(key: .return): focusSelectedTask,
+            ShortcutKey(key: .space): toggleSelectedTask,
             ShortcutKey(key: .delete): deleteSelectedTask,
         ])
         .onAppear {
             if focusedField == nil {
                 focusedField = .scrollView
             }
+            updateMenuCoordinator()
         }
         .onChange(of: focusedField) { oldValue, newValue in
             handleFocusChange(from: oldValue, to: newValue)
@@ -173,7 +246,10 @@ struct TaskListView: View {
                     focusedField = .scrollView
                 }
             }
+
+            updateMenuCoordinator()
         }
+        .onChange(of: menuCoordinatorTrigger) { _, _ in updateMenuCoordinator() }
         .onChange(of: undoManager, initial: true) { _, newValue in
             managedObjectContext.undoManager = newValue
         }

diff --git a/ListlessMac/Views/TaskRowView.swift b/ListlessMac/Views/TaskRowView.swift

@@ -173,11 +173,11 @@ struct TaskRowView: View {
 
     @ViewBuilder
     private var selectionBackground: some View {
-        if task.isCompleted {
-            Color(nsColor: .windowBackgroundColor)
-        } else if isSelected {
+        if isSelected {
             RoundedRectangle(cornerRadius: 6, style: .continuous)
                 .fill(Color.accentColor.opacity(0.2))
+        } else if task.isCompleted {
+            Color(nsColor: .windowBackgroundColor)
         }
     }