Commit 427abbbee0
Verified · cmc
Layout: unified · split
Sources/OrgPresentation/ThemeSpec.swift +40 −1
| @@ -95,6 +95,33 @@ public struct ThemeSpec: Equatable, Sendable { | ||
| 95 | 95 | ("syntax-number", "code: numbers, constants and escapes"), |
| 96 | 96 | ("syntax-property", "code: properties, attributes and tags"), |
| 97 | 97 | ("syntax-label", "code: labels"), |
| 98 | ("sidebar-background", "the folder sidebar and the outline; unset: the system's"), | |
| 99 | ("sidebar-foreground", "file and heading names there; unset: the system's"), | |
| 100 | ("sidebar-header", "folder names there; unset: the system's"), | |
| 101 | ("modeline-background", "the modeline and message line; unset: the system's"), | |
| 102 | ("modeline-foreground", "modeline text; unset: the system's"), | |
| 103 | ("modeline-highlight", "the outline path and the clock in the modeline"), | |
| 104 | ("state-normal", "the NORMAL tag"), | |
| 105 | ("state-insert", "the INSERT tag"), | |
| 106 | ("state-visual", "the VISUAL and V-LINE tags"), | |
| 107 | ("agenda-background", "the agenda and board; unset: the system's"), | |
| 108 | ("agenda-date", "agenda day headers"), | |
| 109 | ("agenda-today", "today's header and the current time"), | |
| 110 | ("agenda-time", "times and the time grid"), | |
| 111 | ("agenda-category", "categories"), | |
| 112 | ("agenda-deadline", "deadlines due"), | |
| 113 | ("agenda-upcoming", "deadlines coming up"), | |
| 114 | ("agenda-scheduled", "scheduled items"), | |
| 115 | ("agenda-scheduled-past", "items scheduled on an earlier day"), | |
| 116 | ("habit-clear", "habit graph: not due yet"), | |
| 117 | ("habit-ready", "habit graph: due"), | |
| 118 | ("habit-alert", "habit graph: due today, last chance"), | |
| 119 | ("habit-overdue", "habit graph: overdue"), | |
| 120 | ] | |
| 121 | ||
| 122 | /// Keys the default theme leaves to the system, so windows keep the macOS look. | |
| 123 | public static let systemDefaults: Set<String> = [ | |
| 124 | "sidebar-background", "sidebar-foreground", "sidebar-header", "modeline-background", "modeline-foreground", "agenda-background", | |
| 98 | 125 | ] |
| 99 | 126 | |
| 100 | 127 | // MARK: - The default theme |
| @@ -117,6 +144,10 @@ public struct ThemeSpec: Equatable, Sendable { | ||
| 117 | 144 | "line-number": "#8c959f", "line-number-current": "#1f2328", |
| 118 | 145 | "syntax-keyword": "#cf222e", "syntax-string": "#0a3069", "syntax-comment": "#6e7781", "syntax-function": "#8250df", |
| 119 | 146 | "syntax-type": "#953800", "syntax-number": "#0550ae", "syntax-property": "#0550ae", "syntax-label": "#8250df", |
| 147 | "modeline-highlight": "#1f2328", "state-normal": "#0969da", "state-insert": "#1a7f37", "state-visual": "#bc4c00", | |
| 148 | "agenda-date": "#1f2328", "agenda-today": "#0969da", "agenda-time": "#6e7781", "agenda-category": "#6e7781", | |
| 149 | "agenda-deadline": "#cf222e", "agenda-upcoming": "#bc4c00", "agenda-scheduled": "#6e7781", "agenda-scheduled-past": "#953800", | |
| 150 | "habit-clear": "#54aeff", "habit-ready": "#4ac26b", "habit-alert": "#d4a72c", "habit-overdue": "#ff8182", | |
| 120 | 151 | ]), |
| 121 | 152 | dark: palette([ |
| 122 | 153 | "background": "#0d1117", "foreground": "#e6edf3", "cursor": "#58a6ff", "selection": "#388bfd66", |
| @@ -129,6 +160,10 @@ public struct ThemeSpec: Equatable, Sendable { | ||
| 129 | 160 | "line-number": "#6e7681", "line-number-current": "#e6edf3", |
| 130 | 161 | "syntax-keyword": "#ff7b72", "syntax-string": "#a5d6ff", "syntax-comment": "#8b949e", "syntax-function": "#d2a8ff", |
| 131 | 162 | "syntax-type": "#ffa657", "syntax-number": "#79c0ff", "syntax-property": "#79c0ff", "syntax-label": "#d2a8ff", |
| 163 | "modeline-highlight": "#e6edf3", "state-normal": "#1f6feb", "state-insert": "#238636", "state-visual": "#9e6a03", | |
| 164 | "agenda-date": "#e6edf3", "agenda-today": "#58a6ff", "agenda-time": "#8b949e", "agenda-category": "#8b949e", | |
| 165 | "agenda-deadline": "#ff7b72", "agenda-upcoming": "#ffa657", "agenda-scheduled": "#8b949e", "agenda-scheduled-past": "#d29922", | |
| 166 | "habit-clear": "#1f6feb", "habit-ready": "#238636", "habit-alert": "#9e6a03", "habit-overdue": "#da3633", | |
| 132 | 167 | ]) |
| 133 | 168 | ) |
| 134 | 169 | |
| @@ -191,7 +226,11 @@ public struct ThemeSpec: Equatable, Sendable { | ||
| 191 | 226 | for (name, colors) in [("theme.light", ThemeSpec.default.light), ("theme.dark", ThemeSpec.default.dark)] { |
| 192 | 227 | out += "\n[\(name)]\n" |
| 193 | 228 | for slot in slots { |
| 194 | guard let color = colors[slot.key] else { continue } | |
| 229 | guard let color = colors[slot.key] else { | |
| 230 | let line = "# \(slot.key) = \"#rrggbb\"" | |
| 231 | out += line + String(repeating: " ", count: max(2, 36 - line.count)) + "# \(slot.description)\n" | |
| 232 | continue | |
| 233 | } | |
| 195 | 234 | let line = "\(slot.key) = \"\(color.hex)\"" |
| 196 | 235 | out += line + String(repeating: " ", count: max(2, 36 - line.count)) + "# \(slot.description)\n" |
| 197 | 236 | } |
Sources/Orgstar/AgendaView.swift +32 −22
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import OrgApp |
| 2 | 2 | import OrgCore |
| 3 | import OrgPresentation | |
| 3 | 4 | import SwiftUI |
| 4 | 5 | |
| 5 | 6 | /// The agenda window: the day/week agenda, the TODO list, tag matches and saved views. |
| @@ -74,6 +75,8 @@ struct AgendaView: View { | ||
| 74 | 75 | |
| 75 | 76 | private var isMatch: Bool { viewName == Self.match || viewName == Self.todoMatch } |
| 76 | 77 | |
| 78 | @Environment(\.orgTheme) private var theme | |
| 79 | ||
| 77 | 80 | var body: some View { |
| 78 | 81 | List(selection: $selection) { |
| 79 | 82 | if isAgenda { |
| @@ -83,7 +86,7 @@ struct AgendaView: View { | ||
| 83 | 86 | } header: { |
| 84 | 87 | Text(day.header) |
| 85 | 88 | .font(.headline) |
| 86 | .foregroundStyle(day.isToday ? Color.accentColor : .primary) | |
| 89 | .foregroundStyle(day.isToday ? theme.color("agenda-today") ?? .accentColor : theme.color("agenda-date") ?? .primary) | |
| 87 | 90 | } |
| 88 | 91 | } |
| 89 | 92 | } else { |
| @@ -94,6 +97,7 @@ struct AgendaView: View { | ||
| 94 | 97 | } |
| 95 | 98 | } |
| 96 | 99 | } |
| 100 | .themedListBackground(theme.color("agenda-background")) | |
| 97 | 101 | .contextMenu(forSelectionType: String.self) { _ in |
| 98 | 102 | } primaryAction: { ids in |
| 99 | 103 | if let id = ids.first { jump(id) } |
| @@ -318,6 +322,7 @@ struct AgendaRow: View { | ||
| 318 | 322 | let item: AgendaItem |
| 319 | 323 | /// The agenda day the row is on, for habit graphs. |
| 320 | 324 | var day: Int? |
| 325 | @Environment(\.orgTheme) private var theme | |
| 321 | 326 | |
| 322 | 327 | var body: some View { |
| 323 | 328 | switch item.kind { |
| @@ -329,25 +334,26 @@ struct AgendaRow: View { | ||
| 329 | 334 | VStack { Divider() } |
| 330 | 335 | } |
| 331 | 336 | .font(.caption) |
| 332 | .foregroundStyle(.tertiary) | |
| 337 | .foregroundStyle(theme.color("agenda-time").map(AnyShapeStyle.init) ?? AnyShapeStyle(.tertiary)) | |
| 333 | 338 | case .currentTime: |
| 334 | 339 | HStack(spacing: 8) { |
| 335 | 340 | Text("now " + Self.trim(item.time)) |
| 336 | 341 | .monospacedDigit() |
| 337 | 342 | .frame(width: 140, alignment: .trailing) |
| 338 | Rectangle().fill(Color.accentColor).frame(height: 1) | |
| 343 | Rectangle().fill(theme.color("agenda-today") ?? .accentColor).frame(height: 1) | |
| 339 | 344 | } |
| 340 | 345 | .font(.caption) |
| 341 | .foregroundStyle(Color.accentColor) | |
| 346 | .foregroundStyle(theme.color("agenda-today") ?? .accentColor) | |
| 342 | 347 | default: |
| 343 | 348 | HStack(spacing: 8) { |
| 344 | 349 | Text(item.category) |
| 345 | 350 | .lineLimit(1) |
| 346 | .foregroundStyle(.secondary) | |
| 351 | .foregroundStyle(theme.color("agenda-category") ?? .secondary) | |
| 347 | 352 | .frame(width: 80, alignment: .leading) |
| 348 | 353 | if item.kind != .todo, item.kind != .tagsMatch { |
| 349 | 354 | Text(Self.trim(item.time)) |
| 350 | 355 | .monospacedDigit() |
| 356 | .foregroundStyle(theme.color("agenda-time") ?? .primary) | |
| 351 | 357 | .frame(width: 80, alignment: .leading) |
| 352 | 358 | } |
| 353 | 359 | if !item.extra.isEmpty { |
| @@ -359,12 +365,12 @@ struct AgendaRow: View { | ||
| 359 | 365 | .lineLimit(1) |
| 360 | 366 | Spacer(minLength: 8) |
| 361 | 367 | if let habit = item.habit, let day { |
| 362 | HabitGraph(cells: habit.graph(today: day)) | |
| 368 | HabitGraph(cells: habit.graph(today: day), theme: theme) | |
| 363 | 369 | } |
| 364 | 370 | if !item.tags.isEmpty { |
| 365 | 371 | Text(":" + item.tags.joined(separator: ":") + ":") |
| 366 | 372 | .font(.caption) |
| 367 | .foregroundStyle(.secondary) | |
| 373 | .foregroundStyle(theme.color("tags") ?? .secondary) | |
| 368 | 374 | } |
| 369 | 375 | } |
| 370 | 376 | } |
| @@ -376,10 +382,10 @@ struct AgendaRow: View { | ||
| 376 | 382 | |
| 377 | 383 | private var leaderColor: Color { |
| 378 | 384 | switch item.kind { |
| 379 | case .deadline: .red | |
| 380 | case .upcomingDeadline: .orange | |
| 381 | case .pastScheduled: .brown | |
| 382 | default: .secondary | |
| 385 | case .deadline: theme.color("agenda-deadline") ?? .red | |
| 386 | case .upcomingDeadline: theme.color("agenda-upcoming") ?? .orange | |
| 387 | case .pastScheduled: theme.color("agenda-scheduled-past") ?? .brown | |
| 388 | default: theme.color("agenda-scheduled") ?? .secondary | |
| 383 | 389 | } |
| 384 | 390 | } |
| 385 | 391 | |
| @@ -388,13 +394,15 @@ struct AgendaRow: View { | ||
| 388 | 394 | let text = item.text.trimmingCharacters(in: .whitespaces) |
| 389 | 395 | guard let todo = item.todo, text.hasPrefix(todo + " ") else { return Text(text) } |
| 390 | 396 | let rest = String(text.dropFirst(todo.count)) |
| 391 | return Text(todo).foregroundColor(item.isDone ? .green : .red).bold() + Text(rest).foregroundColor(item.isDone ? .secondary : .primary) | |
| 397 | let rest_ = Text(rest).foregroundColor(item.isDone ? theme.color("comment") ?? .secondary : theme.color("foreground") ?? .primary) | |
| 398 | return Text(todo).foregroundColor(theme.keywordColor(todo, done: item.isDone)).bold() + rest_ | |
| 392 | 399 | } |
| 393 | 400 | } |
| 394 | 401 | |
| 395 | 402 | /// org-habit's consistency graph: a cell per day, `*` where the habit was done, `!` today. |
| 396 | 403 | struct HabitGraph: View { |
| 397 | 404 | let cells: [Habit.Cell] |
| 405 | var theme: ThemeSpec = .default | |
| 398 | 406 | |
| 399 | 407 | var body: some View { |
| 400 | 408 | HStack(spacing: 1) { |
| @@ -402,7 +410,7 @@ struct HabitGraph: View { | ||
| 402 | 410 | Text(String(cell.glyph)) |
| 403 | 411 | .font(.system(size: 9, weight: .bold, design: .monospaced)) |
| 404 | 412 | .frame(width: 8, height: 14) |
| 405 | .background(Self.color(cell.face)) | |
| 413 | .background(color(cell.face)) | |
| 406 | 414 | .help(Self.label(cell)) |
| 407 | 415 | } |
| 408 | 416 | } |
| @@ -410,16 +418,18 @@ struct HabitGraph: View { | ||
| 410 | 418 | .accessibilityLabel("Done \(cells.filter { $0.glyph == "*" }.count) of the last \(Habit.precedingDays) days") |
| 411 | 419 | } |
| 412 | 420 | |
| 413 | static func color(_ face: Habit.Face) -> Color { | |
| 421 | /// Due-state colors, lighter for days to come, as org-habit's faces. | |
| 422 | func color(_ face: Habit.Face) -> Color { | |
| 423 | func themed(_ key: String, _ fallback: Color) -> Color { theme.color(key) ?? fallback } | |
| 414 | 424 | switch face { |
| 415 | case .clear: .blue.opacity(0.6) | |
| 416 | case .clearFuture: .blue.opacity(0.3) | |
| 417 | case .ready: .green.opacity(0.7) | |
| 418 | case .readyFuture: .green.opacity(0.35) | |
| 419 | case .alert: .yellow.opacity(0.8) | |
| 420 | case .alertFuture: .yellow.opacity(0.4) | |
| 421 | case .overdue: .red.opacity(0.7) | |
| 422 | case .overdueFuture: .red.opacity(0.35) | |
| 425 | case .clear: return themed("habit-clear", .blue).opacity(0.7) | |
| 426 | case .clearFuture: return themed("habit-clear", .blue).opacity(0.35) | |
| 427 | case .ready: return themed("habit-ready", .green).opacity(0.75) | |
| 428 | case .readyFuture: return themed("habit-ready", .green).opacity(0.35) | |
| 429 | case .alert: return themed("habit-alert", .yellow).opacity(0.85) | |
| 430 | case .alertFuture: return themed("habit-alert", .yellow).opacity(0.4) | |
| 431 | case .overdue: return themed("habit-overdue", .red).opacity(0.75) | |
| 432 | case .overdueFuture: return themed("habit-overdue", .red).opacity(0.35) | |
| 423 | 433 | } |
| 424 | 434 | } |
| 425 | 435 | |
Sources/Orgstar/BoardView.swift +5 −2
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import OrgApp |
| 2 | 2 | import OrgCore |
| 3 | import OrgPresentation | |
| 3 | 4 | import SwiftUI |
| 4 | 5 | import UniformTypeIdentifiers |
| 5 | 6 | |
| @@ -22,6 +23,7 @@ struct BoardView: View { | ||
| 22 | 23 | @AppStorage("boardColumns") private var columnText = "EFFORT" |
| 23 | 24 | @AppStorage("agendaSubfolders") private var includeSubfolders = false |
| 24 | 25 | @Environment(\.openWindow) private var openWindow |
| 26 | @Environment(\.orgTheme) private var theme | |
| 25 | 27 | |
| 26 | 28 | init(workspace: WorkspaceModel, session: DocumentSession, commands: AppCommands) { |
| 27 | 29 | self.workspace = workspace |
| @@ -88,7 +90,7 @@ struct BoardView: View { | ||
| 88 | 90 | private var table: some View { |
| 89 | 91 | Table(rows, selection: $selection, sortOrder: $sortOrder) { |
| 90 | 92 | TableColumn("TODO", value: \.todoOrder) { row in |
| 91 | Text(row.entry.todo ?? "").foregroundStyle(row.entry.isDone ? .green : .red).bold() | |
| 93 | Text(row.entry.todo ?? "").foregroundStyle(theme.keywordColor(row.entry.todo ?? "", done: row.entry.isDone)).bold() | |
| 92 | 94 | } |
| 93 | 95 | .width(min: 50, ideal: 60, max: 90) |
| 94 | 96 | TableColumn("P", value: \.priority) { row in Text(row.priority) } |
| @@ -134,13 +136,14 @@ struct BoardView: View { | ||
| 134 | 136 | } |
| 135 | 137 | .padding(12) |
| 136 | 138 | } |
| 139 | .background(theme.color("agenda-background") ?? .clear) | |
| 137 | 140 | } |
| 138 | 141 | |
| 139 | 142 | private func column(_ keyword: String) -> some View { |
| 140 | 143 | let cards = entries.filter { $0.todo == keyword } |
| 141 | 144 | return VStack(alignment: .leading, spacing: 8) { |
| 142 | 145 | HStack { |
| 143 | Text(keyword).font(.headline) | |
| 146 | Text(keyword).font(.headline).foregroundStyle(theme.keywordColor(keyword, done: cards.first?.isDone ?? false)) | |
| 144 | 147 | Text("\(cards.count)").foregroundStyle(.secondary) |
| 145 | 148 | } |
| 146 | 149 | ScrollView { |
Sources/Orgstar/ContentView.swift +7 −1
| @@ -468,6 +468,7 @@ struct Sidebar: View { | ||
| 468 | 468 | @AppStorage("collapsedFolders") private var collapsedText = "" |
| 469 | 469 | @State private var removing: String? |
| 470 | 470 | /// A file operation waiting for a name or a confirmation. |
| 471 | @Environment(\.orgTheme) private var theme | |
| 471 | 472 | @State var pending: FileOperation? |
| 472 | 473 | @State var name = "" |
| 473 | 474 | @State var failure: String? |
| @@ -490,18 +491,20 @@ struct Sidebar: View { | ||
| 490 | 491 | Section(isExpanded: expanded(root)) { |
| 491 | 492 | OutlineGroup(workspace.tree(forRoot: root).children ?? [], children: \.children) { node in |
| 492 | 493 | Label(node.name, systemImage: icon(node)) |
| 493 | .foregroundStyle(node.file?.kind == .conflict ? .orange : .primary) | |
| 494 | .foregroundStyle(node.file?.kind == .conflict ? .orange : theme.color("sidebar-foreground") ?? .primary) | |
| 494 | 495 | .tag(node.file?.path) |
| 495 | 496 | .contextMenu { fileMenu(node) } |
| 496 | 497 | } |
| 497 | 498 | } header: { |
| 498 | 499 | Text((root as NSString).lastPathComponent) |
| 500 | .foregroundStyle(theme.color("sidebar-header") ?? .secondary) | |
| 499 | 501 | .help(root) |
| 500 | 502 | .contextMenu { folderMenu(root) } |
| 501 | 503 | } |
| 502 | 504 | } |
| 503 | 505 | } |
| 504 | 506 | .listStyle(.sidebar) |
| 507 | .themedListBackground(theme.color("sidebar-background")) | |
| 505 | 508 | .safeAreaInset(edge: .bottom, spacing: 0) { |
| 506 | 509 | HStack(spacing: 4) { |
| 507 | 510 | Button("Add Folder…", systemImage: "plus", action: addFolder) |
| @@ -569,6 +572,7 @@ struct Sidebar: View { | ||
| 569 | 572 | |
| 570 | 573 | struct OutlineColumn: View { |
| 571 | 574 | let session: DocumentSession |
| 575 | @Environment(\.orgTheme) private var theme | |
| 572 | 576 | |
| 573 | 577 | var body: some View { |
| 574 | 578 | List(Array(session.outline.enumerated()), id: \.element.start) { _, entry in |
| @@ -576,11 +580,13 @@ struct OutlineColumn: View { | ||
| 576 | 580 | session.reveal(entry.start) |
| 577 | 581 | } label: { |
| 578 | 582 | Text(entry.title.isEmpty ? "(untitled)" : entry.title) |
| 583 | .foregroundStyle(theme.color("sidebar-foreground") ?? .primary) | |
| 579 | 584 | .lineLimit(1) |
| 580 | 585 | .padding(.leading, CGFloat(entry.level - 1) * 12) |
| 581 | 586 | } |
| 582 | 587 | .buttonStyle(.plain) |
| 583 | 588 | } |
| 589 | .themedListBackground(theme.color("sidebar-background")) | |
| 584 | 590 | .overlay { |
| 585 | 591 | if session.path != nil, !session.isOrg { |
| 586 | 592 | ContentUnavailableView("No outline", systemImage: "doc.plaintext", description: Text("Only org files have an outline.")) |
Sources/Orgstar/Keys.swift +7 −4
| @@ -12,6 +12,7 @@ struct KeyHint: Hashable { | ||
| 12 | 12 | /// short pause on a prefix, the keys that can follow it. |
| 13 | 13 | struct EchoArea: View { |
| 14 | 14 | let commands: AppCommands |
| 15 | @Environment(\.orgTheme) private var theme | |
| 15 | 16 | @State private var showingHints = false |
| 16 | 17 | @State private var answer = "" |
| 17 | 18 | @FocusState private var answerFocused: Bool |
| @@ -80,7 +81,8 @@ struct EchoArea: View { | ||
| 80 | 81 | .frame(maxWidth: .infinity, alignment: .leading) |
| 81 | 82 | } |
| 82 | 83 | } |
| 83 | .background(.bar) | |
| 84 | .foregroundStyle(theme.color("modeline-foreground") ?? .primary) | |
| 85 | .background { if let bg = theme.color("modeline-background") { bg } else { Rectangle().fill(.bar) } } | |
| 84 | 86 | .task(id: commands.pendingKeys) { |
| 85 | 87 | showingHints = false |
| 86 | 88 | guard commands.pendingKeys != nil else { return } |
| @@ -93,6 +95,7 @@ struct EchoArea: View { | ||
| 93 | 95 | /// The evil state, as Doom's modeline shows it. |
| 94 | 96 | struct StateTag: View { |
| 95 | 97 | let state: String |
| 98 | @Environment(\.orgTheme) private var theme | |
| 96 | 99 | |
| 97 | 100 | var body: some View { |
| 98 | 101 | Text(label) |
| @@ -113,9 +116,9 @@ struct StateTag: View { | ||
| 113 | 116 | |
| 114 | 117 | private var color: Color { |
| 115 | 118 | switch state { |
| 116 | case "insert": .green | |
| 117 | case "visual", "visual-line": .orange | |
| 118 | default: .blue | |
| 119 | case "insert": theme.color("state-insert") ?? .green | |
| 120 | case "visual", "visual-line": theme.color("state-visual") ?? .orange | |
| 121 | default: theme.color("state-normal") ?? .blue | |
| 119 | 122 | } |
| 120 | 123 | } |
| 121 | 124 | } |
Sources/Orgstar/ModeLine.swift +5 −4
| @@ -12,6 +12,7 @@ struct ModeLine: View { | ||
| 12 | 12 | let clock: ClockModel |
| 13 | 13 | /// Runs an app command (clock out, go to the clocked entry). |
| 14 | 14 | let perform: (String) -> Void |
| 15 | @Environment(\.orgTheme) private var theme | |
| 15 | 16 | @State private var words: Int? |
| 16 | 17 | @State private var crlf = false |
| 17 | 18 | @State private var branch: String? |
| @@ -44,7 +45,7 @@ struct ModeLine: View { | ||
| 44 | 45 | .menuStyle(.borderlessButton) |
| 45 | 46 | .menuIndicator(.hidden) |
| 46 | 47 | .fixedSize() |
| 47 | .foregroundStyle(.primary) | |
| 48 | .foregroundStyle(theme.color("modeline-highlight") ?? .primary) | |
| 48 | 49 | .help("Clocked in since \(running.startedAt.formatted(date: .omitted, time: .shortened))") |
| 49 | 50 | } |
| 50 | 51 | if selection.length > 0, let region = region(text, selection) { |
| @@ -70,10 +71,10 @@ struct ModeLine: View { | ||
| 70 | 71 | .font(.system(.caption, design: .monospaced)) |
| 71 | 72 | .monospacedDigit() |
| 72 | 73 | .lineLimit(1) |
| 73 | .foregroundStyle(.secondary) | |
| 74 | .foregroundStyle(theme.color("modeline-foreground") ?? .secondary) | |
| 74 | 75 | .padding(.horizontal, 10) |
| 75 | 76 | .frame(height: 22) |
| 76 | .background(.bar) | |
| 77 | .background { if let bg = theme.color("modeline-background") { bg } else { Rectangle().fill(.bar) } } | |
| 77 | 78 | .overlay(alignment: .top) { Divider() } |
| 78 | 79 | .task(id: session.path) { |
| 79 | 80 | // HEAD changes outside the app, so look again every few seconds. |
| @@ -104,7 +105,7 @@ struct ModeLine: View { | ||
| 104 | 105 | if !path.isEmpty { |
| 105 | 106 | Text(path.joined(separator: " › ")) |
| 106 | 107 | .truncationMode(.head) |
| 107 | .foregroundStyle(.primary) | |
| 108 | .foregroundStyle(theme.color("modeline-highlight") ?? .primary) | |
| 108 | 109 | .help(path.joined(separator: " › ")) |
| 109 | 110 | } |
| 110 | 111 | } |
Sources/Orgstar/OrgstarApp.swift +3
| @@ -161,6 +161,7 @@ struct OrgstarApp: App { | ||
| 161 | 161 | switch workspace { |
| 162 | 162 | case .success(let model): |
| 163 | 163 | ContentView(workspace: model, session: session, commands: commands, scheduler: scheduler, clock: clock) |
| 164 | .environment(\.orgTheme, ConfigSync.shared.theme) | |
| 164 | 165 | .onAppear { |
| 165 | 166 | delegate.session = session |
| 166 | 167 | delegate.commands = commands |
| @@ -173,12 +174,14 @@ struct OrgstarApp: App { | ||
| 173 | 174 | Window("Agenda", id: "agenda") { |
| 174 | 175 | if case .success(let model) = workspace { |
| 175 | 176 | AgendaView(workspace: model, session: session, commands: commands, scheduler: scheduler) |
| 177 | .environment(\.orgTheme, ConfigSync.shared.theme) | |
| 176 | 178 | } |
| 177 | 179 | } |
| 178 | 180 | .defaultSize(width: 760, height: 720) |
| 179 | 181 | Window("Board", id: "board") { |
| 180 | 182 | if case .success(let model) = workspace { |
| 181 | 183 | BoardView(workspace: model, session: session, commands: commands) |
| 184 | .environment(\.orgTheme, ConfigSync.shared.theme) | |
| 182 | 185 | } |
| 183 | 186 | } |
| 184 | 187 | .defaultSize(width: 1100, height: 700) |
Sources/Orgstar/ThemeColors.swift added +37
| @@ -0,0 +1,37 @@ | ||
| 1 | import AppKit | |
| 2 | import OrgPresentation | |
| 3 | import SwiftUI | |
| 4 | ||
| 5 | extension EnvironmentValues { | |
| 6 | /// The theme's colors, for the views around the editor. | |
| 7 | @Entry var orgTheme: ThemeSpec = .default | |
| 8 | } | |
| 9 | ||
| 10 | extension ThemeSpec { | |
| 11 | /// A color that follows the appearance, or nil when the theme leaves `key` to the system. | |
| 12 | func color(_ key: String) -> Color? { | |
| 13 | guard let light = self.light[key] ?? self.dark[key], let dark = self.dark[key] ?? self.light[key] else { return nil } | |
| 14 | return Color(nsColor: NSColor(name: nil) { appearance in | |
| 15 | let c = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua ? dark : light | |
| 16 | return NSColor(srgbRed: c.red, green: c.green, blue: c.blue, alpha: c.alpha) | |
| 17 | }) | |
| 18 | } | |
| 19 | ||
| 20 | /// A TODO keyword's color: its own from `[theme.todo]`, else the theme's TODO or DONE. | |
| 21 | func keywordColor(_ keyword: String, done: Bool) -> Color { | |
| 22 | if let c = todo[keyword] { return Color(.sRGB, red: c.red, green: c.green, blue: c.blue, opacity: c.alpha) } | |
| 23 | return color(done ? "done" : "todo") ?? (done ? .green : .red) | |
| 24 | } | |
| 25 | } | |
| 26 | ||
| 27 | extension View { | |
| 28 | /// A list's background from the theme, or the system's when the theme has none. | |
| 29 | @ViewBuilder | |
| 30 | func themedListBackground(_ color: Color?) -> some View { | |
| 31 | if let color { | |
| 32 | scrollContentBackground(.hidden).background(color) | |
| 33 | } else { | |
| 34 | self | |
| 35 | } | |
| 36 | } | |
| 37 | } | |
Tests/OrgPresentationTests/PresentationTests.swift +3 −2
| @@ -156,7 +156,8 @@ struct ThemeSpecTests { | ||
| 156 | 156 | let (spec, problems) = ThemeSpec.reading(try TOML.parse(ThemeSpec.defaultThemeFile()), over: ThemeSpec(light: [:], dark: [:])) |
| 157 | 157 | #expect(problems.isEmpty) |
| 158 | 158 | #expect(spec.light == ThemeSpec.default.light && spec.dark == ThemeSpec.default.dark) |
| 159 | #expect(Set(ThemeSpec.default.light.keys) == Set(ThemeSpec.slots.map(\.key))) | |
| 160 | #expect(Set(ThemeSpec.default.dark.keys) == Set(ThemeSpec.slots.map(\.key))) | |
| 159 | let defaulted = Set(ThemeSpec.slots.map(\.key)).subtracting(ThemeSpec.systemDefaults) | |
| 160 | #expect(Set(ThemeSpec.default.light.keys) == defaulted) | |
| 161 | #expect(Set(ThemeSpec.default.dark.keys) == defaulted) | |
| 161 | 162 | } |
| 162 | 163 | } |