← All repositories

listless

A simple list app for Apple platforms

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


commit 5c43b0031d153c642b04bac5856d067d66bdcee4
parent 55ee1373530f1bdcb08649d139d37e17fed37e7a
Author: Michael Camilleri <[email protected]>
Date:   Sun, 15 Mar 2026 00:29:19 +0900

Add Select All support to macOS version

One of the disadvantages of not using standard widgets from AppKit is
that behaviour that a user might expect would be present (such as being
able to select all tasks) is simply not there. Now it is.

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

Diffstat

MListless/Helpers/TaskListTypes.swift | 9+++++++++
MListlessMac/Helpers/AppCommands.swift | 2++
MListlessMac/ListlessMacApp.swift | 5+++++
MListlessMac/Views/TaskListView.swift | 4++++
MTests/UI/ListlessMacUITests.swift | 37+++++++++++++++++++++++++++++++++++++

5 files changed, 57 insertions(+), 0 deletions(-)

diff --git a/Listless/Helpers/TaskListTypes.swift b/Listless/Helpers/TaskListTypes.swift

@@ -53,6 +53,15 @@ struct FocusStateData {
         selectedTaskIDs.count > 1
     }
 
+    /// Select all tasks in display order, anchoring at the first and
+    /// placing the cursor at the last.
+    mutating func selectAll(displayOrder: [UUID]) {
+        guard !displayOrder.isEmpty else { return }
+        anchorTaskID = displayOrder.first
+        cursorTaskID = displayOrder.last
+        selectedTaskIDs = Set(displayOrder)
+    }
+
     /// Extend or contract the selection from the anchor to `targetID`,
     /// selecting all tasks between them in `displayOrder`.
     mutating func extendSelection(to targetID: UUID, displayOrder: [UUID]) {

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

@@ -15,9 +15,11 @@ final class MenuCoordinator {
     var moveSelectedTaskUp: (() -> Void)?
     var moveSelectedTaskDown: (() -> Void)?
     var markSelectedTaskCompleted: (() -> Void)?
+    var selectAllTasks: (() -> Void)?
     var clearCompletedTasks: (() -> Void)?
 
     // Enabled state — read by AppDelegate in menuWillOpen and validateMenuItem.
+    var canSelectAllTasks = false
     var canCopySelectedTask = false
     var canCutSelectedTask = false
     var canPasteAfterSelectedTask = false

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

@@ -56,6 +56,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
         }
         guard let coord = keyWindowCoordinator else { return false }
         switch menuItem.action {
+        case #selector(selectAll(_:)):         return coord.canSelectAllTasks
         case #selector(cut(_:)):              return coord.canCutSelectedTask
         case #selector(copy(_:)):             return coord.canCopySelectedTask
         case #selector(paste(_:)):            return coord.canPasteAfterSelectedTask
@@ -83,6 +84,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
         }
     }
 
+    @objc func selectAll(_ sender: Any?) {
+        keyWindowCoordinator?.selectAllTasks?()
+    }
+
     @objc func cut(_ sender: Any?) {
         keyWindowCoordinator?.cutSelectedTask?()
     }

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

@@ -135,9 +135,13 @@ struct TaskListView: View, TaskListViewProtocol {
         coord.moveSelectedTaskUp = { moveSelectedTaskUp() }
         coord.moveSelectedTaskDown = { moveSelectedTaskDown() }
         coord.markSelectedTaskCompleted = { markSelectedTaskCompleted() }
+        coord.selectAllTasks = {
+            fState.selectAll(displayOrder: allTasksInDisplayOrder.map(\.id))
+        }
         coord.clearCompletedTasks = { clearCompletedTasks() }
         let inNavMode = focusedField == .scrollView
         let singleSelect = !fState.selectedTaskIDs.isEmpty && !fState.hasMultipleSelection
+        coord.canSelectAllTasks = inNavMode && !allTasksInDisplayOrder.isEmpty
         coord.canCopySelectedTask = singleSelect && inNavMode
         coord.canCutSelectedTask = singleSelect && inNavMode
         coord.canPasteAfterSelectedTask = selectedIndex != nil && singleSelect && inNavMode

diff --git a/Tests/UI/ListlessMacUITests.swift b/Tests/UI/ListlessMacUITests.swift

@@ -214,6 +214,43 @@ final class ListlessMacUITests: XCTestCase {
         )
     }
 
+    // MARK: - Select All
+
+    func testSelectAllThenDelete() {
+        createTask("Alpha")
+        createTask("Bravo")
+        createTask("Charlie")
+        navigateToTask(at: 0)
+        app.typeKey("a", modifierFlags: .command)
+        app.typeKey(.delete, modifierFlags: [])
+        XCTAssertTrue(
+            emptyStateLabel.waitForExistence(timeout: 2),
+            "All tasks should be deleted after Select All + Delete"
+        )
+    }
+
+    func testSelectAllIncludesCompletedTasks() {
+        createTask("Active item")
+        createTask("Done item")
+        app.typeKey(.escape, modifierFlags: [])
+
+        // Complete the second task
+        taskCheckbox(at: 1).click()
+        let completed = app.buttons.matching(
+            NSPredicate(format: "identifier == 'task-checkbox' AND value == 'checkmark.circle.fill'")
+        ).firstMatch
+        XCTAssertTrue(completed.waitForExistence(timeout: 3))
+
+        // Navigate to first task, then select all and delete
+        navigateToTask(at: 0)
+        app.typeKey("a", modifierFlags: .command)
+        app.typeKey(.delete, modifierFlags: [])
+        XCTAssertTrue(
+            emptyStateLabel.waitForExistence(timeout: 2),
+            "Both active and completed tasks should be deleted after Select All + Delete"
+        )
+    }
+
     // MARK: - Clear Completed
 
     func testClearCompleted() {