listless

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

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) + ) + } ) } }