276d2bb6-619d-4888-ba11-8a6.../Sources/data/ClipperConfig.swift
徐翔宇 4e22618364 v0.1.0: initial 划词侠 — AI Text Actions plugin for MioIsland
Quick AI actions on clipboard or selected text. Four actions:
- 翻译 (中/英/日)
- 总结 (3-bullet Chinese)
- 改语气 (正式/口语/友好/严谨)
- 邮件回复

Powered by DeepSeek with Server-Sent Events streaming for
typewriter feedback (~600ms first byte). Two source modes:
clipboard (no permission) and selected-text via NSAccessibility
(graceful fallback to clipboard + permission banner if denied).

Five-state UI machine: idle → input → loading (cancellable) →
result → missing-config. Last 20 actions persisted to UserDefaults.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:21:17 +08:00

76 lines
2.6 KiB
Swift

//
// ClipperConfig.swift
// plugin
//
// Loads ~/.config/codeisland/clipper-config.json on demand.
// Schema:
// { "deepseek_api_key": "sk-...",
// "default_translate_target": "zh" }
//
// Never bundles a key. If the file is missing/empty we surface a
// state-E "" screen with a helper that copies the path to the
// clipboard so the user can `open -e` it.
//
import Foundation
struct ClipperConfig {
var deepseekApiKey: String
var defaultTranslateTarget: String // "zh" | "en" | "ja"
static let configPath: String = {
let home = NSHomeDirectory()
return "\(home)/.config/codeisland/clipper-config.json"
}()
static var configDir: String {
let home = NSHomeDirectory()
return "\(home)/.config/codeisland"
}
/// Best-effort load. If anything is missing we return nil so the UI
/// can render the onboarding banner never throws to the caller.
static func load() -> ClipperConfig? {
let url = URL(fileURLWithPath: configPath)
guard let data = try? Data(contentsOf: url) else {
ClipperDebugLog.write("config: file missing at \(configPath)")
return nil
}
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
ClipperDebugLog.write("config: malformed JSON")
return nil
}
let key = (obj["deepseek_api_key"] as? String) ?? ""
let target = (obj["default_translate_target"] as? String) ?? "zh"
if key.trimmingCharacters(in: .whitespaces).isEmpty
|| key.trimmingCharacters(in: .whitespaces) == "TODO" {
ClipperDebugLog.write("config: deepseek_api_key empty/TODO")
return nil
}
return ClipperConfig(deepseekApiKey: key, defaultTranslateTarget: target)
}
/// Write a stub file the user can edit. Idempotent won't clobber
/// an existing real config.
@discardableResult
static func ensureStubExists() -> Bool {
let fm = FileManager.default
if fm.fileExists(atPath: configPath) { return false }
try? fm.createDirectory(atPath: configDir, withIntermediateDirectories: true)
let stub = """
{
"deepseek_api_key": "TODO",
"default_translate_target": "zh"
}
"""
do {
try stub.write(toFile: configPath, atomically: true, encoding: .utf8)
ClipperDebugLog.write("config: wrote stub at \(configPath)")
return true
} catch {
ClipperDebugLog.write("config: stub write failed: \(error)")
return false
}
}
}