listless

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

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() {