krz/orgstar

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

Commit 640a8729ac

640a8729ac6b979b4b037396c5f7a65a361e3d43

parent: f02651ad8e

Verified · cmc

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

Settings: Calendar events and empty days in the agenda

Agenda settings gain "Show days without entries"
(org-agenda-show-all-dates), "Show events from Calendar" and a calendar
chooser (calendar-events, calendar-event-calendars).

Layout: unified · split

Sources/Orgstar/OrgstarApp.swift +50 −1
@@ -2,6 +2,7 @@ import AppKit
22import OrgApp
33import OrgCore
44import OrgEditorAppKit
5import OrgPresentation
56import SwiftUI
67import UserNotifications
78
@@ -519,6 +520,10 @@ struct SettingsView: View {
519520 @AppStorage("agendaSpan") private var agendaSpan = 10
520521 @AppStorage("agendaStartOffset") private var agendaStartOffset = -3
521522 @AppStorage("agendaSubfolders") private var agendaSubfolders = false
523 @AppStorage("agendaShowAllDates") private var agendaShowAllDates = false
524 @AppStorage("calendarEvents") private var calendarEvents = false
525 @AppStorage("calendarEventCalendars") private var calendarEventCalendars = ""
526 @State private var calendars: [(id: String, title: String, color: RGBA?)] = []
522527 @AppStorage("reminders") private var reminders = true
523528 @AppStorage("reminderLead") private var reminderLead = 12
524529 @AppStorage("clockIdleTime") private var clockIdleTime = 0
@@ -675,6 +680,33 @@ struct SettingsView: View {
675680 Stepper("Agenda shows \(agendaSpan) days", value: $agendaSpan, in: 1...366)
676681 Stepper(agendaStartOffset == 0 ? "Agenda starts today" : "Agenda starts \(-agendaStartOffset) days before today", value: $agendaStartOffset, in: -14...0)
677682 Toggle("Include files in subfolders", isOn: $agendaSubfolders)
683 Toggle("Show days without entries", isOn: $agendaShowAllDates)
684 }
685 Section {
686 Toggle("Show events from Calendar", isOn: $calendarEvents)
687 if calendarEvents {
688 if calendars.isEmpty {
689 Text(CalendarEvents.isAuthorized ? "No calendars." : "Allow Orgstar to read your calendars in System Settings ▸ Privacy & Security ▸ Calendars.")
690 .foregroundStyle(.secondary)
691 }
692 ForEach(calendars, id: \.id) { calendar in
693 Toggle(isOn: calendarChosen(calendar)) {
694 HStack(spacing: 6) {
695 Circle()
696 .fill(calendar.color.map { Color(.sRGB, red: $0.red, green: $0.green, blue: $0.blue, opacity: $0.alpha) } ?? .secondary)
697 .frame(width: 8, height: 8)
698 Text(calendar.title)
699 }
700 }
701 }
702 }
703 } footer: {
704 Text("Events show in the agenda read-only; double-click one to open it in Calendar. New calendars show while every calendar is checked.")
705 .settingsFooter()
706 }
707 .task(id: calendarEvents) {
708 let allowed = calendarEvents ? await CalendarAccess.events.requestAccess() : false
709 calendars = allowed ? CalendarAccess.events.calendars() : []
678710 }
679711 Section {
680712 Toggle("Notify before timed agenda entries", isOn: $reminders)
@@ -699,7 +731,24 @@ struct SettingsView: View {
699731 }
700732 }
701733 .formStyle(.grouped)
702 .frame(height: 430)
734 .frame(height: 560)
735 }
736
737 /// `calendar-event-calendars`: empty for every calendar, else the titles checked.
738 private func calendarChosen(_ calendar: (id: String, title: String, color: RGBA?)) -> Binding<Bool> {
739 let chosen = calendarEventCalendars.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty }
740 return Binding {
741 CalendarEvents.chooses(id: calendar.id, title: calendar.title, chosen: chosen)
742 } set: { on in
743 var titles = calendars.map(\.title).filter { title in
744 chosen.isEmpty || chosen.contains(title) || chosen.contains(calendars.first { $0.title == title }?.id ?? "")
745 }
746 titles.removeAll { $0 == calendar.title }
747 if on { titles.append(calendar.title) }
748 // An empty list means every calendar; turning events off hides them all.
749 guard !titles.isEmpty else { return }
750 calendarEventCalendars = Set(titles) == Set(calendars.map(\.title)) ? "" : titles.joined(separator: ", ")
751 }
703752 }
704753
705754 private var capture: some View {