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