Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,17 +184,17 @@ Settings is a custom `NSWindow`, not the system Settings scene. The app still
runs as an accessory app with no Dock icon and no menu bar.

- **General:** Launch at Login, 5m/15m/30m refresh interval, app language,
Always show usage, Low Power Mode, configurable limit alerts, and Sparkle
update controls.
light/dark/system appearance, Always show usage, Low Power Mode,
configurable limit alerts, and Sparkle update controls.
Comment on lines +187 to +188

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a clear list for appearance modes.

Write light, dark, or system appearance instead of light/dark/system appearance. This improves readability and removes the slash compound.

Proposed wording
-  light/dark/system appearance, Always show usage, Low Power Mode,
+  light, dark, or system appearance, Always show usage, Low Power Mode,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
light/dark/system appearance, Always show usage, Low Power Mode,
configurable limit alerts, and Sparkle update controls.
light, dark, or system appearance, Always show usage, Low Power Mode,
configurable limit alerts, and Sparkle update controls.
🧰 Tools
🪛 LanguageTool

[uncategorized] ~187-~187: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...k/system appearance, Always show usage, Low Power Mode, configurable limit alerts, and ...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 187 - 188, Update the README feature description to
replace “light/dark/system appearance” with the clearer wording “light, dark, or
system appearance,” leaving the surrounding list items unchanged.

Source: Linters/SAST tools

- **Display:** used/remaining percentages, Usage and Cost visualization styles,
target display, and island width on non-notched screens.
- **Providers:** Claude/Codex visibility and status, token-counting mode, and a
manual refresh for local cost data.

Preferences are stored in `UserDefaults` under `MacIsland.*` keys (Sparkle
manages its own `SU*` update keys, and Launch at Login uses
`SMAppService.mainApp`). Refresh, display, and provider changes apply live;
changing the app language offers to restart CodexIsland.
`SMAppService.mainApp`). Appearance, refresh, display, and provider changes
apply live; changing the app language offers to restart CodexIsland.

## Build from source

Expand Down
4 changes: 3 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Claude:

| 设置 | 存储 | UserDefaults key | 值 |
| --- | --- | --- | --- |
| 外观 | `AppearanceStore` | `MacIsland.appearance` | `system`, `light`, `dark`,默认 `dark` |
| 图表样式 | `StylePref` | `MacIsland.chartStyle` | `ring`, `bar`, `stepped`, `numeric`, `spark` |
| 成本样式 | `CostStylePref` | `MacIsland.costStyle` | `dollar`, `multi`, `tokens`, `spark` |
| Token 统计 | `TokenCountModeStore` | `MacIsland.tokenCountMode` | `all`, `billable` |
Expand All @@ -100,7 +101,8 @@ Claude:
| Codex 可见 | `ProviderVisibilityStore` | `MacIsland.codexVisible` | Boolean,默认 `true` |
| 登录启动 | `LaunchAtLoginStore` | 由 `SMAppService.mainApp` 管理 | 系统登录项状态 |

刷新间隔会立即生效。`UsageStore` 会重置当前计时器,并用新的间隔重新安排下一次拉取。
外观和刷新间隔都会立即生效;选择“跟随系统”后,设置窗口会随 macOS
浅色/深色外观自动切换。`UsageStore` 会重置当前计时器,并用新的间隔重新安排下一次拉取。

## 从源码构建

Expand Down
5 changes: 5 additions & 0 deletions Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@
"Ring" = "Ring";
"scanning local logs…" = "scanning local logs…";
"Settings" = "Settings";
"Appearance" = "Appearance";
"Choose a light or dark skin, or follow macOS." = "Choose a light or dark skin, or follow macOS.";
"System" = "System";
"Light" = "Light";
"Dark" = "Dark";
"Show on" = "Show on";
"Sparkline" = "Sparkline";
"Spacing" = "Spacing";
Expand Down
5 changes: 5 additions & 0 deletions Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@
"Ring" = "环形";
"scanning local logs…" = "正在扫描本地日志…";
"Settings" = "设置";
"Appearance" = "外观";
"Choose a light or dark skin, or follow macOS." = "选择浅色或深色皮肤,或跟随 macOS。";
"System" = "跟随系统";
"Light" = "浅色";
"Dark" = "深色";
"Show on" = "显示在";
"Sparkline" = "折线";
"Spacing" = "间距";
Expand Down
40 changes: 40 additions & 0 deletions Sources/Model/AppearanceStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import SwiftUI

enum AppAppearance: String, CaseIterable, Hashable {
case system
case light
case dark

var label: String {
switch self {
case .system: "System"
case .light: "Light"
case .dark: "Dark"
}
}

var colorScheme: ColorScheme? {
switch self {
case .system: nil
case .light: .light
case .dark: .dark
}
}
}

@MainActor
final class AppearanceStore: ObservableObject {
static let shared = AppearanceStore()
static let key = "MacIsland.appearance"

@Published var appearance: AppAppearance {
didSet {
UserDefaults.standard.set(appearance.rawValue, forKey: Self.key)
}
Comment on lines +25 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 \
  'AppearanceStore\.shared|preferredColorScheme|NSApp\.appearance|NSApplication\.shared\.appearance|NSAppearance\(named:' \
  Sources

Repository: ericjypark/codex-island

Length of output: 2837


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Tracked Swift files:\n'
git ls-files 'Sources/*.swift' 'Sources/**/*.swift' | sed -n '1,120p'

printf '\nAppearanceStore reference summary:\n'
rg -n 'AppearanceStore|AppAppearance|NSAppearance|window\.appearance|appearance =|\.colorScheme|`@MainActor`' Sources

printf '\nMain app files:\n'
for f in $(git ls-files 'Sources/*.swift' 'Sources/**/*.swift' | grep -E 'App|Entry|main|AppDelegate|Scene' || true); do
  echo "--- $f"
  wc -l "$f"
done

Repository: ericjypark/codex-island

Length of output: 8190


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- Sources/App.swift ---\n'
cat -n Sources/App.swift

printf '\n--- Sources/Window/IslandWindowController.swift ---\n'
wc -l Sources/Window/IslandWindowController.swift
sed -n '1,280p' Sources/Window/IslandWindowController.swift | cat -n

printf '\n--- Sources/Views/IslandRootView.swift ---\n'
wc -l Sources/Views/IslandRootView.swift
sed -n '1,240p' Sources/Views/IslandRootView.swift | cat -n

Repository: ericjypark/codex-island

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Sources/App.swift ---'
cat -n Sources/App.swift

printf '%s\n' ''
printf '%s\n' '--- Sources/Window/IslandWindowController.swift ---'
wc -l Sources/Window/IslandWindowController.swift
sed -n '1,280p' Sources/Window/IslandWindowController.swift | cat -n

printf '%s\n' ''
printf '%s\n' '--- Sources/Views/IslandRootView.swift ---'
wc -l Sources/Views/IslandRootView.swift
sed -n '1,240p' Sources/Views/IslandRootView.swift | cat -n

Repository: ericjypark/codex-island

Length of output: 28505


Wire AppearanceStore into IslandWindowController and apply it to the island window.

IslandWindowController hosts IslandRootView and controls the island window, but it does not observe AppearanceStore.shared. Add that subscription like SettingsWindowController and apply nil for .system, .darkAqua for .dark, and .aqua for .light to the island NSWindow; rely on .preferredColorScheme alone updates the settings SwiftUI scene, not the borderless panel.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Model/AppearanceStore.swift` around lines 25 - 33, Update
IslandWindowController to observe AppearanceStore.shared, matching the
subscription pattern used by SettingsWindowController, and apply the selected
appearance to the island NSWindow: nil for .system, .darkAqua for .dark, and
.aqua for .light. Ensure changes are applied to the borderless panel window
while preserving the existing IslandRootView setup.

}

private init() {
let raw = UserDefaults.standard.string(forKey: Self.key) ?? ""
appearance = AppAppearance(rawValue: raw) ?? .dark
}
}
14 changes: 14 additions & 0 deletions Sources/Theme/Colors.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import AppKit
import SwiftUI

/// Locked color tokens for CodexIsland.
enum IslandColor {
static let settingsBackground = Color(
light: NSColor(calibratedWhite: 0.965, alpha: 1),
dark: NSColor(calibratedRed: 0.020, green: 0.020, blue: 0.027, alpha: 1)
)

/// #0047AB — loading sweep, glow halo.
static let cobalt = Color(red: 0/255, green: 71/255, blue: 171/255)

Expand All @@ -23,3 +29,11 @@ enum IslandColor {
/// as "stop, you're cooked" without going full red-alert pure.
static let alertRed = Color(red: 229/255, green: 72/255, blue: 77/255)
}

private extension Color {
init(light: NSColor, dark: NSColor) {
self.init(nsColor: NSColor(name: nil) { appearance in
appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua ? dark : light
})
}
}
10 changes: 5 additions & 5 deletions Sources/Views/Settings/BrandHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,21 @@ struct BrandHeader: View {
Text("CodexIsland")
.font(Typography.brand)
.tracking(-0.15)
.foregroundStyle(.white.opacity(0.92))
.foregroundStyle(Color.primary.opacity(0.92))
Text(L10n.tr("Your AI usage limits, living in your notch."))
.font(Typography.label)
.foregroundStyle(.white.opacity(0.55))
.foregroundStyle(Color.primary.opacity(0.55))
}

Spacer(minLength: 8)

Text("v\(version)")
.font(Typography.bodyNumber)
.foregroundStyle(.white.opacity(0.34))
.foregroundStyle(Color.primary.opacity(0.34))
.padding(.horizontal, 9)
.padding(.vertical, 4)
.background(
Capsule().fill(.white.opacity(0.04))
Capsule().fill(Color.primary.opacity(0.04))
)
}
.padding(.horizontal, 24)
Expand All @@ -55,7 +55,7 @@ struct BrandHeader: View {
.interpolation(.high)
.aspectRatio(contentMode: .fit)
.frame(width: 26, height: 26)
.foregroundStyle(.white.opacity(0.92))
.foregroundStyle(Color.primary.opacity(0.92))
.shadow(color: IslandColor.cobalt.opacity(0.35), radius: 6)
} else {
// Fallback if the resource is missing in the bundle: a plain
Expand Down
8 changes: 4 additions & 4 deletions Sources/Views/Settings/ChartStylePicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct ChartStylePicker: View {
case .ring:
ZStack {
Circle()
.stroke(.white.opacity(0.10), lineWidth: 3)
.stroke(Color.primary.opacity(0.10), lineWidth: 3)
Circle()
.trim(from: 0, to: 0.35)
.stroke(claude, style: StrokeStyle(lineWidth: 3, lineCap: .round))
Expand All @@ -43,7 +43,7 @@ struct ChartStylePicker: View {
.frame(width: 26, height: 26)
case .bar:
ZStack(alignment: .leading) {
Capsule().fill(.white.opacity(0.10))
Capsule().fill(Color.primary.opacity(0.10))
Capsule().fill(claude)
.frame(width: 28 * 0.35)
}
Expand All @@ -52,7 +52,7 @@ struct ChartStylePicker: View {
HStack(spacing: 1.5) {
ForEach(0..<8) { i in
RoundedRectangle(cornerRadius: 0.75)
.fill(i < 3 ? claude : .white.opacity(0.10))
.fill(i < 3 ? claude : Color.primary.opacity(0.10))
.frame(width: 2, height: 12)
}
}
Expand All @@ -64,7 +64,7 @@ struct ChartStylePicker: View {
.foregroundStyle(claude)
Text("%")
.font(Typography.micro)
.foregroundStyle(.white.opacity(0.5))
.foregroundStyle(Color.primary.opacity(0.5))
}
case .spark:
SparkPath()
Expand Down
6 changes: 3 additions & 3 deletions Sources/Views/Settings/CostStylePicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ struct CostStylePicker: View {
HStack(alignment: .firstTextBaseline, spacing: 1) {
Text("$")
.font(Typography.micro)
.foregroundStyle(.white.opacity(0.5))
.foregroundStyle(Color.primary.opacity(0.5))
Text("87")
.font(Typography.previewNumber)
.foregroundStyle(claude)
}
case .multi:
HStack(alignment: .bottom, spacing: 4) {
Capsule().fill(.white.opacity(0.20))
Capsule().fill(Color.primary.opacity(0.20))
.frame(width: 8, height: 6)
Capsule().fill(claude)
.frame(width: 8, height: 18)
Expand All @@ -54,7 +54,7 @@ struct CostStylePicker: View {
.foregroundStyle(claude)
Text("M")
.font(Typography.micro)
.foregroundStyle(.white.opacity(0.5))
.foregroundStyle(Color.primary.opacity(0.5))
}
case .spark:
CostSparkPath()
Expand Down
19 changes: 11 additions & 8 deletions Sources/Views/Settings/SegmentedControl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@ struct SegmentedControl<Value: Hashable>: View {
Text(itemLabel)
.font(Typography.bodyNumber)
.foregroundStyle(isOn
? Color.white.opacity(0.95)
: .white.opacity(0.55))
? Color.primary.opacity(0.95)
: Color.primary.opacity(0.55))
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background {
RoundedRectangle(cornerRadius: 5)
.fill(isOn ? .white.opacity(0.10) : .clear)
.fill(isOn ? Color.primary.opacity(0.10) : Color.clear)
.overlay {
RoundedRectangle(cornerRadius: 5)
.strokeBorder(.white.opacity(isOn ? 0.08 : 0), lineWidth: 0.5)
.strokeBorder(
Color.primary.opacity(isOn ? 0.08 : 0),
lineWidth: 0.5
)
}
}
}
Expand All @@ -45,7 +48,7 @@ struct SegmentedControl<Value: Hashable>: View {
.padding(2)
.background {
RoundedRectangle(cornerRadius: 7)
.fill(.white.opacity(0.04))
.fill(Color.primary.opacity(0.04))
}
}
}
Expand All @@ -62,15 +65,15 @@ struct PillButton: View {
Button(action: action) {
Text(L10n.tr(label))
.font(Typography.button)
.foregroundStyle(.white.opacity(0.9))
.foregroundStyle(Color.primary.opacity(0.9))
.padding(.horizontal, 12)
.padding(.vertical, 5)
.background {
RoundedRectangle(cornerRadius: 6)
.fill(.white.opacity(0.10))
.fill(Color.primary.opacity(0.10))
.overlay {
RoundedRectangle(cornerRadius: 6)
.strokeBorder(.white.opacity(0.08), lineWidth: 0.5)
.strokeBorder(Color.primary.opacity(0.08), lineWidth: 0.5)
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/Views/Settings/SettingsFooter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ struct SettingsFooter: View {
} label: {
Text(L10n.tr("Quit"))
.font(Typography.label)
.foregroundStyle(.white.opacity(quitHovered ? 0.92 : 0.55))
.foregroundStyle(Color.primary.opacity(quitHovered ? 0.92 : 0.55))
.padding(.horizontal, 11)
.padding(.vertical, 5)
.background {
RoundedRectangle(cornerRadius: 6)
.fill(.white.opacity(quitHovered ? 0.06 : 0.03))
.fill(Color.primary.opacity(quitHovered ? 0.06 : 0.03))
.overlay {
RoundedRectangle(cornerRadius: 6)
.strokeBorder(.white.opacity(0.07), lineWidth: 0.5)
.strokeBorder(Color.primary.opacity(0.07), lineWidth: 0.5)
}
}
}
Expand Down Expand Up @@ -62,14 +62,14 @@ private struct DottedLink: View {
HStack(spacing: 4) {
Text(L10n.tr(title))
.font(Typography.label)
.foregroundStyle(.white.opacity(hovered ? 0.92 : 0.55))
.foregroundStyle(Color.primary.opacity(hovered ? 0.92 : 0.55))
Text("↗")
.font(Typography.micro)
.foregroundStyle(.white.opacity(hovered ? 0.6 : 0.3))
.foregroundStyle(Color.primary.opacity(hovered ? 0.6 : 0.3))
}
.overlay(alignment: .bottom) {
Rectangle()
.fill(.white.opacity(hovered ? 0.32 : 0.18))
.fill(Color.primary.opacity(hovered ? 0.32 : 0.18))
.frame(height: 0.5)
.offset(y: 1)
.mask(
Expand Down
10 changes: 5 additions & 5 deletions Sources/Views/Settings/SettingsRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,25 @@ struct SettingsRow<Trailing: View>: View {
Text(L10n.tr(title))
.font(Typography.rowTitle)
.tracking(-0.07)
.foregroundStyle(.white.opacity(0.92))
.foregroundStyle(Color.primary.opacity(0.92))
if let chip {
Text(chip)
.font(Typography.chip)
.tracking(0.8)
.foregroundStyle(.white.opacity(0.6))
.foregroundStyle(Color.primary.opacity(0.6))
.padding(.horizontal, 5)
.padding(.vertical, 2)
.background(
RoundedRectangle(cornerRadius: 3)
.fill(.white.opacity(0.06))
.fill(Color.primary.opacity(0.06))
)
.accessibilityLabel(L10n.tr("Plan: %@", chip))
}
}
if let subtitle {
Text(L10n.tr(subtitle))
.font(Typography.label)
.foregroundStyle(.white.opacity(0.55))
.foregroundStyle(Color.primary.opacity(0.55))
}
}
.accessibilityElement(children: .combine)
Expand All @@ -73,7 +73,7 @@ struct SettingsRow<Trailing: View>: View {
.padding(.vertical, 11)
.background {
RoundedRectangle(cornerRadius: 8)
.fill(.white.opacity(hovered ? 0.030 : 0))
.fill(Color.primary.opacity(hovered ? 0.030 : 0))
}
.contentShape(Rectangle())
.onHover { hovered = $0 }
Expand Down
6 changes: 3 additions & 3 deletions Sources/Views/Settings/SettingsToggle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ struct SettingsToggle: View {
Button(action: action) {
ZStack(alignment: isOn ? .trailing : .leading) {
Capsule()
.strokeBorder(.white.opacity(hovered ? 0.20 : 0.13), lineWidth: 1)
.strokeBorder(Color.primary.opacity(hovered ? 0.20 : 0.13), lineWidth: 1)
.background {
Capsule().fill(isOn
? IslandColor.cobalt.opacity(0.32)
: .white.opacity(0.07))
: Color.primary.opacity(0.07))
}
.frame(width: trackWidth, height: trackHeight)

Circle()
.fill(isOn ? IslandColor.cobalt : Color.white.opacity(0.5))
.fill(isOn ? IslandColor.cobalt : Color.primary.opacity(0.5))
.frame(width: dotSize, height: dotSize)
.shadow(
color: isOn ? IslandColor.cobalt.opacity(0.85) : .clear,
Expand Down
6 changes: 3 additions & 3 deletions Sources/Views/Settings/StyleTile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ struct StyleTile<Preview: View>: View {
Text(displayLabel)
.font(Typography.micro)
.foregroundStyle(isOn
? Color(red: 0.58, green: 0.75, blue: 1.0)
: .white.opacity(0.55))
? Color.primary.opacity(0.90)
: Color.primary.opacity(0.55))
}
.frame(maxWidth: .infinity)
.padding(.top, 14)
Expand All @@ -30,7 +30,7 @@ struct StyleTile<Preview: View>: View {
RoundedRectangle(cornerRadius: 9)
.fill(isOn
? IslandColor.cobalt.opacity(0.14)
: .white.opacity(0.025))
: Color.primary.opacity(0.025))
.overlay {
RoundedRectangle(cornerRadius: 9)
.strokeBorder(isOn
Expand Down
Loading