← All repositories

listless

A simple list app for Apple platforms

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


commit 9380c94322191d9f818c541c60c719616e9d8356
parent b988b41f1334626252b1c366355685c6bd16c3d8
Author: Michael Camilleri <[email protected]>
Date:   Sun, 22 Mar 2026 10:52:47 +0900

Support themes on macOS version

This commit brings themes to the macOS version. It also renames the
original theme to Pilbara.

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

Diffstat

MListless/Helpers/AccentColor.swift | 18+++++++++++-------
MListlessMac/ListlessMacApp.swift | 22++++++++++++++++++++++
MListlessMac/Views/TaskListView.swift | 5++++-
MListlessMac/Views/TaskRowView.swift | 8+++++++-
MListlessiOS/Views/PullToCreate.swift | 2+-
MListlessiOS/Views/SettingsView.swift | 2+-
MListlessiOS/Views/TaskListView.swift | 2+-
MListlessiOS/Views/TaskRowView.swift | 2+-

8 files changed, 48 insertions(+), 13 deletions(-)

diff --git a/Listless/Helpers/AccentColor.swift b/Listless/Helpers/AccentColor.swift

@@ -1,14 +1,18 @@
 import SwiftUI
 
 enum ColorTheme: Int, CaseIterable, Identifiable {
-    case original = 0
+    case pilbara = 0
     case collaroy = 1
 
     var id: Int { rawValue }
 
+    static var displayOrder: [ColorTheme] {
+        allCases.sorted { $0.displayName < $1.displayName }
+    }
+
     var displayName: String {
         switch self {
-        case .original: "Original"
+        case .pilbara: "Pilbara"
         case .collaroy: "Collaroy"
         }
     }
@@ -17,21 +21,21 @@ enum ColorTheme: Int, CaseIterable, Identifiable {
 
     fileprivate var top: HSB {
         switch self {
-        case .original: (h: 0.98, s: 0.85, b: 1.00)
+        case .pilbara: (h: 0.98, s: 0.85, b: 1.00)
         case .collaroy: (h: 0.58, s: 0.88, b: 1.00)
         }
     }
 
     fileprivate var mid: HSB {
         switch self {
-        case .original: (h: 0.88, s: 0.75, b: 0.95)
+        case .pilbara: (h: 0.88, s: 0.75, b: 0.95)
         case .collaroy: (h: 0.51, s: 0.69, b: 0.90)
         }
     }
 
     fileprivate var bottom: HSB {
         switch self {
-        case .original: (h: 0.72, s: 0.65, b: 0.85)
+        case .pilbara: (h: 0.72, s: 0.65, b: 0.85)
         case .collaroy: (h: 0.44, s: 0.50, b: 0.80)
         }
     }
@@ -48,7 +52,7 @@ private enum TaskAccentColorCache {
     static var colors: [TaskAccentColorKey: Color] = [:]
 }
 
-func taskColor(forIndex index: Int, total: Int, theme: ColorTheme = .original) -> Color {
+func taskColor(forIndex index: Int, total: Int, theme: ColorTheme = .pilbara) -> Color {
     let top = theme.top
     guard total > 1 else { return Color(hue: top.h, saturation: top.s, brightness: top.b) }
 
@@ -64,7 +68,7 @@ func taskColor(forIndex index: Int, total: Int, theme: ColorTheme = .original) -
 }
 
 @MainActor
-func cachedTaskColor(forIndex index: Int, total: Int, theme: ColorTheme = .original) -> Color {
+func cachedTaskColor(forIndex index: Int, total: Int, theme: ColorTheme = .pilbara) -> Color {
     let key = TaskAccentColorKey(index: index, total: total, theme: theme)
     if let cached = TaskAccentColorCache.colors[key] {
         return cached

diff --git a/ListlessMac/ListlessMacApp.swift b/ListlessMac/ListlessMacApp.swift

@@ -14,6 +14,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
     private var syncDiagnosticsWindow: NSWindow?
     private let coordinators = NSMapTable<NSWindow, WindowCoordinator>.weakToStrongObjects()
     private static let appearanceModeKey = "appearanceMode"
+    private static let colorThemeKey = "colorTheme"
 
     private var keyWindowCoordinator: WindowCoordinator? {
         guard let window = NSApp.keyWindow else { return nil }
@@ -66,6 +67,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
             }
             menuItem.state = (currentMode == itemMode) ? .on : .off
             return true
+        case #selector(handleThemeSelection(_:)):
+            let currentTheme = UserDefaults.standard.integer(forKey: Self.colorThemeKey)
+            menuItem.state = (currentTheme == menuItem.tag) ? .on : .off
+            return true
         default:
             break
         }
@@ -147,6 +152,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
     @objc private func handleAppearanceLight() { setAppearanceMode(1) }
     @objc private func handleAppearanceDark() { setAppearanceMode(2) }
 
+    @objc private func handleThemeSelection(_ sender: NSMenuItem) {
+        UserDefaults.standard.set(sender.tag, forKey: Self.colorThemeKey)
+    }
+
     private func setAppearanceMode(_ mode: Int) {
         UserDefaults.standard.set(mode, forKey: Self.appearanceModeKey)
         applyAppearanceMode(mode)
@@ -366,6 +375,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
         viewMenu.addItem(appearanceMenuItem)
         viewMenu.addItem(NSMenuItem.separator())
 
+        let themeMenu = NSMenu(title: "Theme")
+        for theme in ColorTheme.displayOrder {
+            let item = NSMenuItem(title: theme.displayName, action: #selector(handleThemeSelection(_:)), keyEquivalent: "")
+            item.tag = theme.rawValue
+            item.target = self
+            themeMenu.addItem(item)
+        }
+        let themeMenuItem = NSMenuItem(title: "Theme", action: nil, keyEquivalent: "")
+        themeMenuItem.submenu = themeMenu
+        viewMenu.addItem(themeMenuItem)
+
+        viewMenu.addItem(NSMenuItem.separator())
+
         let fullScreenItem = NSMenuItem(title: "Enter Full Screen", action: #selector(NSWindow.toggleFullScreen(_:)), keyEquivalent: "f")
         fullScreenItem.keyEquivalentModifierMask = [.command, .control]
         viewMenu.addItem(fullScreenItem)

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

@@ -9,6 +9,9 @@ struct TaskListView: View, TaskListViewProtocol {
         var draftTitle: String = ""
     }
 
+    @AppStorage("colorTheme") private var colorThemeRaw = 0
+    private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .pilbara }
+
     @Environment(\.undoManager) var undoManager
     @Environment(\.managedObjectContext) var managedObjectContext
 
@@ -275,7 +278,7 @@ struct TaskListView: View, TaskListViewProtocol {
                     let total = max(1, displayActiveTasks.count + 1)
                     let index = displayActiveTasks.count
                     let accentColor = cachedTaskColor(
-                        forIndex: index, total: total
+                        forIndex: index, total: total, theme: colorTheme
                     )
                     let isSelected = fState.isTaskSelected(draftAppendRowID)
                     HStack(alignment: .firstTextBaseline, spacing: 12) {

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

@@ -15,6 +15,9 @@ struct TaskRowView: View {
     let onPaste: (String) -> Void
     @FocusState.Binding var focusedField: FocusField?
 
+    @AppStorage("colorTheme") private var colorThemeRaw = 0
+    private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .pilbara }
+
     @State private var editingTitle: String = ""
     @State private var isCurrentlyEditing: Bool = false
     @State private var cachedAccentColor: Color = .clear
@@ -30,7 +33,7 @@ struct TaskRowView: View {
     @MainActor
     private func computeAccentColor() -> Color {
         guard !task.isCompleted else { return .clear }
-        return cachedTaskColor(forIndex: index, total: totalTasks)
+        return cachedTaskColor(forIndex: index, total: totalTasks, theme: colorTheme)
     }
 
     init(
@@ -160,6 +163,9 @@ struct TaskRowView: View {
                 editingTitle = newValue
             }
         }
+        .onChange(of: colorThemeRaw) { _, _ in
+            cachedAccentColor = computeAccentColor()
+        }
         .onChange(of: index) { _, _ in
             cachedAccentColor = computeAccentColor()
         }

diff --git a/ListlessiOS/Views/PullToCreate.swift b/ListlessiOS/Views/PullToCreate.swift

@@ -5,7 +5,7 @@ struct PullToCreateIndicator: View {
     let threshold: CGFloat
     var hasRowsBelow: Bool = true
     @AppStorage("colorTheme") private var colorThemeRaw = 0
-    private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .original }
+    private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .pilbara }
 
     static let indicatorHeight: CGFloat = 50
 

diff --git a/ListlessiOS/Views/SettingsView.swift b/ListlessiOS/Views/SettingsView.swift

@@ -15,7 +15,7 @@ struct SettingsView: View {
                 }
 
                 Section("Theme") {
-                    ForEach(ColorTheme.allCases) { theme in
+                    ForEach(ColorTheme.displayOrder) { theme in
                         Button {
                             colorThemeRaw = theme.rawValue
                         } label: {

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

@@ -26,7 +26,7 @@ struct TaskListView: View, TaskListViewProtocol {
 
     @AppStorage("headingText") var headingText = "Items"
     @AppStorage("colorTheme") private var colorThemeRaw = 0
-    private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .original }
+    private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .pilbara }
     @Environment(\.undoManager) var undoManager
     @Environment(\.managedObjectContext) var managedObjectContext
 

diff --git a/ListlessiOS/Views/TaskRowView.swift b/ListlessiOS/Views/TaskRowView.swift

@@ -18,7 +18,7 @@ struct TaskRowView: View {
     @FocusState.Binding var focusedField: FocusField?
 
     @AppStorage("colorTheme") private var colorThemeRaw = 0
-    private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .original }
+    private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .pilbara }
     @State private var swipeOffset: CGFloat = 0
     @State private var swipeDirection: TaskRowSwipeGesture.SwipeDirection = .none
     @State private var isSwipeTriggered: Bool = false