@sriinnu/runic 1.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 (584) hide show
  1. package/.swiftformat +50 -0
  2. package/.swiftlint.yml +160 -0
  3. package/CHANGELOG.md +307 -0
  4. package/CONTRIBUTING.md +456 -0
  5. package/Icon.icns +0 -0
  6. package/Icon.icon/Assets/runic.png +0 -0
  7. package/Icon.icon/icon.json +36 -0
  8. package/LICENSE +21 -0
  9. package/Package.resolved +51 -0
  10. package/Package.swift +136 -0
  11. package/Packages/Helix/LICENSE +21 -0
  12. package/Packages/Helix/Package.swift +29 -0
  13. package/Packages/Helix/README.md +976 -0
  14. package/Packages/Helix/Sources/Helix/Binding.swift +111 -0
  15. package/Packages/Helix/Sources/Helix/CommandParser.swift +160 -0
  16. package/Packages/Helix/Sources/Helix/CommandSignature.swift +117 -0
  17. package/Packages/Helix/Sources/Helix/ExpressibleFromArgument.swift +72 -0
  18. package/Packages/Helix/Sources/Helix/HelpGenerator.swift +175 -0
  19. package/Packages/Helix/Sources/Helix/NameSpecification.swift +98 -0
  20. package/Packages/Helix/Sources/Helix/ParsableCommand.swift +204 -0
  21. package/Packages/Helix/Sources/Helix/Platform/DarwinPlatformContext.swift +44 -0
  22. package/Packages/Helix/Sources/Helix/Platform/LinuxPlatformContext.swift +34 -0
  23. package/Packages/Helix/Sources/Helix/Platform/PlatformContext+Default.swift +91 -0
  24. package/Packages/Helix/Sources/Helix/Platform/PlatformContext.swift +94 -0
  25. package/Packages/Helix/Sources/Helix/Platform/PlatformPath.swift +167 -0
  26. package/Packages/Helix/Sources/Helix/Platform/Stdio.swift +99 -0
  27. package/Packages/Helix/Sources/Helix/Platform/WebPlatformContext.swift +146 -0
  28. package/Packages/Helix/Sources/Helix/Platform/WindowsPlatformContext.swift +97 -0
  29. package/Packages/Helix/Sources/Helix/Program.swift +204 -0
  30. package/Packages/Helix/Sources/Helix/PropertyWrappers.swift +367 -0
  31. package/Packages/Silo/LICENSE +21 -0
  32. package/Packages/Silo/Package.swift +34 -0
  33. package/Packages/Silo/README.md +466 -0
  34. package/Packages/Silo/Sources/Silo/BinaryCookiesReader.swift +169 -0
  35. package/Packages/Silo/Sources/Silo/BrowserCookieAnalytics.swift +365 -0
  36. package/Packages/Silo/Sources/Silo/BrowserCookieBackends.swift +594 -0
  37. package/Packages/Silo/Sources/Silo/BrowserCookieClient.swift +326 -0
  38. package/Packages/Silo/Sources/Silo/BrowserCookieExport.swift +154 -0
  39. package/Packages/Silo/Sources/Silo/BrowserCookieImport.swift +119 -0
  40. package/Packages/Silo/Sources/Silo/BrowserCookieMockStore.swift +121 -0
  41. package/Packages/Silo/Sources/Silo/BrowserCookieModels.swift +391 -0
  42. package/Packages/Silo/Sources/Silo/ChromiumCookieReader.swift +240 -0
  43. package/Packages/Silo/Sources/Silo/CryptoSupport.swift +86 -0
  44. package/Packages/Silo/Sources/Silo/FirefoxCookieReader.swift +172 -0
  45. package/Packages/Silo/Sources/Silo/LinuxChromiumCookieReader.swift +172 -0
  46. package/Packages/Silo/Sources/Silo/LinuxSecretService.swift +105 -0
  47. package/Packages/Silo/Sources/Silo/MacOSChromiumCookieReader.swift +191 -0
  48. package/Packages/Silo/Sources/Silo/SQLiteSnapshot.swift +41 -0
  49. package/Packages/Silo/Sources/Silo/WindowsChromiumCookieReader.swift +110 -0
  50. package/README.md +109 -0
  51. package/RuniciOS/App/RuniciOSApp.swift +105 -0
  52. package/RuniciOS/Core/iOSUsageStore.swift +192 -0
  53. package/RuniciOS/Views/ProviderDetailView.swift +312 -0
  54. package/RuniciOS/Views/ProviderListView.swift +228 -0
  55. package/RuniciOS/Widgets/LargeWidget.swift +367 -0
  56. package/RuniciOS/Widgets/LiveActivity.swift +342 -0
  57. package/RuniciOS/Widgets/LockScreenWidget.swift +273 -0
  58. package/RuniciOS/Widgets/MediumWidget.swift +370 -0
  59. package/RuniciOS/Widgets/RunicWidget.swift +274 -0
  60. package/RuniciOS/Widgets/SmallWidget.swift +281 -0
  61. package/RuniciOS/Widgets/WidgetModels.swift +285 -0
  62. package/RuniciOS/Widgets/WidgetTimeline.swift +270 -0
  63. package/Scripts/CREATION-SUMMARY.md +367 -0
  64. package/Scripts/DIRECTORY-STRUCTURE.md +358 -0
  65. package/Scripts/GETTING-STARTED-CHECKLIST.md +364 -0
  66. package/Scripts/INDEX.md +365 -0
  67. package/Scripts/QUICK-REFERENCE.md +164 -0
  68. package/Scripts/README-SCRIPTS.md +926 -0
  69. package/Scripts/build-android.sh +526 -0
  70. package/Scripts/build-ios.sh +529 -0
  71. package/Scripts/build-macos.sh +603 -0
  72. package/Scripts/build-windows.sh +504 -0
  73. package/Scripts/build_icon.sh +49 -0
  74. package/Scripts/changelog-to-html.sh +89 -0
  75. package/Scripts/check-release-assets.sh +10 -0
  76. package/Scripts/ci-config.yml +474 -0
  77. package/Scripts/compile_and_run.sh +219 -0
  78. package/Scripts/compile_and_run_adhoc.sh +4 -0
  79. package/Scripts/docs-list.mjs +119 -0
  80. package/Scripts/make_appcast.sh +58 -0
  81. package/Scripts/package_app.sh +409 -0
  82. package/Scripts/release-lib.sh +214 -0
  83. package/Scripts/release.sh +76 -0
  84. package/Scripts/setup-all.sh +387 -0
  85. package/Scripts/setup-ios.sh +404 -0
  86. package/Scripts/setup-react-native.sh +523 -0
  87. package/Scripts/sign-and-notarize.sh +122 -0
  88. package/Scripts/test_live_update.sh +38 -0
  89. package/Scripts/validate_changelog.sh +23 -0
  90. package/Scripts/verify_appcast.sh +92 -0
  91. package/Sources/Runic/About.swift +93 -0
  92. package/Sources/Runic/AppNotifications.swift +115 -0
  93. package/Sources/Runic/Controllers/StatusItemController+Actions.swift +657 -0
  94. package/Sources/Runic/Controllers/StatusItemController+Animation.swift +518 -0
  95. package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +810 -0
  96. package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +87 -0
  97. package/Sources/Runic/Controllers/StatusItemController+Menu.swift +1379 -0
  98. package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +337 -0
  99. package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +395 -0
  100. package/Sources/Runic/Controllers/StatusItemController.swift +360 -0
  101. package/Sources/Runic/Core/BudgetNotificationManager.swift +80 -0
  102. package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +103 -0
  103. package/Sources/Runic/Core/Login/CodexLoginRunner.swift +154 -0
  104. package/Sources/Runic/Core/Login/CursorLoginRunner.swift +206 -0
  105. package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +95 -0
  106. package/Sources/Runic/Core/Rendering/IconRenderer.swift +1216 -0
  107. package/Sources/Runic/Core/Rendering/IconView.swift +120 -0
  108. package/Sources/Runic/Core/Rendering/ProviderBrandIcon.swift +71 -0
  109. package/Sources/Runic/Core/RunicAnimation.swift +53 -0
  110. package/Sources/Runic/Core/RunicColors.swift +73 -0
  111. package/Sources/Runic/Core/RunicCornerRadius.swift +15 -0
  112. package/Sources/Runic/Core/RunicSpacing.swift +68 -0
  113. package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +143 -0
  114. package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +143 -0
  115. package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +143 -0
  116. package/Sources/Runic/Core/Stores/CohereTokenStore.swift +143 -0
  117. package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +143 -0
  118. package/Sources/Runic/Core/Stores/CustomProviderStore.swift +158 -0
  119. package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +143 -0
  120. package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +143 -0
  121. package/Sources/Runic/Core/Stores/GroqTokenStore.swift +143 -0
  122. package/Sources/Runic/Core/Stores/KimiTokenStore.swift +143 -0
  123. package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +143 -0
  124. package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +143 -0
  125. package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +143 -0
  126. package/Sources/Runic/Core/Stores/MistralTokenStore.swift +143 -0
  127. package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +143 -0
  128. package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +143 -0
  129. package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +30 -0
  130. package/Sources/Runic/Core/Stores/QwenTokenStore.swift +143 -0
  131. package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +143 -0
  132. package/Sources/Runic/Core/Stores/SettingsStore.swift +1702 -0
  133. package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +143 -0
  134. package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +20 -0
  135. package/Sources/Runic/Core/Stores/UsageStore+Status.swift +189 -0
  136. package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +33 -0
  137. package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +67 -0
  138. package/Sources/Runic/Core/Stores/UsageStore.swift +3564 -0
  139. package/Sources/Runic/Core/Stores/XAITokenStore.swift +143 -0
  140. package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +143 -0
  141. package/Sources/Runic/Core/UsageExporter.swift +193 -0
  142. package/Sources/Runic/Date+RelativeDescription.swift +21 -0
  143. package/Sources/Runic/HiddenWindowView.swift +32 -0
  144. package/Sources/Runic/InstallOrigin.swift +9 -0
  145. package/Sources/Runic/KeyboardShortcuts+Names.swift +6 -0
  146. package/Sources/Runic/MouseLocationReader.swift +65 -0
  147. package/Sources/Runic/Notifications+Runic.swift +6 -0
  148. package/Sources/Runic/OpenAICreditsPurchaseWindowController.swift +503 -0
  149. package/Sources/Runic/ProviderRegistry.swift +100 -0
  150. package/Sources/Runic/ProviderSwitcherButtons.swift +267 -0
  151. package/Sources/Runic/Providers/Antigravity/AntigravityLoginFlow.swift +11 -0
  152. package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +14 -0
  153. package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +24 -0
  154. package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +52 -0
  155. package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +42 -0
  156. package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +24 -0
  157. package/Sources/Runic/Providers/Claude/ClaudeLoginFlow.swift +26 -0
  158. package/Sources/Runic/Providers/Claude/ClaudeProviderImplementation.swift +14 -0
  159. package/Sources/Runic/Providers/Codex/CodexLoginFlow.swift +18 -0
  160. package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +15 -0
  161. package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +24 -0
  162. package/Sources/Runic/Providers/Copilot/CopilotLoginFlow.swift +193 -0
  163. package/Sources/Runic/Providers/Copilot/CopilotProviderImplementation.swift +110 -0
  164. package/Sources/Runic/Providers/Cursor/CursorLoginFlow.swift +28 -0
  165. package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +15 -0
  166. package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +24 -0
  167. package/Sources/Runic/Providers/Factory/FactoryLoginFlow.swift +12 -0
  168. package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +15 -0
  169. package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +24 -0
  170. package/Sources/Runic/Providers/Gemini/GeminiLoginFlow.swift +20 -0
  171. package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +15 -0
  172. package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +24 -0
  173. package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +24 -0
  174. package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +32 -0
  175. package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +24 -0
  176. package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +24 -0
  177. package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +24 -0
  178. package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +24 -0
  179. package/Sources/Runic/Providers/Shared/ProviderCatalog.swift +15 -0
  180. package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +48 -0
  181. package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +66 -0
  182. package/Sources/Runic/Providers/Shared/ProviderLoginFlow.swift +12 -0
  183. package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +99 -0
  184. package/Sources/Runic/Providers/Shared/SystemSettingsLinks.swift +18 -0
  185. package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +24 -0
  186. package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +24 -0
  187. package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +24 -0
  188. package/Sources/Runic/Resources/ProviderIcon-antigravity.svg +2 -0
  189. package/Sources/Runic/Resources/ProviderIcon-auggie.svg +3 -0
  190. package/Sources/Runic/Resources/ProviderIcon-azure.svg +5 -0
  191. package/Sources/Runic/Resources/ProviderIcon-bedrock.svg +7 -0
  192. package/Sources/Runic/Resources/ProviderIcon-cerebras.svg +3 -0
  193. package/Sources/Runic/Resources/ProviderIcon-claude.svg +1 -0
  194. package/Sources/Runic/Resources/ProviderIcon-codex.svg +1 -0
  195. package/Sources/Runic/Resources/ProviderIcon-cohere.svg +3 -0
  196. package/Sources/Runic/Resources/ProviderIcon-copilot.svg +1 -0
  197. package/Sources/Runic/Resources/ProviderIcon-cursor.svg +3 -0
  198. package/Sources/Runic/Resources/ProviderIcon-deepseek.svg +4 -0
  199. package/Sources/Runic/Resources/ProviderIcon-factory.svg +3 -0
  200. package/Sources/Runic/Resources/ProviderIcon-fireworks.svg +3 -0
  201. package/Sources/Runic/Resources/ProviderIcon-gemini.svg +1 -0
  202. package/Sources/Runic/Resources/ProviderIcon-groq.svg +1 -0
  203. package/Sources/Runic/Resources/ProviderIcon-kimi.svg +4 -0
  204. package/Sources/Runic/Resources/ProviderIcon-minimax.svg +1 -0
  205. package/Sources/Runic/Resources/ProviderIcon-mistral.svg +5 -0
  206. package/Sources/Runic/Resources/ProviderIcon-openrouter.svg +7 -0
  207. package/Sources/Runic/Resources/ProviderIcon-perplexity.svg +4 -0
  208. package/Sources/Runic/Resources/ProviderIcon-qwen.svg +9 -0
  209. package/Sources/Runic/Resources/ProviderIcon-sambanova.svg +4 -0
  210. package/Sources/Runic/Resources/ProviderIcon-together.svg +5 -0
  211. package/Sources/Runic/Resources/ProviderIcon-vertexai.svg +17 -0
  212. package/Sources/Runic/Resources/ProviderIcon-xai.svg +3 -0
  213. package/Sources/Runic/Resources/ProviderIcon-zai.svg +5 -0
  214. package/Sources/Runic/Resources/RunicMenubarIcon.svg +7 -0
  215. package/Sources/Runic/RunicApp.swift +357 -0
  216. package/Sources/Runic/SessionQuotaNotifications.swift +58 -0
  217. package/Sources/Runic/UsagePaceText.swift +52 -0
  218. package/Sources/Runic/UsageProgressBar.swift +224 -0
  219. package/Sources/Runic/Utilities/DisplayLink.swift +96 -0
  220. package/Sources/Runic/Utilities/LoadingPattern.swift +60 -0
  221. package/Sources/Runic/Utilities/PerformanceConstants.swift +85 -0
  222. package/Sources/Runic/Views/Menu/AlertsMenuView.swift +256 -0
  223. package/Sources/Runic/Views/Menu/ChartPanelStyle.swift +22 -0
  224. package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +275 -0
  225. package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +377 -0
  226. package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +312 -0
  227. package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +312 -0
  228. package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +228 -0
  229. package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +228 -0
  230. package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +162 -0
  231. package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +64 -0
  232. package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +268 -0
  233. package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +104 -0
  234. package/Sources/Runic/Views/Menu/MenuCardView.swift +1900 -0
  235. package/Sources/Runic/Views/Menu/MenuContent.swift +150 -0
  236. package/Sources/Runic/Views/Menu/MenuDescriptor.swift +369 -0
  237. package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +42 -0
  238. package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +235 -0
  239. package/Sources/Runic/Views/Menu/OverviewMenuView.swift +141 -0
  240. package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +209 -0
  241. package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +220 -0
  242. package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +88 -0
  243. package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +201 -0
  244. package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +398 -0
  245. package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +232 -0
  246. package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +417 -0
  247. package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +190 -0
  248. package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +132 -0
  249. package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +618 -0
  250. package/Sources/Runic/Views/Preferences/LiquidComponents.swift +305 -0
  251. package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +271 -0
  252. package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +268 -0
  253. package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +422 -0
  254. package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +641 -0
  255. package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +344 -0
  256. package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +230 -0
  257. package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +322 -0
  258. package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +469 -0
  259. package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +231 -0
  260. package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +50 -0
  261. package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +497 -0
  262. package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +288 -0
  263. package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +3435 -0
  264. package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +25 -0
  265. package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +59 -0
  266. package/Sources/Runic/Views/Preferences/PreferencesView.swift +206 -0
  267. package/Sources/Runic/Views/Rating/QualityRatingView.swift +238 -0
  268. package/Sources/Runic/Views/Team/TeamDashboardView.swift +371 -0
  269. package/Sources/Runic/Views/Team/TeamInviteSheet.swift +175 -0
  270. package/Sources/Runic/Views/Team/TeamManagementView.swift +761 -0
  271. package/Sources/Runic/Views/Team/TeamMemberRow.swift +181 -0
  272. package/Sources/RunicCLI/CLIEntry.swift +942 -0
  273. package/Sources/RunicCLI/CLIRenderer.swift +87 -0
  274. package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +588 -0
  275. package/Sources/RunicCLI/Commands/ModelsCommand.swift +362 -0
  276. package/Sources/RunicCLI/Commands/ProjectsCommand.swift +439 -0
  277. package/Sources/RunicCLI/Commands/ResetCommand.swift +429 -0
  278. package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +168 -0
  279. package/Sources/RunicClaudeWatchdog/main.swift +122 -0
  280. package/Sources/RunicClaudeWebProbe/main.swift +62 -0
  281. package/Sources/RunicCore/AlertRuleStore.swift +187 -0
  282. package/Sources/RunicCore/BrowserCookieImportOrder.swift +25 -0
  283. package/Sources/RunicCore/CopilotUsageModels.swift +84 -0
  284. package/Sources/RunicCore/CostUsageFetcher.swift +119 -0
  285. package/Sources/RunicCore/CostUsageModels.swift +479 -0
  286. package/Sources/RunicCore/CreditsModels.swift +50 -0
  287. package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +269 -0
  288. package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +622 -0
  289. package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +500 -0
  290. package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +679 -0
  291. package/Sources/RunicCore/Host/Process/SubprocessRunner.swift +135 -0
  292. package/Sources/RunicCore/Integrations/GitHubIntegration.swift +173 -0
  293. package/Sources/RunicCore/Logging/JSONStderrLogHandler.swift +79 -0
  294. package/Sources/RunicCore/Logging/OSLogLogHandler.swift +86 -0
  295. package/Sources/RunicCore/Logging/RunicLog.swift +123 -0
  296. package/Sources/RunicCore/Models/EnhancedUsageModels.swift +371 -0
  297. package/Sources/RunicCore/OpenAIDashboardModels.swift +210 -0
  298. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +676 -0
  299. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +492 -0
  300. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardNavigationDelegate.swift +51 -0
  301. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardParser.swift +483 -0
  302. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardScrapeScript.swift +558 -0
  303. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebViewCache.swift +204 -0
  304. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebsiteDataStore.swift +78 -0
  305. package/Sources/RunicCore/PathEnvironment.swift +482 -0
  306. package/Sources/RunicCore/Performance/PerformanceModels.swift +371 -0
  307. package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +409 -0
  308. package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +178 -0
  309. package/Sources/RunicCore/ProjectBudgetStore.swift +99 -0
  310. package/Sources/RunicCore/ProviderCostSnapshot.swift +29 -0
  311. package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +66 -0
  312. package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +685 -0
  313. package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +83 -0
  314. package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +142 -0
  315. package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +141 -0
  316. package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +186 -0
  317. package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +126 -0
  318. package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +168 -0
  319. package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +84 -0
  320. package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +75 -0
  321. package/Sources/RunicCore/Providers/Claude/ClaudeCLISession.swift +332 -0
  322. package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift +180 -0
  323. package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +137 -0
  324. package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +207 -0
  325. package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +654 -0
  326. package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +28 -0
  327. package/Sources/RunicCore/Providers/Claude/ClaudeUsageFetcher.swift +480 -0
  328. package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +780 -0
  329. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +146 -0
  330. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +269 -0
  331. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexTokenRefresher.swift +100 -0
  332. package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +243 -0
  333. package/Sources/RunicCore/Providers/Codex/CodexStatusProbe.swift +125 -0
  334. package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +24 -0
  335. package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +181 -0
  336. package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +83 -0
  337. package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +85 -0
  338. package/Sources/RunicCore/Providers/Copilot/CopilotDeviceFlow.swift +123 -0
  339. package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +114 -0
  340. package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +100 -0
  341. package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +291 -0
  342. package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +386 -0
  343. package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +65 -0
  344. package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +594 -0
  345. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +85 -0
  346. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +108 -0
  347. package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +398 -0
  348. package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +65 -0
  349. package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +1275 -0
  350. package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +84 -0
  351. package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +75 -0
  352. package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +65 -0
  353. package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +835 -0
  354. package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +83 -0
  355. package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +71 -0
  356. package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +83 -0
  357. package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +77 -0
  358. package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +92 -0
  359. package/Sources/RunicCore/Providers/MiniMax/MiniMaxSettingsReader.swift +54 -0
  360. package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +171 -0
  361. package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +911 -0
  362. package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +84 -0
  363. package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +75 -0
  364. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +87 -0
  365. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +207 -0
  366. package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +84 -0
  367. package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +75 -0
  368. package/Sources/RunicCore/Providers/ProviderBranding.swift +25 -0
  369. package/Sources/RunicCore/Providers/ProviderCLIConfig.swift +17 -0
  370. package/Sources/RunicCore/Providers/ProviderDescriptor.swift +129 -0
  371. package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +263 -0
  372. package/Sources/RunicCore/Providers/ProviderSettingsSnapshot.swift +98 -0
  373. package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +501 -0
  374. package/Sources/RunicCore/Providers/ProviderVersionDetector.swift +68 -0
  375. package/Sources/RunicCore/Providers/Providers.swift +184 -0
  376. package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +84 -0
  377. package/Sources/RunicCore/Providers/Qwen/QwenSettingsReader.swift +30 -0
  378. package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +317 -0
  379. package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +84 -0
  380. package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +75 -0
  381. package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +83 -0
  382. package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +75 -0
  383. package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +123 -0
  384. package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +48 -0
  385. package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +144 -0
  386. package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +83 -0
  387. package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +75 -0
  388. package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +72 -0
  389. package/Sources/RunicCore/Providers/Zai/ZaiSettingsReader.swift +41 -0
  390. package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +724 -0
  391. package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +385 -0
  392. package/Sources/RunicCore/Sync/SyncConflictResolver.swift +310 -0
  393. package/Sources/RunicCore/Sync/SyncProtocol.swift +288 -0
  394. package/Sources/RunicCore/Sync/SyncRecord.swift +405 -0
  395. package/Sources/RunicCore/Sync/SyncUsageExample.swift +294 -0
  396. package/Sources/RunicCore/Sync/TeamSyncRecords.swift +775 -0
  397. package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +333 -0
  398. package/Sources/RunicCore/Team/TeamModels.swift +583 -0
  399. package/Sources/RunicCore/Team/TeamShowbackExporter.swift +495 -0
  400. package/Sources/RunicCore/Team/TeamStore.swift +792 -0
  401. package/Sources/RunicCore/TextParsing.swift +55 -0
  402. package/Sources/RunicCore/UsageFetcher.swift +694 -0
  403. package/Sources/RunicCore/UsageFormatter.swift +410 -0
  404. package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +329 -0
  405. package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +448 -0
  406. package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +550 -0
  407. package/Sources/RunicCore/UsageLedger/UsageLedger.swift +29 -0
  408. package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +660 -0
  409. package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +209 -0
  410. package/Sources/RunicCore/UsageLedger/UsageLedgerModels.swift +417 -0
  411. package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +245 -0
  412. package/Sources/RunicCore/UsagePace.swift +81 -0
  413. package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +73 -0
  414. package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +74 -0
  415. package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +208 -0
  416. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Claude.swift +455 -0
  417. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Timestamp.swift +132 -0
  418. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +589 -0
  419. package/Sources/RunicCore/WidgetSnapshot.swift +183 -0
  420. package/Sources/RunicMacroSupport/ProviderRegistrationMacros.swift +14 -0
  421. package/Sources/RunicMacros/ProviderRegistrationMacros.swift +216 -0
  422. package/Sources/RunicWidget/RunicWidgetBundle.swift +79 -0
  423. package/Sources/RunicWidget/RunicWidgetProvider.swift +283 -0
  424. package/Sources/RunicWidget/RunicWidgetViews.swift +680 -0
  425. package/Tests/RunicTests/AntigravityStatusProbeTests.swift +50 -0
  426. package/Tests/RunicTests/AppDelegateTests.swift +43 -0
  427. package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +22 -0
  428. package/Tests/RunicTests/CLIArgumentParsingTests.swift +70 -0
  429. package/Tests/RunicTests/CLICostTests.swift +65 -0
  430. package/Tests/RunicTests/CLIProviderSelectionTests.swift +22 -0
  431. package/Tests/RunicTests/CLISnapshotTests.swift +72 -0
  432. package/Tests/RunicTests/CLIWebFallbackTests.swift +58 -0
  433. package/Tests/RunicTests/ClaudeOAuthTests.swift +164 -0
  434. package/Tests/RunicTests/ClaudeResilienceTests.swift +30 -0
  435. package/Tests/RunicTests/ClaudeUsageTests.swift +370 -0
  436. package/Tests/RunicTests/CodexOAuthTests.swift +122 -0
  437. package/Tests/RunicTests/CodexUsageLogSourceTests.swift +82 -0
  438. package/Tests/RunicTests/CopilotTokenResolverTests.swift +75 -0
  439. package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +75 -0
  440. package/Tests/RunicTests/CostUsageDecodingTests.swift +394 -0
  441. package/Tests/RunicTests/CostUsagePricingTests.swift +50 -0
  442. package/Tests/RunicTests/CostUsageScannerTests.swift +700 -0
  443. package/Tests/RunicTests/CursorStatusProbeTests.swift +323 -0
  444. package/Tests/RunicTests/FactoryStatusProbeTests.swift +69 -0
  445. package/Tests/RunicTests/GeminiAPITestHelpers.swift +94 -0
  446. package/Tests/RunicTests/GeminiLoginAlertTests.swift +28 -0
  447. package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +283 -0
  448. package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +279 -0
  449. package/Tests/RunicTests/GeminiStatusProbeTests.swift +233 -0
  450. package/Tests/RunicTests/GeminiTestEnvironment.swift +87 -0
  451. package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +73 -0
  452. package/Tests/RunicTests/InstallOriginTests.swift +18 -0
  453. package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +9 -0
  454. package/Tests/RunicTests/LiveAccountTests.swift +24 -0
  455. package/Tests/RunicTests/LoadingPatternTests.swift +35 -0
  456. package/Tests/RunicTests/MenuCardModelTests.swift +545 -0
  457. package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +8 -0
  458. package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +8 -0
  459. package/Tests/RunicTests/MiniMaxParsingTests.swift +68 -0
  460. package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +8 -0
  461. package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +112 -0
  462. package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +17 -0
  463. package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +75 -0
  464. package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +27 -0
  465. package/Tests/RunicTests/OpenAIDashboardParserTests.swift +134 -0
  466. package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +53 -0
  467. package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +92 -0
  468. package/Tests/RunicTests/PathBuilderTests.swift +237 -0
  469. package/Tests/RunicTests/ProviderFetchPipelineTests.swift +160 -0
  470. package/Tests/RunicTests/ProviderIconResourcesTests.swift +38 -0
  471. package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +16 -0
  472. package/Tests/RunicTests/ProviderRegistryTests.swift +29 -0
  473. package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +202 -0
  474. package/Tests/RunicTests/ProviderToggleStoreTests.swift +51 -0
  475. package/Tests/RunicTests/RunicTests.swift +120 -0
  476. package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +36 -0
  477. package/Tests/RunicTests/SettingsStoreTests.swift +161 -0
  478. package/Tests/RunicTests/StatusItemAnimationTests.swift +49 -0
  479. package/Tests/RunicTests/StatusMenuTests.swift +434 -0
  480. package/Tests/RunicTests/StatusProbeTests.swift +377 -0
  481. package/Tests/RunicTests/SubprocessRunnerTests.swift +20 -0
  482. package/Tests/RunicTests/SubscriptionDetectionTests.swift +65 -0
  483. package/Tests/RunicTests/TTYCommandRunnerTests.swift +148 -0
  484. package/Tests/RunicTests/TTYIntegrationTests.swift +41 -0
  485. package/Tests/RunicTests/TeamShowbackExporterTests.swift +159 -0
  486. package/Tests/RunicTests/TextParsingTests.swift +12 -0
  487. package/Tests/RunicTests/UsageFormatterTests.swift +112 -0
  488. package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +172 -0
  489. package/Tests/RunicTests/UsageLedgerForecastTests.swift +213 -0
  490. package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +83 -0
  491. package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +170 -0
  492. package/Tests/RunicTests/UsagePaceTests.swift +77 -0
  493. package/Tests/RunicTests/UsagePaceTextTests.swift +109 -0
  494. package/Tests/RunicTests/UsagePolicyEngineTests.swift +91 -0
  495. package/Tests/RunicTests/WidgetSnapshotTests.swift +45 -0
  496. package/Tests/RunicTests/ZaiProviderTests.swift +61 -0
  497. package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +6 -0
  498. package/TestsLinux/PlatformGatingTests.swift +46 -0
  499. package/appcast.xml +105 -0
  500. package/assets/logo-concept-stack.svg +24 -0
  501. package/assets/logo-minimal-bars.svg +15 -0
  502. package/assets/logo.svg +20 -0
  503. package/assets/logo2.svg +43 -0
  504. package/bin/docs-list +5 -0
  505. package/bin/install-runic-cli.sh +32 -0
  506. package/build-and-run.sh +369 -0
  507. package/docs/.nojekyll +1 -0
  508. package/docs/CNAME +1 -0
  509. package/docs/antigravity.md +75 -0
  510. package/docs/architecture.md +32 -0
  511. package/docs/claude.md +93 -0
  512. package/docs/cli-commands/integration_guide.md +371 -0
  513. package/docs/cli-commands/quick_reference.md +362 -0
  514. package/docs/cli-commands/readme.md +361 -0
  515. package/docs/cli.md +156 -0
  516. package/docs/cloudkit_sync_guide.md +495 -0
  517. package/docs/codebase_review.md +288 -0
  518. package/docs/codex-oauth.md +635 -0
  519. package/docs/codex.md +104 -0
  520. package/docs/codexbar.png +0 -0
  521. package/docs/copilot.md +44 -0
  522. package/docs/cursor.md +46 -0
  523. package/docs/design/designcomparisonchart.md +510 -0
  524. package/docs/design/implementationguide.md +792 -0
  525. package/docs/design/menubaricondesignconcepts.md +796 -0
  526. package/docs/design/mockups/hybridiconconcept.svg +62 -0
  527. package/docs/design/mockups/infinityiconconcept.svg +41 -0
  528. package/docs/design/mockups/infinityiconstates.svg +129 -0
  529. package/docs/design/mockups/wavelogorefined.svg +48 -0
  530. package/docs/design/quick_summary.md +426 -0
  531. package/docs/design/readme.md +478 -0
  532. package/docs/design/sizespecifications.md +575 -0
  533. package/docs/design-system.md +836 -0
  534. package/docs/factory.md +103 -0
  535. package/docs/gemini.md +73 -0
  536. package/docs/icon.md +37 -0
  537. package/docs/icon.png +0 -0
  538. package/docs/index.html +175 -0
  539. package/docs/keychain_fix.md +274 -0
  540. package/docs/minimax.md +71 -0
  541. package/docs/packaging.md +25 -0
  542. package/docs/progress-bar-improvements.md +240 -0
  543. package/docs/progress-bar-quick-reference.md +283 -0
  544. package/docs/progress-bar-technical-details.md +432 -0
  545. package/docs/progress-bar-visual-test.swift +375 -0
  546. package/docs/provider.md +202 -0
  547. package/docs/providers.md +86 -0
  548. package/docs/refactor/macros.md +35 -0
  549. package/docs/refresh-loop.md +22 -0
  550. package/docs/releasing-homebrew.md +35 -0
  551. package/docs/releasing.md +121 -0
  552. package/docs/research/concrete_todo_2026-02-22.md +47 -0
  553. package/docs/research/core-feature-research.md +248 -0
  554. package/docs/research/feature_uiux_roadmap_2026-02-22.md +202 -0
  555. package/docs/research/parity_matrix.md +49 -0
  556. package/docs/research/provider_expansion_candidates_2026-02-22.md +80 -0
  557. package/docs/research/provider_model_token_project_matrix_2026-02-23.md +46 -0
  558. package/docs/research/release_gate_checklist.md +57 -0
  559. package/docs/site.css +505 -0
  560. package/docs/sparkle.md +28 -0
  561. package/docs/status.md +28 -0
  562. package/docs/sync/implementation.md +393 -0
  563. package/docs/sync/readme.md +317 -0
  564. package/docs/sync_files_summary.md +391 -0
  565. package/docs/sync_quick_reference.md +225 -0
  566. package/docs/todo.md +11 -0
  567. package/docs/ui.md +29 -0
  568. package/docs/web-integration.md +9 -0
  569. package/docs/widgets.md +85 -0
  570. package/docs/workflows/error-handling-improvements.md +434 -0
  571. package/docs/workflows/folder_structure.md +299 -0
  572. package/docs/workflows/loading_states.md +446 -0
  573. package/docs/workflows/loading_states_examples.md +565 -0
  574. package/docs/workflows/loading_states_flow.md +430 -0
  575. package/docs/workflows/performance_optimizations.md +457 -0
  576. package/docs/workflows/phase1-cli-testing-report.md +313 -0
  577. package/docs/workflows/quick_reference.md +228 -0
  578. package/docs/workflows/readme.md +59 -0
  579. package/docs/workflows/recent_improvements.md +275 -0
  580. package/docs/workflows/sync_enablement_summary.md +400 -0
  581. package/docs/zai.md +43 -0
  582. package/package.json +36 -0
  583. package/runic.png +0 -0
  584. package/version.env +2 -0
@@ -0,0 +1,1216 @@
1
+ import AppKit
2
+ import RunicCore
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
42
+ }
43
+
44
+ enum IconDataMode: Sendable {
45
+ case remaining // Show remaining quota
46
+ case used // Show used quota
47
+ }
48
+
49
+ enum IconRenderer {
50
+ private static let creditsCap: Double = 1000
51
+ private static let baseSize = NSSize(width: 38, height: 22)
52
+ // Render to a 38×22 pt template (76×44 px at 2×) for better visibility.
53
+ private static let outputSize = NSSize(width: 38, height: 22)
54
+ private static let outputScale: CGFloat = 2
55
+ private static let canvasPx = Int(outputSize.width * outputScale)
56
+
57
+ // Infinity symbol base template - loaded lazily from Resources
58
+ private static let waveLogoTemplate: NSImage? = {
59
+ guard let url = Bundle.main.url(forResource: "RunicMenubarIcon", withExtension: "svg"),
60
+ let image = NSImage(contentsOf: url) else {
61
+ return nil
62
+ }
63
+ return image
64
+ }()
65
+
66
+ private struct PixelGrid: Sendable {
67
+ let scale: CGFloat
68
+
69
+ func pt(_ px: Int) -> CGFloat {
70
+ CGFloat(px) / self.scale
71
+ }
72
+
73
+ func rect(x: Int, y: Int, w: Int, h: Int) -> CGRect {
74
+ CGRect(x: self.pt(x), y: self.pt(y), width: self.pt(w), height: self.pt(h))
75
+ }
76
+
77
+ func snapDelta(_ value: CGFloat) -> CGFloat {
78
+ (value * self.scale).rounded() / self.scale
79
+ }
80
+ }
81
+
82
+ private static let grid = PixelGrid(scale: outputScale)
83
+
84
+ private struct IconCacheKey: Hashable {
85
+ let primary: Int
86
+ let weekly: Int
87
+ let credits: Int
88
+ let stale: Bool
89
+ let style: Int
90
+ let indicator: Int
91
+ let appearance: Int
92
+ let dataMode: Int
93
+ }
94
+
95
+ private final class IconCacheStore: @unchecked Sendable {
96
+ private var cache: [IconCacheKey: NSImage] = [:]
97
+ private var order: [IconCacheKey] = []
98
+ private let lock = NSLock()
99
+
100
+ func cachedIcon(for key: IconCacheKey) -> NSImage? {
101
+ self.lock.lock()
102
+ defer { self.lock.unlock() }
103
+ guard let image = self.cache[key] else { return nil }
104
+ if let idx = self.order.firstIndex(of: key) {
105
+ self.order.remove(at: idx)
106
+ self.order.append(key)
107
+ }
108
+ return image
109
+ }
110
+
111
+ func storeIcon(_ image: NSImage, for key: IconCacheKey, limit: Int) {
112
+ self.lock.lock()
113
+ defer { self.lock.unlock() }
114
+ self.cache[key] = image
115
+ self.order.removeAll { $0 == key }
116
+ self.order.append(key)
117
+ while self.order.count > limit {
118
+ let oldest = self.order.removeFirst()
119
+ self.cache.removeValue(forKey: oldest)
120
+ }
121
+ }
122
+ }
123
+
124
+ private static let iconCacheStore = IconCacheStore()
125
+ private static let iconCacheLimit = PerformanceConstants.iconCacheSize
126
+ private static let morphBucketCount = 200
127
+ private static let morphCache = MorphCache(limit: PerformanceConstants.morphCacheSize)
128
+
129
+ private final class MorphCache: @unchecked Sendable {
130
+ private let cache = NSCache<NSNumber, NSImage>()
131
+
132
+ init(limit: Int) {
133
+ self.cache.countLimit = limit
134
+ }
135
+
136
+ func image(for key: NSNumber) -> NSImage? {
137
+ self.cache.object(forKey: key)
138
+ }
139
+
140
+ func set(_ image: NSImage, for key: NSNumber) {
141
+ self.cache.setObject(image, forKey: key)
142
+ }
143
+ }
144
+
145
+ private struct RectPx: Hashable, Sendable {
146
+ let x: Int
147
+ let y: Int
148
+ let w: Int
149
+ let h: Int
150
+
151
+ var midXPx: Int { self.x + self.w / 2 }
152
+ var midYPx: Int { self.y + self.h / 2 }
153
+
154
+ func rect() -> CGRect {
155
+ Self.grid.rect(x: self.x, y: self.y, w: self.w, h: self.h)
156
+ }
157
+
158
+ private static let grid = IconRenderer.grid
159
+ }
160
+
161
+ private enum SigilStyle {
162
+ case codex
163
+ case claude
164
+ case gemini
165
+ case antigravity
166
+ case cursor
167
+ case factory
168
+ case copilot
169
+ case minimax
170
+ case openrouter
171
+ case groq
172
+ case deepseek
173
+ case fireworks
174
+ case mistral
175
+ case perplexity
176
+ case kimi
177
+ case auggie
178
+ case together
179
+ case cohere
180
+ case xai
181
+ case cerebras
182
+ case sambanova
183
+ case azure
184
+ case bedrock
185
+ case vertexai
186
+ case zai
187
+ case qwen
188
+ case combined
189
+ }
190
+
191
+ // swiftlint:disable function_body_length
192
+ static func makeIcon(
193
+ primaryRemaining: Double?,
194
+ weeklyRemaining: Double?,
195
+ creditsRemaining: Double?,
196
+ stale: Bool,
197
+ style: IconStyle,
198
+ blink: CGFloat = 0,
199
+ wiggle: CGFloat = 0,
200
+ tilt: CGFloat = 0,
201
+ statusIndicator: ProviderStatusIndicator = .none,
202
+ appearance: IconAppearance = .template,
203
+ dataMode: IconDataMode = .remaining) -> NSImage
204
+ {
205
+ let shouldCache = blink <= 0.0001 && wiggle <= 0.0001 && tilt <= 0.0001
206
+ let render = {
207
+ self.renderImage(isTemplate: appearance == .template) {
208
+ let topValue = primaryRemaining
209
+ let bottomValue = weeklyRemaining
210
+ let creditsRatio = creditsRemaining.map { min($0 / Self.creditsCap * 100, 100) }
211
+ let pressure = Self.usagePressure(
212
+ primary: topValue,
213
+ weekly: bottomValue,
214
+ credits: creditsRatio,
215
+ dataMode: dataMode)
216
+ let accentColor = Self.vibrantAccentColor(pressure: pressure, stale: stale)
217
+ let baseColor = appearance == .template ? NSColor.labelColor : accentColor
218
+
219
+ // Disable blink effect to prevent flicker
220
+ let opacityMultiplier = 1.0
221
+
222
+ let fillColor = baseColor.withAlphaComponent((stale ? 0.55 : 1.0) * opacityMultiplier)
223
+ let baseAlpha: CGFloat = (stale ? 0.18 : (appearance == .template ? 0.28 : 0.38)) * opacityMultiplier
224
+
225
+ let fillPercent = Self.iconFillPercent(
226
+ primary: topValue,
227
+ weekly: bottomValue,
228
+ credits: creditsRatio,
229
+ dataMode: dataMode)
230
+
231
+ Self.drawWaveLogo(
232
+ fillPercent: fillPercent,
233
+ baseColor: baseColor.withAlphaComponent(baseAlpha),
234
+ fillColor: fillColor)
235
+
236
+ let overlayColor = appearance == .template
237
+ ? NSColor.labelColor
238
+ : NSColor.white.withAlphaComponent(0.92)
239
+ Self.drawStatusOverlay(indicator: statusIndicator, color: overlayColor)
240
+ }
241
+ }
242
+
243
+ if shouldCache {
244
+ let key = IconCacheKey(
245
+ primary: self.quantizedPercent(primaryRemaining),
246
+ weekly: self.quantizedPercent(weeklyRemaining),
247
+ credits: self.quantizedCredits(creditsRemaining),
248
+ stale: stale,
249
+ style: self.styleKey(style),
250
+ indicator: self.indicatorKey(statusIndicator),
251
+ appearance: self.appearanceKey(appearance),
252
+ dataMode: self.dataModeKey(dataMode))
253
+ if let cached = self.cachedIcon(for: key) {
254
+ return cached
255
+ }
256
+ let image = render()
257
+ self.storeIcon(image, for: key)
258
+ return image
259
+ }
260
+
261
+ return render()
262
+ }
263
+
264
+ // swiftlint:enable function_body_length
265
+
266
+ private static func sigilStyle(for style: IconStyle) -> SigilStyle {
267
+ 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
295
+ }
296
+ }
297
+
298
+ private static func chamferedPath(rectPx: RectPx, chamferPx: Int) -> NSBezierPath {
299
+ let rect = rectPx.rect()
300
+ let chamfer = Self.grid.pt(chamferPx)
301
+ let x = rect.minX
302
+ let y = rect.minY
303
+ let w = rect.width
304
+ let h = rect.height
305
+ let path = NSBezierPath()
306
+ path.move(to: NSPoint(x: x + chamfer, y: y))
307
+ path.line(to: NSPoint(x: x + w - chamfer, y: y))
308
+ path.line(to: NSPoint(x: x + w, y: y + chamfer))
309
+ path.line(to: NSPoint(x: x + w, y: y + h - chamfer))
310
+ path.line(to: NSPoint(x: x + w - chamfer, y: y + h))
311
+ path.line(to: NSPoint(x: x + chamfer, y: y + h))
312
+ path.line(to: NSPoint(x: x, y: y + h - chamfer))
313
+ path.line(to: NSPoint(x: x, y: y + chamfer))
314
+ path.close()
315
+ return path
316
+ }
317
+
318
+ private static func drawSigil(_ style: SigilStyle, in rectPx: RectPx) {
319
+ guard let ctx = NSGraphicsContext.current?.cgContext else { return }
320
+ let centerXPx = rectPx.midXPx
321
+ let centerYPx = rectPx.y + rectPx.h / 2
322
+
323
+ func point(x: Int, y: Int) -> NSPoint {
324
+ NSPoint(x: Self.grid.pt(x), y: Self.grid.pt(y))
325
+ }
326
+
327
+ ctx.saveGState()
328
+ ctx.setBlendMode(.clear)
329
+ ctx.setShouldAntialias(true)
330
+
331
+ switch style {
332
+ case .codex:
333
+ let eyeSizePx = 3
334
+ let eyeOffsetPx = 5
335
+ let leftEye = Self.grid.rect(
336
+ x: centerXPx - eyeOffsetPx - eyeSizePx / 2,
337
+ y: centerYPx - eyeSizePx / 2,
338
+ w: eyeSizePx,
339
+ h: eyeSizePx)
340
+ let rightEye = Self.grid.rect(
341
+ x: centerXPx + eyeOffsetPx - eyeSizePx / 2,
342
+ y: centerYPx - eyeSizePx / 2,
343
+ w: eyeSizePx,
344
+ h: eyeSizePx)
345
+ NSBezierPath(rect: leftEye).fill()
346
+ NSBezierPath(rect: rightEye).fill()
347
+ case .claude:
348
+ let slitWidthPx = 2
349
+ let slitHeightPx = 6
350
+ let slitOffsetPx = 5
351
+ let leftSlit = Self.grid.rect(
352
+ x: centerXPx - slitOffsetPx - slitWidthPx / 2,
353
+ y: centerYPx - slitHeightPx / 2,
354
+ w: slitWidthPx,
355
+ h: slitHeightPx)
356
+ let rightSlit = Self.grid.rect(
357
+ x: centerXPx + slitOffsetPx - slitWidthPx / 2,
358
+ y: centerYPx - slitHeightPx / 2,
359
+ w: slitWidthPx,
360
+ h: slitHeightPx)
361
+ NSBezierPath(rect: leftSlit).fill()
362
+ NSBezierPath(rect: rightSlit).fill()
363
+ case .gemini:
364
+ let diamondRadiusPx = 4
365
+ let cx = Self.grid.pt(centerXPx)
366
+ let cy = Self.grid.pt(centerYPx)
367
+ let r = Self.grid.pt(diamondRadiusPx)
368
+ let path = NSBezierPath()
369
+ path.move(to: NSPoint(x: cx, y: cy + r))
370
+ path.line(to: NSPoint(x: cx + r, y: cy))
371
+ path.line(to: NSPoint(x: cx, y: cy - r))
372
+ path.line(to: NSPoint(x: cx - r, y: cy))
373
+ path.close()
374
+ path.fill()
375
+ case .antigravity:
376
+ let dotSizePx = 4
377
+ let dotRect = Self.grid.rect(
378
+ x: centerXPx - dotSizePx / 2,
379
+ y: rectPx.y + rectPx.h - dotSizePx - 1,
380
+ w: dotSizePx,
381
+ h: dotSizePx)
382
+ NSBezierPath(ovalIn: dotRect).fill()
383
+ case .cursor:
384
+ let tipPx = 6
385
+ let baseXPx = centerXPx + 4
386
+ let path = NSBezierPath()
387
+ path.move(to: point(x: baseXPx, y: centerYPx))
388
+ path.line(to: point(x: baseXPx - tipPx, y: centerYPx + tipPx / 2))
389
+ path.line(to: point(x: baseXPx - tipPx, y: centerYPx - tipPx / 2))
390
+ path.close()
391
+ path.fill()
392
+ case .factory:
393
+ let armPx = 6
394
+ let thicknessPx = 2
395
+ let vertical = Self.grid.rect(
396
+ x: centerXPx - thicknessPx / 2,
397
+ y: centerYPx - armPx / 2,
398
+ w: thicknessPx,
399
+ h: armPx)
400
+ let horizontal = Self.grid.rect(
401
+ x: centerXPx - armPx / 2,
402
+ y: centerYPx - thicknessPx / 2,
403
+ w: armPx,
404
+ h: thicknessPx)
405
+ NSBezierPath(rect: vertical).fill()
406
+ NSBezierPath(rect: horizontal).fill()
407
+ case .copilot:
408
+ let eyeRadiusPx = 3
409
+ let eyeOffsetPx = 4
410
+ let leftEye = Self.grid.rect(
411
+ x: centerXPx - eyeOffsetPx - eyeRadiusPx,
412
+ y: centerYPx - eyeRadiusPx,
413
+ w: eyeRadiusPx * 2,
414
+ h: eyeRadiusPx * 2)
415
+ let rightEye = Self.grid.rect(
416
+ x: centerXPx + eyeOffsetPx - eyeRadiusPx,
417
+ y: centerYPx - eyeRadiusPx,
418
+ w: eyeRadiusPx * 2,
419
+ h: eyeRadiusPx * 2)
420
+ let bridge = Self.grid.rect(
421
+ x: centerXPx - 2,
422
+ y: centerYPx - 1,
423
+ w: 4,
424
+ h: 2)
425
+ NSBezierPath(ovalIn: leftEye).fill()
426
+ NSBezierPath(ovalIn: rightEye).fill()
427
+ NSBezierPath(rect: bridge).fill()
428
+ case .minimax:
429
+ let triWidthPx = 6
430
+ let triHeightPx = 5
431
+ let leftCx = centerXPx - 4
432
+ let rightCx = centerXPx + 4
433
+ let leftPath = NSBezierPath()
434
+ leftPath.move(to: point(x: leftCx, y: centerYPx + triHeightPx / 2))
435
+ leftPath.line(to: point(x: leftCx - triWidthPx / 2, y: centerYPx - triHeightPx / 2))
436
+ leftPath.line(to: point(x: leftCx + triWidthPx / 2, y: centerYPx - triHeightPx / 2))
437
+ leftPath.close()
438
+ let rightPath = NSBezierPath()
439
+ rightPath.move(to: point(x: rightCx, y: centerYPx + triHeightPx / 2))
440
+ rightPath.line(to: point(x: rightCx - triWidthPx / 2, y: centerYPx - triHeightPx / 2))
441
+ rightPath.line(to: point(x: rightCx + triWidthPx / 2, y: centerYPx - triHeightPx / 2))
442
+ rightPath.close()
443
+ leftPath.fill()
444
+ rightPath.fill()
445
+ case .openrouter:
446
+ let outerRadiusPx = 4
447
+ let innerRadiusPx = 2
448
+ let outerRect = Self.grid.rect(
449
+ x: centerXPx - outerRadiusPx,
450
+ y: centerYPx - outerRadiusPx,
451
+ w: outerRadiusPx * 2,
452
+ h: outerRadiusPx * 2)
453
+ let innerRect = Self.grid.rect(
454
+ x: centerXPx - innerRadiusPx,
455
+ y: centerYPx - innerRadiusPx,
456
+ w: innerRadiusPx * 2,
457
+ h: innerRadiusPx * 2)
458
+ let ring = NSBezierPath()
459
+ ring.appendOval(in: outerRect)
460
+ ring.appendOval(in: innerRect)
461
+ ring.windingRule = .evenOdd
462
+ ring.fill()
463
+ case .groq:
464
+ let slashWidthPx = 2
465
+ let slashHeightPx = 8
466
+ let path = NSBezierPath()
467
+ path.move(to: point(x: centerXPx - slashWidthPx, y: centerYPx - slashHeightPx / 2))
468
+ path.line(to: point(x: centerXPx, y: centerYPx - slashHeightPx / 2))
469
+ path.line(to: point(x: centerXPx + slashWidthPx, y: centerYPx + slashHeightPx / 2))
470
+ path.line(to: point(x: centerXPx, y: centerYPx + slashHeightPx / 2))
471
+ path.close()
472
+ path.fill()
473
+ case .deepseek:
474
+ let radiusPx = 4
475
+ let dotPx = 2
476
+ let outerRect = Self.grid.rect(
477
+ x: centerXPx - radiusPx,
478
+ y: centerYPx - radiusPx,
479
+ w: radiusPx * 2,
480
+ h: radiusPx * 2)
481
+ NSBezierPath(ovalIn: outerRect).fill()
482
+ let innerRect = Self.grid.rect(
483
+ x: centerXPx - dotPx / 2,
484
+ y: centerYPx - dotPx / 2,
485
+ w: dotPx,
486
+ h: dotPx)
487
+ NSBezierPath(ovalIn: innerRect).fill()
488
+ case .fireworks:
489
+ let armPx = 5
490
+ let armWidthPx = 2
491
+ let vertical = Self.grid.rect(
492
+ x: centerXPx - armWidthPx / 2,
493
+ y: centerYPx - armPx,
494
+ w: armWidthPx,
495
+ h: armPx * 2)
496
+ let horizontal = Self.grid.rect(
497
+ x: centerXPx - armPx,
498
+ y: centerYPx - armWidthPx / 2,
499
+ w: armPx * 2,
500
+ h: armWidthPx)
501
+ NSBezierPath(rect: vertical).fill()
502
+ NSBezierPath(rect: horizontal).fill()
503
+ let diag = NSBezierPath()
504
+ diag.move(to: point(x: centerXPx - 3, y: centerYPx - 3))
505
+ diag.line(to: point(x: centerXPx - 1, y: centerYPx - 3))
506
+ diag.line(to: point(x: centerXPx + 3, y: centerYPx + 1))
507
+ diag.line(to: point(x: centerXPx + 3, y: centerYPx + 3))
508
+ diag.line(to: point(x: centerXPx + 1, y: centerYPx + 3))
509
+ diag.line(to: point(x: centerXPx - 3, y: centerYPx - 1))
510
+ diag.close()
511
+ diag.fill()
512
+ case .mistral:
513
+ let barWidthPx = 2
514
+ let barGapPx = 1
515
+ let barHeightPx = 6
516
+ let totalWidth = barWidthPx * 3 + barGapPx * 2
517
+ let startX = centerXPx - totalWidth / 2
518
+ for idx in 0 ..< 3 {
519
+ let x = startX + idx * (barWidthPx + barGapPx)
520
+ let rect = Self.grid.rect(
521
+ x: x,
522
+ y: centerYPx - barHeightPx / 2 + (idx == 1 ? 1 : 0),
523
+ w: barWidthPx,
524
+ h: barHeightPx - (idx == 1 ? 2 : 0))
525
+ NSBezierPath(rect: rect).fill()
526
+ }
527
+ case .perplexity:
528
+ let loopRadiusPx = 4
529
+ let stemWidthPx = 2
530
+ let stemHeightPx = 5
531
+ let loopRect = Self.grid.rect(
532
+ x: centerXPx - loopRadiusPx,
533
+ y: centerYPx - loopRadiusPx,
534
+ w: loopRadiusPx * 2,
535
+ h: loopRadiusPx * 2)
536
+ let innerRect = Self.grid.rect(
537
+ x: centerXPx - 1,
538
+ y: centerYPx - 1,
539
+ w: 2,
540
+ h: 2)
541
+ let loop = NSBezierPath()
542
+ loop.appendOval(in: loopRect)
543
+ loop.appendOval(in: innerRect)
544
+ loop.windingRule = .evenOdd
545
+ loop.fill()
546
+ let stemRect = Self.grid.rect(
547
+ x: centerXPx + 1,
548
+ y: centerYPx - loopRadiusPx - stemHeightPx + 1,
549
+ w: stemWidthPx,
550
+ h: stemHeightPx)
551
+ NSBezierPath(rect: stemRect).fill()
552
+ case .kimi:
553
+ let ringRect = Self.grid.rect(
554
+ x: centerXPx - 4,
555
+ y: centerYPx - 4,
556
+ w: 8,
557
+ h: 8)
558
+ let coreRect = Self.grid.rect(
559
+ x: centerXPx - 1,
560
+ y: centerYPx - 1,
561
+ w: 2,
562
+ h: 2)
563
+ let ring = NSBezierPath()
564
+ ring.appendOval(in: ringRect)
565
+ ring.appendOval(in: coreRect)
566
+ ring.windingRule = .evenOdd
567
+ ring.fill()
568
+ case .auggie:
569
+ let armPx = 5
570
+ let left = NSBezierPath()
571
+ left.move(to: point(x: centerXPx - armPx, y: centerYPx - 3))
572
+ left.line(to: point(x: centerXPx - 1, y: centerYPx + 4))
573
+ left.line(to: point(x: centerXPx + 1, y: centerYPx + 4))
574
+ left.line(to: point(x: centerXPx - 3, y: centerYPx - 3))
575
+ left.close()
576
+ left.fill()
577
+ let right = NSBezierPath()
578
+ right.move(to: point(x: centerXPx + armPx, y: centerYPx - 3))
579
+ right.line(to: point(x: centerXPx + 1, y: centerYPx + 4))
580
+ right.line(to: point(x: centerXPx - 1, y: centerYPx + 4))
581
+ right.line(to: point(x: centerXPx + 3, y: centerYPx - 3))
582
+ right.close()
583
+ right.fill()
584
+ case .together:
585
+ let topRect = Self.grid.rect(
586
+ x: centerXPx - 4,
587
+ y: centerYPx + 1,
588
+ w: 8,
589
+ h: 2)
590
+ let bottomRect = Self.grid.rect(
591
+ x: centerXPx - 4,
592
+ y: centerYPx - 3,
593
+ w: 8,
594
+ h: 2)
595
+ let joinRect = Self.grid.rect(
596
+ x: centerXPx - 1,
597
+ y: centerYPx - 3,
598
+ w: 2,
599
+ h: 6)
600
+ NSBezierPath(rect: topRect).fill()
601
+ NSBezierPath(rect: bottomRect).fill()
602
+ NSBezierPath(rect: joinRect).fill()
603
+ case .cohere:
604
+ let outerRect = Self.grid.rect(
605
+ x: centerXPx - 4,
606
+ y: centerYPx - 4,
607
+ w: 8,
608
+ h: 8)
609
+ let innerRect = Self.grid.rect(
610
+ x: centerXPx - 2,
611
+ y: centerYPx - 2,
612
+ w: 4,
613
+ h: 4)
614
+ let cPath = NSBezierPath()
615
+ cPath.appendOval(in: outerRect)
616
+ cPath.appendOval(in: innerRect)
617
+ cPath.windingRule = .evenOdd
618
+ cPath.fill()
619
+ let cutRect = Self.grid.rect(
620
+ x: centerXPx + 1,
621
+ y: centerYPx - 4,
622
+ w: 4,
623
+ h: 8)
624
+ NSBezierPath(rect: cutRect).fill()
625
+ case .xai:
626
+ let left = NSBezierPath()
627
+ left.move(to: point(x: centerXPx - 4, y: centerYPx - 4))
628
+ left.line(to: point(x: centerXPx - 1, y: centerYPx - 1))
629
+ left.line(to: point(x: centerXPx - 4, y: centerYPx + 4))
630
+ left.line(to: point(x: centerXPx - 2, y: centerYPx + 4))
631
+ left.line(to: point(x: centerXPx + 1, y: centerYPx + 1))
632
+ left.line(to: point(x: centerXPx + 4, y: centerYPx + 4))
633
+ left.line(to: point(x: centerXPx + 4, y: centerYPx + 2))
634
+ left.line(to: point(x: centerXPx + 1, y: centerYPx - 1))
635
+ left.line(to: point(x: centerXPx + 4, y: centerYPx - 4))
636
+ left.line(to: point(x: centerXPx + 2, y: centerYPx - 4))
637
+ left.line(to: point(x: centerXPx - 1, y: centerYPx - 1))
638
+ left.line(to: point(x: centerXPx - 4, y: centerYPx - 4))
639
+ left.close()
640
+ left.fill()
641
+ case .cerebras:
642
+ let outerRect = Self.grid.rect(
643
+ x: centerXPx - 4,
644
+ y: centerYPx - 4,
645
+ w: 8,
646
+ h: 8)
647
+ let innerRect = Self.grid.rect(
648
+ x: centerXPx - 2,
649
+ y: centerYPx - 2,
650
+ w: 4,
651
+ h: 4)
652
+ let ring = NSBezierPath()
653
+ ring.appendOval(in: outerRect)
654
+ ring.appendOval(in: innerRect)
655
+ ring.windingRule = .evenOdd
656
+ ring.fill()
657
+ let notchRect = Self.grid.rect(
658
+ x: centerXPx + 1,
659
+ y: centerYPx - 2,
660
+ w: 4,
661
+ h: 4)
662
+ NSBezierPath(rect: notchRect).fill()
663
+ case .sambanova:
664
+ let topArc = NSBezierPath()
665
+ topArc.move(to: point(x: centerXPx - 4, y: centerYPx + 3))
666
+ topArc.curve(
667
+ to: point(x: centerXPx + 4, y: centerYPx + 3),
668
+ controlPoint1: point(x: centerXPx - 1, y: centerYPx + 6),
669
+ controlPoint2: point(x: centerXPx + 1, y: centerYPx + 6))
670
+ topArc.line(to: point(x: centerXPx + 4, y: centerYPx + 1))
671
+ topArc.curve(
672
+ to: point(x: centerXPx - 4, y: centerYPx + 1),
673
+ controlPoint1: point(x: centerXPx + 1, y: centerYPx + 4),
674
+ controlPoint2: point(x: centerXPx - 1, y: centerYPx + 4))
675
+ topArc.close()
676
+ topArc.fill()
677
+ let bottomArc = NSBezierPath()
678
+ bottomArc.move(to: point(x: centerXPx + 4, y: centerYPx - 3))
679
+ bottomArc.curve(
680
+ to: point(x: centerXPx - 4, y: centerYPx - 3),
681
+ controlPoint1: point(x: centerXPx + 1, y: centerYPx - 6),
682
+ controlPoint2: point(x: centerXPx - 1, y: centerYPx - 6))
683
+ bottomArc.line(to: point(x: centerXPx - 4, y: centerYPx - 1))
684
+ bottomArc.curve(
685
+ to: point(x: centerXPx + 4, y: centerYPx - 1),
686
+ controlPoint1: point(x: centerXPx - 1, y: centerYPx - 4),
687
+ controlPoint2: point(x: centerXPx + 1, y: centerYPx - 4))
688
+ bottomArc.close()
689
+ bottomArc.fill()
690
+ case .azure:
691
+ let outerRect = Self.grid.rect(
692
+ x: centerXPx - 4,
693
+ y: centerYPx - 4,
694
+ w: 8,
695
+ h: 8)
696
+ let innerRect = Self.grid.rect(
697
+ x: centerXPx - 2,
698
+ y: centerYPx - 2,
699
+ w: 4,
700
+ h: 4)
701
+ let ring = NSBezierPath()
702
+ ring.appendOval(in: outerRect)
703
+ ring.appendOval(in: innerRect)
704
+ ring.windingRule = .evenOdd
705
+ ring.fill()
706
+ let slash = NSBezierPath()
707
+ slash.move(to: point(x: centerXPx - 5, y: centerYPx - 1))
708
+ slash.line(to: point(x: centerXPx - 3, y: centerYPx - 3))
709
+ slash.line(to: point(x: centerXPx + 5, y: centerYPx + 5))
710
+ slash.line(to: point(x: centerXPx + 3, y: centerYPx + 7))
711
+ slash.close()
712
+ slash.fill()
713
+ case .bedrock:
714
+ let top = NSBezierPath()
715
+ top.move(to: point(x: centerXPx - 4, y: centerYPx + 2))
716
+ top.line(to: point(x: centerXPx, y: centerYPx + 6))
717
+ top.line(to: point(x: centerXPx + 4, y: centerYPx + 2))
718
+ top.line(to: point(x: centerXPx + 2, y: centerYPx))
719
+ top.line(to: point(x: centerXPx, y: centerYPx + 2))
720
+ top.line(to: point(x: centerXPx - 2, y: centerYPx))
721
+ top.close()
722
+ top.fill()
723
+ let bottom = NSBezierPath()
724
+ bottom.move(to: point(x: centerXPx - 4, y: centerYPx - 2))
725
+ bottom.line(to: point(x: centerXPx, y: centerYPx - 6))
726
+ bottom.line(to: point(x: centerXPx + 4, y: centerYPx - 2))
727
+ bottom.line(to: point(x: centerXPx + 2, y: centerYPx))
728
+ bottom.line(to: point(x: centerXPx, y: centerYPx - 2))
729
+ bottom.line(to: point(x: centerXPx - 2, y: centerYPx))
730
+ bottom.close()
731
+ bottom.fill()
732
+ case .vertexai:
733
+ let diamond = NSBezierPath()
734
+ diamond.move(to: point(x: centerXPx, y: centerYPx + 6))
735
+ diamond.line(to: point(x: centerXPx + 5, y: centerYPx))
736
+ diamond.line(to: point(x: centerXPx, y: centerYPx - 6))
737
+ diamond.line(to: point(x: centerXPx - 5, y: centerYPx))
738
+ diamond.close()
739
+ diamond.fill()
740
+ case .zai:
741
+ let slashGapPx = 3
742
+ let slashWidthPx = 2
743
+ let slashHeightPx = 7
744
+ let leftPath = NSBezierPath()
745
+ leftPath.move(to: point(x: centerXPx - slashGapPx, y: centerYPx - slashHeightPx / 2))
746
+ leftPath.line(to: point(x: centerXPx - slashGapPx + slashWidthPx, y: centerYPx - slashHeightPx / 2))
747
+ leftPath.line(to: point(x: centerXPx - slashGapPx + slashWidthPx + 2, y: centerYPx + slashHeightPx / 2))
748
+ leftPath.line(to: point(x: centerXPx - slashGapPx + 2, y: centerYPx + slashHeightPx / 2))
749
+ leftPath.close()
750
+ let rightPath = NSBezierPath()
751
+ rightPath.move(to: point(x: centerXPx + slashGapPx, y: centerYPx - slashHeightPx / 2))
752
+ rightPath.line(to: point(x: centerXPx + slashGapPx + slashWidthPx, y: centerYPx - slashHeightPx / 2))
753
+ rightPath.line(to: point(x: centerXPx + slashGapPx + slashWidthPx + 2, y: centerYPx + slashHeightPx / 2))
754
+ rightPath.line(to: point(x: centerXPx + slashGapPx + 2, y: centerYPx + slashHeightPx / 2))
755
+ rightPath.close()
756
+ leftPath.fill()
757
+ rightPath.fill()
758
+ case .qwen:
759
+ // Stylised "Q" ring -- outer circle punch with inner cutout.
760
+ let ringOuter = 5
761
+ let ringInner = 3
762
+ let outerRect = Self.grid.rect(
763
+ x: centerXPx - ringOuter,
764
+ y: centerYPx - ringOuter,
765
+ w: ringOuter * 2,
766
+ h: ringOuter * 2)
767
+ let innerRect = Self.grid.rect(
768
+ x: centerXPx - ringInner,
769
+ y: centerYPx - ringInner,
770
+ w: ringInner * 2,
771
+ h: ringInner * 2)
772
+ NSBezierPath(ovalIn: outerRect).fill()
773
+ ctx.setBlendMode(.normal)
774
+ NSGraphicsContext.current?.cgContext.setFillColor(NSColor.clear.cgColor)
775
+ ctx.setBlendMode(.clear)
776
+ NSBezierPath(ovalIn: innerRect).fill()
777
+ case .combined:
778
+ let diamondRadiusPx = 4
779
+ let cx = Self.grid.pt(centerXPx)
780
+ let cy = Self.grid.pt(centerYPx)
781
+ let r = Self.grid.pt(diamondRadiusPx)
782
+ let path = NSBezierPath()
783
+ path.move(to: NSPoint(x: cx, y: cy + r))
784
+ path.line(to: NSPoint(x: cx + r, y: cy))
785
+ path.line(to: NSPoint(x: cx, y: cy - r))
786
+ path.line(to: NSPoint(x: cx - r, y: cy))
787
+ path.close()
788
+ path.fill()
789
+ }
790
+
791
+ ctx.restoreGState()
792
+ }
793
+
794
+ /// Morph helper: unbraids a simplified knot into our bar icon.
795
+ static func makeMorphIcon(
796
+ progress: Double,
797
+ style: IconStyle,
798
+ appearance: IconAppearance = .template) -> NSImage
799
+ {
800
+ let clamped = max(0, min(progress, 1))
801
+ let key = self.morphCacheKey(progress: clamped, style: style, appearance: appearance)
802
+ if let cached = self.morphCache.image(for: key) {
803
+ return cached
804
+ }
805
+ let baseColor = appearance == .template
806
+ ? NSColor.labelColor
807
+ : self.vibrantAccentColor(pressure: 0.15, stale: false)
808
+ let image = self.renderImage(isTemplate: appearance == .template) {
809
+ self.drawUnbraidMorph(t: clamped, style: style, color: baseColor, appearance: appearance)
810
+ }
811
+ self.morphCache.set(image, for: key)
812
+ return image
813
+ }
814
+
815
+ private static func iconFillPercent(
816
+ primary: Double?,
817
+ weekly: Double?,
818
+ credits: Double?,
819
+ dataMode: IconDataMode) -> Double
820
+ {
821
+ let value = primary ?? weekly ?? credits
822
+ guard let value else { return 0 }
823
+ let clamped = max(0, min(value / 100, 1))
824
+ switch dataMode {
825
+ case .remaining:
826
+ return clamped
827
+ case .used:
828
+ return clamped
829
+ }
830
+ }
831
+
832
+ private static func drawWaveLogo(
833
+ fillPercent: Double,
834
+ baseColor: NSColor,
835
+ fillColor: NSColor)
836
+ {
837
+ guard let waveLogo = self.waveLogoTemplate,
838
+ let ctx = NSGraphicsContext.current?.cgContext else {
839
+ return
840
+ }
841
+
842
+ let rect = CGRect(origin: .zero, size: self.outputSize)
843
+ let clamped = max(0, min(fillPercent, 1))
844
+
845
+ // Base silhouette
846
+ ctx.saveGState()
847
+ waveLogo.draw(in: rect, from: .zero, operation: .sourceOver, fraction: 1.0)
848
+ ctx.setBlendMode(.sourceIn)
849
+ baseColor.setFill()
850
+ ctx.fill(rect)
851
+ ctx.restoreGState()
852
+
853
+ // Usage fill: left-to-right reveal within the infinity symbol.
854
+ guard clamped > 0 else { return }
855
+ ctx.saveGState()
856
+ waveLogo.draw(in: rect, from: .zero, operation: .sourceOver, fraction: 1.0)
857
+ ctx.setBlendMode(.sourceIn)
858
+ fillColor.setFill()
859
+ let fillRect = CGRect(x: rect.minX, y: rect.minY, width: rect.width * clamped, height: rect.height)
860
+ ctx.fill(fillRect)
861
+ ctx.restoreGState()
862
+ }
863
+
864
+ private static func usagePressure(
865
+ primary: Double?,
866
+ weekly: Double?,
867
+ credits: Double?,
868
+ dataMode: IconDataMode) -> Double
869
+ {
870
+ let values = [primary, weekly, credits].compactMap { $0 }
871
+ guard !values.isEmpty else { return 0 }
872
+ let normalized = values.map { max(0, min($0 / 100, 1)) }
873
+ switch dataMode {
874
+ case .used:
875
+ return normalized.max() ?? 0
876
+ case .remaining:
877
+ let minRemaining = normalized.min() ?? 1
878
+ return max(0, min(1, 1 - minRemaining))
879
+ }
880
+ }
881
+
882
+ private static func vibrantAccentColor(pressure: Double, stale: Bool) -> NSColor {
883
+ let clamped = CGFloat(max(0, min(pressure, 1)))
884
+ let safe = NSColor(calibratedRed: 0.12, green: 0.86, blue: 0.78, alpha: 1)
885
+ let warn = NSColor(calibratedRed: 1.00, green: 0.72, blue: 0.30, alpha: 1)
886
+ 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)
890
+ } else {
891
+ color = self.mixColor(warn, hot, p: (clamped - 0.5) / 0.5)
892
+ }
893
+ if stale {
894
+ return color.withAlphaComponent(0.72)
895
+ }
896
+ return color
897
+ }
898
+
899
+ private static func mixColor(_ a: NSColor, _ b: NSColor, p: CGFloat) -> NSColor {
900
+ guard let ca = a.usingColorSpace(.deviceRGB),
901
+ let cb = b.usingColorSpace(.deviceRGB)
902
+ else {
903
+ return a
904
+ }
905
+ let clamped = max(0, min(p, 1))
906
+ let red = ca.redComponent + (cb.redComponent - ca.redComponent) * clamped
907
+ let green = ca.greenComponent + (cb.greenComponent - ca.greenComponent) * clamped
908
+ let blue = ca.blueComponent + (cb.blueComponent - ca.blueComponent) * clamped
909
+ let alpha = ca.alphaComponent + (cb.alphaComponent - ca.alphaComponent) * clamped
910
+ return NSColor(calibratedRed: red, green: green, blue: blue, alpha: alpha)
911
+ }
912
+
913
+ private static func quantizedPercent(_ value: Double?) -> Int {
914
+ guard let value else { return -1 }
915
+ return Int((value * 10).rounded())
916
+ }
917
+
918
+ private static func quantizedCredits(_ value: Double?) -> Int {
919
+ guard let value else { return -1 }
920
+ let clamped = max(0, min(value, self.creditsCap))
921
+ return Int((clamped * 10).rounded())
922
+ }
923
+
924
+ private static func styleKey(_ style: IconStyle) -> Int {
925
+ switch style {
926
+ case .codex: 0
927
+ case .claude: 1
928
+ case .zai: 2
929
+ case .gemini: 3
930
+ case .antigravity: 4
931
+ case .cursor: 5
932
+ case .factory: 6
933
+ case .copilot: 7
934
+ case .minimax: 8
935
+ case .openrouter: 9
936
+ case .groq: 10
937
+ case .deepseek: 11
938
+ case .fireworks: 12
939
+ case .mistral: 13
940
+ case .perplexity: 14
941
+ case .kimi: 15
942
+ case .auggie: 16
943
+ case .together: 17
944
+ case .cohere: 18
945
+ case .xai: 19
946
+ case .cerebras: 20
947
+ case .sambanova: 21
948
+ case .azure: 22
949
+ case .bedrock: 23
950
+ case .vertexai: 24
951
+ case .qwen: 25
952
+ case .combined: 99
953
+ }
954
+ }
955
+
956
+ private static func indicatorKey(_ indicator: ProviderStatusIndicator) -> Int {
957
+ switch indicator {
958
+ case .none: 0
959
+ case .minor: 1
960
+ case .major: 2
961
+ case .critical: 3
962
+ case .maintenance: 4
963
+ case .unknown: 5
964
+ }
965
+ }
966
+
967
+ private static func appearanceKey(_ appearance: IconAppearance) -> Int {
968
+ switch appearance {
969
+ case .template: 0
970
+ case .vibrant: 1
971
+ }
972
+ }
973
+
974
+ private static func dataModeKey(_ mode: IconDataMode) -> Int {
975
+ switch mode {
976
+ case .remaining: 0
977
+ case .used: 1
978
+ }
979
+ }
980
+
981
+ private static func morphCacheKey(
982
+ progress: Double,
983
+ style: IconStyle,
984
+ appearance: IconAppearance) -> NSNumber
985
+ {
986
+ let bucket = Int((progress * Double(self.morphBucketCount)).rounded())
987
+ let key = self.styleKey(style) * 10000 + self.appearanceKey(appearance) * 1000 + bucket
988
+ return NSNumber(value: key)
989
+ }
990
+
991
+ private static func cachedIcon(for key: IconCacheKey) -> NSImage? {
992
+ self.iconCacheStore.cachedIcon(for: key)
993
+ }
994
+
995
+ private static func storeIcon(_ image: NSImage, for key: IconCacheKey) {
996
+ self.iconCacheStore.storeIcon(image, for: key, limit: self.iconCacheLimit)
997
+ }
998
+
999
+ private static func drawUnbraidMorph(
1000
+ t: Double,
1001
+ style: IconStyle,
1002
+ color: NSColor,
1003
+ appearance: IconAppearance)
1004
+ {
1005
+ let t = CGFloat(max(0, min(t, 1)))
1006
+ let size = Self.baseSize
1007
+ let center = CGPoint(x: size.width / 2, y: size.height / 2)
1008
+ let baseColor = color
1009
+
1010
+ struct Segment {
1011
+ let startCenter: CGPoint
1012
+ let endCenter: CGPoint
1013
+ let startAngle: CGFloat
1014
+ let endAngle: CGFloat
1015
+ let startLength: CGFloat
1016
+ let endLength: CGFloat
1017
+ let startThickness: CGFloat
1018
+ let endThickness: CGFloat
1019
+ let fadeOut: Bool
1020
+ }
1021
+
1022
+ let segments: [Segment] = [
1023
+ // Upper ribbon -> top bar
1024
+ .init(
1025
+ startCenter: center.offset(dx: 0, dy: 2),
1026
+ endCenter: CGPoint(x: center.x, y: 9.0),
1027
+ startAngle: -30,
1028
+ endAngle: 0,
1029
+ startLength: 16,
1030
+ endLength: 14,
1031
+ startThickness: 3.4,
1032
+ endThickness: 3.0,
1033
+ fadeOut: false),
1034
+ // Lower ribbon -> bottom bar
1035
+ .init(
1036
+ startCenter: center.offset(dx: 0, dy: -2),
1037
+ endCenter: CGPoint(x: center.x, y: 4.0),
1038
+ startAngle: 210,
1039
+ endAngle: 0,
1040
+ startLength: 16,
1041
+ endLength: 12,
1042
+ startThickness: 3.4,
1043
+ endThickness: 2.4,
1044
+ fadeOut: false),
1045
+ // Side ribbon fades away
1046
+ .init(
1047
+ startCenter: center,
1048
+ endCenter: center.offset(dx: 0, dy: 6),
1049
+ startAngle: 90,
1050
+ endAngle: 0,
1051
+ startLength: 16,
1052
+ endLength: 8,
1053
+ startThickness: 3.4,
1054
+ endThickness: 1.8,
1055
+ fadeOut: true),
1056
+ ]
1057
+
1058
+ for seg in segments {
1059
+ let p = seg.fadeOut ? t * 1.1 : t
1060
+ let c = seg.startCenter.lerp(to: seg.endCenter, p: p)
1061
+ let angle = seg.startAngle.lerp(to: seg.endAngle, p: p)
1062
+ let length = seg.startLength.lerp(to: seg.endLength, p: p)
1063
+ let thickness = seg.startThickness.lerp(to: seg.endThickness, p: p)
1064
+ let alpha = seg.fadeOut ? (1 - p) : 1
1065
+
1066
+ self.drawRoundedRibbon(
1067
+ center: c,
1068
+ length: length,
1069
+ thickness: thickness,
1070
+ angle: angle,
1071
+ color: baseColor.withAlphaComponent(alpha))
1072
+ }
1073
+
1074
+ // Cross-fade in bar fill emphasis near the end of the morph.
1075
+ if t > 0.55 {
1076
+ let barT = (t - 0.55) / 0.45
1077
+ let bars = self.makeIcon(
1078
+ primaryRemaining: 100,
1079
+ weeklyRemaining: 100,
1080
+ creditsRemaining: nil,
1081
+ stale: false,
1082
+ style: style,
1083
+ appearance: appearance)
1084
+ bars.draw(in: CGRect(origin: .zero, size: size), from: .zero, operation: .sourceOver, fraction: barT)
1085
+ }
1086
+ }
1087
+
1088
+ private static func drawRoundedRibbon(
1089
+ center: CGPoint,
1090
+ length: CGFloat,
1091
+ thickness: CGFloat,
1092
+ angle: CGFloat,
1093
+ color: NSColor)
1094
+ {
1095
+ var transform = AffineTransform.identity
1096
+ transform.translate(x: center.x, y: center.y)
1097
+ transform.rotate(byDegrees: angle)
1098
+ transform.translate(x: -center.x, y: -center.y)
1099
+
1100
+ let rect = CGRect(
1101
+ x: center.x - length / 2,
1102
+ y: center.y - thickness / 2,
1103
+ width: length,
1104
+ height: thickness)
1105
+
1106
+ let path = NSBezierPath(roundedRect: rect, xRadius: thickness / 2, yRadius: thickness / 2)
1107
+ path.transform(using: transform)
1108
+ color.setFill()
1109
+ path.fill()
1110
+ }
1111
+
1112
+ private static func drawStatusOverlay(indicator: ProviderStatusIndicator, color: NSColor) {
1113
+ guard indicator.hasIssue else { return }
1114
+ switch indicator {
1115
+ case .minor, .maintenance:
1116
+ let size: CGFloat = 4
1117
+ let rect = Self.snapRect(
1118
+ x: Self.baseSize.width - size - 2,
1119
+ y: 2,
1120
+ width: size,
1121
+ height: size)
1122
+ let path = NSBezierPath(ovalIn: rect)
1123
+ color.setFill()
1124
+ path.fill()
1125
+ case .major, .critical, .unknown:
1126
+ let lineRect = Self.snapRect(
1127
+ x: Self.baseSize.width - 6,
1128
+ y: 4,
1129
+ width: 2.0,
1130
+ height: 6)
1131
+ let linePath = NSBezierPath(roundedRect: lineRect, xRadius: 1, yRadius: 1)
1132
+ color.setFill()
1133
+ linePath.fill()
1134
+
1135
+ let dotRect = Self.snapRect(
1136
+ x: Self.baseSize.width - 6,
1137
+ y: 2,
1138
+ width: 2.0,
1139
+ height: 2.0)
1140
+ NSBezierPath(ovalIn: dotRect).fill()
1141
+ case .none:
1142
+ break
1143
+ }
1144
+ }
1145
+
1146
+ private static func withScaledContext(_ draw: () -> Void) {
1147
+ guard let ctx = NSGraphicsContext.current?.cgContext else {
1148
+ draw()
1149
+ return
1150
+ }
1151
+ ctx.saveGState()
1152
+ ctx.setShouldAntialias(true)
1153
+ ctx.interpolationQuality = .none
1154
+ draw()
1155
+ ctx.restoreGState()
1156
+ }
1157
+
1158
+ private static func snap(_ value: CGFloat) -> CGFloat {
1159
+ (value * self.outputScale).rounded() / self.outputScale
1160
+ }
1161
+
1162
+ private static func snapRect(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) -> CGRect {
1163
+ CGRect(x: self.snap(x), y: self.snap(y), width: self.snap(width), height: self.snap(height))
1164
+ }
1165
+
1166
+ private static func renderImage(isTemplate: Bool, _ draw: () -> Void) -> NSImage {
1167
+ let image = NSImage(size: Self.outputSize)
1168
+
1169
+ if let rep = NSBitmapImageRep(
1170
+ bitmapDataPlanes: nil,
1171
+ pixelsWide: Int(Self.outputSize.width * Self.outputScale),
1172
+ pixelsHigh: Int(Self.outputSize.height * Self.outputScale),
1173
+ bitsPerSample: 8,
1174
+ samplesPerPixel: 4,
1175
+ hasAlpha: true,
1176
+ isPlanar: false,
1177
+ colorSpaceName: .deviceRGB,
1178
+ bytesPerRow: 0,
1179
+ bitsPerPixel: 0)
1180
+ {
1181
+ rep.size = Self.outputSize // points
1182
+ image.addRepresentation(rep)
1183
+
1184
+ NSGraphicsContext.saveGraphicsState()
1185
+ if let ctx = NSGraphicsContext(bitmapImageRep: rep) {
1186
+ NSGraphicsContext.current = ctx
1187
+ Self.withScaledContext(draw)
1188
+ }
1189
+ NSGraphicsContext.restoreGraphicsState()
1190
+ } else {
1191
+ // Fallback to legacy focus if the bitmap rep fails for any reason.
1192
+ image.lockFocus()
1193
+ Self.withScaledContext(draw)
1194
+ image.unlockFocus()
1195
+ }
1196
+
1197
+ image.isTemplate = isTemplate
1198
+ return image
1199
+ }
1200
+ }
1201
+
1202
+ extension CGPoint {
1203
+ fileprivate func lerp(to other: CGPoint, p: CGFloat) -> CGPoint {
1204
+ CGPoint(x: self.x + (other.x - self.x) * p, y: self.y + (other.y - self.y) * p)
1205
+ }
1206
+
1207
+ fileprivate func offset(dx: CGFloat, dy: CGFloat) -> CGPoint {
1208
+ CGPoint(x: self.x + dx, y: self.y + dy)
1209
+ }
1210
+ }
1211
+
1212
+ extension CGFloat {
1213
+ fileprivate func lerp(to other: CGFloat, p: CGFloat) -> CGFloat {
1214
+ self + (other - self) * p
1215
+ }
1216
+ }