← All repositories

listless

A simple list app for Apple platforms

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


commit 7c31167dbea9ffaaa2fd6e9f8d46f96ff207c105
parent 98daee6a460db2678bb95bcbddda008d866e3c7a
Author: Michael Camilleri <[email protected]>
Date:   Fri, 13 Mar 2026 20:12:38 +0900

Support multi-selection with the mouse

This commit adds support for creating contiguous selections using the
mouse and Shift.

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

Diffstat

MListless/Extensions/TaskListView+Logic.swift | 17+++++++++++++++--
MListlessMac/Extensions/TaskListView+Toolbar.swift | 8++------
MListlessMac/Views/TaskListView.swift | 20+++++++++++++++++---

3 files changed, 34 insertions(+), 11 deletions(-)

diff --git a/Listless/Extensions/TaskListView+Logic.swift b/Listless/Extensions/TaskListView+Logic.swift

@@ -271,8 +271,18 @@ extension TaskListViewProtocol {
         deleteTask(task)
     }
 
-    func selectTask(_ taskID: UUID) {
-        fState.selectedTaskID = taskID
+    func selectTask(_ taskID: UUID, extendSelection: Bool = false) {
+        if extendSelection && fState.selectedTaskID != nil {
+            if fState.anchorTaskID == nil {
+                fState.anchorTaskID = fState.cursorTaskID
+            }
+            fState.extendSelection(
+                to: taskID,
+                displayOrder: allTasksInDisplayOrder.map(\.id)
+            )
+        } else {
+            fState.selectedTaskID = taskID
+        }
     }
 
     func deleteTask(_ task: TaskItem) {
@@ -401,6 +411,9 @@ extension TaskListViewProtocol {
         guard !ids.isEmpty else { return .handled }
         let tasksToToggle = allTasksInDisplayOrder.filter { ids.contains($0.id) }
         guard !tasksToToggle.isEmpty else { return .handled }
+        let hasActive = tasksToToggle.contains { !$0.isCompleted }
+        let hasCompleted = tasksToToggle.contains { $0.isCompleted }
+        guard !(hasActive && hasCompleted) else { return .handled }
         for task in tasksToToggle {
             toggleCompletion(task)
         }

diff --git a/ListlessMac/Extensions/TaskListView+Toolbar.swift b/ListlessMac/Extensions/TaskListView+Toolbar.swift

@@ -33,15 +33,11 @@ extension TaskListView {
                 .help("Create a new item")
 
                 Button {
-                    if let currentID = fState.selectedTaskID,
-                        let task = allTasksInDisplayOrder.first(where: { $0.id == currentID })
-                    {
-                        deleteTask(task)
-                    }
+                    _ = deleteSelectedTask()
                 } label: {
                     Label("Delete", systemImage: "trash")
                 }
-                .disabled(fState.selectedTaskID == nil || focusedField != .scrollView)
+                .disabled(!canDeleteSelectionFromList)
                 .help("Delete selected item")
 
                 Divider()

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

@@ -60,7 +60,11 @@ struct TaskListView: View, TaskListViewProtocol {
 
     var canMarkSelectionCompleted: Bool {
         guard focusedField == .scrollView else { return false }
-        return allTasksInDisplayOrder.contains(where: { fState.isTaskSelected($0.id) })
+        let selected = allTasksInDisplayOrder.filter { fState.isTaskSelected($0.id) }
+        guard !selected.isEmpty else { return false }
+        let hasActive = selected.contains { !$0.isCompleted }
+        let hasCompleted = selected.contains { $0.isCompleted }
+        return !(hasActive && hasCompleted)
     }
 
     var markCompletedMenuTitle: String {
@@ -188,7 +192,12 @@ struct TaskListView: View, TaskListViewProtocol {
                         onToggle: { toggleCompletion($0) },
                         onTitleChange: { updateTitle($0, $1) },
                         onDelete: { deleteTask($0) },
-                        onSelect: { selectTask($0) },
+                        onSelect: {
+                            selectTask(
+                                $0,
+                                extendSelection: NSEvent.modifierFlags.contains(.shift)
+                            )
+                        },
                         onStartEdit: { startEditing($0) },
                         onEndEdit: { endEditing($0, shouldCreateNewTask: $1) },
                         onPaste: { createTask(title: $0, afterTaskID: taskID) }
@@ -330,7 +339,12 @@ struct TaskListView: View, TaskListViewProtocol {
                         onToggle: { toggleCompletion($0) },
                         onTitleChange: { updateTitle($0, $1) },
                         onDelete: { deleteTask($0) },
-                        onSelect: { selectTask($0) }
+                        onSelect: {
+                            selectTask(
+                                $0,
+                                extendSelection: NSEvent.modifierFlags.contains(.shift)
+                            )
+                        }
                     )
                 }
             }