From ab7d2619d79bb95805e584188267b02a0e06ace6 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Mon, 6 Apr 2026 11:29:23 +0530 Subject: [PATCH 1/5] Update fantastical open cmds --- DynamicIsland/Localizable.xcstrings | 30 ++++++-------- .../components/Settings/SettingsView.swift | 28 +++++++++++++ DynamicIsland/models/Constants.swift | 16 ++++++++ DynamicIsland/models/EventModel.swift | 39 ++++++++++++++++++- 4 files changed, 94 insertions(+), 19 deletions(-) diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 31e415e..5812abf 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -2042,7 +2042,6 @@ }, "±10 Seconds" : { "comment" : "Display name for the \"±10 Seconds\" music skip behavior option.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -2665,7 +2664,6 @@ }, "Accent" : { "comment" : "Display name for the caps lock indicator tint mode.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -3143,7 +3141,6 @@ }, "Adaptive gradient" : { "comment" : "Display name for the timer icon color mode: adaptive gradient.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -5194,7 +5191,6 @@ }, "Apple Music" : { "comment" : "Media controller type for Apple Music", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -5917,7 +5913,6 @@ }, "Bar" : { "comment" : "Name of the timer progress style that shows a horizontal bar.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -11787,6 +11782,9 @@ } } } + }, + "Default View" : { + }, "Default: dynamic.m4a" : { "localizations" : { @@ -12212,7 +12210,6 @@ }, "Digital" : { "comment" : "The display name for the \"Digital\" reminder presentation style.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -17300,7 +17297,6 @@ }, "Green" : { "comment" : "Display name for the green caps lock indicator tint mode.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -25124,7 +25120,6 @@ }, "Now Playing" : { "comment" : "Name of the media controller type for the \"Now Playing\" option.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -25809,7 +25804,6 @@ }, "Panel" : { "comment" : "Display name for the clipboard display mode \"Panel\".", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -28661,7 +28655,6 @@ }, "Ring" : { "comment" : "The name of the timer progress style that shows a ring shape.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -29919,7 +29912,6 @@ }, "Separate Tab" : { "comment" : "Display name for the clipboard display mode option to open the clipboard in a separate tab.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -31687,7 +31679,6 @@ }, "Shows screen assistant as a dropdown attached to the AI button" : { "comment" : "Description of the screen assistant display mode option that shows the screen assistant as a dropdown attached to the AI button.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -31700,7 +31691,6 @@ }, "Shows screen assistant in a floating panel near the notch" : { "comment" : "Description of the screen assistant display mode option that shows the screen assistant in a floating panel near the notch.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -31854,7 +31844,6 @@ }, "Skip forward or backward by ten seconds" : { "comment" : "Description of a music skip behavior option.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -32517,7 +32506,6 @@ }, "Spotify" : { "comment" : "Spotify media controller type.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -32706,7 +32694,6 @@ }, "Standard previous/next track controls" : { "comment" : "Description of the music skip behavior option to use standard previous/next track controls.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -34131,6 +34118,9 @@ } } } + }, + "Third-party Calendar Integration" : { + }, "This duration powers the \"Custom\" option inside the timer popover for quick access." : { "comment" : "A footer description for the custom timer settings, explaining their purpose.", @@ -34995,7 +34985,6 @@ }, "Track Skip" : { "comment" : "Display name for the \"Track Skip\" music skip behavior.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { @@ -35992,6 +35981,9 @@ } } } + }, + "Use Fantastical" : { + }, "Use MacBook icon when on battery" : { "localizations" : { @@ -37194,6 +37186,9 @@ } } } + }, + "When enabled, clicking on calendar events will open Fantastical instead of Apple Calendar. More integrations coming soon." : { + }, "When enabled, Focus appears briefly (on/off) and then collapses instead of staying visible." : { "comment" : "A help text for the setting that allows the focus indicator to collapse instead of staying visible.", @@ -37763,7 +37758,6 @@ }, "Youtube Music" : { "comment" : "Spotify music service.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ru" : { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 58dfa77..ac46e8f 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2752,6 +2752,8 @@ struct CalendarSettings: View { @Default(.lockScreenCalendarSelectionMode) private var lockScreenCalendarSelectionMode @Default(.lockScreenSelectedCalendarIDs) private var lockScreenSelectedCalendarIDs @Default(.lockScreenShowCalendarEventAfterStartEnabled) private var lockScreenShowCalendarEventAfterStartEnabled + @Default(.useFantasticalCalendar) private var useFantasticalCalendar + @Default(.fantasticalDefaultView) private var fantasticalDefaultView private func highlightID(_ title: String) -> String { SettingsTab.calendar.highlightID(for: title) @@ -3045,6 +3047,32 @@ struct CalendarSettings: View { .disabled(!lockScreenShowCalendarEvent) .settingsHighlight(id: highlightID("Show start time after event begins")) } + + // MARK: - Third-party Calendar Integration + Section { + Defaults.Toggle(key: .useFantasticalCalendar) { + HStack { + Image(systemName: "calendar.badge.clock") + .foregroundColor(.red) + .frame(width: 20, height: 20) + Text("Use Fantastical") + } + } + .settingsHighlight(id: highlightID("Use Fantastical")) + + if useFantasticalCalendar { + Picker("Default View", selection: $fantasticalDefaultView) { + ForEach(FantasticalViewStyle.allCases, id: \.self) { style in + Text(style.displayName).tag(style) + } + } + .settingsHighlight(id: highlightID("Fantastical Default View")) + } + } header: { + Text("Third-party Calendar Integration") + } footer: { + Text("When enabled, clicking on calendar events will open Fantastical instead of Apple Calendar. More integrations coming soon.") + } Section(header: Text("Select Calendars")) { let grouped = Dictionary(grouping: calendarManager.allCalendars, by: \.accountName) diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 77e53b8..8d28529 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -256,6 +256,18 @@ enum CalendarSelectionState: Codable, Defaults.Serializable { case selected(Set) } +enum FantasticalViewStyle: String, CaseIterable, Codable, Defaults.Serializable { + case mini = "mini" + case calendar = "calendar" + + var displayName: String { + switch self { + case .mini: return "Mini View" + case .calendar: return "Full Calendar" + } + } +} + enum ClipboardDisplayMode: String, CaseIterable, Codable, Defaults.Serializable { case popover = "popover" // Traditional popover attached to button case panel = "panel" // Floating panel near notch @@ -854,6 +866,10 @@ extension Defaults.Keys { static let lockScreenShowCalendarStartTimeAfterBegins = Key("lockScreenShowCalendarStartTimeAfterBegins", default: true) static let lockScreenWeatherWidgetRowOrder = Key("lockScreenWeatherWidgetRowOrder", default: "weather_calendar_focus") + // MARK: Third-party Calendar Integration + static let useFantasticalCalendar = Key("useFantasticalCalendar", default: false) + static let fantasticalDefaultView = Key("fantasticalDefaultView", default: .mini) + // MARK: Battery static let showPowerStatusNotifications = Key("showPowerStatusNotifications", default: true) static let showBatteryIndicator = Key("showBatteryIndicator", default: BatteryActivityManager.shared.hasBattery()) diff --git a/DynamicIsland/models/EventModel.swift b/DynamicIsland/models/EventModel.swift index fe42bd2..390ef1e 100644 --- a/DynamicIsland/models/EventModel.swift +++ b/DynamicIsland/models/EventModel.swift @@ -21,6 +21,7 @@ */ import Foundation +import Defaults struct EventModel: Equatable, Identifiable { let id: String @@ -97,7 +98,15 @@ extension EventModel { var isMeeting: Bool { !participants.isEmpty } func calendarAppURL() -> URL? { - + // Check if Fantastical is enabled + if Defaults[.useFantasticalCalendar] { + return fantasticalURL() + } + return appleCalendarURL() + } + + /// Returns URL to open event in Apple Calendar + private func appleCalendarURL() -> URL? { guard let id = id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { return nil } @@ -123,6 +132,34 @@ extension EventModel { } return URL(string: "ical://ekevent\(date)/\(id)?method=show&options=more") } + + /// Returns URL to open date in Fantastical + private func fantasticalURL() -> URL? { + // Reminders still use Apple's Reminders app + guard !type.isReminder else { + return URL(string: "x-apple-reminderkit://remcdreminder/\(id)") + } + + let viewStyle = Defaults[.fantasticalDefaultView] + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let dateString = dateFormatter.string(from: start) + + // x-fantastical3://show/mini/yyyy-MM-dd or x-fantastical3://show/calendar/yyyy-MM-dd + return URL(string: "x-fantastical3://show/\(viewStyle.rawValue)/\(dateString)") + } + + /// Returns URL to open Fantastical at current date (for general calendar access) + static func fantasticalShowURL(for date: Date? = nil) -> URL? { + let viewStyle = Defaults[.fantasticalDefaultView] + if let date = date { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let dateString = dateFormatter.string(from: date) + return URL(string: "x-fantastical3://show/\(viewStyle.rawValue)/\(dateString)") + } + return URL(string: "x-fantastical3://show/\(viewStyle.rawValue)") + } } struct Participant: Hashable { From 563afde92f4d9666a3871938c96173ae33e38e3d Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Mon, 6 Apr 2026 13:41:55 +0530 Subject: [PATCH 2/5] Update Notion Calendar --- DynamicIsland/Localizable.xcstrings | 11 ++-- .../components/Settings/SettingsView.swift | 34 +++++++----- DynamicIsland/models/Constants.swift | 31 ++++++++++- DynamicIsland/models/EventModel.swift | 54 +++++++++++++++++-- 4 files changed, 110 insertions(+), 20 deletions(-) diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 5812abf..aea21d2 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -7231,6 +7231,9 @@ } } } + }, + "Calendar App" : { + }, "Calendar or Reminder access is denied. Please enable it in System Settings." : { "comment" : "A message displayed when a user has denied access to the calendar or reminders on their device. It instructs the user to open System Settings to grant access.", @@ -14925,6 +14928,9 @@ } } } + }, + "Enable third-party calendar app launch" : { + }, "Enable third-party DDC app integration" : { "comment" : "A toggle switch that enables or disables integration with a third-party Dynamic Display Control (DDC) app.", @@ -35981,9 +35987,6 @@ } } } - }, - "Use Fantastical" : { - }, "Use MacBook icon when on battery" : { "localizations" : { @@ -37187,7 +37190,7 @@ } } }, - "When enabled, clicking on calendar events will open Fantastical instead of Apple Calendar. More integrations coming soon." : { + "When enabled, clicking on calendar events will open the selected third-party calendar app instead of Apple Calendar." : { }, "When enabled, Focus appears briefly (on/off) and then collapses instead of staying visible." : { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index ac46e8f..9d39231 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2752,7 +2752,8 @@ struct CalendarSettings: View { @Default(.lockScreenCalendarSelectionMode) private var lockScreenCalendarSelectionMode @Default(.lockScreenSelectedCalendarIDs) private var lockScreenSelectedCalendarIDs @Default(.lockScreenShowCalendarEventAfterStartEnabled) private var lockScreenShowCalendarEventAfterStartEnabled - @Default(.useFantasticalCalendar) private var useFantasticalCalendar + @Default(.enableThirdPartyCalendarApp) private var enableThirdPartyCalendarApp + @Default(.selectedCalendarApp) private var selectedCalendarApp @Default(.fantasticalDefaultView) private var fantasticalDefaultView private func highlightID(_ title: String) -> String { @@ -3050,28 +3051,37 @@ struct CalendarSettings: View { // MARK: - Third-party Calendar Integration Section { - Defaults.Toggle(key: .useFantasticalCalendar) { + Defaults.Toggle(key: .enableThirdPartyCalendarApp) { HStack { - Image(systemName: "calendar.badge.clock") - .foregroundColor(.red) + Image(systemName: selectedCalendarApp.iconName) + .foregroundColor(selectedCalendarApp.iconColor) .frame(width: 20, height: 20) - Text("Use Fantastical") + Text("Enable third-party calendar app launch") } } - .settingsHighlight(id: highlightID("Use Fantastical")) + .settingsHighlight(id: highlightID("Enable third-party calendar app launch")) - if useFantasticalCalendar { - Picker("Default View", selection: $fantasticalDefaultView) { - ForEach(FantasticalViewStyle.allCases, id: \.self) { style in - Text(style.displayName).tag(style) + if enableThirdPartyCalendarApp { + Picker("Calendar App", selection: $selectedCalendarApp) { + ForEach(ThirdPartyCalendarApp.allCases) { app in + Text(app.displayName).tag(app) } } - .settingsHighlight(id: highlightID("Fantastical Default View")) + .settingsHighlight(id: highlightID("Calendar App")) + + if selectedCalendarApp == .fantastical { + Picker("Default View", selection: $fantasticalDefaultView) { + ForEach(FantasticalViewStyle.allCases, id: \.self) { style in + Text(style.displayName).tag(style) + } + } + .settingsHighlight(id: highlightID("Fantastical Default View")) + } } } header: { Text("Third-party Calendar Integration") } footer: { - Text("When enabled, clicking on calendar events will open Fantastical instead of Apple Calendar. More integrations coming soon.") + Text("When enabled, clicking on calendar events will open the selected third-party calendar app instead of Apple Calendar.") } Section(header: Text("Select Calendars")) { diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 8d28529..a8ad397 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -268,6 +268,34 @@ enum FantasticalViewStyle: String, CaseIterable, Codable, Defaults.Serializable } } +enum ThirdPartyCalendarApp: String, CaseIterable, Codable, Defaults.Serializable, Identifiable { + case fantastical = "fantastical" + case notionCalendar = "notionCalendar" + + var id: String { rawValue } + + var displayName: String { + switch self { + case .fantastical: return "Fantastical" + case .notionCalendar: return "Notion Calendar" + } + } + + var iconName: String { + switch self { + case .fantastical: return "calendar.badge.clock" + case .notionCalendar: return "calendar.badge.plus" + } + } + + var iconColor: Color { + switch self { + case .fantastical: return .red + case .notionCalendar: return .blue + } + } +} + enum ClipboardDisplayMode: String, CaseIterable, Codable, Defaults.Serializable { case popover = "popover" // Traditional popover attached to button case panel = "panel" // Floating panel near notch @@ -867,7 +895,8 @@ extension Defaults.Keys { static let lockScreenWeatherWidgetRowOrder = Key("lockScreenWeatherWidgetRowOrder", default: "weather_calendar_focus") // MARK: Third-party Calendar Integration - static let useFantasticalCalendar = Key("useFantasticalCalendar", default: false) + static let enableThirdPartyCalendarApp = Key("enableThirdPartyCalendarApp", default: false) + static let selectedCalendarApp = Key("selectedCalendarApp", default: .fantastical) static let fantasticalDefaultView = Key("fantasticalDefaultView", default: .mini) // MARK: Battery diff --git a/DynamicIsland/models/EventModel.swift b/DynamicIsland/models/EventModel.swift index 390ef1e..7fbc6cb 100644 --- a/DynamicIsland/models/EventModel.swift +++ b/DynamicIsland/models/EventModel.swift @@ -22,6 +22,7 @@ import Foundation import Defaults +import AppKit struct EventModel: Equatable, Identifiable { let id: String @@ -98,9 +99,14 @@ extension EventModel { var isMeeting: Bool { !participants.isEmpty } func calendarAppURL() -> URL? { - // Check if Fantastical is enabled - if Defaults[.useFantasticalCalendar] { - return fantasticalURL() + // Check if a third-party calendar app is enabled + if Defaults[.enableThirdPartyCalendarApp] { + switch Defaults[.selectedCalendarApp] { + case .fantastical: + return fantasticalURL() + case .notionCalendar: + return notionCalendarURL() + } } return appleCalendarURL() } @@ -160,6 +166,48 @@ extension EventModel { } return URL(string: "x-fantastical3://show/\(viewStyle.rawValue)") } + + /// Returns URL to open event in Notion Calendar (formerly Cron) + private func notionCalendarURL() -> URL? { + // Reminders still use Apple's Reminders app + guard !type.isReminder else { + return URL(string: "x-apple-reminderkit://remcdreminder/\(id)") + } + + let formatter = ISO8601DateFormatter() + + var components = URLComponents() + components.scheme = "cron" + components.host = "showEvent" + components.queryItems = [ + URLQueryItem(name: "accountEmail", value: calendar.accountName), + URLQueryItem(name: "iCalUID", value: id), + URLQueryItem(name: "startDate", value: formatter.string(from: start)), + URLQueryItem(name: "endDate", value: formatter.string(from: end)), + URLQueryItem(name: "title", value: title) + ] + + if let url = components.url { + return url + } + + // Fallback: just launch Notion Calendar app + Self.launchNotionCalendar() + return nil + } + + /// Launches Notion Calendar app directly (fallback or general access) + static func launchNotionCalendar() { + // Launch via bundle identifier (Notion Calendar's bundle ID is "com.cron.electron") + if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.cron.electron") { + let config = NSWorkspace.OpenConfiguration() + NSWorkspace.shared.openApplication(at: url, configuration: config) { _, error in + if let error = error { + print("Error launching Notion Calendar: \(error.localizedDescription)") + } + } + } + } } struct Participant: Hashable { From 78463086bae9d3b62dee0e56d7742f387355c6d3 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Mon, 6 Apr 2026 14:06:34 +0530 Subject: [PATCH 3/5] Update 3rd party calendar icons --- .../components/Settings/SettingsView.swift | 163 ++++++++++++++---- DynamicIsland/models/Constants.swift | 20 ++- 2 files changed, 150 insertions(+), 33 deletions(-) diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 9d39231..3c783db 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2074,7 +2074,15 @@ private struct ExternalDisplayIntegrationsSection: View { if enableThirdPartyDDCIntegration { Picker("Provider", selection: $thirdPartyDDCProvider) { ForEach(ThirdPartyDDCProvider.allCases) { provider in - Text(provider.displayName).tag(provider) + HStack { + AppIconImage( + bundleIdentifiers: provider.bundleIdentifiers, + symbolFallback: "display", + symbolColor: .secondary + ) + Text(provider.displayName) + } + .tag(provider) } } .settingsHighlight(id: highlightID("Third-party DDC provider")) @@ -3053,9 +3061,12 @@ struct CalendarSettings: View { Section { Defaults.Toggle(key: .enableThirdPartyCalendarApp) { HStack { - Image(systemName: selectedCalendarApp.iconName) - .foregroundColor(selectedCalendarApp.iconColor) - .frame(width: 20, height: 20) + AppIconImage( + bundleIdentifiers: selectedCalendarApp.bundleIdentifiers, + symbolFallback: selectedCalendarApp.fallbackIconName, + symbolColor: selectedCalendarApp.fallbackIconColor, + size: 20 + ) Text("Enable third-party calendar app launch") } } @@ -3064,7 +3075,15 @@ struct CalendarSettings: View { if enableThirdPartyCalendarApp { Picker("Calendar App", selection: $selectedCalendarApp) { ForEach(ThirdPartyCalendarApp.allCases) { app in - Text(app.displayName).tag(app) + HStack { + AppIconImage( + bundleIdentifiers: app.bundleIdentifiers, + symbolFallback: app.fallbackIconName, + symbolColor: app.fallbackIconColor + ) + Text(app.displayName) + } + .tag(app) } } .settingsHighlight(id: highlightID("Calendar App")) @@ -3500,19 +3519,7 @@ struct Shelf: View { Picker("Quick Share Service", selection: $quickShareProvider) { ForEach(quickShareService.availableProviders, id: \.id) { provider in HStack { - Group { - if let imgData = provider.imageData, let nsImg = NSImage(data: imgData) { - Image(nsImage: nsImg) - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - .clipShape(RoundedRectangle(cornerRadius: 3)) - } else { - Image(systemName: "square.and.arrow.up") - .frame(width: 16, height: 16) - } - } - .foregroundColor(.accentColor) + QuickShareProviderIconImage(provider: provider, size: 16) Text(provider.id) } .tag(provider.id) @@ -3523,19 +3530,7 @@ struct Shelf: View { if let selectedProvider { HStack { - Group { - if let imgData = selectedProvider.imageData, let nsImg = NSImage(data: imgData) { - Image(nsImage: nsImg) - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - .clipShape(RoundedRectangle(cornerRadius: 3)) - } else { - Image(systemName: "square.and.arrow.up") - .frame(width: 16, height: 16) - } - } - .foregroundColor(.accentColor) + QuickShareProviderIconImage(provider: selectedProvider, size: 16) VStack(alignment: .leading, spacing: 2) { Text("Currently selected: \(selectedProvider.id)") .font(.caption) @@ -7971,3 +7966,109 @@ struct TerminalSettings: View { .navigationTitle("Terminal") } } + +// MARK: - Reusable App Icon View + +/// Fetches the real app icon from the system using bundle identifiers, +/// falling back to an asset catalog image or an SF Symbol. +struct AppIconImage: View { + let bundleIdentifiers: [String] + var assetFallback: String? = nil + var symbolFallback: String = "app.fill" + var symbolColor: Color = .accentColor + var size: CGFloat = 16 + + var body: some View { + Group { + if let nsImage = resolvedIcon() { + Image(nsImage: nsImage) + .resizable() + .scaledToFit() + .clipShape(RoundedRectangle(cornerRadius: size * 0.2)) + } else if let assetFallback, let nsImage = NSImage(named: NSImage.Name(assetFallback)) { + Image(nsImage: nsImage) + .resizable() + .scaledToFit() + .clipShape(RoundedRectangle(cornerRadius: size * 0.2)) + } else { + Image(systemName: symbolFallback) + .foregroundColor(symbolColor) + } + } + .frame(width: size, height: size) + } + + private func resolvedIcon() -> NSImage? { + for bundleID in bundleIdentifiers { + if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) { + let icon = NSWorkspace.shared.icon(forFile: appURL.path) + // NSWorkspace returns a valid icon even for generic apps; + // resize to keep memory low. + let thumb = NSImage(size: NSSize(width: 32, height: 32)) + thumb.lockFocus() + icon.draw(in: NSRect(origin: .zero, size: NSSize(width: 32, height: 32)), + from: NSRect(origin: .zero, size: icon.size), + operation: .copy, fraction: 1.0) + thumb.unlockFocus() + return thumb + } + } + return nil + } +} + +private struct QuickShareProviderIconImage: View { + let provider: QuickShareProvider + var size: CGFloat = 16 + + var body: some View { + Group { + if let imgData = provider.imageData, let nsImg = NSImage(data: imgData) { + Image(nsImage: nsImg) + .resizable() + .scaledToFit() + .clipShape(RoundedRectangle(cornerRadius: size * 0.2)) + } else { + AppIconImage( + bundleIdentifiers: provider.bundleIdentifiersFallback, + assetFallback: provider.assetFallbackName, + symbolFallback: provider.symbolFallbackName, + symbolColor: .accentColor, + size: size + ) + } + } + .frame(width: size, height: size) + } +} + +private extension QuickShareProvider { + var bundleIdentifiersFallback: [String] { + switch id { + case "LocalSend": + return ["org.localsend.localsend_app", "org.localsend.localsend"] + case "AirDrop": + return ["com.apple.finder"] + case "Mail": + return ["com.apple.mail"] + case "Messages": + return ["com.apple.MobileSMS", "com.apple.iChat"] + case "Notes": + return ["com.apple.Notes"] + case "Reminders": + return ["com.apple.reminders"] + case "Add to Safari Reading List": + return ["com.apple.Safari"] + default: + return [] + } + } + + var assetFallbackName: String? { + id == "LocalSend" ? "LocalSend" : nil + } + + var symbolFallbackName: String { + id == "System Share Menu" ? "square.and.arrow.up.on.square" : "square.and.arrow.up" + } +} diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index a8ad397..7d9ebc9 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -281,14 +281,22 @@ enum ThirdPartyCalendarApp: String, CaseIterable, Codable, Defaults.Serializable } } - var iconName: String { + /// Bundle identifiers to try when looking up the app icon (first match wins). + var bundleIdentifiers: [String] { + switch self { + case .fantastical: return ["com.flexibits.fantastical2.mac", "com.flexibits.fantastical"] + case .notionCalendar: return ["com.cron.electron"] + } + } + + var fallbackIconName: String { switch self { case .fantastical: return "calendar.badge.clock" case .notionCalendar: return "calendar.badge.plus" } } - var iconColor: Color { + var fallbackIconColor: Color { switch self { case .fantastical: return .red case .notionCalendar: return .blue @@ -370,6 +378,14 @@ enum ThirdPartyDDCProvider: String, CaseIterable, Codable, Defaults.Serializable return "Lunar" } } + + /// Bundle identifiers to try when looking up the app icon. + var bundleIdentifiers: [String] { + switch self { + case .betterDisplay: return ["pro.betterdisplay.BetterDisplay"] + case .lunar: return ["fyi.lunar.Lunar"] + } + } } enum HideNotchOption: String, Defaults.Serializable { From bd2ae3b8bfb6e37ab246f5591b9dae4542cfcf18 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Mon, 6 Apr 2026 14:12:55 +0530 Subject: [PATCH 4/5] Update stuff icons --- DynamicIsland/components/Settings/SettingsView.swift | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 3c783db..95a1051 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -3061,12 +3061,9 @@ struct CalendarSettings: View { Section { Defaults.Toggle(key: .enableThirdPartyCalendarApp) { HStack { - AppIconImage( - bundleIdentifiers: selectedCalendarApp.bundleIdentifiers, - symbolFallback: selectedCalendarApp.fallbackIconName, - symbolColor: selectedCalendarApp.fallbackIconColor, - size: 20 - ) + Image(systemName: "ellipsis.calendar") + .foregroundStyle(.secondary) + .frame(width: 20, height: 20) Text("Enable third-party calendar app launch") } } From 29b80226b40b7a38192c87966cb7d7ec7ab61142 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 8 Apr 2026 13:46:38 +0530 Subject: [PATCH 5/5] Update references to SwiftTerm and rtaudio --- DynamicIsland/audio/AudioBridge.h | 5 +++-- DynamicIsland/audio/AudioBridge.mm | 5 +++-- DynamicIsland/audio/AudioProcessor.hpp | 6 +++--- DynamicIsland/audio/AudioTap.swift | 6 +++--- DynamicIsland/audio/RealTimeAudioSpectrum.swift | 6 +++--- DynamicIsland/managers/TerminalManager.swift | 1 + ReadMe.md | 5 +++++ 7 files changed, 21 insertions(+), 13 deletions(-) diff --git a/DynamicIsland/audio/AudioBridge.h b/DynamicIsland/audio/AudioBridge.h index a2fef37..2f17375 100644 --- a/DynamicIsland/audio/AudioBridge.h +++ b/DynamicIsland/audio/AudioBridge.h @@ -1,11 +1,12 @@ /* * Atoll (DynamicIsland) - * Copyright (C) 2024-2026 Atoll Contributors + * Copyright (C) 2024-2026 Atoll Contributors and zeph + * Adapted from rtaudio (https://github.com/ZephyrCodesStuff/rtaudio) * * Objective-C bridge to expose C++ AudioProcessor to Swift. * * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by + * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. */ diff --git a/DynamicIsland/audio/AudioBridge.mm b/DynamicIsland/audio/AudioBridge.mm index c781c58..e3456f9 100644 --- a/DynamicIsland/audio/AudioBridge.mm +++ b/DynamicIsland/audio/AudioBridge.mm @@ -1,11 +1,12 @@ /* * Atoll (DynamicIsland) - * Copyright (C) 2024-2026 Atoll Contributors + * Copyright (C) 2024-2026 Atoll Contributors and zeph + * Adapted from rtaudio (https://github.com/ZephyrCodesStuff/rtaudio) * * Objective-C++ implementation bridging AudioProcessor to Swift. * * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by + * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. */ diff --git a/DynamicIsland/audio/AudioProcessor.hpp b/DynamicIsland/audio/AudioProcessor.hpp index 970b9f0..72d40f1 100644 --- a/DynamicIsland/audio/AudioProcessor.hpp +++ b/DynamicIsland/audio/AudioProcessor.hpp @@ -1,12 +1,12 @@ /* * Atoll (DynamicIsland) - * Copyright (C) 2024-2026 Atoll Contributors + * Copyright (C) 2024-2026 Atoll Contributors and zeph * * Real-time audio processor using Accelerate framework for efficient FFT - * and band filtering. Adapted from rtaudio project. + * and band filtering. Adapted from rtaudio project (https://github.com/ZephyrCodesStuff/rtaudio) * * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by + * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. */ diff --git a/DynamicIsland/audio/AudioTap.swift b/DynamicIsland/audio/AudioTap.swift index 69ea2d3..60613d4 100644 --- a/DynamicIsland/audio/AudioTap.swift +++ b/DynamicIsland/audio/AudioTap.swift @@ -1,13 +1,13 @@ /* * Atoll (DynamicIsland) - * Copyright (C) 2024-2026 Atoll Contributors + * Copyright (C) 2024-2026 Atoll Contributors and zeph * * CoreAudio tap for capturing real-time audio from music applications. * Uses macOS 14.2+ Process Tap API for efficient audio capture. - * Adapted from rtaudio project. + * Adapted from rtaudio project (https://github.com/ZephyrCodesStuff/rtaudio) * * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by + * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. */ diff --git a/DynamicIsland/audio/RealTimeAudioSpectrum.swift b/DynamicIsland/audio/RealTimeAudioSpectrum.swift index ddb4e8b..fb6688f 100644 --- a/DynamicIsland/audio/RealTimeAudioSpectrum.swift +++ b/DynamicIsland/audio/RealTimeAudioSpectrum.swift @@ -1,12 +1,12 @@ /* * Atoll (DynamicIsland) - * Copyright (C) 2024-2026 Atoll Contributors + * Copyright (C) 2024-2026 Atoll Contributors and zeph * * Real-time audio spectrum visualization using CoreAudio tap data. - * Adapted from rtaudio project. + * Adapted from rtaudio project (https://github.com/ZephyrCodesStuff/rtaudio) * * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by + * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. */ diff --git a/DynamicIsland/managers/TerminalManager.swift b/DynamicIsland/managers/TerminalManager.swift index de0aad7..5b972f1 100644 --- a/DynamicIsland/managers/TerminalManager.swift +++ b/DynamicIsland/managers/TerminalManager.swift @@ -1,6 +1,7 @@ /* * Atoll (DynamicIsland) * Copyright (C) 2024-2026 Atoll Contributors + * Adapted from SwiftTerm (https://github.com/migueldeicaza/SwiftTerm) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/ReadMe.md b/ReadMe.md index 762dba5..9250741 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -112,7 +112,12 @@ Atoll builds upon the work of several open-source projects and draws inspiration - [**SkyLightWindow**](https://github.com/Lakr233/SkyLightWindow) - window rendering for Lock Screen Widgets +- [**rtaudio**](https://github.com/ZephyrCodesStuff/rtaudio) - Live music visualizer using C++ was adapted from this project + +- [**SwiftTerm**](https://github.com/migueldeicaza/SwiftTerm) - Terminal tab implementation in the standard mode was adapted from this project + - Wick - Thanks Nate for allowing us to replicate the iOS like Timer design for the Lock Screen Widget + ## Contributors