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