krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Commit 3a3dedba06

3a3dedba062790e1580c66d38606eda4b02cb501

parent: 29fd24ba30

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-09 05:35 UTC

Agenda: move the now line each minute

Both apps redraw on each minute boundary and refresh when the day changes, so the now line and today's statuses stay current. The Mac no longer rereads the agenda files every minute.

Layout: unified · split

Sources/Orgstar/AgendaView.swift +11 −7
@@ -38,6 +38,8 @@ struct AgendaView: View {
3838 /// The selected line: `day:index` (`day:o` + index in the overdue group), or `list:index`.
3939 @State private var selection: String?
4040 @State private var tick = 0
41 /// The time the now line shows, kept to the minute while the agenda is open.
42 @State private var now = Date()
4143 /// The view picker moved to the agenda for a followed timestamp, which sets its own days.
4244 @State private var keepRequestedDays = false
4345 @State private var message: String?
@@ -295,10 +297,12 @@ struct AgendaView: View {
295297 }
296298 .task {
297299 loadViews()
298 // The time grid's "now" line and the day itself move on.
300 // The now line moves each minute; a new day refreshes the statuses.
299301 while !Task.isCancelled {
300 try? await Task.sleep(for: .seconds(60))
301 tick += 1
302 let next = Calendar.current.nextDate(after: Date(), matching: DateComponents(second: 0), matchingPolicy: .nextTime) ?? Date().addingTimeInterval(60)
303 try? await Task.sleep(for: .seconds(next.timeIntervalSinceNow))
304 now = Date()
305 if Days.absolute(now, calendar: .current) != agenda.today { tick += 1 }
302306 }
303307 }
304308 }
@@ -325,12 +329,12 @@ struct AgendaView: View {
325329 .padding(.horizontal, 16)
326330 .padding(.vertical, 10)
327331 }
328 let now = group.nowIndex(now: Self.hhmm(Date()))
332 let nowAt = group.nowIndex(now: Self.hhmm(now))
329333 ForEach(Array(group.rows.enumerated()), id: \.element.id) { index, row in
330 if index == now { nowLine }
334 if index == nowAt { nowLine }
331335 rowView(Line(id: "\(group.day):\(index)", row: row, day: group.day))
332336 }
333 if let now, now == group.rows.count { nowLine }
337 if let nowAt, nowAt == group.rows.count { nowLine }
334338 }
335339
336340 private func header(@ViewBuilder _ content: () -> some View) -> some View {
@@ -400,7 +404,7 @@ struct AgendaView: View {
400404
401405 private var nowLine: some View {
402406 HStack(spacing: 6) {
403 Text(Date().formatted(.dateTime.hour(.twoDigits(amPM: .omitted)).minute()))
407 Text(now.formatted(.dateTime.hour(.twoDigits(amPM: .omitted)).minute()))
404408 .font(.system(size: 10, weight: .semibold).monospacedDigit())
405409 .frame(width: AgendaRowView.gutterWidth, alignment: .leading)
406410 Circle().frame(width: 6, height: 6)
Sources/OrgstarMobile/AgendaScreen.swift +17 −5
@@ -18,6 +18,8 @@ struct AgendaScreen: View {
1818 @Environment(MobileModel.self) private var model
1919 @Environment(\.colorScheme) private var colorScheme
2020 @State private var tick = 0
21 /// The time the now line shows, kept to the minute while the agenda is open.
22 @State private var now = Date()
2123 @State private var views = AgendaViewLoader.builtIn
2224 @State private var viewName = "Agenda"
2325 @State private var filter = AgendaFilter()
@@ -173,6 +175,15 @@ struct AgendaScreen: View {
173175 views = loaded.views
174176 if let problem = loaded.problems.first { message = problem }
175177 }
178 // The now line moves each minute; a new day refreshes the statuses.
179 .task {
180 while !Task.isCancelled {
181 let next = Calendar.current.nextDate(after: Date(), matching: DateComponents(second: 0), matchingPolicy: .nextTime) ?? Date().addingTimeInterval(60)
182 try? await Task.sleep(for: .seconds(next.timeIntervalSinceNow))
183 now = Date()
184 if Days.absolute(now, calendar: .current) != agenda.today { tick += 1 }
185 }
186 }
176187 // Calendar access is asked for when calendar-events is turned on.
177188 .task(id: model.settingsVersion) {
178189 guard OrgPreferences.calendarEvents(), !CalendarEvents.isAuthorized else { return }
@@ -232,17 +243,17 @@ struct AgendaScreen: View {
232243 if group.isEmpty {
233244 Text("Nothing scheduled").font(.subheadline).foregroundStyle(.secondary)
234245 }
235 let now = nowIndex(group)
246 let nowAt = nowIndex(group)
236247 ForEach(Array(group.rows.enumerated()), id: \.element.id) { index, item in
237 if index == now { NowLine(color: todayColor) }
248 if index == nowAt { NowLine(time: now, color: todayColor) }
238249 row(item, day: group.day)
239250 }
240 if let now, now == group.rows.count { NowLine(color: todayColor) }
251 if let nowAt, nowAt == group.rows.count { NowLine(time: now, color: todayColor) }
241252 }
242253 }
243254
244255 private func nowIndex(_ group: AgendaDayGroup) -> Int? {
245 let c = Calendar.current.dateComponents([.hour, .minute], from: Date())
256 let c = Calendar.current.dateComponents([.hour, .minute], from: now)
246257 return group.nowIndex(now: (c.hour ?? 0) * 100 + (c.minute ?? 0))
247258 }
248259
@@ -672,11 +683,12 @@ struct AgendaRowView: View {
672683
673684/// The current time between timed rows, as Org's `now - - -` line.
674685struct NowLine: View {
686 let time: Date
675687 let color: Color
676688
677689 var body: some View {
678690 HStack(spacing: 6) {
679 Text(Date.now.formatted(date: .omitted, time: .shortened))
691 Text(time.formatted(date: .omitted, time: .shortened))
680692 .font(.caption2.weight(.semibold).monospacedDigit())
681693 .foregroundStyle(color)
682694 Circle().fill(color).frame(width: 6, height: 6)