listless

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

commit df715444409c0e5546b7ff6b98eb8087c591b511
parent d881c5950e4acb08a8cc7c4d64ec9241b06f2d86
Author: Michael Camilleri <[email protected]>
Date:   Mon, 23 Feb 2026 16:05:01 +0900

Improve macOS menus

Co-Authored-By: Codex GPT 5.3 <[email protected]>

Diffstat:
MListless.xcodeproj/project.pbxproj | 6++++++
MListless.xcodeproj/xcshareddata/xcschemes/Listless macOS.xcscheme | 20+++++++-------------
MListless/Extensions/TaskListView+Logic.swift | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
AListlessMac/Helpers/AppCommands.swift | 26++++++++++++++++++++++++++
MListlessMac/ListlessMacApp.swift | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
MListlessMac/Views/TaskListView.swift | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
MListlessMac/Views/TaskRowView.swift | 6+++---
7 files changed, 375 insertions(+), 20 deletions(-)

diff --git a/Listless.xcodeproj/project.pbxproj b/Listless.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ 99977BFA37FBAAA49AF6B71E /* TaskStoreEdgeCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2BBE01D99CDA278BCB9F49 /* TaskStoreEdgeCaseTests.swift */; }; 99D17075DA3F00F52A18BB4D /* AccentColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DD7EDA74DAAFA27C84CA08 /* AccentColor.swift */; }; A0AA8FD4C542E9AEB2437BC2 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14858BDFD1FD5119F1F24A6 /* PersistenceController.swift */; }; + B7CFDCA5EA48EDE1C768FA21 /* AppCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C79ABB39A40D3E1828716C7 /* AppCommands.swift */; }; BA6953E0EFE6F8255F05A3FD /* AccentColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DD7EDA74DAAFA27C84CA08 /* AccentColor.swift */; }; C169823665158AA347A63990 /* TaskListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51B8129962E5CC78ECDDC2B /* TaskListView.swift */; }; C1FE091454864C4BBBBEB077 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB8A3BEB346267B30B4675F /* TaskItem.swift */; }; @@ -87,6 +88,7 @@ 3FA75CA18C6270D68A755AEB /* TaskListView+Drag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+Drag.swift"; sourceTree = "<group>"; }; 4669414A460FC0758D5B49A8 /* KeyboardNavigationModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardNavigationModifier.swift; sourceTree = "<group>"; }; 466F9B0E407DF1F5B4789531 /* PlatformScrollIndicatorsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformScrollIndicatorsModifier.swift; sourceTree = "<group>"; }; + 4C79ABB39A40D3E1828716C7 /* AppCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCommands.swift; sourceTree = "<group>"; }; 4FC64B9F9370041BEDBD1E14 /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; }; 567DBAC2A39FA2760D006AAB /* PullToClear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullToClear.swift; sourceTree = "<group>"; }; 5A6D74EDDD7E3FC150064FB5 /* TaskListView+PullToClear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+PullToClear.swift"; sourceTree = "<group>"; }; @@ -162,6 +164,7 @@ 42CC0393E2524624AEC54D03 /* Helpers */ = { isa = PBXGroup; children = ( + 4C79ABB39A40D3E1828716C7 /* AppCommands.swift */, D640E7D21735C62A30A26DA4 /* ClickableTextField.swift */, 9F5D8B5866362D422A2A331C /* HoverCursorModifier.swift */, 466F9B0E407DF1F5B4789531 /* PlatformScrollIndicatorsModifier.swift */, @@ -460,6 +463,7 @@ buildActionMask = 2147483647; files = ( 99D17075DA3F00F52A18BB4D /* AccentColor.swift in Sources */, + B7CFDCA5EA48EDE1C768FA21 /* AppCommands.swift in Sources */, DB5FF6C1AA57D4C9BDDD50FD /* ClickableTextField.swift in Sources */, E4BD761E34CBB84CE80F7F49 /* CloudKitErrorClassifier.swift in Sources */, E429067963379F99DD184FED /* CloudKitSyncMonitor.swift in Sources */, @@ -554,6 +558,7 @@ "@executable_path/../Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = net.inqk.listless.mac; + PRODUCT_NAME = Listless; SDKROOT = macosx; }; name = Debug; @@ -572,6 +577,7 @@ "@executable_path/../Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = net.inqk.listless.mac; + PRODUCT_NAME = Listless; SDKROOT = macosx; }; name = Release; diff --git a/Listless.xcodeproj/xcshareddata/xcschemes/Listless macOS.xcscheme b/Listless.xcodeproj/xcshareddata/xcschemes/Listless macOS.xcscheme @@ -1,11 +1,10 @@ <?xml version="1.0" encoding="UTF-8"?> <Scheme LastUpgradeVersion = "1430" - version = "1.7"> + version = "1.3"> <BuildAction parallelizeBuildables = "YES" - buildImplicitDependencies = "YES" - runPostActionsOnFailure = "NO"> + buildImplicitDependencies = "YES"> <BuildActionEntries> <BuildActionEntry buildForTesting = "YES" @@ -16,7 +15,7 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB" - BuildableName = "Listless macOS.app" + BuildableName = "Listless.app" BlueprintName = "Listless macOS" ReferencedContainer = "container:Listless.xcodeproj"> </BuildableReference> @@ -27,13 +26,12 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - onlyGenerateCoverageForSpecifiedTargets = "NO"> + shouldUseLaunchSchemeArgsEnv = "YES"> <MacroExpansion> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB" - BuildableName = "Listless macOS.app" + BuildableName = "Listless.app" BlueprintName = "Listless macOS" ReferencedContainer = "container:Listless.xcodeproj"> </BuildableReference> @@ -56,13 +54,11 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB" - BuildableName = "Listless macOS.app" + BuildableName = "Listless.app" BlueprintName = "Listless macOS" ReferencedContainer = "container:Listless.xcodeproj"> </BuildableReference> </BuildableProductRunnable> - <CommandLineArguments> - </CommandLineArguments> </LaunchAction> <ProfileAction buildConfiguration = "Release" @@ -75,13 +71,11 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "0FB4F07A37999BBC6DFE4DBB" - BuildableName = "Listless macOS.app" + BuildableName = "Listless.app" BlueprintName = "Listless macOS" ReferencedContainer = "container:Listless.xcodeproj"> </BuildableReference> </BuildableProductRunnable> - <CommandLineArguments> - </CommandLineArguments> </ProfileAction> <AnalyzeAction buildConfiguration = "Debug"> diff --git a/Listless/Extensions/TaskListView+Logic.swift b/Listless/Extensions/TaskListView+Logic.swift @@ -256,6 +256,24 @@ extension TaskListView { return .handled } + func createNewTaskFromShortcut() -> KeyPress.Result { + createNewTask() + focusedField = nil + return .handled + } + + func moveSelectedTaskUpFromShortcut() -> KeyPress.Result { + guard focusedField == .scrollView else { return .ignored } + moveSelectedTaskUp() + return .handled + } + + func moveSelectedTaskDownFromShortcut() -> KeyPress.Result { + guard focusedField == .scrollView else { return .ignored } + moveSelectedTaskDown() + return .handled + } + func focusSelectedTask() -> KeyPress.Result { guard focusedField == .scrollView else { return .ignored } guard let currentID = selectedTaskID else { return .handled } @@ -286,6 +304,39 @@ extension TaskListView { return .handled } + func moveSelectedTaskUp() { + guard focusedField == .scrollView else { return } + guard let currentID = selectedTaskID else { return } + guard let currentIndex = activeTasks.firstIndex(where: { $0.id == currentID }) else { return } + guard currentIndex > 0 else { return } + + do { + try store.moveTask(taskID: currentID, toIndex: currentIndex - 1) + } catch { + presentStoreError(error) + } + } + + func moveSelectedTaskDown() { + guard focusedField == .scrollView else { return } + guard let currentID = selectedTaskID else { return } + guard let currentIndex = activeTasks.firstIndex(where: { $0.id == currentID }) else { return } + guard currentIndex < activeTasks.count - 1 else { return } + + do { + try store.moveTask(taskID: currentID, toIndex: currentIndex + 1) + } catch { + presentStoreError(error) + } + } + + func markSelectedTaskCompleted() { + guard focusedField == .scrollView else { return } + guard let currentID = selectedTaskID else { return } + guard let task = allTasksInDisplayOrder.first(where: { $0.id == currentID }) else { return } + toggleCompletion(task) + } + // MARK: - Focus Management func focusTextField(_ taskID: UUID) { diff --git a/ListlessMac/Helpers/AppCommands.swift b/ListlessMac/Helpers/AppCommands.swift @@ -0,0 +1,26 @@ +import Foundation + +// Bridges SwiftUI view state to AppKit menu items without using SwiftUI's Commands API. +@MainActor +final class MenuCoordinator { + static let shared = MenuCoordinator() + private init() {} + + // Actions — set by TaskListView on each relevant state change. + var newTask: (() -> Void)? + var deleteSelectedTask: (() -> Void)? + var moveSelectedTaskUp: (() -> Void)? + var moveSelectedTaskDown: (() -> Void)? + var markSelectedTaskCompleted: (() -> Void)? + var clearCompletedTasks: (() -> Void)? + + // Enabled state — read by AppDelegate in menuWillOpen and validateMenuItem. + var canDeleteSelectedTask = false + var canMoveSelectedTaskUp = false + var canMoveSelectedTaskDown = false + var canMarkSelectedTaskCompleted = false + var canClearCompletedTasks = false + + // Dynamic titles — read by AppDelegate in validateMenuItem. + var markCompletedTitle: String = "Mark as Completed" +} diff --git a/ListlessMac/ListlessMacApp.swift b/ListlessMac/ListlessMacApp.swift @@ -1,22 +1,224 @@ import SwiftUI +import AppKit + +private let customMenuTag = 1001 + +@MainActor +class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation { + func applicationDidFinishLaunching(_ notification: Notification) { + removeFormatMenu() + + // SwiftUI builds menus asynchronously; watch for items appearing. + NotificationCenter.default.addObserver( + self, + selector: #selector(handleMenuDidAddItem(_:)), + name: NSMenu.didAddItemNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(handleAppDidBecomeActive(_:)), + name: NSApplication.didBecomeActiveNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(handleWindowDidBecomeKey(_:)), + name: NSWindow.didBecomeKeyNotification, + object: nil + ) + + // SwiftUI/AppKit can finish menu construction after launch callbacks; apply + // our menu patch repeatedly in the first moments to avoid startup races. + refreshMenus() + let startupDelays: [TimeInterval] = [0.0, 0.05, 0.15, 0.35] + for delay in startupDelays { + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + self?.refreshMenus() + } + } + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc private func handleMenuDidAddItem(_ notification: Notification) { + Task { @MainActor [weak self] in + self?.removeFormatMenu() + self?.setupFileMenuIfNeeded() + self?.setupEditMenuIfNeeded() + } + } + + @objc private func handleAppDidBecomeActive(_ notification: Notification) { + refreshMenus() + } + + @objc private func handleWindowDidBecomeKey(_ notification: Notification) { + refreshMenus() + } + + private func refreshMenus() { + removeFormatMenu() + setupFileMenuIfNeeded() + setupEditMenuIfNeeded() + } + + // MARK: - Menu Setup + + @MainActor private func removeFormatMenu() { + guard let mainMenu = NSApp.mainMenu else { return } + mainMenu.items + .filter { $0.title == "Format" } + .forEach { mainMenu.removeItem($0) } + } + + @MainActor private func setupFileMenuIfNeeded() { + guard let fileMenu = NSApp.mainMenu?.items.first(where: { $0.title == "File" })?.submenu else { return } + + // Always update the New Window shortcut in-place — runs on every notification + // in case SwiftUI re-adds a fresh item. Never remove it; removal causes SwiftUI + // to re-add it at the bottom with the original shortcut. + if let newWindowItem = fileMenu.items.first(where: { $0.action == NSSelectorFromString("menuAction:") }) { + newWindowItem.keyEquivalent = "n" + newWindowItem.keyEquivalentModifierMask = [.command, .shift] + } + + guard !fileMenu.items.contains(where: { $0.tag == customMenuTag }) else { return } + + // Defer until both Close and Close All are present — the build order of these + // items is non-deterministic, so we wait for both before touching anything. + guard fileMenu.items.contains(where: { $0.action == NSSelectorFromString("performClose:") }), + fileMenu.items.contains(where: { $0.action == NSSelectorFromString("closeAll:") }) else { return } + + guard let newWindowItem = fileMenu.items.first(where: { $0.action == NSSelectorFromString("menuAction:") }) else { return } + let insertIndex = fileMenu.index(of: newWindowItem) + + // Insert sep1 and New Task before New Window, then sep2 immediately after it. + // We own both separators so the layout is stable regardless of where the + // system separator between Close/Close All ends up. + let sep1 = NSMenuItem.separator() + sep1.tag = customMenuTag + fileMenu.insertItem(sep1, at: insertIndex) + + let newTaskItem = NSMenuItem(title: "New Task", action: #selector(handleNewTask), keyEquivalent: "n") + newTaskItem.keyEquivalentModifierMask = .command + newTaskItem.target = self + newTaskItem.tag = customMenuTag + fileMenu.insertItem(newTaskItem, at: insertIndex) + + // New Window is now at insertIndex + 2; place sep2 immediately after it. + let sep2 = NSMenuItem.separator() + sep2.tag = customMenuTag + fileMenu.insertItem(sep2, at: insertIndex + 3) + } + + @MainActor private func setupEditMenuIfNeeded() { + guard let editMenu = NSApp.mainMenu?.items.first(where: { $0.title == "Edit" })?.submenu else { return } + + // NOTE: Any new menu key equivalents added in this file should also be mapped in + // TaskListView.keyboardNavigation(...). SwiftUI's onKeyPress layer can intercept + // events before AppKit key equivalents, so duplicating shortcut bindings at the + // SwiftUI layer keeps shortcut handling reliable. + + // Modify the system "Delete" item in-place so it stays in its expected position + // but dispatches our action with our preferred shortcut (⌫). Runs on every + // notification in case SwiftUI re-adds a fresh item; left untagged so the guard + // below can still fire correctly on first setup. + if let systemDelete = editMenu.items.first(where: { $0.action == NSSelectorFromString("delete:") }) { + systemDelete.action = #selector(handleDeleteTask) + systemDelete.target = self + systemDelete.keyEquivalent = "\u{08}" + systemDelete.keyEquivalentModifierMask = [] + } + + guard !editMenu.items.contains(where: { $0.tag == customMenuTag }) else { return } + + func addSep() { + let s = NSMenuItem.separator() + s.tag = customMenuTag + editMenu.addItem(s) + } + + func addItem(title: String, action: Selector, key: String, modifiers: NSEvent.ModifierFlags) { + let i = NSMenuItem(title: title, action: action, keyEquivalent: key) + i.keyEquivalentModifierMask = modifiers + i.target = self + i.tag = customMenuTag + editMenu.addItem(i) + } + + addSep() + addItem(title: "Move Up", action: #selector(handleMoveUp), key: "\u{F700}", modifiers: .command) + addItem(title: "Move Down", action: #selector(handleMoveDown), key: "\u{F701}", modifiers: .command) + addItem(title: "Mark as Completed", action: #selector(handleMarkCompleted), key: " ", modifiers: []) + addSep() + addItem(title: "Clear Completed", action: #selector(handleClearCompleted), key: "", modifiers: []) + } + + // MARK: - NSMenuItemValidation + // AppKit calls this automatically for each item targeting self, both when the + // menu opens and when keyboard shortcuts are evaluated. + + func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { + let coord = MenuCoordinator.shared + switch menuItem.action { + case #selector(handleDeleteTask): return coord.canDeleteSelectedTask + case #selector(handleMoveUp): return coord.canMoveSelectedTaskUp + case #selector(handleMoveDown): return coord.canMoveSelectedTaskDown + case #selector(handleMarkCompleted): + menuItem.title = coord.markCompletedTitle + return coord.canMarkSelectedTaskCompleted + case #selector(handleClearCompleted): return coord.canClearCompletedTasks + default: return true + } + } + + // MARK: - Actions + + @objc private func handleNewTask() { + MenuCoordinator.shared.newTask?() + } + + @objc private func handleDeleteTask() { + MenuCoordinator.shared.deleteSelectedTask?() + } + + @objc private func handleMoveUp() { + MenuCoordinator.shared.moveSelectedTaskUp?() + } + + @objc private func handleMoveDown() { + MenuCoordinator.shared.moveSelectedTaskDown?() + } + + @objc private func handleMarkCompleted() { + MenuCoordinator.shared.markSelectedTaskCompleted?() + } + + @objc private func handleClearCompleted() { + MenuCoordinator.shared.clearCompletedTasks?() + } +} @main struct ListlessMacApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate private let persistenceController: PersistenceController init() { - // Use in-memory storage during UI tests let isUITesting = ProcessInfo.processInfo.arguments.contains("UI_TESTING") persistenceController = isUITesting ? PersistenceController(inMemory: true) : .shared } var body: some Scene { - WindowGroup { + WindowGroup(id: "main") { TaskListView( store: TaskStore(persistenceController: persistenceController), syncMonitor: persistenceController.syncMonitor ) - .environment(\.managedObjectContext, persistenceController.viewContext) + .environment(\.managedObjectContext, persistenceController.viewContext) } .windowStyle(.hiddenTitleBar) } diff --git a/ListlessMac/Views/TaskListView.swift b/ListlessMac/Views/TaskListView.swift @@ -32,6 +32,75 @@ struct TaskListView: View { @State var pullOffset: CGFloat = 0 var vStackSpacing: CGFloat { 0 } + var selectedIndex: Int? { + guard let currentID = selectedTaskID else { return nil } + return activeTasks.firstIndex(where: { $0.id == currentID }) + } + + var canDeleteSelectionFromList: Bool { + selectedTaskID != nil && focusedField == .scrollView + } + + var canMarkSelectionCompleted: Bool { + guard focusedField == .scrollView else { return false } + guard let currentID = selectedTaskID else { return false } + return allTasksInDisplayOrder.contains(where: { $0.id == currentID }) + } + + var markCompletedMenuTitle: String { + guard let currentID = selectedTaskID, + let task = allTasksInDisplayOrder.first(where: { $0.id == currentID }), + task.isCompleted else { + return "Mark as Completed" + } + return "Mark as Incomplete" + } + + var canMoveSelectionUp: Bool { + guard focusedField == .scrollView else { return false } + guard let index = selectedIndex else { return false } + return index > 0 + } + + var canMoveSelectionDown: Bool { + guard focusedField == .scrollView else { return false } + guard let index = selectedIndex else { return false } + return index < activeTasks.count - 1 + } + + struct MenuState: Equatable { + let selectedTaskID: UUID? + let isScrollViewFocused: Bool + let activeTaskCount: Int + let completedTaskCount: Int + let selectedIndex: Int? + } + + var menuCoordinatorTrigger: MenuState { + MenuState( + selectedTaskID: selectedTaskID, + isScrollViewFocused: focusedField == .scrollView, + activeTaskCount: activeTasks.count, + completedTaskCount: completedTasks.count, + selectedIndex: selectedIndex + ) + } + + func updateMenuCoordinator() { + let coord = MenuCoordinator.shared + coord.newTask = { createNewTask(); focusedField = nil } + coord.deleteSelectedTask = { _ = deleteSelectedTask() } + coord.moveSelectedTaskUp = { moveSelectedTaskUp() } + coord.moveSelectedTaskDown = { moveSelectedTaskDown() } + coord.markSelectedTaskCompleted = { markSelectedTaskCompleted() } + coord.clearCompletedTasks = { clearCompletedTasks() } + coord.canDeleteSelectedTask = canDeleteSelectionFromList + coord.canMoveSelectedTaskUp = canMoveSelectionUp + coord.canMoveSelectedTaskDown = canMoveSelectionDown + coord.canMarkSelectedTaskCompleted = canMarkSelectionCompleted + coord.markCompletedTitle = markCompletedMenuTitle + coord.canClearCompletedTasks = !completedTasks.isEmpty + } init(store: TaskStore, syncMonitor: CloudKitSyncMonitor) { self.store = store @@ -149,16 +218,20 @@ struct TaskListView: View { .focusEffectDisabled() .accessibilityIdentifier("task-list-scrollview") .keyboardNavigation([ + ShortcutKey(key: "n", modifiers: .command): createNewTaskFromShortcut, ShortcutKey(key: .upArrow): navigateUp, ShortcutKey(key: .downArrow): navigateDown, - ShortcutKey(key: .space): toggleSelectedTask, + ShortcutKey(key: .upArrow, modifiers: .command): moveSelectedTaskUpFromShortcut, + ShortcutKey(key: .downArrow, modifiers: .command): moveSelectedTaskDownFromShortcut, ShortcutKey(key: .return): focusSelectedTask, + ShortcutKey(key: .space): toggleSelectedTask, ShortcutKey(key: .delete): deleteSelectedTask, ]) .onAppear { if focusedField == nil { focusedField = .scrollView } + updateMenuCoordinator() } .onChange(of: focusedField) { oldValue, newValue in handleFocusChange(from: oldValue, to: newValue) @@ -173,7 +246,10 @@ struct TaskListView: View { focusedField = .scrollView } } + + updateMenuCoordinator() } + .onChange(of: menuCoordinatorTrigger) { _, _ in updateMenuCoordinator() } .onChange(of: undoManager, initial: true) { _, newValue in managedObjectContext.undoManager = newValue } diff --git a/ListlessMac/Views/TaskRowView.swift b/ListlessMac/Views/TaskRowView.swift @@ -173,11 +173,11 @@ struct TaskRowView: View { @ViewBuilder private var selectionBackground: some View { - if task.isCompleted { - Color(nsColor: .windowBackgroundColor) - } else if isSelected { + if isSelected { RoundedRectangle(cornerRadius: 6, style: .continuous) .fill(Color.accentColor.opacity(0.2)) + } else if task.isCompleted { + Color(nsColor: .windowBackgroundColor) } }