@sriinnu/runic 2.0.0 → 2.11.1

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 (610) hide show
  1. package/CHANGELOG.md +256 -0
  2. package/LICENSE +373 -21
  3. package/README.md +96 -89
  4. package/THIRD-PARTY-NOTICES.md +651 -0
  5. package/package.json +14 -4
  6. package/.swiftformat +0 -50
  7. package/.swiftlint.yml +0 -160
  8. package/CONTRIBUTING.md +0 -456
  9. package/Icon.icns +0 -0
  10. package/Icon.icon/Assets/runic.png +0 -0
  11. package/Icon.icon/icon.json +0 -36
  12. package/Package.resolved +0 -51
  13. package/Package.swift +0 -136
  14. package/Packages/Helix/LICENSE +0 -21
  15. package/Packages/Helix/Package.swift +0 -29
  16. package/Packages/Helix/README.md +0 -976
  17. package/Packages/Helix/Sources/Helix/Binding.swift +0 -111
  18. package/Packages/Helix/Sources/Helix/CommandParser.swift +0 -160
  19. package/Packages/Helix/Sources/Helix/CommandSignature.swift +0 -117
  20. package/Packages/Helix/Sources/Helix/ExpressibleFromArgument.swift +0 -72
  21. package/Packages/Helix/Sources/Helix/HelpGenerator.swift +0 -175
  22. package/Packages/Helix/Sources/Helix/NameSpecification.swift +0 -98
  23. package/Packages/Helix/Sources/Helix/ParsableCommand.swift +0 -204
  24. package/Packages/Helix/Sources/Helix/Platform/DarwinPlatformContext.swift +0 -44
  25. package/Packages/Helix/Sources/Helix/Platform/LinuxPlatformContext.swift +0 -34
  26. package/Packages/Helix/Sources/Helix/Platform/PlatformContext+Default.swift +0 -91
  27. package/Packages/Helix/Sources/Helix/Platform/PlatformContext.swift +0 -94
  28. package/Packages/Helix/Sources/Helix/Platform/PlatformPath.swift +0 -167
  29. package/Packages/Helix/Sources/Helix/Platform/Stdio.swift +0 -99
  30. package/Packages/Helix/Sources/Helix/Platform/WebPlatformContext.swift +0 -146
  31. package/Packages/Helix/Sources/Helix/Platform/WindowsPlatformContext.swift +0 -97
  32. package/Packages/Helix/Sources/Helix/Program.swift +0 -204
  33. package/Packages/Helix/Sources/Helix/PropertyWrappers.swift +0 -367
  34. package/Packages/Silo/LICENSE +0 -21
  35. package/Packages/Silo/Package.swift +0 -34
  36. package/Packages/Silo/README.md +0 -466
  37. package/Packages/Silo/Sources/Silo/BinaryCookiesReader.swift +0 -169
  38. package/Packages/Silo/Sources/Silo/BrowserCookieAnalytics.swift +0 -365
  39. package/Packages/Silo/Sources/Silo/BrowserCookieBackends.swift +0 -594
  40. package/Packages/Silo/Sources/Silo/BrowserCookieClient.swift +0 -326
  41. package/Packages/Silo/Sources/Silo/BrowserCookieExport.swift +0 -154
  42. package/Packages/Silo/Sources/Silo/BrowserCookieImport.swift +0 -119
  43. package/Packages/Silo/Sources/Silo/BrowserCookieMockStore.swift +0 -121
  44. package/Packages/Silo/Sources/Silo/BrowserCookieModels.swift +0 -391
  45. package/Packages/Silo/Sources/Silo/ChromiumCookieReader.swift +0 -240
  46. package/Packages/Silo/Sources/Silo/CryptoSupport.swift +0 -86
  47. package/Packages/Silo/Sources/Silo/FirefoxCookieReader.swift +0 -172
  48. package/Packages/Silo/Sources/Silo/LinuxChromiumCookieReader.swift +0 -172
  49. package/Packages/Silo/Sources/Silo/LinuxSecretService.swift +0 -105
  50. package/Packages/Silo/Sources/Silo/MacOSChromiumCookieReader.swift +0 -191
  51. package/Packages/Silo/Sources/Silo/SQLiteSnapshot.swift +0 -41
  52. package/Packages/Silo/Sources/Silo/WindowsChromiumCookieReader.swift +0 -110
  53. package/RuniciOS/App/RuniciOSApp.swift +0 -105
  54. package/RuniciOS/Core/RunicTypographyiOS.swift +0 -24
  55. package/RuniciOS/Core/iOSUsageStore.swift +0 -192
  56. package/RuniciOS/Views/ProviderDetailView.swift +0 -312
  57. package/RuniciOS/Views/ProviderListView.swift +0 -228
  58. package/RuniciOS/Widgets/LargeWidget.swift +0 -367
  59. package/RuniciOS/Widgets/LiveActivity.swift +0 -342
  60. package/RuniciOS/Widgets/LockScreenWidget.swift +0 -273
  61. package/RuniciOS/Widgets/MediumWidget.swift +0 -370
  62. package/RuniciOS/Widgets/RunicWidget.swift +0 -274
  63. package/RuniciOS/Widgets/SmallWidget.swift +0 -281
  64. package/RuniciOS/Widgets/WidgetModels.swift +0 -285
  65. package/RuniciOS/Widgets/WidgetTimeline.swift +0 -270
  66. package/Scripts/CREATION-SUMMARY.md +0 -367
  67. package/Scripts/DIRECTORY-STRUCTURE.md +0 -358
  68. package/Scripts/GETTING-STARTED-CHECKLIST.md +0 -364
  69. package/Scripts/INDEX.md +0 -365
  70. package/Scripts/QUICK-REFERENCE.md +0 -164
  71. package/Scripts/README-SCRIPTS.md +0 -926
  72. package/Scripts/build-android.sh +0 -526
  73. package/Scripts/build-ios.sh +0 -529
  74. package/Scripts/build-macos.sh +0 -603
  75. package/Scripts/build-windows.sh +0 -504
  76. package/Scripts/build_icon.sh +0 -49
  77. package/Scripts/changelog-to-html.sh +0 -89
  78. package/Scripts/check-release-assets.sh +0 -10
  79. package/Scripts/ci-config.yml +0 -474
  80. package/Scripts/compile_and_run.sh +0 -219
  81. package/Scripts/compile_and_run_adhoc.sh +0 -4
  82. package/Scripts/docs-list.mjs +0 -119
  83. package/Scripts/make_appcast.sh +0 -58
  84. package/Scripts/package_app.sh +0 -409
  85. package/Scripts/release-lib.sh +0 -214
  86. package/Scripts/release.sh +0 -76
  87. package/Scripts/setup-all.sh +0 -387
  88. package/Scripts/setup-ios.sh +0 -404
  89. package/Scripts/setup-react-native.sh +0 -523
  90. package/Scripts/sign-and-notarize.sh +0 -122
  91. package/Scripts/test_live_update.sh +0 -38
  92. package/Scripts/validate_changelog.sh +0 -23
  93. package/Scripts/verify_appcast.sh +0 -92
  94. package/Sources/Runic/About.swift +0 -93
  95. package/Sources/Runic/AppNotifications.swift +0 -117
  96. package/Sources/Runic/Controllers/StatusItemController+Actions.swift +0 -670
  97. package/Sources/Runic/Controllers/StatusItemController+Animation.swift +0 -519
  98. package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +0 -824
  99. package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +0 -90
  100. package/Sources/Runic/Controllers/StatusItemController+Menu.swift +0 -1412
  101. package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +0 -339
  102. package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +0 -423
  103. package/Sources/Runic/Controllers/StatusItemController.swift +0 -360
  104. package/Sources/Runic/Core/BudgetNotificationManager.swift +0 -81
  105. package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +0 -103
  106. package/Sources/Runic/Core/Login/CodexLoginRunner.swift +0 -154
  107. package/Sources/Runic/Core/Login/CursorLoginRunner.swift +0 -206
  108. package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +0 -92
  109. package/Sources/Runic/Core/Rendering/IconRenderer.swift +0 -1222
  110. package/Sources/Runic/Core/Rendering/IconView.swift +0 -122
  111. package/Sources/Runic/Core/Rendering/ProviderBrandIcon.swift +0 -71
  112. package/Sources/Runic/Core/RunicAnimation.swift +0 -53
  113. package/Sources/Runic/Core/RunicColors.swift +0 -73
  114. package/Sources/Runic/Core/RunicCornerRadius.swift +0 -15
  115. package/Sources/Runic/Core/RunicSpacing.swift +0 -68
  116. package/Sources/Runic/Core/RunicTypography.swift +0 -206
  117. package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +0 -117
  118. package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +0 -117
  119. package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +0 -117
  120. package/Sources/Runic/Core/Stores/CohereTokenStore.swift +0 -117
  121. package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +0 -117
  122. package/Sources/Runic/Core/Stores/CustomProviderStore.swift +0 -158
  123. package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +0 -117
  124. package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +0 -117
  125. package/Sources/Runic/Core/Stores/GroqTokenStore.swift +0 -117
  126. package/Sources/Runic/Core/Stores/KimiTokenStore.swift +0 -117
  127. package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +0 -117
  128. package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +0 -117
  129. package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +0 -117
  130. package/Sources/Runic/Core/Stores/MistralTokenStore.swift +0 -117
  131. package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +0 -117
  132. package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +0 -117
  133. package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +0 -30
  134. package/Sources/Runic/Core/Stores/QwenTokenStore.swift +0 -117
  135. package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +0 -117
  136. package/Sources/Runic/Core/Stores/SettingsStore.swift +0 -1749
  137. package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +0 -117
  138. package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +0 -33
  139. package/Sources/Runic/Core/Stores/UsageStore+Status.swift +0 -189
  140. package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +0 -33
  141. package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +0 -67
  142. package/Sources/Runic/Core/Stores/UsageStore.swift +0 -3631
  143. package/Sources/Runic/Core/Stores/XAITokenStore.swift +0 -117
  144. package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +0 -117
  145. package/Sources/Runic/Core/UsageExporter.swift +0 -197
  146. package/Sources/Runic/Date+RelativeDescription.swift +0 -21
  147. package/Sources/Runic/HiddenWindowView.swift +0 -32
  148. package/Sources/Runic/InstallOrigin.swift +0 -9
  149. package/Sources/Runic/KeyboardShortcuts+Names.swift +0 -6
  150. package/Sources/Runic/MouseLocationReader.swift +0 -65
  151. package/Sources/Runic/Notifications+Runic.swift +0 -6
  152. package/Sources/Runic/OpenAICreditsPurchaseWindowController.swift +0 -503
  153. package/Sources/Runic/ProviderRegistry.swift +0 -100
  154. package/Sources/Runic/ProviderSwitcherButtons.swift +0 -267
  155. package/Sources/Runic/Providers/Antigravity/AntigravityLoginFlow.swift +0 -11
  156. package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +0 -14
  157. package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +0 -24
  158. package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +0 -52
  159. package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +0 -42
  160. package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +0 -24
  161. package/Sources/Runic/Providers/Claude/ClaudeLoginFlow.swift +0 -26
  162. package/Sources/Runic/Providers/Claude/ClaudeProviderImplementation.swift +0 -14
  163. package/Sources/Runic/Providers/Codex/CodexLoginFlow.swift +0 -18
  164. package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +0 -15
  165. package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +0 -24
  166. package/Sources/Runic/Providers/Copilot/CopilotLoginFlow.swift +0 -193
  167. package/Sources/Runic/Providers/Copilot/CopilotProviderImplementation.swift +0 -110
  168. package/Sources/Runic/Providers/Cursor/CursorLoginFlow.swift +0 -28
  169. package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +0 -15
  170. package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +0 -24
  171. package/Sources/Runic/Providers/Factory/FactoryLoginFlow.swift +0 -12
  172. package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +0 -15
  173. package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +0 -24
  174. package/Sources/Runic/Providers/Gemini/GeminiLoginFlow.swift +0 -20
  175. package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +0 -15
  176. package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +0 -24
  177. package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +0 -24
  178. package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +0 -32
  179. package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +0 -24
  180. package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +0 -24
  181. package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +0 -24
  182. package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +0 -24
  183. package/Sources/Runic/Providers/Shared/ProviderCatalog.swift +0 -15
  184. package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +0 -51
  185. package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +0 -66
  186. package/Sources/Runic/Providers/Shared/ProviderLoginFlow.swift +0 -12
  187. package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +0 -99
  188. package/Sources/Runic/Providers/Shared/SystemSettingsLinks.swift +0 -18
  189. package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +0 -24
  190. package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +0 -24
  191. package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +0 -24
  192. package/Sources/Runic/Resources/Fonts/Caveat-Bold.ttf +0 -0
  193. package/Sources/Runic/Resources/Fonts/Caveat-Regular.ttf +0 -0
  194. package/Sources/Runic/Resources/Fonts/FiraCode-Bold.ttf +0 -0
  195. package/Sources/Runic/Resources/Fonts/FiraCode-Light.ttf +0 -0
  196. package/Sources/Runic/Resources/Fonts/FiraCode-Medium.ttf +0 -0
  197. package/Sources/Runic/Resources/Fonts/FiraCode-Regular.ttf +0 -0
  198. package/Sources/Runic/Resources/Fonts/FiraCode-SemiBold.ttf +0 -0
  199. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Bold.ttf +0 -0
  200. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Medium.ttf +0 -0
  201. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Regular.ttf +0 -0
  202. package/Sources/Runic/Resources/Fonts/IBMPlexMono-SemiBold.ttf +0 -0
  203. package/Sources/Runic/Resources/Fonts/Inconsolata-Bold.ttf +0 -0
  204. package/Sources/Runic/Resources/Fonts/Inconsolata-Medium.ttf +0 -0
  205. package/Sources/Runic/Resources/Fonts/Inconsolata-Regular.ttf +0 -0
  206. package/Sources/Runic/Resources/Fonts/Inconsolata-SemiBold.ttf +0 -0
  207. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Bold.ttf +0 -0
  208. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Medium.ttf +0 -0
  209. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Regular.ttf +0 -0
  210. package/Sources/Runic/Resources/Fonts/JetBrainsMono-SemiBold.ttf +0 -0
  211. package/Sources/Runic/Resources/Fonts/SpaceMono-Bold.ttf +0 -0
  212. package/Sources/Runic/Resources/Fonts/SpaceMono-Regular.ttf +0 -0
  213. package/Sources/Runic/Resources/ProviderIcon-antigravity.svg +0 -2
  214. package/Sources/Runic/Resources/ProviderIcon-auggie.svg +0 -3
  215. package/Sources/Runic/Resources/ProviderIcon-azure.svg +0 -5
  216. package/Sources/Runic/Resources/ProviderIcon-bedrock.svg +0 -7
  217. package/Sources/Runic/Resources/ProviderIcon-cerebras.svg +0 -3
  218. package/Sources/Runic/Resources/ProviderIcon-claude.svg +0 -1
  219. package/Sources/Runic/Resources/ProviderIcon-codex.svg +0 -1
  220. package/Sources/Runic/Resources/ProviderIcon-cohere.svg +0 -3
  221. package/Sources/Runic/Resources/ProviderIcon-copilot.svg +0 -1
  222. package/Sources/Runic/Resources/ProviderIcon-cursor.svg +0 -3
  223. package/Sources/Runic/Resources/ProviderIcon-deepseek.svg +0 -4
  224. package/Sources/Runic/Resources/ProviderIcon-factory.svg +0 -3
  225. package/Sources/Runic/Resources/ProviderIcon-fireworks.svg +0 -3
  226. package/Sources/Runic/Resources/ProviderIcon-gemini.svg +0 -1
  227. package/Sources/Runic/Resources/ProviderIcon-groq.svg +0 -1
  228. package/Sources/Runic/Resources/ProviderIcon-kimi.svg +0 -4
  229. package/Sources/Runic/Resources/ProviderIcon-minimax.svg +0 -1
  230. package/Sources/Runic/Resources/ProviderIcon-mistral.svg +0 -5
  231. package/Sources/Runic/Resources/ProviderIcon-openrouter.svg +0 -7
  232. package/Sources/Runic/Resources/ProviderIcon-perplexity.svg +0 -4
  233. package/Sources/Runic/Resources/ProviderIcon-qwen.svg +0 -9
  234. package/Sources/Runic/Resources/ProviderIcon-sambanova.svg +0 -4
  235. package/Sources/Runic/Resources/ProviderIcon-together.svg +0 -5
  236. package/Sources/Runic/Resources/ProviderIcon-vertexai.svg +0 -17
  237. package/Sources/Runic/Resources/ProviderIcon-xai.svg +0 -3
  238. package/Sources/Runic/Resources/ProviderIcon-zai.svg +0 -5
  239. package/Sources/Runic/Resources/RunicMenubarIcon.svg +0 -7
  240. package/Sources/Runic/Resources/provider-context-windows.json +0 -20
  241. package/Sources/Runic/RunicApp.swift +0 -367
  242. package/Sources/Runic/SessionQuotaNotifications.swift +0 -58
  243. package/Sources/Runic/UsagePaceText.swift +0 -52
  244. package/Sources/Runic/UsageProgressBar.swift +0 -228
  245. package/Sources/Runic/Utilities/DisplayLink.swift +0 -96
  246. package/Sources/Runic/Utilities/LoadingPattern.swift +0 -62
  247. package/Sources/Runic/Utilities/PerformanceConstants.swift +0 -85
  248. package/Sources/Runic/Views/Menu/AlertsMenuView.swift +0 -254
  249. package/Sources/Runic/Views/Menu/ChartPanelStyle.swift +0 -22
  250. package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +0 -275
  251. package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +0 -377
  252. package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +0 -312
  253. package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +0 -311
  254. package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +0 -231
  255. package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +0 -228
  256. package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +0 -157
  257. package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +0 -63
  258. package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +0 -248
  259. package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +0 -104
  260. package/Sources/Runic/Views/Menu/MenuCardView.swift +0 -1889
  261. package/Sources/Runic/Views/Menu/MenuContent.swift +0 -153
  262. package/Sources/Runic/Views/Menu/MenuDescriptor.swift +0 -371
  263. package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +0 -35
  264. package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +0 -241
  265. package/Sources/Runic/Views/Menu/OverviewMenuView.swift +0 -281
  266. package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +0 -214
  267. package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +0 -220
  268. package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +0 -86
  269. package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +0 -214
  270. package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +0 -398
  271. package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +0 -231
  272. package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +0 -369
  273. package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +0 -191
  274. package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +0 -132
  275. package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +0 -621
  276. package/Sources/Runic/Views/Preferences/LiquidComponents.swift +0 -306
  277. package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +0 -271
  278. package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +0 -268
  279. package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +0 -418
  280. package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +0 -637
  281. package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +0 -344
  282. package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +0 -230
  283. package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +0 -321
  284. package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +0 -469
  285. package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +0 -250
  286. package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +0 -50
  287. package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +0 -572
  288. package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +0 -291
  289. package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +0 -3480
  290. package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +0 -29
  291. package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +0 -61
  292. package/Sources/Runic/Views/Preferences/PreferencesView.swift +0 -213
  293. package/Sources/Runic/Views/Rating/QualityRatingView.swift +0 -238
  294. package/Sources/Runic/Views/Team/TeamDashboardView.swift +0 -376
  295. package/Sources/Runic/Views/Team/TeamInviteSheet.swift +0 -175
  296. package/Sources/Runic/Views/Team/TeamManagementView.swift +0 -762
  297. package/Sources/Runic/Views/Team/TeamMemberRow.swift +0 -181
  298. package/Sources/RunicCLI/CLIEntry.swift +0 -983
  299. package/Sources/RunicCLI/CLIRenderer.swift +0 -87
  300. package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +0 -588
  301. package/Sources/RunicCLI/Commands/ModelsCommand.swift +0 -363
  302. package/Sources/RunicCLI/Commands/ProjectsCommand.swift +0 -453
  303. package/Sources/RunicCLI/Commands/ResetCommand.swift +0 -430
  304. package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +0 -167
  305. package/Sources/RunicClaudeWatchdog/main.swift +0 -122
  306. package/Sources/RunicClaudeWebProbe/main.swift +0 -62
  307. package/Sources/RunicCore/AlertRuleStore.swift +0 -187
  308. package/Sources/RunicCore/BrowserCookieImportOrder.swift +0 -25
  309. package/Sources/RunicCore/CopilotUsageModels.swift +0 -84
  310. package/Sources/RunicCore/CostUsageFetcher.swift +0 -119
  311. package/Sources/RunicCore/CostUsageModels.swift +0 -479
  312. package/Sources/RunicCore/CreditsModels.swift +0 -50
  313. package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +0 -268
  314. package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +0 -631
  315. package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +0 -444
  316. package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +0 -679
  317. package/Sources/RunicCore/Host/Process/SubprocessRunner.swift +0 -135
  318. package/Sources/RunicCore/Integrations/GitHubIntegration.swift +0 -173
  319. package/Sources/RunicCore/Logging/JSONStderrLogHandler.swift +0 -79
  320. package/Sources/RunicCore/Logging/OSLogLogHandler.swift +0 -86
  321. package/Sources/RunicCore/Logging/RunicLog.swift +0 -123
  322. package/Sources/RunicCore/Models/EnhancedUsageModels.swift +0 -370
  323. package/Sources/RunicCore/OpenAIDashboardModels.swift +0 -210
  324. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +0 -676
  325. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +0 -492
  326. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardNavigationDelegate.swift +0 -51
  327. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardParser.swift +0 -483
  328. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardScrapeScript.swift +0 -558
  329. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebViewCache.swift +0 -204
  330. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebsiteDataStore.swift +0 -78
  331. package/Sources/RunicCore/PathEnvironment.swift +0 -482
  332. package/Sources/RunicCore/Performance/PerformanceModels.swift +0 -371
  333. package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +0 -426
  334. package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +0 -178
  335. package/Sources/RunicCore/ProjectBudgetStore.swift +0 -102
  336. package/Sources/RunicCore/ProviderCostSnapshot.swift +0 -29
  337. package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +0 -68
  338. package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +0 -685
  339. package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +0 -83
  340. package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +0 -142
  341. package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +0 -141
  342. package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +0 -188
  343. package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +0 -126
  344. package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +0 -164
  345. package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +0 -84
  346. package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +0 -75
  347. package/Sources/RunicCore/Providers/Claude/ClaudeCLISession.swift +0 -332
  348. package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift +0 -180
  349. package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +0 -137
  350. package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +0 -209
  351. package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +0 -654
  352. package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +0 -30
  353. package/Sources/RunicCore/Providers/Claude/ClaudeUsageFetcher.swift +0 -480
  354. package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +0 -780
  355. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +0 -147
  356. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +0 -280
  357. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexTokenRefresher.swift +0 -100
  358. package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +0 -245
  359. package/Sources/RunicCore/Providers/Codex/CodexStatusProbe.swift +0 -125
  360. package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +0 -26
  361. package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +0 -183
  362. package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +0 -83
  363. package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +0 -85
  364. package/Sources/RunicCore/Providers/Copilot/CopilotDeviceFlow.swift +0 -123
  365. package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +0 -117
  366. package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +0 -100
  367. package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +0 -293
  368. package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +0 -385
  369. package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +0 -67
  370. package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +0 -595
  371. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +0 -85
  372. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +0 -108
  373. package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +0 -398
  374. package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +0 -67
  375. package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +0 -1276
  376. package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +0 -84
  377. package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +0 -75
  378. package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +0 -67
  379. package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +0 -839
  380. package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +0 -83
  381. package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +0 -71
  382. package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +0 -83
  383. package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +0 -77
  384. package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +0 -97
  385. package/Sources/RunicCore/Providers/MiniMax/MiniMaxSettingsReader.swift +0 -54
  386. package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +0 -168
  387. package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +0 -911
  388. package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +0 -84
  389. package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +0 -75
  390. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +0 -87
  391. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +0 -216
  392. package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +0 -84
  393. package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +0 -75
  394. package/Sources/RunicCore/Providers/ProviderBranding.swift +0 -25
  395. package/Sources/RunicCore/Providers/ProviderCLIConfig.swift +0 -17
  396. package/Sources/RunicCore/Providers/ProviderDescriptor.swift +0 -129
  397. package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +0 -263
  398. package/Sources/RunicCore/Providers/ProviderSettingsSnapshot.swift +0 -98
  399. package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +0 -501
  400. package/Sources/RunicCore/Providers/ProviderVersionDetector.swift +0 -68
  401. package/Sources/RunicCore/Providers/Providers.swift +0 -184
  402. package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +0 -86
  403. package/Sources/RunicCore/Providers/Qwen/QwenSettingsReader.swift +0 -30
  404. package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +0 -328
  405. package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +0 -84
  406. package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +0 -75
  407. package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +0 -83
  408. package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +0 -75
  409. package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +0 -123
  410. package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +0 -48
  411. package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +0 -144
  412. package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +0 -83
  413. package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +0 -75
  414. package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +0 -74
  415. package/Sources/RunicCore/Providers/Zai/ZaiSettingsReader.swift +0 -41
  416. package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +0 -745
  417. package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +0 -384
  418. package/Sources/RunicCore/Sync/SyncConflictResolver.swift +0 -307
  419. package/Sources/RunicCore/Sync/SyncProtocol.swift +0 -287
  420. package/Sources/RunicCore/Sync/SyncRecord.swift +0 -402
  421. package/Sources/RunicCore/Sync/SyncUsageExample.swift +0 -288
  422. package/Sources/RunicCore/Sync/TeamSyncRecords.swift +0 -768
  423. package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +0 -338
  424. package/Sources/RunicCore/Team/TeamModels.swift +0 -584
  425. package/Sources/RunicCore/Team/TeamShowbackExporter.swift +0 -495
  426. package/Sources/RunicCore/Team/TeamStore.swift +0 -809
  427. package/Sources/RunicCore/TextParsing.swift +0 -55
  428. package/Sources/RunicCore/UsageFetcher.swift +0 -694
  429. package/Sources/RunicCore/UsageFormatter.swift +0 -410
  430. package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +0 -441
  431. package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +0 -502
  432. package/Sources/RunicCore/UsageLedger/LedgerCache.swift +0 -161
  433. package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +0 -550
  434. package/Sources/RunicCore/UsageLedger/UsageLedger.swift +0 -29
  435. package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +0 -675
  436. package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +0 -209
  437. package/Sources/RunicCore/UsageLedger/UsageLedgerModels.swift +0 -417
  438. package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +0 -284
  439. package/Sources/RunicCore/UsagePace.swift +0 -81
  440. package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +0 -73
  441. package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +0 -74
  442. package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +0 -208
  443. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Claude.swift +0 -455
  444. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Timestamp.swift +0 -132
  445. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +0 -589
  446. package/Sources/RunicCore/WidgetSnapshot.swift +0 -183
  447. package/Sources/RunicMacroSupport/ProviderRegistrationMacros.swift +0 -14
  448. package/Sources/RunicMacros/ProviderRegistrationMacros.swift +0 -216
  449. package/Sources/RunicWidget/RunicWidgetBundle.swift +0 -79
  450. package/Sources/RunicWidget/RunicWidgetProvider.swift +0 -283
  451. package/Sources/RunicWidget/RunicWidgetViews.swift +0 -680
  452. package/Tests/RunicTests/AntigravityStatusProbeTests.swift +0 -50
  453. package/Tests/RunicTests/AppDelegateTests.swift +0 -48
  454. package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +0 -21
  455. package/Tests/RunicTests/CLIArgumentParsingTests.swift +0 -69
  456. package/Tests/RunicTests/CLICostTests.swift +0 -64
  457. package/Tests/RunicTests/CLIProviderSelectionTests.swift +0 -21
  458. package/Tests/RunicTests/CLISnapshotTests.swift +0 -71
  459. package/Tests/RunicTests/CLIWebFallbackTests.swift +0 -57
  460. package/Tests/RunicTests/ClaudeOAuthTests.swift +0 -163
  461. package/Tests/RunicTests/ClaudeResilienceTests.swift +0 -29
  462. package/Tests/RunicTests/ClaudeUsageTests.swift +0 -369
  463. package/Tests/RunicTests/CodexOAuthTests.swift +0 -121
  464. package/Tests/RunicTests/CodexUsageLogSourceTests.swift +0 -80
  465. package/Tests/RunicTests/CopilotTokenResolverTests.swift +0 -74
  466. package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +0 -74
  467. package/Tests/RunicTests/CostUsageDecodingTests.swift +0 -392
  468. package/Tests/RunicTests/CostUsagePricingTests.swift +0 -48
  469. package/Tests/RunicTests/CostUsageScannerTests.swift +0 -698
  470. package/Tests/RunicTests/CursorStatusProbeTests.swift +0 -322
  471. package/Tests/RunicTests/FactoryStatusProbeTests.swift +0 -67
  472. package/Tests/RunicTests/GeminiAPITestHelpers.swift +0 -94
  473. package/Tests/RunicTests/GeminiLoginAlertTests.swift +0 -27
  474. package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +0 -283
  475. package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +0 -279
  476. package/Tests/RunicTests/GeminiStatusProbeTests.swift +0 -232
  477. package/Tests/RunicTests/GeminiTestEnvironment.swift +0 -87
  478. package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +0 -72
  479. package/Tests/RunicTests/InstallOriginTests.swift +0 -17
  480. package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +0 -9
  481. package/Tests/RunicTests/LiveAccountTests.swift +0 -24
  482. package/Tests/RunicTests/LoadingPatternTests.swift +0 -34
  483. package/Tests/RunicTests/MenuCardModelTests.swift +0 -544
  484. package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +0 -11
  485. package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +0 -11
  486. package/Tests/RunicTests/MiniMaxParsingTests.swift +0 -67
  487. package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +0 -11
  488. package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +0 -110
  489. package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +0 -16
  490. package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +0 -74
  491. package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +0 -26
  492. package/Tests/RunicTests/OpenAIDashboardParserTests.swift +0 -133
  493. package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +0 -64
  494. package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +0 -131
  495. package/Tests/RunicTests/PathBuilderTests.swift +0 -236
  496. package/Tests/RunicTests/ProviderFetchPipelineTests.swift +0 -159
  497. package/Tests/RunicTests/ProviderIconResourcesTests.swift +0 -37
  498. package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +0 -15
  499. package/Tests/RunicTests/ProviderRegistryTests.swift +0 -29
  500. package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +0 -236
  501. package/Tests/RunicTests/ProviderToggleStoreTests.swift +0 -50
  502. package/Tests/RunicTests/RunicTests.swift +0 -119
  503. package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +0 -35
  504. package/Tests/RunicTests/SettingsStoreTests.swift +0 -265
  505. package/Tests/RunicTests/StatusItemAnimationTests.swift +0 -54
  506. package/Tests/RunicTests/StatusMenuTests.swift +0 -475
  507. package/Tests/RunicTests/StatusProbeTests.swift +0 -376
  508. package/Tests/RunicTests/SubprocessRunnerTests.swift +0 -18
  509. package/Tests/RunicTests/SubscriptionDetectionTests.swift +0 -64
  510. package/Tests/RunicTests/TTYCommandRunnerTests.swift +0 -147
  511. package/Tests/RunicTests/TTYIntegrationTests.swift +0 -41
  512. package/Tests/RunicTests/TeamShowbackExporterTests.swift +0 -157
  513. package/Tests/RunicTests/TextParsingTests.swift +0 -11
  514. package/Tests/RunicTests/UsageFormatterTests.swift +0 -111
  515. package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +0 -170
  516. package/Tests/RunicTests/UsageLedgerForecastTests.swift +0 -211
  517. package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +0 -81
  518. package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +0 -168
  519. package/Tests/RunicTests/UsagePaceTests.swift +0 -76
  520. package/Tests/RunicTests/UsagePaceTextTests.swift +0 -108
  521. package/Tests/RunicTests/UsagePolicyEngineTests.swift +0 -89
  522. package/Tests/RunicTests/WidgetSnapshotTests.swift +0 -43
  523. package/Tests/RunicTests/ZaiProviderTests.swift +0 -59
  524. package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +0 -9
  525. package/TestsLinux/PlatformGatingTests.swift +0 -46
  526. package/appcast.xml +0 -104
  527. package/assets/logo-concept-stack.svg +0 -24
  528. package/assets/logo-minimal-bars.svg +0 -15
  529. package/assets/logo.svg +0 -20
  530. package/assets/logo2.svg +0 -43
  531. package/bin/docs-list +0 -5
  532. package/build-and-run.sh +0 -369
  533. package/docs/.nojekyll +0 -1
  534. package/docs/CNAME +0 -1
  535. package/docs/antigravity.md +0 -75
  536. package/docs/architecture.md +0 -32
  537. package/docs/claude.md +0 -93
  538. package/docs/cli-commands/integration_guide.md +0 -371
  539. package/docs/cli-commands/quick_reference.md +0 -362
  540. package/docs/cli-commands/readme.md +0 -361
  541. package/docs/cli.md +0 -156
  542. package/docs/cloudkit_sync_guide.md +0 -495
  543. package/docs/codebase_review.md +0 -288
  544. package/docs/codex-oauth.md +0 -635
  545. package/docs/codex.md +0 -104
  546. package/docs/codexbar.png +0 -0
  547. package/docs/copilot.md +0 -44
  548. package/docs/cursor.md +0 -46
  549. package/docs/design/designcomparisonchart.md +0 -510
  550. package/docs/design/implementationguide.md +0 -792
  551. package/docs/design/menubaricondesignconcepts.md +0 -796
  552. package/docs/design/mockups/hybridiconconcept.svg +0 -62
  553. package/docs/design/mockups/infinityiconconcept.svg +0 -41
  554. package/docs/design/mockups/infinityiconstates.svg +0 -129
  555. package/docs/design/mockups/wavelogorefined.svg +0 -48
  556. package/docs/design/quick_summary.md +0 -426
  557. package/docs/design/readme.md +0 -478
  558. package/docs/design/sizespecifications.md +0 -575
  559. package/docs/design-system.md +0 -836
  560. package/docs/factory.md +0 -103
  561. package/docs/gemini.md +0 -73
  562. package/docs/icon.md +0 -37
  563. package/docs/icon.png +0 -0
  564. package/docs/index.html +0 -175
  565. package/docs/keychain_fix.md +0 -274
  566. package/docs/minimax.md +0 -71
  567. package/docs/packaging.md +0 -25
  568. package/docs/progress-bar-improvements.md +0 -240
  569. package/docs/progress-bar-quick-reference.md +0 -283
  570. package/docs/progress-bar-technical-details.md +0 -432
  571. package/docs/progress-bar-visual-test.swift +0 -375
  572. package/docs/provider.md +0 -202
  573. package/docs/providers.md +0 -86
  574. package/docs/refactor/macros.md +0 -35
  575. package/docs/refresh-loop.md +0 -22
  576. package/docs/releasing-homebrew.md +0 -35
  577. package/docs/releasing.md +0 -121
  578. package/docs/research/concrete_todo_2026-02-22.md +0 -47
  579. package/docs/research/core-feature-research.md +0 -248
  580. package/docs/research/feature_uiux_roadmap_2026-02-22.md +0 -202
  581. package/docs/research/parity_matrix.md +0 -49
  582. package/docs/research/provider_expansion_candidates_2026-02-22.md +0 -80
  583. package/docs/research/provider_model_token_project_matrix_2026-02-23.md +0 -46
  584. package/docs/research/release_gate_checklist.md +0 -57
  585. package/docs/site.css +0 -505
  586. package/docs/sparkle.md +0 -28
  587. package/docs/status.md +0 -28
  588. package/docs/sync/implementation.md +0 -393
  589. package/docs/sync/readme.md +0 -317
  590. package/docs/sync_files_summary.md +0 -391
  591. package/docs/sync_quick_reference.md +0 -225
  592. package/docs/todo.md +0 -11
  593. package/docs/ui.md +0 -29
  594. package/docs/web-integration.md +0 -9
  595. package/docs/widgets.md +0 -85
  596. package/docs/workflows/error-handling-improvements.md +0 -434
  597. package/docs/workflows/folder_structure.md +0 -299
  598. package/docs/workflows/loading_states.md +0 -446
  599. package/docs/workflows/loading_states_examples.md +0 -565
  600. package/docs/workflows/loading_states_flow.md +0 -430
  601. package/docs/workflows/performance_optimizations.md +0 -457
  602. package/docs/workflows/phase1-cli-testing-report.md +0 -313
  603. package/docs/workflows/quick_reference.md +0 -228
  604. package/docs/workflows/readme.md +0 -59
  605. package/docs/workflows/recent_improvements.md +0 -275
  606. package/docs/workflows/sync_enablement_summary.md +0 -400
  607. package/docs/zai.md +0 -43
  608. package/runic.png +0 -0
  609. package/runic.svg +0 -15
  610. package/version.env +0 -2
@@ -1,1412 +0,0 @@
1
- import AppKit
2
- import Observation
3
- import RunicCore
4
- import SwiftUI
5
-
6
- // MARK: - NSMenu construction
7
-
8
- extension StatusItemController {
9
- static let menuCardBaseWidth: CGFloat = 340
10
- private static let menuOpenPingDelay: Duration = PerformanceConstants.menuOpenPingDelay
11
-
12
- func menuCardWidth(for providers: [UsageProvider], menu: NSMenu? = nil) -> CGFloat {
13
- _ = menu
14
- let baseWidth = Self.menuCardBaseWidth
15
- guard self.usesSidebarSwitcher(for: providers) else { return baseWidth }
16
- return baseWidth + MenuCardSidebarMetrics.sidebarWidth(
17
- for: providers.count,
18
- iconSize: self.settings.providerSwitcherIconSize)
19
- }
20
-
21
- func usesSidebarSwitcher(for providers: [UsageProvider]) -> Bool {
22
- guard self.shouldMergeIcons, providers.count > 1 else { return false }
23
- return self.settings.providerSwitcherLayout == .sidebar
24
- }
25
-
26
- func makeMenu() -> NSMenu {
27
- guard self.shouldMergeIcons else {
28
- return self.makeMenu(for: nil)
29
- }
30
- let menu = NSMenu()
31
- menu.autoenablesItems = false
32
- menu.delegate = self
33
- return menu
34
- }
35
-
36
- func menuWillOpen(_ menu: NSMenu) {
37
- if self.isHostedSubviewMenu(menu) {
38
- self.refreshHostedSubviewHeights(in: menu)
39
- self.openMenus[ObjectIdentifier(menu)] = menu
40
- return
41
- }
42
- if menu.supermenu != nil {
43
- return
44
- }
45
-
46
- var provider: UsageProvider?
47
- if self.shouldMergeIcons {
48
- self.selectedMenuProvider = self.resolvedMenuProvider()
49
- self.lastMenuProvider = self.selectedMenuProvider ?? .codex
50
- provider = self.selectedMenuProvider
51
- } else {
52
- if let menuProvider = self.menuProviders[ObjectIdentifier(menu)] {
53
- self.lastMenuProvider = menuProvider
54
- provider = menuProvider
55
- } else if menu === self.fallbackMenu {
56
- self.lastMenuProvider = self.store.enabledProviders().first ?? .codex
57
- provider = nil
58
- } else {
59
- let resolved = self.store.enabledProviders().first ?? .codex
60
- self.lastMenuProvider = resolved
61
- provider = resolved
62
- }
63
- }
64
-
65
- if self.menuNeedsRefresh(menu) {
66
- self.populateMenu(menu, provider: provider)
67
- self.markMenuFresh(menu)
68
- }
69
- self.refreshMenuCardHeights(in: menu)
70
- self.openMenus[ObjectIdentifier(menu)] = menu
71
- self.scheduleOpenMenuPing(for: menu)
72
- }
73
-
74
- func menuDidClose(_ menu: NSMenu) {
75
- self.openMenus.removeValue(forKey: ObjectIdentifier(menu))
76
- self.menuPingTasks.removeValue(forKey: ObjectIdentifier(menu))?.cancel()
77
- for menuItem in menu.items {
78
- (menuItem.view as? MenuCardHighlighting)?.setHighlighted(false)
79
- }
80
- }
81
-
82
- func menu(_ menu: NSMenu, willHighlight item: NSMenuItem?) {
83
- for menuItem in menu.items {
84
- let highlighted = menuItem == item && menuItem.isEnabled
85
- (menuItem.view as? MenuCardHighlighting)?.setHighlighted(highlighted)
86
- }
87
- }
88
-
89
- func populateMenu(_ menu: NSMenu, provider: UsageProvider?) {
90
- menu.removeAllItems()
91
-
92
- let selectedProvider = provider
93
- let enabledProviders = self.store.enabledProviders()
94
- let menuWidth = self.menuCardWidth(for: enabledProviders, menu: menu)
95
- let descriptor = MenuDescriptor.build(
96
- provider: selectedProvider,
97
- store: self.store,
98
- settings: self.settings,
99
- account: self.account,
100
- updateReady: self.updater.updateStatus.isUpdateReady)
101
- let dashboard = self.store.openAIDashboard
102
- let currentProvider = selectedProvider ?? enabledProviders.first ?? .codex
103
- let openAIWebEligible = currentProvider == .codex &&
104
- self.store.openAIDashboardRequiresLogin == false &&
105
- dashboard != nil
106
- let hasCreditsHistory = openAIWebEligible && !(dashboard?.dailyBreakdown ?? []).isEmpty
107
- let hasUsageBreakdown = openAIWebEligible && !(dashboard?.usageBreakdown ?? []).isEmpty
108
- let hasCostHistory = self.settings.isCostUsageEffectivelyEnabled(for: currentProvider) &&
109
- (self.store.tokenSnapshot(for: currentProvider)?.daily.isEmpty == false)
110
- let hasOpenAIWebMenuItems = hasCreditsHistory || hasUsageBreakdown || hasCostHistory
111
- var addedOpenAIWebItems = false
112
-
113
- let useSidebarSwitcher = self.usesSidebarSwitcher(for: enabledProviders)
114
- let sidebarConfig: MenuCardSidebarConfig? = if useSidebarSwitcher {
115
- MenuCardSidebarConfig(
116
- providers: enabledProviders,
117
- selected: selectedProvider,
118
- iconProvider: { [weak self] provider, iconSize in
119
- self?.switcherIcon(for: provider, size: iconSize) ?? NSImage()
120
- },
121
- weeklyRemainingProvider: { [weak self] provider in
122
- self?.switcherWeeklyRemaining(for: provider)
123
- },
124
- onSelect: { [weak self, weak menu] provider in
125
- guard let self, let menu else { return }
126
- self.selectedMenuProvider = provider
127
- self.lastMenuProvider = provider
128
- // Defer repopulation to avoid reentrancy: the SwiftUI button
129
- // that triggered this closure lives inside the menu item that
130
- // populateMenu will destroy via removeAllItems().
131
- Task { @MainActor [weak self, weak menu] in
132
- guard let self, let menu else { return }
133
- self.populateMenu(menu, provider: provider)
134
- self.markMenuFresh(menu)
135
- self.refreshMenuCardHeights(in: menu)
136
- self.applyIcon(phase: nil)
137
- }
138
- })
139
- } else {
140
- nil
141
- }
142
-
143
- if self.shouldMergeIcons, enabledProviders.count > 1, !useSidebarSwitcher {
144
- // Provider tab bar (Feature 6) — replaces plain switcher when available
145
- let tabBarView = self.makeProviderTabBar(
146
- providers: enabledProviders,
147
- selected: selectedProvider,
148
- width: menuWidth,
149
- menu: menu)
150
- if let tabBarView {
151
- let monoTabBar = tabBarView.runicTypography()
152
- let hosting = MenuHostingView(rootView: monoTabBar)
153
- let controller = NSHostingController(rootView: monoTabBar)
154
- let size = controller.sizeThatFits(in: CGSize(width: menuWidth, height: .greatestFiniteMagnitude))
155
- hosting.frame = NSRect(origin: .zero, size: NSSize(width: menuWidth, height: size.height))
156
- let tabItem = NSMenuItem()
157
- tabItem.view = hosting
158
- tabItem.isEnabled = false
159
- tabItem.representedObject = "providerTabBar"
160
- menu.addItem(tabItem)
161
- } else {
162
- let switcherItem = self.makeProviderSwitcherItem(
163
- providers: enabledProviders,
164
- selected: selectedProvider,
165
- menu: menu)
166
- menu.addItem(switcherItem)
167
- }
168
- menu.addItem(.separator())
169
- }
170
-
171
- // Overview tab — show all providers at a glance
172
- if selectedProvider == nil, enabledProviders.count > 1 {
173
- let overviewView = self.makeOverviewView(
174
- providers: enabledProviders,
175
- width: menuWidth)
176
- let monoOverview = overviewView.runicTypography()
177
- let hosting = MenuHostingView(rootView: monoOverview)
178
- let controller = NSHostingController(rootView: monoOverview)
179
- let size = controller.sizeThatFits(in: CGSize(width: menuWidth, height: .greatestFiniteMagnitude))
180
- hosting.frame = NSRect(origin: .zero, size: NSSize(width: menuWidth, height: size.height))
181
- let overviewItem = NSMenuItem()
182
- overviewItem.view = hosting
183
- overviewItem.isEnabled = false
184
- overviewItem.representedObject = "overviewCard"
185
- menu.addItem(overviewItem)
186
- menu.addItem(.separator())
187
- }
188
-
189
- if let model = self.menuCardModel(for: selectedProvider) {
190
- if hasOpenAIWebMenuItems, !useSidebarSwitcher {
191
- let webItems = OpenAIWebMenuItems(
192
- hasUsageBreakdown: hasUsageBreakdown,
193
- hasCreditsHistory: hasCreditsHistory,
194
- hasCostHistory: hasCostHistory)
195
- self.addMenuCardSections(
196
- to: menu,
197
- model: model,
198
- provider: currentProvider,
199
- width: menuWidth,
200
- sidebar: sidebarConfig,
201
- webItems: webItems)
202
- addedOpenAIWebItems = true
203
- } else {
204
- let cardView = self.menuCardContent(width: menuWidth, sidebar: sidebarConfig, showIcons: true) {
205
- UsageMenuCardView(model: model, width: $0)
206
- }
207
- menu.addItem(self.makeMenuCardItem(cardView, id: "menuCard", width: menuWidth))
208
- if currentProvider == .codex, model.creditsText != nil {
209
- menu.addItem(self.makeBuyCreditsItem())
210
- }
211
- menu.addItem(.separator())
212
- }
213
- }
214
-
215
- if hasOpenAIWebMenuItems {
216
- if !addedOpenAIWebItems {
217
- if hasUsageBreakdown {
218
- _ = self.addUsageBreakdownSubmenu(to: menu)
219
- }
220
- if hasCreditsHistory {
221
- _ = self.addCreditsHistorySubmenu(to: menu)
222
- }
223
- if hasCostHistory {
224
- _ = self.addCostHistorySubmenu(to: menu, provider: currentProvider)
225
- }
226
- }
227
- menu.addItem(.separator())
228
- }
229
-
230
- // Timeline & activity chart submenus
231
- let hasTimeline = self.addUsageTimelineSubmenu(to: menu, provider: currentProvider)
232
- let hasHourly = self.addHourlyActivitySubmenu(to: menu, provider: currentProvider)
233
- let hasWeekly = self.addWeeklyActivitySubmenu(to: menu, provider: currentProvider)
234
- if hasTimeline || hasHourly || hasWeekly {
235
- menu.addItem(.separator())
236
- }
237
-
238
- // Utilization & window comparison chart submenus
239
- let hasUtilization = self.addSubscriptionUtilizationSubmenu(to: menu, provider: currentProvider)
240
- let hasWindowComparison = self.addUsageWindowComparisonSubmenu(to: menu, provider: currentProvider)
241
- if hasUtilization || hasWindowComparison {
242
- menu.addItem(.separator())
243
- }
244
-
245
- // Project & model breakdown submenus
246
- let hasProjectBreakdown = self.addProjectBreakdownSubmenu(to: menu, provider: currentProvider)
247
- let hasModelBreakdown = self.addModelBreakdownSubmenu(to: menu, provider: currentProvider)
248
- if hasProjectBreakdown || hasModelBreakdown {
249
- menu.addItem(.separator())
250
- }
251
-
252
- // Export Usage submenu
253
- self.addExportUsageSubmenu(to: menu)
254
-
255
- // Custom providers section
256
- self.addCustomProvidersSection(to: menu)
257
-
258
- let actionableSections = descriptor.sections.filter { section in
259
- section.entries.contains { entry in
260
- if case .action = entry { return true }
261
- return false
262
- }
263
- }
264
- for (index, section) in actionableSections.enumerated() {
265
- for entry in section.entries {
266
- switch entry {
267
- case let .text(text, style):
268
- let item = NSMenuItem(title: text, action: nil, keyEquivalent: "")
269
- item.isEnabled = false
270
- if style == .headline {
271
- let font = RunicFont.nsFont(size: NSFont.systemFontSize, weight: .semibold)
272
- item.attributedTitle = NSAttributedString(string: text, attributes: [.font: font])
273
- } else if style == .secondary {
274
- let font = RunicFont.nsFont(size: NSFont.smallSystemFontSize)
275
- item.attributedTitle = NSAttributedString(
276
- string: text,
277
- attributes: [.font: font, .foregroundColor: NSColor.secondaryLabelColor])
278
- }
279
- menu.addItem(item)
280
- case let .action(title, action):
281
- if case .refresh = action {
282
- menu.addItem(self.makePersistentRefreshItem(title: title))
283
- break
284
- }
285
- let (selector, represented) = self.selector(for: action)
286
- let item = NSMenuItem(title: title, action: selector, keyEquivalent: "")
287
- item.target = self
288
- item.representedObject = represented
289
- if let iconName = action.systemImageName,
290
- let image = NSImage(systemSymbolName: iconName, accessibilityDescription: nil)
291
- {
292
- image.isTemplate = true
293
- image.size = NSSize(width: 16, height: 16)
294
- item.image = image
295
- }
296
- if case let .switchAccount(targetProvider) = action,
297
- let subtitle = self.switchAccountSubtitle(for: targetProvider)
298
- {
299
- item.isEnabled = false
300
- self.applySubtitle(subtitle, to: item, title: title)
301
- }
302
- menu.addItem(item)
303
- case .divider:
304
- menu.addItem(.separator())
305
- }
306
- }
307
- if index < actionableSections.count - 1 {
308
- menu.addItem(.separator())
309
- }
310
- }
311
-
312
- self.applyRunicFont(to: menu)
313
- }
314
-
315
- func makeMenu(for provider: UsageProvider?) -> NSMenu {
316
- let menu = NSMenu()
317
- menu.autoenablesItems = false
318
- menu.delegate = self
319
- if let provider {
320
- self.menuProviders[ObjectIdentifier(menu)] = provider
321
- }
322
- return menu
323
- }
324
-
325
- private func makeProviderSwitcherItem(
326
- providers: [UsageProvider],
327
- selected: UsageProvider?,
328
- menu: NSMenu) -> NSMenuItem
329
- {
330
- let view = ProviderSwitcherView(
331
- providers: providers,
332
- selected: selected,
333
- width: self.menuCardWidth(for: providers, menu: menu),
334
- showsIcons: self.settings.switcherShowsIcons,
335
- iconSizePreference: self.settings.providerSwitcherIconSize,
336
- iconProvider: { [weak self] provider, iconSize in
337
- self?.switcherIcon(for: provider, size: iconSize) ?? NSImage()
338
- },
339
- weeklyRemainingProvider: { [weak self] provider in
340
- self?.switcherWeeklyRemaining(for: provider)
341
- },
342
- onSelect: { [weak self, weak menu] provider in
343
- guard let self, let menu else { return }
344
- self.selectedMenuProvider = provider
345
- self.lastMenuProvider = provider
346
- self.populateMenu(menu, provider: provider)
347
- self.markMenuFresh(menu)
348
- self.refreshMenuCardHeights(in: menu)
349
- self.applyIcon(phase: nil)
350
- })
351
- let item = NSMenuItem()
352
- item.view = view
353
- item.isEnabled = false
354
- return item
355
- }
356
-
357
- private func makeProviderTabBar(
358
- providers: [UsageProvider],
359
- selected: UsageProvider?,
360
- width: CGFloat,
361
- menu: NSMenu) -> ProviderTabBarView?
362
- {
363
- guard providers.count > 1 else { return nil }
364
- let overviewColor = Color(nsColor: .controlAccentColor)
365
-
366
- var tabs: [ProviderTabBarView.TabItem] = [
367
- ProviderTabBarView.TabItem(
368
- id: "overview",
369
- label: "Overview",
370
- icon: nil,
371
- provider: nil,
372
- isSelected: selected == nil,
373
- brandColor: overviewColor),
374
- ]
375
-
376
- for provider in providers {
377
- let meta = self.store.metadata(for: provider)
378
- let icon = ProviderBrandIcon.image(for: provider, size: 18)
379
- let descriptor = ProviderDescriptorRegistry.descriptor(for: provider)
380
- let brandColor = Color(
381
- red: Double(descriptor.branding.color.red),
382
- green: Double(descriptor.branding.color.green),
383
- blue: Double(descriptor.branding.color.blue))
384
- tabs.append(ProviderTabBarView.TabItem(
385
- id: provider.rawValue,
386
- label: Self.abbreviatedProviderName(meta.displayName),
387
- icon: icon,
388
- provider: provider,
389
- isSelected: selected == provider,
390
- brandColor: brandColor))
391
- }
392
-
393
- return ProviderTabBarView(
394
- tabs: tabs,
395
- width: width,
396
- onSelect: { [weak self, weak menu] provider in
397
- guard let self, let menu else { return }
398
- self.selectedMenuProvider = provider
399
- self.lastMenuProvider = provider
400
- Task { @MainActor [weak self, weak menu] in
401
- guard let self, let menu else { return }
402
- self.populateMenu(menu, provider: provider)
403
- self.markMenuFresh(menu)
404
- self.refreshMenuCardHeights(in: menu)
405
- self.applyIcon(phase: nil)
406
- }
407
- })
408
- }
409
-
410
- private func makeOverviewView(
411
- providers: [UsageProvider],
412
- width: CGFloat) -> OverviewMenuView
413
- {
414
- let calendar = Calendar.current
415
- let todayStart = calendar.startOfDay(for: Date())
416
- let weekAgo = calendar.date(byAdding: .day, value: -7, to: todayStart) ?? todayStart
417
- let formatter = DateFormatter()
418
- formatter.dateFormat = "yyyy-MM-dd"
419
- formatter.timeZone = TimeZone.current
420
-
421
- var summaries: [OverviewMenuView.ProviderSummary] = []
422
- var chartPoints: [OverviewMenuView.DailyPoint] = []
423
- var totalToday = 0
424
-
425
- for provider in providers {
426
- let meta = self.store.metadata(for: provider)
427
- let snapshot = self.store.snapshot(for: provider)
428
- let icon = ProviderBrandIcon.image(for: provider, size: 16)
429
- let descriptor = ProviderDescriptorRegistry.descriptor(for: provider)
430
- let brandColor = Color(
431
- red: Double(descriptor.branding.color.red),
432
- green: Double(descriptor.branding.color.green),
433
- blue: Double(descriptor.branding.color.blue))
434
- let usedPercent = snapshot?.primary.usedPercent ?? 0
435
- let todayTokens = self.store.ledgerDailySummary(for: provider)?.totals.totalTokens ?? 0
436
- totalToday += todayTokens
437
-
438
- let resetDesc = snapshot?.primary.resetDescription
439
- let windowLabel = snapshot?.primary.label?.trimmingCharacters(in: .whitespacesAndNewlines)
440
- let topModel = self.store.ledgerTopModel(for: provider)
441
- let topModelContext: String? = if let model = topModel?.model {
442
- UsageFormatter.modelContextLabel(for: model)
443
- } else {
444
- Self.defaultContextLabel(for: provider)
445
- }
446
-
447
- summaries.append(OverviewMenuView.ProviderSummary(
448
- id: provider.rawValue,
449
- provider: provider,
450
- name: meta.displayName,
451
- icon: icon,
452
- usedPercent: usedPercent,
453
- todayTokens: todayTokens,
454
- brandColor: brandColor,
455
- resetDescription: resetDesc,
456
- windowLabel: windowLabel,
457
- topModelContext: topModelContext))
458
-
459
- // Chart data — last 7 days
460
- let dailySummaries = self.store.ledgerAllDailySummary(for: provider)
461
- for summary in dailySummaries where summary.dayStart >= weekAgo {
462
- chartPoints.append(OverviewMenuView.DailyPoint(
463
- id: "\(provider.rawValue)-\(summary.dayKey)",
464
- date: summary.dayStart,
465
- tokens: summary.totals.totalTokens,
466
- provider: meta.displayName,
467
- color: brandColor))
468
- }
469
- }
470
-
471
- // Only show providers with data
472
- let activeSummaries = summaries.filter { $0.usedPercent > 0 || $0.todayTokens > 0 }
473
-
474
- return OverviewMenuView(
475
- summaries: activeSummaries.isEmpty ? summaries : activeSummaries,
476
- chartPoints: chartPoints,
477
- totalTodayTokens: totalToday,
478
- totalProviders: UsageProvider.allCases.count,
479
- width: width)
480
- }
481
-
482
- private static func abbreviatedProviderName(_ name: String) -> String {
483
- if name.count <= 8 { return name }
484
- // Abbreviate long names
485
- let abbreviations: [String: String] = [
486
- "Antigravity": "AntiG",
487
- "OpenRouter": "ORouter",
488
- "Perplexity": "Perplx",
489
- "SambaNova": "SambaN",
490
- "Azure OpenAI": "Azure",
491
- ]
492
- return abbreviations[name] ?? String(name.prefix(6))
493
- }
494
-
495
- /// Default flagship model context window per provider, loaded from
496
- /// `Resources/provider-context-windows.json`. Update the JSON to add/change
497
- /// context windows without recompiling.
498
- private static let defaultContextWindows: [String: Int] = {
499
- struct Entry: Decodable { let contextK: Int }
500
- guard let url = Bundle.main.url(forResource: "provider-context-windows", withExtension: "json"),
501
- let data = try? Data(contentsOf: url),
502
- let dict = try? JSONDecoder().decode([String: Entry].self, from: data)
503
- else { return [:] }
504
- return dict.mapValues(\.contextK)
505
- }()
506
-
507
- private static func defaultContextLabel(for provider: UsageProvider) -> String? {
508
- guard let k = self.defaultContextWindows[provider.rawValue] else { return nil }
509
- return k >= 1000 ? "ctx \(k / 1000)M" : "ctx \(k)K"
510
- }
511
-
512
- private func resolvedMenuProvider() -> UsageProvider? {
513
- let enabled = self.store.enabledProviders()
514
- if enabled.isEmpty { return .codex }
515
- if let selected = self.selectedMenuProvider, enabled.contains(selected) {
516
- return selected
517
- }
518
- return enabled.first
519
- }
520
-
521
- private func menuNeedsRefresh(_ menu: NSMenu) -> Bool {
522
- let key = ObjectIdentifier(menu)
523
- return self.menuVersions[key] != self.menuContentVersion
524
- }
525
-
526
- private func markMenuFresh(_ menu: NSMenu) {
527
- let key = ObjectIdentifier(menu)
528
- self.menuVersions[key] = self.menuContentVersion
529
- }
530
-
531
- func refreshOpenMenusIfNeeded() {
532
- guard !self.openMenus.isEmpty else { return }
533
- for (key, menu) in self.openMenus {
534
- guard key == ObjectIdentifier(menu) else {
535
- self.openMenus.removeValue(forKey: key)
536
- continue
537
- }
538
-
539
- if self.isHostedSubviewMenu(menu) {
540
- self.refreshHostedSubviewHeights(in: menu)
541
- continue
542
- }
543
-
544
- if self.menuNeedsRefresh(menu) {
545
- let provider = self.menuProvider(for: menu)
546
- self.populateMenu(menu, provider: provider)
547
- self.markMenuFresh(menu)
548
- self.refreshMenuCardHeights(in: menu)
549
- }
550
- }
551
- }
552
-
553
- private func menuProvider(for menu: NSMenu) -> UsageProvider? {
554
- if self.shouldMergeIcons {
555
- return self.selectedMenuProvider ?? self.resolvedMenuProvider()
556
- }
557
- if let provider = self.menuProviders[ObjectIdentifier(menu)] {
558
- return provider
559
- }
560
- if menu === self.fallbackMenu {
561
- return nil
562
- }
563
- return self.store.enabledProviders().first ?? .codex
564
- }
565
-
566
- private func scheduleOpenMenuPing(for menu: NSMenu) {
567
- guard self.settings.refreshFrequency != .manual else { return }
568
- Task { await self.store.refresh(trigger: .menuOpen) }
569
- let key = ObjectIdentifier(menu)
570
- self.menuPingTasks[key]?.cancel()
571
- self.menuPingTasks[key] = Task { @MainActor [weak self, weak menu] in
572
- guard let self, let menu else { return }
573
- try? await Task.sleep(for: Self.menuOpenPingDelay)
574
- guard !Task.isCancelled else { return }
575
- guard self.openMenus[ObjectIdentifier(menu)] != nil else { return }
576
- guard !self.store.isRefreshing else { return }
577
- let provider = self.menuProvider(for: menu) ?? self.resolvedMenuProvider()
578
- let isStale = provider.map { self.store.isStale(provider: $0) } ?? self.store.isStale
579
- let hasSnapshot = provider.map { self.store.snapshot(for: $0) != nil } ?? true
580
- guard isStale || !hasSnapshot else { return }
581
- await self.store.refresh(trigger: .menuOpen)
582
- }
583
- }
584
-
585
- func refreshMenuCardHeights(in menu: NSMenu) {
586
- let cardItems = menu.items.filter { item in
587
- (item.representedObject as? String)?.hasPrefix("menuCard") == true
588
- }
589
- for item in cardItems {
590
- guard let view = item.view else { continue }
591
- let width = self.menuCardWidth(for: self.store.enabledProviders(), menu: menu)
592
- let height = self.menuCardHeight(for: view, width: width)
593
- view.frame = NSRect(
594
- origin: .zero,
595
- size: NSSize(width: width, height: height))
596
- }
597
- }
598
-
599
- private func switcherIcon(for provider: UsageProvider, size: CGFloat) -> NSImage {
600
- if let brand = ProviderBrandIcon.image(for: provider, size: size) {
601
- return brand
602
- }
603
-
604
- let snapshot = self.store.snapshot(for: provider)
605
- let showUsed = self.settings.usageBarsShowUsed
606
- let primary = showUsed ? snapshot?.primary.usedPercent : snapshot?.primary.remainingPercent
607
- let weekly = showUsed ? snapshot?.secondary?.usedPercent : snapshot?.secondary?.remainingPercent
608
- let credits = provider == .codex ? self.store.credits?.remaining : nil
609
- let stale = self.store.isStale(provider: provider)
610
- let style = self.store.style(for: provider)
611
- let indicator = self.store.statusIndicator(for: provider)
612
- let image = IconRenderer.makeIcon(
613
- primaryRemaining: primary,
614
- weeklyRemaining: weekly,
615
- creditsRemaining: credits,
616
- stale: stale,
617
- style: style,
618
- blink: 0,
619
- wiggle: 0,
620
- tilt: 0,
621
- statusIndicator: indicator)
622
- image.isTemplate = true
623
- image.size = NSSize(width: size, height: size)
624
- return image
625
- }
626
-
627
- private func switcherWeeklyRemaining(for provider: UsageProvider) -> Double? {
628
- let snapshot = self.store.snapshot(for: provider)
629
- let window: RateWindow? = if provider == .factory {
630
- snapshot?.secondary ?? snapshot?.primary
631
- } else {
632
- snapshot?.primary ?? snapshot?.secondary
633
- }
634
- guard let window else { return nil }
635
- if self.settings.usageBarsShowUsed {
636
- return window.usedPercent
637
- }
638
- return window.remainingPercent
639
- }
640
-
641
- private func addExportUsageSubmenu(to menu: NSMenu) {
642
- let exportItem = NSMenuItem(title: "Export Usage\u{2026}", action: nil, keyEquivalent: "")
643
- if let image = NSImage(systemSymbolName: "square.and.arrow.up", accessibilityDescription: "Export") {
644
- image.isTemplate = true
645
- image.size = NSSize(width: 16, height: 16)
646
- exportItem.image = image
647
- }
648
- let submenu = NSMenu(title: "Export Usage")
649
- let csvItem = NSMenuItem(
650
- title: "Export as CSV\u{2026}",
651
- action: #selector(self.exportUsageCSV(_:)),
652
- keyEquivalent: "")
653
- csvItem.target = self
654
- let jsonItem = NSMenuItem(
655
- title: "Export as JSON\u{2026}",
656
- action: #selector(self.exportUsageJSON(_:)),
657
- keyEquivalent: "")
658
- jsonItem.target = self
659
- submenu.addItem(csvItem)
660
- submenu.addItem(jsonItem)
661
- exportItem.submenu = submenu
662
- menu.addItem(exportItem)
663
- menu.addItem(.separator())
664
- }
665
-
666
- private func selector(for action: MenuDescriptor.MenuAction) -> (Selector, Any?) {
667
- switch action {
668
- case .installUpdate: (#selector(self.installUpdate), nil)
669
- case .refresh: (#selector(self.refreshNow), nil)
670
- case .dashboard: (#selector(self.openDashboard), nil)
671
- case .statusPage: (#selector(self.openStatusPage), nil)
672
- case let .switchAccount(provider): (#selector(self.runSwitchAccount(_:)), provider.rawValue)
673
- case .settings: (#selector(self.showSettingsGeneral), nil)
674
- case .about: (#selector(self.showSettingsAbout), nil)
675
- case .quit: (#selector(self.quit), nil)
676
- case let .copyError(message): (#selector(self.copyError(_:)), message)
677
- }
678
- }
679
-
680
- @objc func menuCardNoOp(_ sender: NSMenuItem) {
681
- _ = sender
682
- }
683
-
684
- private func isHostedSubviewMenu(_ menu: NSMenu) -> Bool {
685
- let ids: Set = [
686
- "usageBreakdownChart",
687
- "creditsHistoryChart",
688
- "costHistoryChart",
689
- "projectBreakdownChart",
690
- "modelBreakdownChart",
691
- ]
692
- return menu.items.contains { item in
693
- guard let id = item.representedObject as? String else { return false }
694
- return ids.contains(id)
695
- }
696
- }
697
-
698
- private func refreshHostedSubviewHeights(in menu: NSMenu) {
699
- let enabledProviders = self.store.enabledProviders()
700
- let width = self.menuCardWidth(for: enabledProviders, menu: menu)
701
-
702
- for item in menu.items {
703
- guard let view = item.view else { continue }
704
- view.frame = NSRect(origin: .zero, size: NSSize(width: width, height: 1))
705
- view.layoutSubtreeIfNeeded()
706
- let height = view.fittingSize.height
707
- view.frame = NSRect(origin: .zero, size: NSSize(width: width, height: height))
708
- }
709
- }
710
- }
711
-
712
- // MARK: - ProviderSwitcherView (NSView-based segmented control)
713
-
714
- private final class ProviderSwitcherView: NSView {
715
- private struct Segment {
716
- let provider: UsageProvider
717
- let image: NSImage
718
- let title: String
719
- }
720
-
721
- private struct WeeklyIndicator {
722
- let provider: UsageProvider
723
- let track: NSView
724
- let fill: NSView
725
- }
726
-
727
- private let segments: [Segment]
728
- private let onSelect: (UsageProvider) -> Void
729
- private let showsIcons: Bool
730
- private let weeklyRemainingProvider: (UsageProvider) -> Double?
731
- private var buttons: [NSButton] = []
732
- private var weeklyIndicators: [ObjectIdentifier: WeeklyIndicator] = [:]
733
- private var hoverTrackingArea: NSTrackingArea?
734
- private var segmentWidths: [CGFloat] = []
735
- private let selectedBackground = NSColor.controlAccentColor.cgColor
736
- private let unselectedBackground = NSColor.clear.cgColor
737
- private let selectedTextColor = NSColor.white
738
- private let unselectedTextColor = NSColor.secondaryLabelColor
739
- private let stackedIcons: Bool
740
- private let useTwoRows: Bool
741
- private let rowSpacing: CGFloat
742
- private let rowHeight: CGFloat
743
- private var preferredWidth: CGFloat = 0
744
- private var hoveredButtonTag: Int?
745
- private let lightModeOverlayLayer = CALayer()
746
-
747
- init(
748
- providers: [UsageProvider],
749
- selected: UsageProvider?,
750
- width: CGFloat,
751
- showsIcons: Bool,
752
- iconSizePreference: ProviderSwitcherIconSize,
753
- iconProvider: (UsageProvider, CGFloat) -> NSImage,
754
- weeklyRemainingProvider: @escaping (UsageProvider) -> Double?,
755
- onSelect: @escaping (UsageProvider) -> Void)
756
- {
757
- let minimumGap: CGFloat = 1
758
- let iconSize: CGFloat = iconSizePreference == .small ? 28 : 34
759
- self.segments = providers.map { provider in
760
- let fullTitle = Self.switcherTitle(for: provider)
761
- let icon = iconProvider(provider, iconSize)
762
- icon.size = NSSize(width: iconSize, height: iconSize)
763
- return Segment(
764
- provider: provider,
765
- image: icon,
766
- title: fullTitle)
767
- }
768
- self.onSelect = onSelect
769
- self.showsIcons = showsIcons
770
- self.weeklyRemainingProvider = weeklyRemainingProvider
771
- self.stackedIcons = showsIcons && providers.count > 3
772
- let initialOuterPadding = Self.switcherOuterPadding(
773
- for: width,
774
- count: self.segments.count,
775
- minimumGap: minimumGap)
776
- let initialMaxAllowedSegmentWidth = Self.maxAllowedUniformSegmentWidth(
777
- for: width,
778
- count: self.segments.count,
779
- outerPadding: initialOuterPadding,
780
- minimumGap: minimumGap)
781
- self.useTwoRows = Self.shouldUseTwoRows(
782
- count: self.segments.count,
783
- maxAllowedSegmentWidth: initialMaxAllowedSegmentWidth,
784
- stackedIcons: self.stackedIcons)
785
- self.rowSpacing = self.stackedIcons ? 3 : 3
786
- self.rowHeight = self.stackedIcons ? 56 : 42
787
- let height: CGFloat = self.useTwoRows ? (self.rowHeight * 2 + self.rowSpacing) : self.rowHeight
788
- self.preferredWidth = width
789
- super.init(frame: NSRect(x: 0, y: 0, width: width, height: height))
790
- Self.clearButtonWidthCache()
791
- self.wantsLayer = true
792
- self.layer?.masksToBounds = false
793
- self.lightModeOverlayLayer.masksToBounds = false
794
- self.layer?.insertSublayer(self.lightModeOverlayLayer, at: 0)
795
- self.updateLightModeStyling()
796
-
797
- let layoutCount = self.useTwoRows
798
- ? Int(ceil(Double(self.segments.count) / 2.0))
799
- : self.segments.count
800
- let outerPadding: CGFloat = Self.switcherOuterPadding(
801
- for: width,
802
- count: layoutCount,
803
- minimumGap: minimumGap)
804
- let maxAllowedSegmentWidth = Self.maxAllowedUniformSegmentWidth(
805
- for: width,
806
- count: layoutCount,
807
- outerPadding: outerPadding,
808
- minimumGap: minimumGap)
809
-
810
- func makeButton(index: Int, segment: Segment) -> NSButton {
811
- let button: NSButton
812
- if self.stackedIcons {
813
- let stacked = StackedToggleButton(
814
- title: segment.title,
815
- image: segment.image,
816
- iconSize: iconSize,
817
- target: self,
818
- action: #selector(self.handleSelection(_:)))
819
- button = stacked
820
- } else if self.showsIcons {
821
- let inline = InlineIconToggleButton(
822
- title: segment.title,
823
- image: segment.image,
824
- iconSize: iconSize,
825
- target: self,
826
- action: #selector(self.handleSelection(_:)))
827
- button = inline
828
- } else {
829
- button = PaddedToggleButton(
830
- title: segment.title,
831
- target: self,
832
- action: #selector(self.handleSelection(_:)))
833
- }
834
- button.tag = index
835
- if !self.showsIcons {
836
- button.image = nil
837
- button.imagePosition = .noImage
838
- }
839
-
840
- let remaining = self.weeklyRemainingProvider(segment.provider)
841
- self.addWeeklyIndicator(to: button, provider: segment.provider, remainingPercent: remaining)
842
- button.bezelStyle = .regularSquare
843
- button.isBordered = false
844
- button.controlSize = .small
845
- button.font = RunicFont.nsFont(size: 12, weight: .medium)
846
- button.setButtonType(.toggle)
847
- button.contentTintColor = self.unselectedTextColor
848
- button.alignment = .center
849
- button.wantsLayer = true
850
- button.layer?.cornerRadius = CGFloat(RunicCornerRadius.md)
851
- button.state = (selected == segment.provider) ? .on : .off
852
- button.toolTip = nil
853
- button.translatesAutoresizingMaskIntoConstraints = false
854
- self.buttons.append(button)
855
- return button
856
- }
857
-
858
- for (index, segment) in self.segments.enumerated() {
859
- let button = makeButton(index: index, segment: segment)
860
- self.addSubview(button)
861
- }
862
-
863
- let uniformWidth: CGFloat
864
- if self.useTwoRows || !self.stackedIcons {
865
- uniformWidth = self.applyUniformSegmentWidth(maxAllowedWidth: maxAllowedSegmentWidth)
866
- if uniformWidth > 0 {
867
- self.segmentWidths = Array(repeating: uniformWidth, count: self.buttons.count)
868
- }
869
- } else {
870
- self.segmentWidths = self.applyNonUniformSegmentWidths(
871
- totalWidth: width,
872
- outerPadding: outerPadding,
873
- minimumGap: minimumGap)
874
- uniformWidth = 0
875
- }
876
-
877
- self.applyLayout(
878
- outerPadding: outerPadding,
879
- minimumGap: minimumGap,
880
- uniformWidth: uniformWidth)
881
- if width > 0 {
882
- self.preferredWidth = width
883
- self.frame.size.width = width
884
- }
885
-
886
- self.updateButtonStyles()
887
- }
888
-
889
- override func layout() {
890
- super.layout()
891
- self.lightModeOverlayLayer.frame = self.bounds
892
- }
893
-
894
- override func viewDidChangeEffectiveAppearance() {
895
- super.viewDidChangeEffectiveAppearance()
896
- self.updateLightModeStyling()
897
- self.updateButtonStyles()
898
- }
899
-
900
- override func viewDidMoveToWindow() {
901
- super.viewDidMoveToWindow()
902
- self.window?.acceptsMouseMovedEvents = true
903
- }
904
-
905
- override func updateTrackingAreas() {
906
- super.updateTrackingAreas()
907
-
908
- if let hoverTrackingArea {
909
- self.removeTrackingArea(hoverTrackingArea)
910
- }
911
-
912
- let trackingArea = NSTrackingArea(
913
- rect: .zero,
914
- options: [
915
- .activeAlways,
916
- .inVisibleRect,
917
- .mouseEnteredAndExited,
918
- .mouseMoved,
919
- ],
920
- owner: self,
921
- userInfo: nil)
922
- self.addTrackingArea(trackingArea)
923
- self.hoverTrackingArea = trackingArea
924
- }
925
-
926
- override func mouseMoved(with event: NSEvent) {
927
- let location = self.convert(event.locationInWindow, from: nil)
928
- let hoveredTag = self.buttons.first(where: { $0.frame.contains(location) })?.tag
929
- guard hoveredTag != self.hoveredButtonTag else { return }
930
- self.hoveredButtonTag = hoveredTag
931
- self.updateButtonStyles()
932
- }
933
-
934
- override func mouseExited(with event: NSEvent) {
935
- guard self.hoveredButtonTag != nil else { return }
936
- self.hoveredButtonTag = nil
937
- self.updateButtonStyles()
938
- }
939
-
940
- private func applyLayout(
941
- outerPadding: CGFloat,
942
- minimumGap: CGFloat,
943
- uniformWidth: CGFloat)
944
- {
945
- if self.useTwoRows {
946
- self.applyTwoRowLayout(
947
- outerPadding: outerPadding,
948
- minimumGap: minimumGap,
949
- uniformWidth: uniformWidth)
950
- return
951
- }
952
-
953
- if self.buttons.count == 2 {
954
- let left = self.buttons[0]
955
- let right = self.buttons[1]
956
- let gap = right.leadingAnchor.constraint(greaterThanOrEqualTo: left.trailingAnchor, constant: minimumGap)
957
- gap.priority = .defaultHigh
958
- NSLayoutConstraint.activate([
959
- left.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: outerPadding),
960
- left.centerYAnchor.constraint(equalTo: self.centerYAnchor),
961
- right.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -outerPadding),
962
- right.centerYAnchor.constraint(equalTo: self.centerYAnchor),
963
- gap,
964
- ])
965
- return
966
- }
967
-
968
- if self.buttons.count == 3 {
969
- let left = self.buttons[0]
970
- let mid = self.buttons[1]
971
- let right = self.buttons[2]
972
-
973
- let leftGap = mid.leadingAnchor.constraint(greaterThanOrEqualTo: left.trailingAnchor, constant: minimumGap)
974
- leftGap.priority = .defaultHigh
975
- let rightGap = right.leadingAnchor.constraint(
976
- greaterThanOrEqualTo: mid.trailingAnchor,
977
- constant: minimumGap)
978
- rightGap.priority = .defaultHigh
979
-
980
- NSLayoutConstraint.activate([
981
- left.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: outerPadding),
982
- left.centerYAnchor.constraint(equalTo: self.centerYAnchor),
983
- mid.centerXAnchor.constraint(equalTo: self.centerXAnchor),
984
- mid.centerYAnchor.constraint(equalTo: self.centerYAnchor),
985
- right.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -outerPadding),
986
- right.centerYAnchor.constraint(equalTo: self.centerYAnchor),
987
- leftGap,
988
- rightGap,
989
- ])
990
- return
991
- }
992
-
993
- if self.buttons.count >= 4 {
994
- let widths = self.segmentWidths.isEmpty
995
- ? self.buttons.map { ceil($0.fittingSize.width) }
996
- : self.segmentWidths
997
- let layoutWidth = self.preferredWidth > 0 ? self.preferredWidth : self.bounds.width
998
- let availableWidth = max(0, layoutWidth - outerPadding * 2)
999
- let gaps = max(1, widths.count - 1)
1000
- let computedGap = gaps > 0
1001
- ? max(minimumGap, (availableWidth - widths.reduce(0, +)) / CGFloat(gaps))
1002
- : 0
1003
- let rowContainer = NSView()
1004
- rowContainer.translatesAutoresizingMaskIntoConstraints = false
1005
- self.addSubview(rowContainer)
1006
-
1007
- NSLayoutConstraint.activate([
1008
- rowContainer.topAnchor.constraint(equalTo: self.topAnchor),
1009
- rowContainer.bottomAnchor.constraint(equalTo: self.bottomAnchor),
1010
- rowContainer.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: outerPadding),
1011
- rowContainer.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -outerPadding),
1012
- ])
1013
-
1014
- var xOffset: CGFloat = 0
1015
- for (index, button) in self.buttons.enumerated() {
1016
- let width = index < widths.count ? widths[index] : 0
1017
- if self.stackedIcons {
1018
- NSLayoutConstraint.activate([
1019
- button.leadingAnchor.constraint(equalTo: rowContainer.leadingAnchor, constant: xOffset),
1020
- button.topAnchor.constraint(equalTo: rowContainer.topAnchor),
1021
- ])
1022
- } else {
1023
- NSLayoutConstraint.activate([
1024
- button.leadingAnchor.constraint(equalTo: rowContainer.leadingAnchor, constant: xOffset),
1025
- button.centerYAnchor.constraint(equalTo: rowContainer.centerYAnchor),
1026
- ])
1027
- }
1028
- xOffset += width + computedGap
1029
- }
1030
- return
1031
- }
1032
-
1033
- if let first = self.buttons.first {
1034
- NSLayoutConstraint.activate([
1035
- first.centerXAnchor.constraint(equalTo: self.centerXAnchor),
1036
- first.centerYAnchor.constraint(equalTo: self.centerYAnchor),
1037
- ])
1038
- }
1039
- }
1040
-
1041
- private func applyTwoRowLayout(
1042
- outerPadding: CGFloat,
1043
- minimumGap: CGFloat,
1044
- uniformWidth: CGFloat)
1045
- {
1046
- let splitIndex = Int(ceil(Double(self.buttons.count) / 2.0))
1047
- let topButtons = Array(self.buttons.prefix(splitIndex))
1048
- let bottomButtons = Array(self.buttons.dropFirst(splitIndex))
1049
-
1050
- let columns = max(topButtons.count, bottomButtons.count)
1051
- let layoutWidth = self.preferredWidth > 0 ? self.preferredWidth : self.bounds.width
1052
- let availableWidth = max(0, layoutWidth - outerPadding * 2)
1053
- let gaps = max(1, columns - 1)
1054
- let totalWidth = uniformWidth * CGFloat(columns)
1055
- let computedGap = gaps > 0
1056
- ? max(minimumGap, (availableWidth - totalWidth) / CGFloat(gaps))
1057
- : 0
1058
- let gridContainer = NSView()
1059
- gridContainer.translatesAutoresizingMaskIntoConstraints = false
1060
- self.addSubview(gridContainer)
1061
-
1062
- NSLayoutConstraint.activate([
1063
- gridContainer.topAnchor.constraint(equalTo: self.topAnchor),
1064
- gridContainer.bottomAnchor.constraint(equalTo: self.bottomAnchor),
1065
- gridContainer.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: outerPadding),
1066
- gridContainer.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -outerPadding),
1067
- ])
1068
-
1069
- let topRow = NSView()
1070
- topRow.translatesAutoresizingMaskIntoConstraints = false
1071
- gridContainer.addSubview(topRow)
1072
-
1073
- let bottomRow = NSView()
1074
- bottomRow.translatesAutoresizingMaskIntoConstraints = false
1075
- gridContainer.addSubview(bottomRow)
1076
-
1077
- NSLayoutConstraint.activate([
1078
- topRow.leadingAnchor.constraint(equalTo: gridContainer.leadingAnchor),
1079
- topRow.trailingAnchor.constraint(equalTo: gridContainer.trailingAnchor),
1080
- topRow.topAnchor.constraint(equalTo: gridContainer.topAnchor),
1081
- topRow.heightAnchor.constraint(equalToConstant: self.rowHeight),
1082
- bottomRow.leadingAnchor.constraint(equalTo: gridContainer.leadingAnchor),
1083
- bottomRow.trailingAnchor.constraint(equalTo: gridContainer.trailingAnchor),
1084
- bottomRow.bottomAnchor.constraint(equalTo: gridContainer.bottomAnchor),
1085
- bottomRow.heightAnchor.constraint(equalToConstant: self.rowHeight),
1086
- bottomRow.topAnchor.constraint(equalTo: topRow.bottomAnchor, constant: self.rowSpacing),
1087
- ])
1088
-
1089
- for index in 0..<columns {
1090
- let xOffset = CGFloat(index) * (uniformWidth + computedGap)
1091
- if index < topButtons.count {
1092
- let button = topButtons[index]
1093
- NSLayoutConstraint.activate([
1094
- button.leadingAnchor.constraint(equalTo: gridContainer.leadingAnchor, constant: xOffset),
1095
- button.centerYAnchor.constraint(equalTo: topRow.centerYAnchor),
1096
- ])
1097
- }
1098
- if index < bottomButtons.count {
1099
- let button = bottomButtons[index]
1100
- NSLayoutConstraint.activate([
1101
- button.leadingAnchor.constraint(equalTo: gridContainer.leadingAnchor, constant: xOffset),
1102
- button.centerYAnchor.constraint(equalTo: bottomRow.centerYAnchor),
1103
- ])
1104
- }
1105
- }
1106
- }
1107
-
1108
- private static func shouldUseTwoRows(
1109
- count: Int,
1110
- maxAllowedSegmentWidth: CGFloat,
1111
- stackedIcons: Bool) -> Bool
1112
- {
1113
- guard count > 1 else { return false }
1114
- let minimumComfortableAverage: CGFloat = stackedIcons ? 52 : 54
1115
- return maxAllowedSegmentWidth < minimumComfortableAverage
1116
- }
1117
-
1118
- private static func switcherOuterPadding(for width: CGFloat, count: Int, minimumGap: CGFloat) -> CGFloat {
1119
- let preferred: CGFloat = MenuCardMetrics.horizontalPadding
1120
- let reduced: CGFloat = 8
1121
- let minimal: CGFloat = 6
1122
-
1123
- func averageButtonWidth(outerPadding: CGFloat) -> CGFloat {
1124
- let available = width - outerPadding * 2 - minimumGap * CGFloat(max(0, count - 1))
1125
- guard count > 0 else { return 0 }
1126
- return available / CGFloat(count)
1127
- }
1128
-
1129
- let minimumComfortableAverage: CGFloat = count >= 5 ? 50 : 56
1130
-
1131
- if averageButtonWidth(outerPadding: preferred) >= minimumComfortableAverage { return preferred }
1132
- if averageButtonWidth(outerPadding: reduced) >= minimumComfortableAverage { return reduced }
1133
- return minimal
1134
- }
1135
-
1136
- @available(*, unavailable)
1137
- required init?(coder: NSCoder) {
1138
- nil
1139
- }
1140
-
1141
- override var intrinsicContentSize: NSSize {
1142
- NSSize(width: self.preferredWidth, height: self.frame.size.height)
1143
- }
1144
-
1145
- @objc private func handleSelection(_ sender: NSButton) {
1146
- let index = sender.tag
1147
- guard self.segments.indices.contains(index) else { return }
1148
- for (idx, button) in self.buttons.enumerated() {
1149
- button.state = (idx == index) ? .on : .off
1150
- }
1151
- self.updateButtonStyles()
1152
- self.onSelect(self.segments[index].provider)
1153
- }
1154
-
1155
- private func updateButtonStyles() {
1156
- for button in self.buttons {
1157
- let isSelected = button.state == .on
1158
- let isHovered = self.hoveredButtonTag == button.tag
1159
- button.contentTintColor = isSelected ? self.selectedTextColor : self.unselectedTextColor
1160
- button.layer?.backgroundColor = if isSelected {
1161
- self.selectedBackground
1162
- } else if isHovered {
1163
- self.hoverPlateColor()
1164
- } else {
1165
- self.unselectedBackground
1166
- }
1167
- self.updateWeeklyIndicatorVisibility(for: button)
1168
- (button as? StackedToggleButton)?.setContentTintColor(button.contentTintColor)
1169
- (button as? InlineIconToggleButton)?.setContentTintColor(button.contentTintColor)
1170
- }
1171
- }
1172
-
1173
- private func isLightMode() -> Bool {
1174
- self.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .aqua
1175
- }
1176
-
1177
- private func updateLightModeStyling() {
1178
- guard self.isLightMode() else {
1179
- self.lightModeOverlayLayer.backgroundColor = nil
1180
- return
1181
- }
1182
- self.lightModeOverlayLayer.backgroundColor = NSColor.black.withAlphaComponent(0.035).cgColor
1183
- }
1184
-
1185
- private func hoverPlateColor() -> CGColor {
1186
- if self.isLightMode() {
1187
- return NSColor.black.withAlphaComponent(0.095).cgColor
1188
- }
1189
- return NSColor.labelColor.withAlphaComponent(0.06).cgColor
1190
- }
1191
-
1192
- private static var buttonWidthCache: [ObjectIdentifier: CGFloat] = [:]
1193
-
1194
- private static func maxToggleWidth(for button: NSButton) -> CGFloat {
1195
- let buttonId = ObjectIdentifier(button)
1196
-
1197
- if let cached = buttonWidthCache[buttonId] {
1198
- return cached
1199
- }
1200
-
1201
- let originalState = button.state
1202
- defer { button.state = originalState }
1203
-
1204
- button.state = .off
1205
- button.layoutSubtreeIfNeeded()
1206
- let offWidth = button.fittingSize.width
1207
-
1208
- button.state = .on
1209
- button.layoutSubtreeIfNeeded()
1210
- let onWidth = button.fittingSize.width
1211
-
1212
- let maxWidth = max(offWidth, onWidth)
1213
- self.buttonWidthCache[buttonId] = maxWidth
1214
- return maxWidth
1215
- }
1216
-
1217
- private static func clearButtonWidthCache() {
1218
- self.buttonWidthCache.removeAll()
1219
- }
1220
-
1221
- private func applyUniformSegmentWidth(maxAllowedWidth: CGFloat) -> CGFloat {
1222
- guard !self.buttons.isEmpty else { return 0 }
1223
-
1224
- var desiredWidths: [CGFloat] = []
1225
- desiredWidths.reserveCapacity(self.buttons.count)
1226
-
1227
- for (index, button) in self.buttons.enumerated() {
1228
- if self.stackedIcons,
1229
- self.segments.indices.contains(index)
1230
- {
1231
- let font = RunicFont.nsFont(size: NSFont.smallSystemFontSize)
1232
- let titleWidth = ceil((self.segments[index].title as NSString).size(withAttributes: [.font: font])
1233
- .width)
1234
- let contentPadding: CGFloat = 6 + 6
1235
- let extraSlack: CGFloat = 1
1236
- desiredWidths.append(ceil(titleWidth + contentPadding + extraSlack))
1237
- } else {
1238
- desiredWidths.append(ceil(Self.maxToggleWidth(for: button)))
1239
- }
1240
- }
1241
-
1242
- let maxDesired = desiredWidths.max() ?? 0
1243
- let evenMaxDesired = maxDesired.truncatingRemainder(dividingBy: 2) == 0 ? maxDesired : maxDesired + 1
1244
- let evenMaxAllowed = maxAllowedWidth > 0
1245
- ? (maxAllowedWidth.truncatingRemainder(dividingBy: 2) == 0 ? maxAllowedWidth : maxAllowedWidth - 1)
1246
- : 0
1247
- let finalWidth: CGFloat = if evenMaxAllowed > 0 {
1248
- min(evenMaxDesired, evenMaxAllowed)
1249
- } else {
1250
- evenMaxDesired
1251
- }
1252
-
1253
- if finalWidth > 0 {
1254
- for button in self.buttons {
1255
- button.widthAnchor.constraint(equalToConstant: finalWidth).isActive = true
1256
- }
1257
- }
1258
-
1259
- return finalWidth
1260
- }
1261
-
1262
- @discardableResult
1263
- private func applyNonUniformSegmentWidths(
1264
- totalWidth: CGFloat,
1265
- outerPadding: CGFloat,
1266
- minimumGap: CGFloat) -> [CGFloat]
1267
- {
1268
- guard !self.buttons.isEmpty else { return [] }
1269
-
1270
- let count = self.buttons.count
1271
- let available = totalWidth -
1272
- outerPadding * 2 -
1273
- minimumGap * CGFloat(max(0, count - 1))
1274
- guard available > 0 else { return [] }
1275
-
1276
- func evenFloor(_ value: CGFloat) -> CGFloat {
1277
- var v = floor(value)
1278
- if Int(v) % 2 != 0 { v -= 1 }
1279
- return v
1280
- }
1281
-
1282
- let desired = self.buttons.map { ceil(Self.maxToggleWidth(for: $0)) }
1283
- let desiredSum = desired.reduce(0, +)
1284
- let avg = floor(available / CGFloat(count))
1285
- let minWidth = max(24, min(40, avg))
1286
-
1287
- var widths: [CGFloat]
1288
- if desiredSum <= available {
1289
- widths = desired
1290
- } else {
1291
- let totalCapacity = max(0, desiredSum - minWidth * CGFloat(count))
1292
- if totalCapacity <= 0 {
1293
- widths = Array(repeating: available / CGFloat(count), count: count)
1294
- } else {
1295
- let overflow = desiredSum - available
1296
- widths = desired.map { desiredWidth in
1297
- let capacity = max(0, desiredWidth - minWidth)
1298
- let shrink = overflow * (capacity / totalCapacity)
1299
- return desiredWidth - shrink
1300
- }
1301
- }
1302
- }
1303
-
1304
- widths = widths.map { max(minWidth, evenFloor($0)) }
1305
- var used = widths.reduce(0, +)
1306
-
1307
- while available - used >= 2 {
1308
- if let best = widths.indices
1309
- .filter({ desired[$0] - widths[$0] >= 2 })
1310
- .max(by: { lhs, rhs in
1311
- (desired[lhs] - widths[lhs]) < (desired[rhs] - widths[rhs])
1312
- })
1313
- {
1314
- widths[best] += 2
1315
- used += 2
1316
- continue
1317
- }
1318
-
1319
- guard let best = widths.indices.min(by: { lhs, rhs in widths[lhs] < widths[rhs] }) else { break }
1320
- widths[best] += 2
1321
- used += 2
1322
- }
1323
-
1324
- for (index, button) in self.buttons.enumerated() where index < widths.count {
1325
- button.widthAnchor.constraint(equalToConstant: widths[index]).isActive = true
1326
- }
1327
-
1328
- return widths
1329
- }
1330
-
1331
- private static func maxAllowedUniformSegmentWidth(
1332
- for totalWidth: CGFloat,
1333
- count: Int,
1334
- outerPadding: CGFloat,
1335
- minimumGap: CGFloat) -> CGFloat
1336
- {
1337
- guard count > 0 else { return 0 }
1338
- let available = totalWidth -
1339
- outerPadding * 2 -
1340
- minimumGap * CGFloat(max(0, count - 1))
1341
- guard available > 0 else { return 0 }
1342
- return floor(available / CGFloat(count))
1343
- }
1344
-
1345
- private static func paddedImage(_ image: NSImage, leading: CGFloat) -> NSImage {
1346
- let size = NSSize(width: image.size.width + leading, height: image.size.height)
1347
- let newImage = NSImage(size: size)
1348
- newImage.lockFocus()
1349
- let y = (size.height - image.size.height) / 2
1350
- image.draw(
1351
- at: NSPoint(x: leading, y: y),
1352
- from: NSRect(origin: .zero, size: image.size),
1353
- operation: .sourceOver,
1354
- fraction: 1.0)
1355
- newImage.unlockFocus()
1356
- newImage.isTemplate = image.isTemplate
1357
- return newImage
1358
- }
1359
-
1360
- private func addWeeklyIndicator(to view: NSView, provider: UsageProvider, remainingPercent: Double?) {
1361
- guard let remainingPercent else { return }
1362
-
1363
- let track = NSView()
1364
- track.wantsLayer = true
1365
- track.layer?.backgroundColor = NSColor.tertiaryLabelColor
1366
- .withAlphaComponent(CGFloat(RunicColors.Opacity.medium)).cgColor
1367
- track.layer?.cornerRadius = 3
1368
- track.layer?.masksToBounds = true
1369
- track.translatesAutoresizingMaskIntoConstraints = false
1370
- view.addSubview(track)
1371
-
1372
- let fill = NSView()
1373
- fill.wantsLayer = true
1374
- fill.layer?.backgroundColor = Self.weeklyIndicatorColor(for: provider).cgColor
1375
- fill.layer?.cornerRadius = 3
1376
- fill.translatesAutoresizingMaskIntoConstraints = false
1377
- track.addSubview(fill)
1378
-
1379
- let ratio = CGFloat(max(0, min(1, remainingPercent / 100)))
1380
-
1381
- NSLayoutConstraint.activate([
1382
- track.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 6),
1383
- track.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -6),
1384
- track.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -1),
1385
- track.heightAnchor.constraint(equalToConstant: 5),
1386
- fill.leadingAnchor.constraint(equalTo: track.leadingAnchor),
1387
- fill.topAnchor.constraint(equalTo: track.topAnchor),
1388
- fill.bottomAnchor.constraint(equalTo: track.bottomAnchor),
1389
- ])
1390
-
1391
- fill.widthAnchor.constraint(equalTo: track.widthAnchor, multiplier: ratio).isActive = true
1392
-
1393
- self.weeklyIndicators[ObjectIdentifier(view)] = WeeklyIndicator(provider: provider, track: track, fill: fill)
1394
- self.updateWeeklyIndicatorVisibility(for: view)
1395
- }
1396
-
1397
- private func updateWeeklyIndicatorVisibility(for view: NSView) {
1398
- guard let indicator = self.weeklyIndicators[ObjectIdentifier(view)] else { return }
1399
- let isSelected = (view as? NSButton)?.state == .on
1400
- indicator.track.isHidden = isSelected
1401
- indicator.fill.isHidden = isSelected
1402
- }
1403
-
1404
- private static func weeklyIndicatorColor(for provider: UsageProvider) -> NSColor {
1405
- let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
1406
- return NSColor(deviceRed: color.red, green: color.green, blue: color.blue, alpha: 1)
1407
- }
1408
-
1409
- private static func switcherTitle(for provider: UsageProvider) -> String {
1410
- ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName
1411
- }
1412
- }