162 lines · 6.1 KB · Swift
RawDownload
import KeyboardShortcutsimport SwiftUI @MainActorstruct AdvancedPane: View {    @Bindable var settings: SettingsStore    @State private var isInstallingCLI = false    @State private var cliStatus: String?     var body: some View {        ScrollView(.vertical, showsIndicators: true) {            VStack(alignment: .leading, spacing: 16) {                SettingsSection(contentSpacing: 8) {                    Text(L("section_keyboard_shortcut"))                        .font(.caption)                        .foregroundStyle(.secondary)                        .textCase(.uppercase)                    HStack(alignment: .center, spacing: 12) {                        Text(L("open_menu_shortcut_title"))                            .font(.body)                        Spacer()                        KeyboardShortcuts.Recorder(for: .openMenu)                    }                    Text(L("open_menu_shortcut_subtitle"))                        .font(.footnote)                        .foregroundStyle(.tertiary)                }                 Divider()                 SettingsSection(contentSpacing: 10) {                    HStack(spacing: 12) {                        Button {                            Task { await self.installCLI() }                        } label: {                            if self.isInstallingCLI {                                ProgressView().controlSize(.small)                            } else {                                Text(L("install_cli"))                            }                        }                        .disabled(self.isInstallingCLI)                         if let status = self.cliStatus {                            Text(status)                                .font(.footnote)                                .foregroundStyle(.tertiary)                                .lineLimit(2)                        }                    }                    Text(L("install_cli_subtitle"))                        .font(.footnote)                        .foregroundStyle(.tertiary)                }                 Divider()                 SettingsSection(contentSpacing: 10) {                    PreferenceToggleRow(                        title: L("show_debug_settings_title"),                        subtitle: L("show_debug_settings_subtitle"),                        binding: self.$settings.debugMenuEnabled)                    PreferenceToggleRow(                        title: L("surprise_me_title"),                        subtitle: L("surprise_me_subtitle"),                        binding: self.$settings.randomBlinkEnabled)                    PreferenceToggleRow(                        title: L("weekly_limit_confetti_title"),                        subtitle: L("weekly_limit_confetti_subtitle"),                        binding: self.$settings.confettiOnWeeklyLimitResetsEnabled)                }                 Divider()                 SettingsSection(contentSpacing: 10) {                    PreferenceToggleRow(                        title: L("hide_personal_info_title"),                        subtitle: L("hide_personal_info_subtitle"),                        binding: self.$settings.hidePersonalInfo)                    PreferenceToggleRow(                        title: L("show_provider_storage_usage_title"),                        subtitle: L("show_provider_storage_usage_subtitle"),                        binding: self.$settings.providerStorageFootprintsEnabled)                }                 Divider()                 SettingsSection(                    title: L("section_keychain_access"),                    caption: L("keychain_access_caption"))                {                    PreferenceToggleRow(                        title: L("disable_keychain_access_title"),                        subtitle: L("disable_keychain_access_subtitle"),                        binding: self.$settings.debugDisableKeychainAccess)                }            }            .frame(maxWidth: .infinity, alignment: .leading)            .padding(.horizontal, 20)            .padding(.vertical, 12)        }    }} extension AdvancedPane {    private func installCLI() async {        if self.isInstallingCLI { return }        self.isInstallingCLI = true        defer { self.isInstallingCLI = false }         let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Helpers/CodexBarCLI")        let fm = FileManager.default        guard fm.fileExists(atPath: helperURL.path) else {            self.cliStatus = L("cli_not_found")            return        }         let destinations = [            "/usr/local/bin/codexbar",            "/opt/homebrew/bin/codexbar",        ]         var results: [String] = []        for dest in destinations {            let dir = (dest as NSString).deletingLastPathComponent            guard fm.fileExists(atPath: dir) else { continue }            guard fm.isWritableFile(atPath: dir) else {                results.append("No write access: \(dir)")                continue            }             if fm.fileExists(atPath: dest) {                if Self.isLink(atPath: dest, pointingTo: helperURL.path) {                    results.append("Installed: \(dir)")                } else {                    results.append("Exists: \(dir)")                }                continue            }             do {                try fm.createSymbolicLink(atPath: dest, withDestinationPath: helperURL.path)                results.append("Installed: \(dir)")            } catch {                results.append("Failed: \(dir)")            }        }         self.cliStatus = results.isEmpty            ? L("no_writable_bin_dirs")            : results.joined(separator: " · ")    }     private static func isLink(atPath path: String, pointingTo destination: String) -> Bool {        guard let link = try? FileManager.default.destinationOfSymbolicLink(atPath: path) else { return false }        let dir = (path as NSString).deletingLastPathComponent        let resolved = URL(fileURLWithPath: link, relativeTo: URL(fileURLWithPath: dir))            .standardizedFileURL            .path        return resolved == destination    }}