From 041cc282a9bf50349e928b96c0c6debe86419ab8 Mon Sep 17 00:00:00 2001 From: prompt.ac/@jeffrey Date: Fri, 17 Jul 2026 23:52:11 +0000 Subject: [PATCH] date-wizard: focus the menu on upcoming days --- date-wizard/README.md | 18 +++++++++--------- date-wizard/Sources/DateWizard/AppDelegate.swift | 17 ----------------- date-wizard/Sources/DateWizard/MenuBarDays.swift | 8 +++----- date-wizard/Sources/DateWizard/WizardController.swift | 130 +++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------------------------- 4 file(s) changed, 49 insertion(s)(+), 124 deletion(s)(-) diff --git a/date-wizard/README.md b/date-wizard/README.md --- a/date-wizard/README.md +++ b/date-wizard/README.md @@ -1,6 +1,6 @@ # DateWizard -A native macOS AppKit pop-up that shows your **week** and lets you schedule +A native macOS AppKit pop-up that shows your upcoming **appointments agenda** and lets you schedule against the [AesthetiCal](https://aesthetic.computer) backend. Your editable *aesthetical* events sit next to your Google calendar(s), overlaid read-only. @@ -46,17 +46,16 @@ `access_token` is sent as `Authorization: Bearer …` on every `/api/cal` call. The token is treated as expired when the file is missing, unparseable, or `expires_at` is in the past — at which point you're prompted to sign in again. -## Using the week +## Using the agenda -- **‹ / ›** page weeks; **Today** jumps back to the current week. -- **Refresh** reloads both editable + connected-calendar events. -- **+ Event** (or clicking an empty slot) opens the editor sheet. +- The main window is one chronological, scrollable list covering the next 14 days. +- **+ Event** opens the editor sheet. - **Aesthetical events** draw with a solid fill (green = public, accent = private) and are **editable** — click one to edit or delete it. - **Google / connected-calendar events** draw dashed with a source-color dot and are **read-only** — clicking shows details, no edit. -- A red **now** line marks the current time in today's column. -- A small **legend** along the bottom maps fill colors to sources. +- Former week/day drill-down views are retired; Today and legacy day launch + actions open the same reliable upcoming agenda. ## Connect a Google calendar @@ -93,8 +92,9 @@ ``` Sources/DateWizard/ main.swift NSApplication bootstrap AppDelegate.swift window + WizardController; ensure auth then load week - WizardController.swift owns state (week, events, token); paging/refresh/editor/sign-in screen - WeekView.swift custom NSView: 7-column Sun→Sat grid, hour rows, now line, legend + WizardController.swift owns state (agenda, events, token); refresh/editor/sign-in screen + AgendaView.swift chronological, scrollable upcoming appointments list + WeekView.swift deprecated grid implementation retained for shared event types EventEditor.swift native sheet: Title/Start/End/Note/privacy + Add/Save/Delete CalAPI.swift URLSession networking + Codable models Auth.swift reads the shared AC session token (~/.ac-token) from ac-login diff --git a/date-wizard/Sources/DateWizard/AppDelegate.swift b/date-wizard/Sources/DateWizard/AppDelegate.swift --- a/date-wizard/Sources/DateWizard/AppDelegate.swift +++ b/date-wizard/Sources/DateWizard/AppDelegate.swift @@ -35,7 +35,6 @@ NSApp.setActivationPolicy(.regular) controller?.toggleUpcoming() } menu.onToday = { [weak controller] in controller?.revealToday() } - menu.onSelectDay = { [weak controller] date in controller?.revealDay(date) } menu.install() menuBar = menu @@ -43,22 +42,6 @@ // Keep the menu bar strip's lit dot in sync with the wizard's focus. controller.onFocusedDayChanged = { [weak menu] date in menu?.setFocusedDay(date) } // Feed the next appointment to the wand's countdown badge. controller.onNextEventChanged = { [weak menu] date, title in menu?.setNextEvent(date, title: title) } - - // Launch flags: open straight into a single-day view. Applied BEFORE - // start() so the initial load fetches the day (not the week first). - // --tomorrow tomorrow - // --today today - // --day=<+/-N> N days from today (e.g. --day=2) - let args = CommandLine.arguments - let cal = Calendar.current - if args.contains("--tomorrow") { - controller.setDayMode(cal.date(byAdding: .day, value: 1, to: Date()) ?? Date()) - } else if args.contains("--today") { - controller.setDayMode(Date()) - } else if let dayArg = args.first(where: { $0.hasPrefix("--day=") }), - let n = Int(dayArg.dropFirst("--day=".count)) { - controller.setDayMode(cal.date(byAdding: .day, value: n, to: Date()) ?? Date()) - } // Ensure auth (sign-in screen if no shared ~/.ac-token), then load. controller.start() diff --git a/date-wizard/Sources/DateWizard/MenuBarDays.swift b/date-wizard/Sources/DateWizard/MenuBarDays.swift --- a/date-wizard/Sources/DateWizard/MenuBarDays.swift +++ b/date-wizard/Sources/DateWizard/MenuBarDays.swift @@ -1,10 +1,9 @@ // MenuBarDays.swift — DateWizard's always-on menu bar presence. // // A strip of seven circles (Sun→Sat: S M T W T F S) painted in the shared -// ROYGBIV DayPalette (see DayStrip for the drawing). The focused day — the -// day showing in the wizard — is lit and ringed; today carries a faint -// marker; the rest dim back. Hovering a dot lights it; left-clicking a dot -// opens that day; right-click drops a menu. +// ROYGBIV DayPalette (see DayStrip for the drawing). The current production +// face is the wand + next-appointment countdown; left-click opens the agenda +// and right-click drops the wizard menu. import AppKit // NSResponder shim that forwards tracking-area mouse events to closures @@ -24,7 +23,6 @@ // Callbacks wired by the AppDelegate. var onOpen: (() -> Void)? var onToggle: (() -> Void)? var onToday: (() -> Void)? - var onSelectDay: ((Date) -> Void)? private var statusItem: NSStatusItem! private weak var button: NSStatusBarButton? diff --git a/date-wizard/Sources/DateWizard/WizardController.swift b/date-wizard/Sources/DateWizard/WizardController.swift --- a/date-wizard/Sources/DateWizard/WizardController.swift +++ b/date-wizard/Sources/DateWizard/WizardController.swift @@ -1,7 +1,7 @@ // WizardController.swift — owns the window + all state (focused week, the // events, the auth token). Drives: // • the sign-in screen (when there's no shared AC token / on a 401), -// • the week view (paging, refresh), +// • the rolling upcoming agenda, // • the add/edit/delete editor sheet for aesthetical events, // • a "Connect a Google calendar" action. import AppKit @@ -45,8 +45,7 @@ // missing/garbled end time can't keep an appointment alive in the agenda (and // masking the ones behind it) indefinitely. static let maxTimedDuration: TimeInterval = 12 * 3600 - // Broadcast the focused day (nil in week mode) so the menu bar strip can - // light the same dot. Wired by the AppDelegate. + // Legacy day-focus broadcast. The agenda-only UI always publishes nil. var onFocusedDayChanged: ((Date?) -> Void)? // Broadcast the start of the next upcoming appointment (nil = none ahead) so @@ -55,10 +54,9 @@ var onNextEventChanged: ((Date?, String?) -> Void)? // ── UI ─────────────────────────────────────────────────────────── private var backdrop: BackdropView! - // Inner radial glow framing the window in the focused day's color (only - // in single-day mode). A subtle edge highlight, not a full wash. + // Legacy day-view surfaces. They remain typed here while the old grid code + // is retired, but are no longer installed in the window hierarchy. private var dayGlow: DayGlowView! - // In-window twin of the menu bar strip: seven ROYGBIV day dots. private var dayDots: DayDotsView! private var weekView: WeekView! // The ordered "no day selected" agenda list (rolling upcoming events), @@ -68,8 +66,6 @@ private var agendaView: AgendaView! private var titleLabel: NSTextField! private var statusLabel: NSTextField! private var addButton: NSButton! // "+ Event" — the only toolbar button. - // "‹ Upcoming" — jumps back from a single day to the agenda list. Hidden - // while the agenda is already showing. private var upcomingButton: NSButton! // Vestigial: there's no mode-toggle button in the window anymore (mode is // driven by launch flags / CLI). Live code still harmlessly sets its title. @@ -90,7 +86,8 @@ private var refreshButton: NSButton! @available(*, deprecated, message: "Removed from window UI — calendar setup lives in the CLI.") private var connectButton: NSButton! - // Display span: 7 = week (Sun→Sat), 1 = a single day. + // Kept at seven while the old day-grid implementation is retired. The + // visible surface is always the rolling agenda. private var dayCount: Int = 7 // Auth overlay (shown when unauthenticated). @@ -195,7 +192,7 @@ let pad: CGFloat = 12 let topH: CGFloat = 40 - // Toolbar row (top): title · day-dots · + Event. Week/day paging, + // Toolbar row (top): title · + Event. Week/day paging, // refresh, "today", and calendar management all live in the // ac-login / wizard CLI — the window stays deliberately minimal. titleLabel = NSTextField(labelWithString: weekRangeTitle()) @@ -204,14 +201,6 @@ titleLabel.frame = NSRect(x: pad, y: 560 - topH + 4, width: 245, height: 28) titleLabel.autoresizingMask = [.maxXMargin, .minYMargin] cv.addSubview(titleLabel) - // Day-dots, to the right of the title — the in-window twin of the menu - // bar strip. Click a dot to jump to that day of the week. - dayDots = DayDotsView(frame: NSRect(x: pad + 252, y: 560 - topH + 7, - width: 190, height: 26)) - dayDots.autoresizingMask = [.minYMargin] - dayDots.onSelect = { [weak self] idx in self?.focusDayInCurrentWeek(idx) } - cv.addSubview(dayDots) - // + Event, far right. addButton = NSButton(title: "+ Event", target: self, action: #selector(addEventAction)) addButton.bezelStyle = .rounded @@ -220,31 +209,12 @@ addButton.autoresizingMask = [.minXMargin, .minYMargin] addButton.frame = NSRect(x: 840 - pad - 78, y: 560 - topH + 6, width: 78, height: 26) cv.addSubview(addButton) - // "‹ Upcoming" — return to the agenda list from a single-day view. Sits - // just left of + Event; hidden while the agenda is already showing. - upcomingButton = NSButton(title: "‹ Upcoming", target: self, action: #selector(showUpcomingAction)) - upcomingButton.bezelStyle = .rounded - upcomingButton.font = .systemFont(ofSize: 12) - upcomingButton.autoresizingMask = [.minXMargin, .minYMargin] - upcomingButton.frame = NSRect(x: 840 - pad - 78 - 8 - 96, y: 560 - topH + 6, width: 96, height: 26) - upcomingButton.isHidden = true - cv.addSubview(upcomingButton) - let bodyRect = NSRect(x: pad, y: pad + 22, width: 840 - pad * 2, height: 560 - topH - pad * 2 - 22) - // Week/day grid (used for the single-day view). - weekView = WeekView(frame: bodyRect) - weekView.autoresizingMask = [.width, .height] - weekView.weekStart = weekStart - weekView.dayCount = dayCount - weekView.delegate = self - weekView.isHidden = (dayCount != 1) - cv.addSubview(weekView) - - // Agenda list (the "no day selected" face), in a scroll view over the - // same body rect. Transparent so the living backdrop shows through. + // The sole calendar surface: a rolling appointments agenda. Transparent + // so the living backdrop shows through. agendaScroll = NSScrollView(frame: bodyRect) agendaScroll.autoresizingMask = [.width, .height] agendaScroll.drawsBackground = false @@ -256,7 +226,7 @@ agendaView = AgendaView(frame: NSRect(x: 0, y: 0, width: bodyRect.width, height: bodyRect.height)) agendaView.delegate = self agendaScroll.documentView = agendaView - agendaScroll.isHidden = (dayCount == 1) + agendaScroll.isHidden = false cv.addSubview(agendaScroll) // Status line (bottom). @@ -268,40 +238,20 @@ statusLabel.autoresizingMask = [.width, .maxYMargin] statusLabel.lineBreakMode = .byTruncatingTail cv.addSubview(statusLabel) - // Inner radial glow, added last so it frames everything (click-through). - dayGlow = DayGlowView(frame: cv.bounds) - dayGlow.autoresizingMask = [.width, .height] - cv.addSubview(dayGlow) - updateDayTheme() } // Single source of truth for the day-color theme: washes the pane, lights - // the in-window dots, colors the title, and tells the menu bar which day - // is focused. In week mode there's no single focus (the week shows all - // seven), so the wash clears and the title returns to neutral. + // Agenda-only theme: no day is focused, so legacy day accents stay clear. private func updateDayTheme() { - let dayMode = (dayCount == 1) - let dayColor = DayPalette.color(for: weekStart) - - // Frame the window in the day color (edge glow), not a full wash. - dayGlow?.color = dayMode ? dayColor : nil - - // Only mark today while a single day is in focus. On the "Upcoming" - // agenda nothing is selected, and DayStrip promotes today to the lit - // "selected" look when there's no selection — which read as a stray - // highlighted day (e.g. "F") on a screen that isn't about any one day. - dayDots?.todayIndex = dayMode ? DayPalette.index(for: Date()) : nil - dayDots?.selectedIndex = dayMode ? DayPalette.index(for: weekStart) : nil - - titleLabel?.textColor = dayMode ? dayColor : .labelColor - - // Grid for a single day, agenda list for "no day selected". - weekView?.isHidden = !dayMode - agendaScroll?.isHidden = dayMode - upcomingButton?.isHidden = !dayMode - - onFocusedDayChanged?(dayMode ? weekStart : nil) + dayGlow?.color = nil + dayDots?.todayIndex = nil + dayDots?.selectedIndex = nil + titleLabel?.textColor = .labelColor + weekView?.isHidden = true + agendaScroll?.isHidden = false + upcomingButton?.isHidden = true + onFocusedDayChanged?(nil) } // A dot in the in-window strip was clicked: focus that weekday within the @@ -339,19 +289,19 @@ // No day selected → the rolling agenda. return "Upcoming" } - // Show the window and jump to today (menu bar "Go to Today"). + // "Today" now opens the same rolling agenda; there is no secondary day UI. func revealToday() { showWindow(nil) NSApp.activate(ignoringOtherApps: true) - goToday() + showUpcomingAction() } - // Show the window focused on a single day (menu bar day-circle tap). + // Compatibility entry point for older callers: day requests land on the + // agenda rather than navigating to the retired hour grid. func revealDay(_ date: Date) { showWindow(nil) NSApp.activate(ignoringOtherApps: true) - setDayMode(date) - refresh() + showUpcomingAction() } // Show the window on the rolling "Upcoming" agenda with no day focused — @@ -383,13 +333,7 @@ @objc private func prevWeek() { shiftWeek(by: -dayCount) } @available(*, deprecated, message: "Removed from window UI — paging lives in the CLI.") @objc private func nextWeek() { shiftWeek(by: dayCount) } @objc private func goToday() { - weekStart = (dayCount == 1) - ? Calendar.current.startOfDay(for: Date()) - : WeekView.startOfWeek(for: Date()) - weekView.weekStart = weekStart - titleLabel.stringValue = weekRangeTitle() - updateDayTheme() - refresh() + showUpcomingAction() } // Leave a single-day view and return to the rolling agenda ("no day @@ -398,8 +342,8 @@ @objc private func showUpcomingAction() { dayCount = 7 weekStart = WeekView.startOfWeek(for: Date()) modeButton?.title = "Day" - weekView.dayCount = dayCount - weekView.weekStart = weekStart + weekView?.dayCount = dayCount + weekView?.weekStart = weekStart titleLabel.stringValue = weekRangeTitle() updateDayTheme() refresh() @@ -431,15 +375,12 @@ updateDayTheme() refresh() } - // Pre-set a single-day view BEFORE start() (used by the --tomorrow / --day - // launch flag). Does NOT refresh — start() does the single initial load, so - // we don't race a stale week fetch against the day fetch. + // Compatibility shim for old --today/--tomorrow/--day launchers. Day mode + // is retired, so all of them now open the rolling agenda. + @available(*, deprecated, message: "DateWizard is agenda-only; day mode is retired.") func setDayMode(_ date: Date) { - dayCount = 1 - weekStart = Calendar.current.startOfDay(for: date) - modeButton?.title = "Week" - weekView?.dayCount = dayCount - weekView?.weekStart = weekStart + dayCount = 7 + weekStart = WeekView.startOfWeek(for: Date()) titleLabel?.stringValue = weekRangeTitle() updateDayTheme() } @@ -474,7 +415,6 @@ func refresh() { guard let token = auth.currentToken() else { showAuthScreen(); return } api.setToken(token) loadUpcoming() - if dayCount == 1 { loadDayGrid() } } // Rolling window [start of today, +upcomingDays). Feeds the agenda list and @@ -927,6 +867,10 @@ note: event.note, visibility: event.visibility) } func weekView(_ view: WeekView, didSelectReadOnly event: LaidEvent) { + presentReadOnlyDetails(event) + } + + private func presentReadOnlyDetails(_ event: LaidEvent) { // Read-only details — no edit. let alert = NSAlert() alert.messageText = event.title @@ -950,7 +894,7 @@ note: event.note, visibility: event.visibility) } func agendaView(_ view: AgendaView, didSelectReadOnly event: LaidEvent) { - weekView(weekView, didSelectReadOnly: event) + presentReadOnlyDetails(event) } func weekView(_ view: WeekView, didRequestNewEventAt day: Date, hour: Int) { -- tangled.sh