mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-31 11:31:03 +00:00
Groundwork for #1219. The menubar app hardcoded ~530 English strings across Views/, the presentation structs, the status-item menu and the update alerts, so there was nothing for a translator to translate. Every user-facing literal now goes through `L(_:)` / `L(_:_:)`, which look the string up in `Localizable.strings` in the SwiftPM target resource bundle. The key *is* the English copy, so English stays the development language and a missing translation degrades to correct English instead of a dotted identifier. `en.lproj` is therefore an identity table; it exists so the bundle advertises `en` and so a translation can be diffed against it. Why the lookups are explicit rather than relying on SwiftUI's implicit `LocalizedStringKey`: SwiftPM emits target resources into a sibling bundle (`CodeBurnMenubar_CodeBurnMenubar.bundle`) that the packaging scripts copy into `Contents/Resources`. `Bundle.main` has no `.lproj` at all, so `Text("literal")` would always miss. Naming `Bundle.module` is the one form that resolves the same way in `swift run`, in `swift test` and in the packaged `.app`. Enum raw values that double as identity (`Period`, `MenubarScope`, `InsightMode`, `AccentPreset`, `ProviderFilter`) keep their raw value and gain a `displayLabel`, so persistence and cache keys are untouched by translation. Three display-only date formatters that were pinned to `en_US_POSIX` with fixed patterns now use `setLocalizedDateFormatFromTemplate`, and the calendar popover's weekday row comes from the locale's own symbols. The `yyyy-MM-dd` formatter stays POSIX: it parses and builds data keys, not display text. Not extracted, deliberately: provider, model and plan names; units; currency codes; shell commands and paths; quota window labels that policy code matches on by English substring (`QuotaSummary.headlineWindow`); and anything the `codeburn` CLI produces. Also copies the SwiftPM resource bundle into the app in build-local.sh, using the named-bundle + `[[ -d ]]` pattern package-app.sh already uses. Without it the assembled app finds no strings table (and already trapped on first icon load, the resource-bundle half of #1262).
124 lines
5.8 KiB
Swift
124 lines
5.8 KiB
Swift
import Foundation
|
|
|
|
/// Closed set of terminal emulators CodeBurn knows how to drive (#877).
|
|
///
|
|
/// SECURITY: this type exists specifically so that a *user preference* can never become a
|
|
/// free-form string inside an AppleScript. The preference is persisted as a raw value that is
|
|
/// parsed back through `init(rawValue:)`; anything unrecognised collapses to `.default`. Every
|
|
/// application name that reaches `osascript` is a hardcoded literal in `script(command:)` --
|
|
/// never a stored or interpolated string. The only interpolated value is the command, which
|
|
/// callers must have already validated token-by-token with `CodeburnCLI.isSafe`.
|
|
///
|
|
/// Only terminals with a real "run this in an interactive session and leave the window open"
|
|
/// scripting verb are listed. Terminal.app has `do script`; iTerm2 has
|
|
/// `write text` on a session. Ghostty, WezTerm, Warp, Alacritty and kitty expose no equivalent
|
|
/// AppleScript verb -- launching them with `-e <cmd>` tears the window down the moment the
|
|
/// command exits, which would make "Full Report" flash and vanish, so they intentionally stay
|
|
/// out of this enum rather than shipping broken.
|
|
enum PreferredTerminal: String, CaseIterable, Identifiable, Sendable {
|
|
case terminal
|
|
case iTerm2 = "iterm2"
|
|
|
|
var id: String { rawValue }
|
|
|
|
var label: String {
|
|
switch self {
|
|
case .terminal: return L("Terminal (macOS default)")
|
|
case .iTerm2: return "iTerm2"
|
|
}
|
|
}
|
|
|
|
/// Bundle locations probed with a plain `fileExists` check, mirroring the pre-#877
|
|
/// behaviour.
|
|
///
|
|
/// This is a simplicity/determinism choice, NOT a security boundary. A fixed list needs no
|
|
/// LaunchServices database state, gives the same answer on every machine, and keeps the
|
|
/// Settings "(not installed)" hint honest without a framework round-trip.
|
|
///
|
|
/// It is worth being precise about what it does *not* buy, because an earlier version of
|
|
/// this comment overstated it. `NSWorkspace.urlForApplication(withBundleIdentifier:)` does
|
|
/// register and resolve bundles outside the standard folders -- an app dropped in
|
|
/// ~/Downloads is picked up within seconds -- but when a copy also exists in /Applications,
|
|
/// LaunchServices ranks /Applications first, and it still does so when the ~/Downloads copy
|
|
/// advertises a *higher* CFBundleShortVersionString. So on a normally installed machine the
|
|
/// two approaches resolve to the same bundle; the fixed list only differs by declining to
|
|
/// find an install the user put somewhere unusual, in which case we fall back to
|
|
/// Terminal.app rather than driving a bundle from a transient location such as a mounted
|
|
/// DMG. Neither approach checks a code signature or team ID, so neither authenticates the
|
|
/// app it drives.
|
|
var appPaths: [String] {
|
|
switch self {
|
|
case .terminal:
|
|
return [
|
|
"/System/Applications/Utilities/Terminal.app",
|
|
"/Applications/Utilities/Terminal.app",
|
|
]
|
|
case .iTerm2:
|
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
|
return [
|
|
"/Applications/iTerm.app",
|
|
"\(home)/Applications/iTerm.app",
|
|
]
|
|
}
|
|
}
|
|
|
|
var isInstalled: Bool {
|
|
appPaths.contains(where: FileManager.default.fileExists(atPath:))
|
|
}
|
|
|
|
/// AppleScript that brings the terminal forward, opens a window and runs `command`.
|
|
///
|
|
/// `command` is the ONLY interpolated value; callers guarantee it is whitespace-joined argv
|
|
/// where every token passed `CodeburnCLI.isSafe` (no quotes, no `$`, no backticks, no `;`),
|
|
/// or a hardcoded literal. The `tell application` target is a compile-time literal per case.
|
|
func script(command: String) -> String {
|
|
switch self {
|
|
case .terminal:
|
|
return """
|
|
tell application "Terminal"
|
|
activate
|
|
do script "\(command)"
|
|
end tell
|
|
"""
|
|
case .iTerm2:
|
|
// iTerm2 has no `do script`. A window must be created from a profile first, then
|
|
// text is written into its session.
|
|
//
|
|
// The target MUST be "iTerm", not "iTerm2", even though the app calls itself iTerm2
|
|
// and its CFBundleName is "iTerm2". AppleScript resolves the name of a *not yet
|
|
// running* app through LaunchServices by bundle file name, and the bundle is
|
|
// `iTerm.app`. Measured on iTerm2 3.6.11: with the app quit,
|
|
// `tell application "iTerm2"` fails to even compile (-2741, "expected , but found
|
|
// class name" -- `text` binds to the built-in class because iTerm2's terminology
|
|
// never loads) while `tell application "iTerm"` compiles, cold-launches the app and
|
|
// runs the command. `"iTerm2"` only works while iTerm2 already happens to be
|
|
// running, which made the bug easy to miss when testing interactively.
|
|
return """
|
|
tell application "iTerm"
|
|
activate
|
|
set newWindow to (create window with default profile)
|
|
tell current session of newWindow
|
|
write text "\(command)"
|
|
end tell
|
|
end tell
|
|
"""
|
|
}
|
|
}
|
|
|
|
// MARK: - Persistence
|
|
|
|
static let defaultsKey = "CodeBurnPreferredTerminal"
|
|
|
|
/// Terminal.app, i.e. exactly the pre-#877 behaviour, so users who never open Settings
|
|
/// see no change.
|
|
static let `default`: PreferredTerminal = .terminal
|
|
|
|
static func saved(defaults: UserDefaults = .standard) -> PreferredTerminal {
|
|
guard let raw = defaults.string(forKey: defaultsKey) else { return .default }
|
|
return PreferredTerminal(rawValue: raw) ?? .default
|
|
}
|
|
|
|
func persist(defaults: UserDefaults = .standard) {
|
|
defaults.set(rawValue, forKey: Self.defaultsKey)
|
|
}
|
|
}
|