@sriinnu/runic 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +1 -1
  3. package/README.md +47 -29
  4. package/RuniciOS/Core/RunicTypographyiOS.swift +24 -0
  5. package/RuniciOS/Views/ProviderDetailView.swift +9 -9
  6. package/RuniciOS/Views/ProviderListView.swift +10 -10
  7. package/RuniciOS/Widgets/LargeWidget.swift +14 -14
  8. package/RuniciOS/Widgets/LiveActivity.swift +19 -19
  9. package/RuniciOS/Widgets/LockScreenWidget.swift +15 -15
  10. package/RuniciOS/Widgets/MediumWidget.swift +10 -10
  11. package/RuniciOS/Widgets/RunicWidget.swift +5 -5
  12. package/RuniciOS/Widgets/SmallWidget.swift +9 -9
  13. package/Scripts/package_app.sh +2 -2
  14. package/Sources/Runic/About.swift +3 -3
  15. package/Sources/Runic/AppNotifications.swift +5 -3
  16. package/Sources/Runic/Controllers/StatusItemController+Actions.swift +17 -4
  17. package/Sources/Runic/Controllers/StatusItemController+Animation.swift +5 -4
  18. package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +30 -16
  19. package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +6 -3
  20. package/Sources/Runic/Controllers/StatusItemController+Menu.swift +47 -14
  21. package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +6 -4
  22. package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +40 -12
  23. package/Sources/Runic/Controllers/StatusItemController.swift +43 -43
  24. package/Sources/Runic/Core/BudgetNotificationManager.swift +2 -1
  25. package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +2 -2
  26. package/Sources/Runic/Core/Login/CodexLoginRunner.swift +1 -1
  27. package/Sources/Runic/Core/Login/CursorLoginRunner.swift +4 -4
  28. package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +3 -6
  29. package/Sources/Runic/Core/Rendering/IconRenderer.swift +91 -85
  30. package/Sources/Runic/Core/Rendering/IconView.swift +3 -1
  31. package/Sources/Runic/Core/RunicColors.swift +6 -6
  32. package/Sources/Runic/Core/RunicSpacing.swift +7 -7
  33. package/Sources/Runic/Core/RunicTypography.swift +206 -0
  34. package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +10 -36
  35. package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +10 -36
  36. package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +10 -36
  37. package/Sources/Runic/Core/Stores/CohereTokenStore.swift +10 -36
  38. package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +10 -36
  39. package/Sources/Runic/Core/Stores/CustomProviderStore.swift +10 -10
  40. package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +10 -36
  41. package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +10 -36
  42. package/Sources/Runic/Core/Stores/GroqTokenStore.swift +10 -36
  43. package/Sources/Runic/Core/Stores/KimiTokenStore.swift +10 -36
  44. package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +10 -36
  45. package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +10 -36
  46. package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +10 -36
  47. package/Sources/Runic/Core/Stores/MistralTokenStore.swift +10 -36
  48. package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +10 -36
  49. package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +10 -36
  50. package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +1 -1
  51. package/Sources/Runic/Core/Stores/QwenTokenStore.swift +10 -36
  52. package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +10 -36
  53. package/Sources/Runic/Core/Stores/SettingsStore.swift +99 -52
  54. package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +10 -36
  55. package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +19 -6
  56. package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +1 -1
  57. package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +1 -1
  58. package/Sources/Runic/Core/Stores/UsageStore.swift +234 -167
  59. package/Sources/Runic/Core/Stores/XAITokenStore.swift +10 -36
  60. package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +10 -36
  61. package/Sources/Runic/Core/UsageExporter.swift +8 -4
  62. package/Sources/Runic/ProviderRegistry.swift +1 -1
  63. package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +1 -1
  64. package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +1 -1
  65. package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +1 -1
  66. package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +1 -1
  67. package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +1 -1
  68. package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +1 -1
  69. package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +1 -1
  70. package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +1 -1
  71. package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +1 -1
  72. package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +1 -1
  73. package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +1 -1
  74. package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +1 -1
  75. package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +1 -1
  76. package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +1 -1
  77. package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +1 -1
  78. package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +1 -1
  79. package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +1 -1
  80. package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +1 -1
  81. package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +1 -1
  82. package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +5 -2
  83. package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +1 -1
  84. package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +1 -1
  85. package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +1 -1
  86. package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +1 -1
  87. package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +1 -1
  88. package/Sources/Runic/Resources/Fonts/Caveat-Bold.ttf +0 -0
  89. package/Sources/Runic/Resources/Fonts/Caveat-Regular.ttf +0 -0
  90. package/Sources/Runic/Resources/Fonts/FiraCode-Bold.ttf +0 -0
  91. package/Sources/Runic/Resources/Fonts/FiraCode-Light.ttf +0 -0
  92. package/Sources/Runic/Resources/Fonts/FiraCode-Medium.ttf +0 -0
  93. package/Sources/Runic/Resources/Fonts/FiraCode-Regular.ttf +0 -0
  94. package/Sources/Runic/Resources/Fonts/FiraCode-SemiBold.ttf +0 -0
  95. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Bold.ttf +0 -0
  96. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Medium.ttf +0 -0
  97. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Regular.ttf +0 -0
  98. package/Sources/Runic/Resources/Fonts/IBMPlexMono-SemiBold.ttf +0 -0
  99. package/Sources/Runic/Resources/Fonts/Inconsolata-Bold.ttf +0 -0
  100. package/Sources/Runic/Resources/Fonts/Inconsolata-Medium.ttf +0 -0
  101. package/Sources/Runic/Resources/Fonts/Inconsolata-Regular.ttf +0 -0
  102. package/Sources/Runic/Resources/Fonts/Inconsolata-SemiBold.ttf +0 -0
  103. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Bold.ttf +0 -0
  104. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Medium.ttf +0 -0
  105. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Regular.ttf +0 -0
  106. package/Sources/Runic/Resources/Fonts/JetBrainsMono-SemiBold.ttf +0 -0
  107. package/Sources/Runic/Resources/Fonts/SpaceMono-Bold.ttf +0 -0
  108. package/Sources/Runic/Resources/Fonts/SpaceMono-Regular.ttf +0 -0
  109. package/Sources/Runic/Resources/provider-context-windows.json +20 -0
  110. package/Sources/Runic/RunicApp.swift +46 -36
  111. package/Sources/Runic/SessionQuotaNotifications.swift +2 -2
  112. package/Sources/Runic/UsagePaceText.swift +1 -1
  113. package/Sources/Runic/UsageProgressBar.swift +13 -9
  114. package/Sources/Runic/Utilities/LoadingPattern.swift +4 -2
  115. package/Sources/Runic/Utilities/PerformanceConstants.swift +28 -28
  116. package/Sources/Runic/Views/Menu/AlertsMenuView.swift +19 -21
  117. package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +19 -19
  118. package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +7 -7
  119. package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +5 -5
  120. package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +29 -30
  121. package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +20 -17
  122. package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +14 -14
  123. package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +15 -20
  124. package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +3 -4
  125. package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +5 -25
  126. package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +1 -1
  127. package/Sources/Runic/Views/Menu/MenuCardView.swift +120 -131
  128. package/Sources/Runic/Views/Menu/MenuContent.swift +11 -8
  129. package/Sources/Runic/Views/Menu/MenuDescriptor.swift +4 -2
  130. package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +1 -8
  131. package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +20 -14
  132. package/Sources/Runic/Views/Menu/OverviewMenuView.swift +198 -58
  133. package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +16 -11
  134. package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +27 -27
  135. package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +4 -6
  136. package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +24 -11
  137. package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +5 -5
  138. package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +7 -8
  139. package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +28 -76
  140. package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +8 -7
  141. package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +7 -7
  142. package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +50 -47
  143. package/Sources/Runic/Views/Preferences/LiquidComponents.swift +15 -14
  144. package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +7 -7
  145. package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +27 -27
  146. package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +38 -42
  147. package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +39 -43
  148. package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +21 -21
  149. package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +7 -7
  150. package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +49 -50
  151. package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +10 -10
  152. package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +40 -21
  153. package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +7 -7
  154. package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +108 -33
  155. package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +26 -23
  156. package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +271 -226
  157. package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +6 -2
  158. package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +7 -5
  159. package/Sources/Runic/Views/Preferences/PreferencesView.swift +38 -31
  160. package/Sources/Runic/Views/Rating/QualityRatingView.swift +11 -11
  161. package/Sources/Runic/Views/Team/TeamDashboardView.swift +29 -24
  162. package/Sources/Runic/Views/Team/TeamInviteSheet.swift +16 -16
  163. package/Sources/Runic/Views/Team/TeamManagementView.swift +50 -49
  164. package/Sources/Runic/Views/Team/TeamMemberRow.swift +8 -8
  165. package/Sources/RunicCLI/CLIEntry.swift +83 -42
  166. package/Sources/RunicCLI/CLIRenderer.swift +1 -1
  167. package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +60 -60
  168. package/Sources/RunicCLI/Commands/ModelsCommand.swift +45 -44
  169. package/Sources/RunicCLI/Commands/ProjectsCommand.swift +83 -69
  170. package/Sources/RunicCLI/Commands/ResetCommand.swift +50 -49
  171. package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +31 -32
  172. package/Sources/RunicClaudeWebProbe/main.swift +1 -1
  173. package/Sources/RunicCore/AlertRuleStore.swift +20 -20
  174. package/Sources/RunicCore/BrowserCookieImportOrder.swift +3 -3
  175. package/Sources/RunicCore/CostUsageModels.swift +1 -1
  176. package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +29 -30
  177. package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +80 -71
  178. package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +53 -109
  179. package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +3 -3
  180. package/Sources/RunicCore/Integrations/GitHubIntegration.swift +3 -3
  181. package/Sources/RunicCore/Logging/OSLogLogHandler.swift +6 -6
  182. package/Sources/RunicCore/Models/EnhancedUsageModels.swift +47 -48
  183. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +1 -1
  184. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +1 -1
  185. package/Sources/RunicCore/Performance/PerformanceModels.swift +4 -4
  186. package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +81 -64
  187. package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +2 -2
  188. package/Sources/RunicCore/ProjectBudgetStore.swift +15 -12
  189. package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +4 -2
  190. package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +2 -2
  191. package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +2 -2
  192. package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +3 -3
  193. package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +8 -8
  194. package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +5 -3
  195. package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +6 -6
  196. package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +3 -7
  197. package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +2 -2
  198. package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +2 -2
  199. package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +3 -3
  200. package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +4 -2
  201. package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +4 -4
  202. package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +3 -1
  203. package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +1 -1
  204. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +1 -0
  205. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +12 -1
  206. package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +4 -2
  207. package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +3 -1
  208. package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +5 -3
  209. package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +2 -2
  210. package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +3 -3
  211. package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +7 -4
  212. package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +4 -4
  213. package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +5 -3
  214. package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +9 -10
  215. package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +4 -2
  216. package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +1 -0
  217. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +2 -2
  218. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +2 -2
  219. package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +4 -4
  220. package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +4 -2
  221. package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +3 -2
  222. package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +2 -2
  223. package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +2 -2
  224. package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +4 -2
  225. package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +6 -2
  226. package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +2 -2
  227. package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +2 -2
  228. package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +2 -2
  229. package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +2 -2
  230. package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +10 -5
  231. package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +8 -11
  232. package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +10 -10
  233. package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +2 -2
  234. package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +2 -2
  235. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +2 -2
  236. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +25 -16
  237. package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +2 -2
  238. package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +2 -2
  239. package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +2 -2
  240. package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +2 -2
  241. package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +5 -3
  242. package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +27 -16
  243. package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +2 -2
  244. package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +2 -2
  245. package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +2 -2
  246. package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +2 -2
  247. package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +5 -5
  248. package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +1 -1
  249. package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +3 -3
  250. package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +2 -2
  251. package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +2 -2
  252. package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +4 -2
  253. package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +51 -30
  254. package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +76 -77
  255. package/Sources/RunicCore/Sync/SyncConflictResolver.swift +55 -58
  256. package/Sources/RunicCore/Sync/SyncProtocol.swift +21 -22
  257. package/Sources/RunicCore/Sync/SyncRecord.swift +46 -49
  258. package/Sources/RunicCore/Sync/SyncUsageExample.swift +44 -50
  259. package/Sources/RunicCore/Sync/TeamSyncRecords.swift +94 -101
  260. package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +51 -46
  261. package/Sources/RunicCore/Team/TeamModels.swift +111 -110
  262. package/Sources/RunicCore/Team/TeamStore.swift +126 -109
  263. package/Sources/RunicCore/UsageFetcher.swift +2 -2
  264. package/Sources/RunicCore/UsageFormatter.swift +25 -25
  265. package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +141 -29
  266. package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +59 -5
  267. package/Sources/RunicCore/UsageLedger/LedgerCache.swift +161 -0
  268. package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +1 -1
  269. package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +40 -25
  270. package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +1 -1
  271. package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +55 -16
  272. package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +6 -6
  273. package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +1 -1
  274. package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +2 -2
  275. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +5 -5
  276. package/Tests/RunicTests/AppDelegateTests.swift +8 -3
  277. package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +2 -3
  278. package/Tests/RunicTests/CLIArgumentParsingTests.swift +3 -4
  279. package/Tests/RunicTests/CLICostTests.swift +6 -7
  280. package/Tests/RunicTests/CLIProviderSelectionTests.swift +2 -3
  281. package/Tests/RunicTests/CLISnapshotTests.swift +3 -4
  282. package/Tests/RunicTests/CLIWebFallbackTests.swift +3 -4
  283. package/Tests/RunicTests/ClaudeOAuthTests.swift +11 -12
  284. package/Tests/RunicTests/ClaudeResilienceTests.swift +3 -4
  285. package/Tests/RunicTests/ClaudeUsageTests.swift +20 -21
  286. package/Tests/RunicTests/CodexOAuthTests.swift +7 -8
  287. package/Tests/RunicTests/CodexUsageLogSourceTests.swift +1 -3
  288. package/Tests/RunicTests/CopilotTokenResolverTests.swift +5 -6
  289. package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +4 -5
  290. package/Tests/RunicTests/CostUsageDecodingTests.swift +15 -17
  291. package/Tests/RunicTests/CostUsagePricingTests.swift +5 -7
  292. package/Tests/RunicTests/CostUsageScannerTests.swift +10 -12
  293. package/Tests/RunicTests/CursorStatusProbeTests.swift +15 -16
  294. package/Tests/RunicTests/FactoryStatusProbeTests.swift +3 -5
  295. package/Tests/RunicTests/GeminiLoginAlertTests.swift +3 -4
  296. package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +11 -11
  297. package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +8 -8
  298. package/Tests/RunicTests/GeminiStatusProbeTests.swift +20 -21
  299. package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +2 -3
  300. package/Tests/RunicTests/InstallOriginTests.swift +1 -2
  301. package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +2 -2
  302. package/Tests/RunicTests/LiveAccountTests.swift +3 -3
  303. package/Tests/RunicTests/LoadingPatternTests.swift +3 -4
  304. package/Tests/RunicTests/MenuCardModelTests.swift +33 -34
  305. package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +4 -1
  306. package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +4 -1
  307. package/Tests/RunicTests/MiniMaxParsingTests.swift +4 -5
  308. package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +4 -1
  309. package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +4 -6
  310. package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +1 -2
  311. package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +5 -6
  312. package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +2 -3
  313. package/Tests/RunicTests/OpenAIDashboardParserTests.swift +12 -13
  314. package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +17 -6
  315. package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +52 -13
  316. package/Tests/RunicTests/PathBuilderTests.swift +14 -15
  317. package/Tests/RunicTests/ProviderFetchPipelineTests.swift +3 -4
  318. package/Tests/RunicTests/ProviderIconResourcesTests.swift +1 -2
  319. package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +1 -2
  320. package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +53 -19
  321. package/Tests/RunicTests/ProviderToggleStoreTests.swift +10 -11
  322. package/Tests/RunicTests/RunicTests.swift +6 -7
  323. package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +5 -6
  324. package/Tests/RunicTests/SettingsStoreTests.swift +149 -45
  325. package/Tests/RunicTests/StatusItemAnimationTests.swift +8 -3
  326. package/Tests/RunicTests/StatusMenuTests.swift +63 -22
  327. package/Tests/RunicTests/StatusProbeTests.swift +28 -29
  328. package/Tests/RunicTests/SubprocessRunnerTests.swift +1 -3
  329. package/Tests/RunicTests/SubscriptionDetectionTests.swift +8 -9
  330. package/Tests/RunicTests/TTYCommandRunnerTests.swift +8 -9
  331. package/Tests/RunicTests/TeamShowbackExporterTests.swift +2 -4
  332. package/Tests/RunicTests/TextParsingTests.swift +1 -2
  333. package/Tests/RunicTests/UsageFormatterTests.swift +15 -16
  334. package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +12 -14
  335. package/Tests/RunicTests/UsageLedgerForecastTests.swift +4 -6
  336. package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +7 -9
  337. package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +6 -8
  338. package/Tests/RunicTests/UsagePaceTests.swift +5 -6
  339. package/Tests/RunicTests/UsagePaceTextTests.swift +8 -9
  340. package/Tests/RunicTests/UsagePolicyEngineTests.swift +3 -5
  341. package/Tests/RunicTests/WidgetSnapshotTests.swift +1 -3
  342. package/Tests/RunicTests/ZaiProviderTests.swift +3 -5
  343. package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +4 -1
  344. package/appcast.xml +36 -37
  345. package/package.json +1 -1
  346. package/runic.png +0 -0
  347. package/runic.svg +15 -0
  348. package/version.env +2 -2
@@ -1,49 +1,49 @@
1
1
  import AppKit
2
2
  import RunicCore
3
3
 
4
- /// **IconRenderer** - Generates menubar status item icons with usage visualization
5
- ///
6
- /// **Purpose:**
7
- /// Creates pixel-perfect 18×18pt (36×36px @2x) template images for the macOS menubar.
8
- /// Renders usage bars, stale indicators, and provider status into the icon.
9
- ///
10
- /// **Responsibilities:**
11
- /// - Generate icons showing session/weekly usage patterns
12
- /// - Apply stale data dimming when data is > 5 minutes old
13
- /// - Cache rendered icons for performance (64 icon cache, 512 morph cache)
14
- /// - Support template (adapts to menubar theme) and vibrant appearances
15
- /// - Render loading animations and error states
16
- ///
17
- /// **Performance:**
18
- /// - **O(1) icon cache lookup** via IconCacheKey hash
19
- /// - **O(1) morph cache** for animation frames
20
- /// - Cap cache sizes via `PerformanceConstants`
21
- /// - All rendering happens off main thread in `dispatchPrecondition(condition: .notOnQueue(.main))`
22
- ///
23
- /// **Dependencies:**
24
- /// - `PerformanceConstants` - Cache sizes and timing
25
- /// - `UsageProvider` - Provider-specific rendering
26
- /// - `AppKit/CoreGraphics` - Image generation
27
- ///
28
- /// **Usage:**
29
- /// ```swift
30
- /// let icon = IconRenderer.render(
31
- /// state: .usage(snap, status),
32
- /// indicator: .none,
33
- /// appearance: .template,
34
- /// dataMode: .remaining
35
- /// )
36
- /// statusItem.button?.image = icon
37
- /// ```
38
-
39
- enum IconAppearance: Sendable {
40
- case template // Adapts to menubar theme (light/dark mode)
41
- case vibrant // Full color rendering
4
+ // **IconRenderer** - Generates menubar status item icons with usage visualization
5
+ //
6
+ // **Purpose:**
7
+ // Creates pixel-perfect 18×18pt (36×36px @2x) template images for the macOS menubar.
8
+ // Renders usage bars, stale indicators, and provider status into the icon.
9
+ //
10
+ // **Responsibilities:**
11
+ // - Generate icons showing session/weekly usage patterns
12
+ // - Apply stale data dimming when data is > 5 minutes old
13
+ // - Cache rendered icons for performance (64 icon cache, 512 morph cache)
14
+ // - Support template (adapts to menubar theme) and vibrant appearances
15
+ // - Render loading animations and error states
16
+ //
17
+ // **Performance:**
18
+ // - **O(1) icon cache lookup** via IconCacheKey hash
19
+ // - **O(1) morph cache** for animation frames
20
+ // - Cap cache sizes via `PerformanceConstants`
21
+ // - All rendering happens off main thread in `dispatchPrecondition(condition: .notOnQueue(.main))`
22
+ //
23
+ // **Dependencies:**
24
+ // - `PerformanceConstants` - Cache sizes and timing
25
+ // - `UsageProvider` - Provider-specific rendering
26
+ // - `AppKit/CoreGraphics` - Image generation
27
+ //
28
+ // **Usage:**
29
+ // ```swift
30
+ // let icon = IconRenderer.render(
31
+ // state: .usage(snap, status),
32
+ // indicator: .none,
33
+ // appearance: .template,
34
+ // dataMode: .remaining
35
+ // )
36
+ // statusItem.button?.image = icon
37
+ // ```
38
+
39
+ enum IconAppearance {
40
+ case template // Adapts to menubar theme (light/dark mode)
41
+ case vibrant // Full color rendering
42
42
  }
43
43
 
44
- enum IconDataMode: Sendable {
45
- case remaining // Show remaining quota
46
- case used // Show used quota
44
+ enum IconDataMode {
45
+ case remaining // Show remaining quota
46
+ case used // Show used quota
47
47
  }
48
48
 
49
49
  enum IconRenderer {
@@ -53,17 +53,18 @@ enum IconRenderer {
53
53
  private static let outputSize = NSSize(width: 38, height: 22)
54
54
  private static let outputScale: CGFloat = 2
55
55
  private static let canvasPx = Int(outputSize.width * outputScale)
56
-
57
- // Infinity symbol base template - loaded lazily from Resources
56
+
57
+ /// Infinity symbol base template - loaded lazily from Resources
58
58
  private static let waveLogoTemplate: NSImage? = {
59
59
  guard let url = Bundle.main.url(forResource: "RunicMenubarIcon", withExtension: "svg"),
60
- let image = NSImage(contentsOf: url) else {
60
+ let image = NSImage(contentsOf: url)
61
+ else {
61
62
  return nil
62
63
  }
63
64
  return image
64
65
  }()
65
66
 
66
- private struct PixelGrid: Sendable {
67
+ private struct PixelGrid {
67
68
  let scale: CGFloat
68
69
 
69
70
  func pt(_ px: Int) -> CGFloat {
@@ -142,14 +143,19 @@ enum IconRenderer {
142
143
  }
143
144
  }
144
145
 
145
- private struct RectPx: Hashable, Sendable {
146
+ private struct RectPx: Hashable {
146
147
  let x: Int
147
148
  let y: Int
148
149
  let w: Int
149
150
  let h: Int
150
151
 
151
- var midXPx: Int { self.x + self.w / 2 }
152
- var midYPx: Int { self.y + self.h / 2 }
152
+ var midXPx: Int {
153
+ self.x + self.w / 2
154
+ }
155
+
156
+ var midYPx: Int {
157
+ self.y + self.h / 2
158
+ }
153
159
 
154
160
  func rect() -> CGRect {
155
161
  Self.grid.rect(x: self.x, y: self.y, w: self.w, h: self.h)
@@ -215,13 +221,13 @@ enum IconRenderer {
215
221
  dataMode: dataMode)
216
222
  let accentColor = Self.vibrantAccentColor(pressure: pressure, stale: stale)
217
223
  let baseColor = appearance == .template ? NSColor.labelColor : accentColor
218
-
224
+
219
225
  // Disable blink effect to prevent flicker
220
226
  let opacityMultiplier = 1.0
221
-
227
+
222
228
  let fillColor = baseColor.withAlphaComponent((stale ? 0.55 : 1.0) * opacityMultiplier)
223
229
  let baseAlpha: CGFloat = (stale ? 0.18 : (appearance == .template ? 0.28 : 0.38)) * opacityMultiplier
224
-
230
+
225
231
  let fillPercent = Self.iconFillPercent(
226
232
  primary: topValue,
227
233
  weekly: bottomValue,
@@ -265,33 +271,33 @@ enum IconRenderer {
265
271
 
266
272
  private static func sigilStyle(for style: IconStyle) -> SigilStyle {
267
273
  switch style {
268
- case .codex: return .codex
269
- case .claude: return .claude
270
- case .zai: return .zai
271
- case .gemini: return .gemini
272
- case .antigravity: return .antigravity
273
- case .cursor: return .cursor
274
- case .factory: return .factory
275
- case .copilot: return .copilot
276
- case .minimax: return .minimax
277
- case .openrouter: return .openrouter
278
- case .groq: return .groq
279
- case .deepseek: return .deepseek
280
- case .fireworks: return .fireworks
281
- case .mistral: return .mistral
282
- case .perplexity: return .perplexity
283
- case .kimi: return .kimi
284
- case .auggie: return .auggie
285
- case .together: return .together
286
- case .cohere: return .cohere
287
- case .xai: return .xai
288
- case .cerebras: return .cerebras
289
- case .sambanova: return .sambanova
290
- case .azure: return .azure
291
- case .bedrock: return .bedrock
292
- case .vertexai: return .vertexai
293
- case .qwen: return .qwen
294
- case .combined: return .combined
274
+ case .codex: .codex
275
+ case .claude: .claude
276
+ case .zai: .zai
277
+ case .gemini: .gemini
278
+ case .antigravity: .antigravity
279
+ case .cursor: .cursor
280
+ case .factory: .factory
281
+ case .copilot: .copilot
282
+ case .minimax: .minimax
283
+ case .openrouter: .openrouter
284
+ case .groq: .groq
285
+ case .deepseek: .deepseek
286
+ case .fireworks: .fireworks
287
+ case .mistral: .mistral
288
+ case .perplexity: .perplexity
289
+ case .kimi: .kimi
290
+ case .auggie: .auggie
291
+ case .together: .together
292
+ case .cohere: .cohere
293
+ case .xai: .xai
294
+ case .cerebras: .cerebras
295
+ case .sambanova: .sambanova
296
+ case .azure: .azure
297
+ case .bedrock: .bedrock
298
+ case .vertexai: .vertexai
299
+ case .qwen: .qwen
300
+ case .combined: .combined
295
301
  }
296
302
  }
297
303
 
@@ -515,7 +521,7 @@ enum IconRenderer {
515
521
  let barHeightPx = 6
516
522
  let totalWidth = barWidthPx * 3 + barGapPx * 2
517
523
  let startX = centerXPx - totalWidth / 2
518
- for idx in 0 ..< 3 {
524
+ for idx in 0..<3 {
519
525
  let x = startX + idx * (barWidthPx + barGapPx)
520
526
  let rect = Self.grid.rect(
521
527
  x: x,
@@ -835,7 +841,8 @@ enum IconRenderer {
835
841
  fillColor: NSColor)
836
842
  {
837
843
  guard let waveLogo = self.waveLogoTemplate,
838
- let ctx = NSGraphicsContext.current?.cgContext else {
844
+ let ctx = NSGraphicsContext.current?.cgContext
845
+ else {
839
846
  return
840
847
  }
841
848
 
@@ -867,7 +874,7 @@ enum IconRenderer {
867
874
  credits: Double?,
868
875
  dataMode: IconDataMode) -> Double
869
876
  {
870
- let values = [primary, weekly, credits].compactMap { $0 }
877
+ let values = [primary, weekly, credits].compactMap(\.self)
871
878
  guard !values.isEmpty else { return 0 }
872
879
  let normalized = values.map { max(0, min($0 / 100, 1)) }
873
880
  switch dataMode {
@@ -884,11 +891,10 @@ enum IconRenderer {
884
891
  let safe = NSColor(calibratedRed: 0.12, green: 0.86, blue: 0.78, alpha: 1)
885
892
  let warn = NSColor(calibratedRed: 1.00, green: 0.72, blue: 0.30, alpha: 1)
886
893
  let hot = NSColor(calibratedRed: 1.00, green: 0.31, blue: 0.44, alpha: 1)
887
- let color: NSColor
888
- if clamped <= 0.5 {
889
- color = self.mixColor(safe, warn, p: clamped / 0.5)
894
+ let color: NSColor = if clamped <= 0.5 {
895
+ self.mixColor(safe, warn, p: clamped / 0.5)
890
896
  } else {
891
- color = self.mixColor(warn, hot, p: (clamped - 0.5) / 0.5)
897
+ self.mixColor(warn, hot, p: (clamped - 0.5) / 0.5)
892
898
  }
893
899
  if stale {
894
900
  return color.withAlphaComponent(0.72)
@@ -19,7 +19,9 @@ struct IconView: View {
19
19
  private let cycleIntervalTicks = 20
20
20
  private let patterns = LoadingPattern.allCases
21
21
 
22
- private var isLoading: Bool { self.showLoadingAnimation && self.snapshot == nil }
22
+ private var isLoading: Bool {
23
+ self.showLoadingAnimation && self.snapshot == nil
24
+ }
23
25
 
24
26
  var body: some View {
25
27
  Group {
@@ -14,12 +14,12 @@ enum RunicColors {
14
14
  /// Ordered palette for chart series. Colors chosen for distinguishability in
15
15
  /// both light and dark mode and for common color-vision deficiencies.
16
16
  static let chartPalette: [Color] = [
17
- Color(red: 0.26, green: 0.55, blue: 0.96), // blue
18
- Color(red: 0.94, green: 0.53, blue: 0.18), // orange
19
- Color(red: 0.46, green: 0.75, blue: 0.36), // green
20
- Color(red: 0.80, green: 0.45, blue: 0.92), // purple
21
- Color(red: 0.26, green: 0.78, blue: 0.86), // teal
22
- Color(red: 0.94, green: 0.74, blue: 0.26), // yellow
17
+ Color(red: 0.26, green: 0.55, blue: 0.96), // blue
18
+ Color(red: 0.94, green: 0.53, blue: 0.18), // orange
19
+ Color(red: 0.46, green: 0.75, blue: 0.36), // green
20
+ Color(red: 0.80, green: 0.45, blue: 0.92), // purple
21
+ Color(red: 0.26, green: 0.78, blue: 0.86), // teal
22
+ Color(red: 0.94, green: 0.74, blue: 0.26), // yellow
23
23
  ]
24
24
 
25
25
  /// Returns a chart color for the given index, wrapping around if needed.
@@ -33,16 +33,16 @@ enum RunicSpacing {
33
33
  // MARK: - Semantic Spacing
34
34
 
35
35
  /// Horizontal padding for menu content
36
- static let menuHorizontalPadding: CGFloat = sm // 12pt
36
+ static let menuHorizontalPadding: CGFloat = sm // 12pt
37
37
 
38
38
  /// Vertical padding for menu content
39
- static let menuVerticalPadding: CGFloat = xs // 8pt
39
+ static let menuVerticalPadding: CGFloat = xs // 8pt
40
40
 
41
41
  /// Spacing between cards in menu
42
- static let cardSpacing: CGFloat = xs // 8pt
42
+ static let cardSpacing: CGFloat = xs // 8pt
43
43
 
44
44
  /// Spacing between major sections
45
- static let sectionSpacing: CGFloat = lg // 24pt
45
+ static let sectionSpacing: CGFloat = lg // 24pt
46
46
 
47
47
  // MARK: - Preferences Spacing
48
48
 
@@ -50,13 +50,13 @@ enum RunicSpacing {
50
50
  static let compact: CGFloat = 6
51
51
 
52
52
  /// Standardized spacing for toggle rows
53
- static let toggleRowSpacing: CGFloat = xs // 8pt
53
+ static let toggleRowSpacing: CGFloat = xs // 8pt
54
54
 
55
55
  /// Spacing after section headers
56
- static let sectionHeaderSpacing: CGFloat = md // 16pt
56
+ static let sectionHeaderSpacing: CGFloat = md // 16pt
57
57
 
58
58
  /// Spacing within stepper controls
59
- static let stepperControlSpacing: CGFloat = xs // 8pt
59
+ static let stepperControlSpacing: CGFloat = xs // 8pt
60
60
 
61
61
  // MARK: - Chart Metrics
62
62
 
@@ -0,0 +1,206 @@
1
+ import CoreText
2
+ import SwiftUI
3
+
4
+ // MARK: - Typography tokens
5
+
6
+ /// App-wide font design token.
7
+ enum RunicTypography {
8
+ static let typeSize: DynamicTypeSize = .xLarge
9
+
10
+ /// Register all TTF/OTF fonts found in the app bundle's Fonts directory.
11
+ /// Call once at launch before any UI is created.
12
+ static func registerFonts() {
13
+ guard let fontsURL = Bundle.main.url(forResource: "Fonts", withExtension: nil)
14
+ ?? Bundle.main.resourceURL?.appendingPathComponent("Fonts")
15
+ else { return }
16
+ guard let enumerator = FileManager.default.enumerator(
17
+ at: fontsURL,
18
+ includingPropertiesForKeys: nil,
19
+ options: [.skipsHiddenFiles])
20
+ else { return }
21
+ for case let fileURL as URL in enumerator
22
+ where fileURL.pathExtension.lowercased() == "ttf" || fileURL.pathExtension.lowercased() == "otf"
23
+ {
24
+ CTFontManagerRegisterFontsForURL(fileURL as CFURL, .process, nil)
25
+ }
26
+ }
27
+
28
+ /// Discover bundled font family names from the Fonts directory.
29
+ static func discoverBundledFontFamilies() -> [String] {
30
+ guard let fontsURL = Bundle.main.url(forResource: "Fonts", withExtension: nil)
31
+ ?? Bundle.main.resourceURL?.appendingPathComponent("Fonts")
32
+ else { return [] }
33
+ guard let enumerator = FileManager.default.enumerator(
34
+ at: fontsURL,
35
+ includingPropertiesForKeys: nil,
36
+ options: [.skipsHiddenFiles])
37
+ else { return [] }
38
+ var families = Set<String>()
39
+ for case let fileURL as URL in enumerator
40
+ where fileURL.pathExtension.lowercased() == "ttf" || fileURL.pathExtension.lowercased() == "otf"
41
+ {
42
+ if let descriptors = CTFontManagerCreateFontDescriptorsFromURL(fileURL as CFURL) as? [CTFontDescriptor] {
43
+ for desc in descriptors {
44
+ if let name = CTFontDescriptorCopyAttribute(desc, kCTFontFamilyNameAttribute) as? String {
45
+ families.insert(name)
46
+ }
47
+ }
48
+ }
49
+ }
50
+ return families.sorted()
51
+ }
52
+ }
53
+
54
+ // MARK: - Available font choices
55
+
56
+ /// A selectable font option shown in preferences.
57
+ struct RunicFontChoice: Identifiable, Hashable {
58
+ let id: String
59
+ let displayName: String
60
+
61
+ /// System fonts — always available, no bundling.
62
+ static let sfPro = RunicFontChoice(id: "__sf_pro__", displayName: "SF Pro")
63
+ static let sfMono = RunicFontChoice(id: "__sf_mono__", displayName: "SF Mono")
64
+
65
+ /// Font used to render this choice's name in the picker (preview in its own typeface).
66
+ var previewFont: Font {
67
+ switch self.id {
68
+ case Self.sfPro.id: .system(.body)
69
+ case Self.sfMono.id: .system(.body, design: .monospaced)
70
+ default: Font.custom(self.id, fixedSize: 13)
71
+ }
72
+ }
73
+
74
+ /// Build the full list: system fonts first, then bundled custom fonts.
75
+ static func availableChoices() -> [RunicFontChoice] {
76
+ var choices: [RunicFontChoice] = [.sfPro, .sfMono]
77
+ for family in RunicTypography.discoverBundledFontFamilies() {
78
+ choices.append(RunicFontChoice(id: family, displayName: family))
79
+ }
80
+ return choices
81
+ }
82
+ }
83
+
84
+ // MARK: - Dynamic SwiftUI Font Scale
85
+
86
+ @MainActor
87
+ enum RunicFont {
88
+ /// The active font family identifier. Updated from SettingsStore.
89
+ static var family: String = "Fira Code"
90
+
91
+ private static var isSystemFont: Bool {
92
+ family.hasPrefix("__")
93
+ }
94
+
95
+ /// Font design used by `.runicTypography()` for system-font fallback.
96
+ static var systemFallbackDesign: Font.Design {
97
+ family == RunicFontChoice.sfPro.id ? .default : .monospaced
98
+ }
99
+
100
+ // MARK: Semantic sizes
101
+
102
+ static var caption2: Font {
103
+ makeFont(size: 10, relativeTo: .caption2)
104
+ }
105
+
106
+ static var caption: Font {
107
+ makeFont(size: 11, relativeTo: .caption)
108
+ }
109
+
110
+ static var footnote: Font {
111
+ makeFont(size: 12, relativeTo: .footnote)
112
+ }
113
+
114
+ static var callout: Font {
115
+ makeFont(size: 13, relativeTo: .callout)
116
+ }
117
+
118
+ static var body: Font {
119
+ makeFont(size: 13, relativeTo: .body)
120
+ }
121
+
122
+ static var subheadline: Font {
123
+ makeFont(size: 12, relativeTo: .subheadline)
124
+ }
125
+
126
+ static var headline: Font {
127
+ makeFont(size: 13, relativeTo: .headline).weight(.bold)
128
+ }
129
+
130
+ static var title3: Font {
131
+ makeFont(size: 15, relativeTo: .title3)
132
+ }
133
+
134
+ static var title2: Font {
135
+ makeFont(size: 17, relativeTo: .title2)
136
+ }
137
+
138
+ static var title: Font {
139
+ makeFont(size: 22, relativeTo: .title)
140
+ }
141
+
142
+ static var largeTitle: Font {
143
+ makeFont(size: 26, relativeTo: .largeTitle)
144
+ }
145
+
146
+ /// Custom fixed size.
147
+ static func system(size: CGFloat) -> Font {
148
+ switch self.family {
149
+ case RunicFontChoice.sfPro.id: .system(size: size)
150
+ case RunicFontChoice.sfMono.id: .system(size: size, design: .monospaced)
151
+ default: Font.custom(self.family, fixedSize: size)
152
+ }
153
+ }
154
+
155
+ /// Custom fixed size with weight.
156
+ static func system(size: CGFloat, weight: Font.Weight) -> Font {
157
+ switch self.family {
158
+ case RunicFontChoice.sfPro.id: .system(size: size, weight: weight)
159
+ case RunicFontChoice.sfMono.id: .system(size: size, weight: weight, design: .monospaced)
160
+ default: Font.custom(self.family, fixedSize: size).weight(weight)
161
+ }
162
+ }
163
+
164
+ // MARK: Private
165
+
166
+ private static func makeFont(size: CGFloat, relativeTo style: Font.TextStyle) -> Font {
167
+ switch self.family {
168
+ case RunicFontChoice.sfPro.id: .system(style)
169
+ case RunicFontChoice.sfMono.id: .system(style, design: .monospaced)
170
+ default: Font.custom(self.family, size: size, relativeTo: style)
171
+ }
172
+ }
173
+ }
174
+
175
+ // MARK: - AppKit NSFont
176
+
177
+ extension RunicFont {
178
+ static func nsFont(size: CGFloat, weight: NSFont.Weight = .regular) -> NSFont {
179
+ switch self.family {
180
+ case RunicFontChoice.sfPro.id:
181
+ return .systemFont(ofSize: size, weight: weight)
182
+ case RunicFontChoice.sfMono.id:
183
+ return .monospacedSystemFont(ofSize: size, weight: weight)
184
+ default:
185
+ let traits: [NSFontDescriptor.TraitKey: Any] = [.weight: weight]
186
+ let descriptor = NSFontDescriptor(fontAttributes: [
187
+ .family: family,
188
+ .traits: traits,
189
+ ])
190
+ return NSFont(descriptor: descriptor, size: size)
191
+ ?? .monospacedSystemFont(ofSize: size, weight: weight)
192
+ }
193
+ }
194
+ }
195
+
196
+ // MARK: - View modifier
197
+
198
+ extension View {
199
+ /// Apply the selected font as the default, with system fallback design and xLarge dynamic type.
200
+ func runicTypography() -> some View {
201
+ self
202
+ .font(RunicFont.body)
203
+ .fontDesign(RunicFont.systemFallbackDesign)
204
+ .dynamicTypeSize(RunicTypography.typeSize)
205
+ }
206
+ }
@@ -1,5 +1,5 @@
1
- import RunicCore
2
1
  import Foundation
2
+ import RunicCore
3
3
  import Security
4
4
 
5
5
  protocol AuggieTokenStoring: Sendable {
@@ -53,29 +53,16 @@ struct KeychainAuggieTokenStore: AuggieTokenStoring {
53
53
  }
54
54
 
55
55
  let data = cleaned!.data(using: .utf8)!
56
- let query = self.baseQuery(dataProtection: false)
57
- let attributes: [String: Any] = [
58
- kSecValueData as String: data,
59
- kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
60
- ]
61
-
62
- // Try update first.
63
- let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
64
- if updateStatus == errSecSuccess { return }
65
-
66
- if updateStatus != errSecItemNotFound {
67
- // Item exists but update failed — delete and re-add to reset ACL.
68
- try? self.deleteToken(dataProtection: false)
69
- }
70
56
 
71
- var addQuery = query
72
- for (key, value) in attributes { addQuery[key] = value }
57
+ // Always delete-then-add to ensure SecAccess ACL is set.
58
+ // SecItemUpdate does NOT update the ACL, so existing items with
59
+ // missing or stale ACLs would continue to prompt for passwords.
60
+ try? self.deleteToken(dataProtection: false)
61
+ try? self.deleteToken(dataProtection: true)
73
62
 
74
- // Grant the calling app permanent access so macOS never shows a
75
- // Keychain password dialog for this item.
76
- if let access = Self.createSelfAccess() {
77
- addQuery[kSecAttrAccess as String] = access
78
- }
63
+ var addQuery = self.baseQuery(dataProtection: false)
64
+ addQuery[kSecValueData as String] = data
65
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
79
66
 
80
67
  let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
68
  guard addStatus == errSecSuccess else {
@@ -91,6 +78,7 @@ struct KeychainAuggieTokenStore: AuggieTokenStoring {
91
78
  var query = self.baseQuery(dataProtection: dataProtection)
92
79
  query[kSecMatchLimit as String] = kSecMatchLimitOne
93
80
  query[kSecReturnData as String] = true
81
+ query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
94
82
 
95
83
  let status = SecItemCopyMatching(query as CFDictionary, &result)
96
84
  if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
@@ -126,18 +114,4 @@ struct KeychainAuggieTokenStore: AuggieTokenStoring {
126
114
  }
127
115
  return q
128
116
  }
129
-
130
- /// Creates a SecAccess that grants the calling application permanent
131
- /// read/write access without triggering the macOS Keychain password dialog.
132
- private static func createSelfAccess() -> SecAccess? {
133
- var trustedSelf: SecTrustedApplication?
134
- guard SecTrustedApplicationCreateFromPath(nil, &trustedSelf) == errSecSuccess,
135
- let trustedSelf else { return nil }
136
- var access: SecAccess?
137
- guard SecAccessCreate(
138
- "Runic API Token" as CFString,
139
- [trustedSelf] as CFArray,
140
- &access) == errSecSuccess else { return nil }
141
- return access
142
- }
143
117
  }
@@ -1,5 +1,5 @@
1
- import RunicCore
2
1
  import Foundation
2
+ import RunicCore
3
3
  import Security
4
4
 
5
5
  protocol AzureOpenAITokenStoring: Sendable {
@@ -53,29 +53,16 @@ struct KeychainAzureOpenAITokenStore: AzureOpenAITokenStoring {
53
53
  }
54
54
 
55
55
  let data = cleaned!.data(using: .utf8)!
56
- let query = self.baseQuery(dataProtection: false)
57
- let attributes: [String: Any] = [
58
- kSecValueData as String: data,
59
- kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
60
- ]
61
-
62
- // Try update first.
63
- let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
64
- if updateStatus == errSecSuccess { return }
65
-
66
- if updateStatus != errSecItemNotFound {
67
- // Item exists but update failed — delete and re-add to reset ACL.
68
- try? self.deleteToken(dataProtection: false)
69
- }
70
56
 
71
- var addQuery = query
72
- for (key, value) in attributes { addQuery[key] = value }
57
+ // Always delete-then-add to ensure SecAccess ACL is set.
58
+ // SecItemUpdate does NOT update the ACL, so existing items with
59
+ // missing or stale ACLs would continue to prompt for passwords.
60
+ try? self.deleteToken(dataProtection: false)
61
+ try? self.deleteToken(dataProtection: true)
73
62
 
74
- // Grant the calling app permanent access so macOS never shows a
75
- // Keychain password dialog for this item.
76
- if let access = Self.createSelfAccess() {
77
- addQuery[kSecAttrAccess as String] = access
78
- }
63
+ var addQuery = self.baseQuery(dataProtection: false)
64
+ addQuery[kSecValueData as String] = data
65
+ addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
79
66
 
80
67
  let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
81
68
  guard addStatus == errSecSuccess else {
@@ -91,6 +78,7 @@ struct KeychainAzureOpenAITokenStore: AzureOpenAITokenStoring {
91
78
  var query = self.baseQuery(dataProtection: dataProtection)
92
79
  query[kSecMatchLimit as String] = kSecMatchLimitOne
93
80
  query[kSecReturnData as String] = true
81
+ query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIFail
94
82
 
95
83
  let status = SecItemCopyMatching(query as CFDictionary, &result)
96
84
  if status == errSecItemNotFound || status == errSecInteractionNotAllowed {
@@ -126,18 +114,4 @@ struct KeychainAzureOpenAITokenStore: AzureOpenAITokenStoring {
126
114
  }
127
115
  return q
128
116
  }
129
-
130
- /// Creates a SecAccess that grants the calling application permanent
131
- /// read/write access without triggering the macOS Keychain password dialog.
132
- private static func createSelfAccess() -> SecAccess? {
133
- var trustedSelf: SecTrustedApplication?
134
- guard SecTrustedApplicationCreateFromPath(nil, &trustedSelf) == errSecSuccess,
135
- let trustedSelf else { return nil }
136
- var access: SecAccess?
137
- guard SecAccessCreate(
138
- "Runic API Token" as CFString,
139
- [trustedSelf] as CFArray,
140
- &access) == errSecSuccess else { return nil }
141
- return access
142
- }
143
117
  }