macOS: add settings previews

This commit is contained in:
Peter Steinberger
2025-12-09 18:04:11 +01:00
parent d5cd1058ab
commit 959ba94eca
16 changed files with 360 additions and 58 deletions

View File

@@ -177,3 +177,13 @@ private struct AboutMetaRow: View {
}
}
}
#if DEBUG
struct AboutSettings_Previews: PreviewProvider {
private static let updater = DisabledUpdaterController()
static var previews: some View {
AboutSettings(updater: updater)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -5,99 +5,116 @@ import SwiftUI
@MainActor
final class AppState: ObservableObject {
private let isPreview: Bool
private func ifNotPreview(_ action: () -> Void) {
guard !self.isPreview else { return }
action()
}
enum ConnectionMode: String {
case local
case remote
}
@Published var isPaused: Bool {
didSet { UserDefaults.standard.set(self.isPaused, forKey: pauseDefaultsKey) }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.isPaused, forKey: pauseDefaultsKey) } }
}
@Published var launchAtLogin: Bool {
didSet { Task { AppStateStore.updateLaunchAtLogin(enabled: self.launchAtLogin) } }
didSet { self.ifNotPreview { Task { AppStateStore.updateLaunchAtLogin(enabled: self.launchAtLogin) } } }
}
@Published var onboardingSeen: Bool {
didSet { UserDefaults.standard.set(self.onboardingSeen, forKey: "clawdis.onboardingSeen") }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.onboardingSeen, forKey: "clawdis.onboardingSeen") } }
}
@Published var debugPaneEnabled: Bool {
didSet { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") } }
}
@Published var swabbleEnabled: Bool {
didSet {
UserDefaults.standard.set(self.swabbleEnabled, forKey: swabbleEnabledKey)
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
self.ifNotPreview {
UserDefaults.standard.set(self.swabbleEnabled, forKey: swabbleEnabledKey)
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
}
}
}
@Published var swabbleTriggerWords: [String] {
didSet {
// Preserve the raw editing state; sanitization happens when we actually use the triggers.
UserDefaults.standard.set(self.swabbleTriggerWords, forKey: swabbleTriggersKey)
if self.swabbleEnabled {
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
self.ifNotPreview {
UserDefaults.standard.set(self.swabbleTriggerWords, forKey: swabbleTriggersKey)
if self.swabbleEnabled {
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
}
}
}
}
@Published var voiceWakeTriggerChime: VoiceWakeChime {
didSet { self.storeChime(self.voiceWakeTriggerChime, key: voiceWakeTriggerChimeKey) }
didSet { self.ifNotPreview { self.storeChime(self.voiceWakeTriggerChime, key: voiceWakeTriggerChimeKey) } }
}
@Published var voiceWakeSendChime: VoiceWakeChime {
didSet { self.storeChime(self.voiceWakeSendChime, key: voiceWakeSendChimeKey) }
didSet { self.ifNotPreview { self.storeChime(self.voiceWakeSendChime, key: voiceWakeSendChimeKey) } }
}
@Published var iconAnimationsEnabled: Bool {
didSet { UserDefaults.standard.set(self.iconAnimationsEnabled, forKey: iconAnimationsEnabledKey) }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.iconAnimationsEnabled, forKey: iconAnimationsEnabledKey) } }
}
@Published var showDockIcon: Bool {
didSet {
UserDefaults.standard.set(self.showDockIcon, forKey: showDockIconKey)
AppActivationPolicy.apply(showDockIcon: self.showDockIcon)
self.ifNotPreview {
UserDefaults.standard.set(self.showDockIcon, forKey: showDockIconKey)
AppActivationPolicy.apply(showDockIcon: self.showDockIcon)
}
}
}
@Published var voiceWakeMicID: String {
didSet {
UserDefaults.standard.set(self.voiceWakeMicID, forKey: voiceWakeMicKey)
if self.swabbleEnabled {
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
self.ifNotPreview {
UserDefaults.standard.set(self.voiceWakeMicID, forKey: voiceWakeMicKey)
if self.swabbleEnabled {
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
}
}
}
}
@Published var voiceWakeLocaleID: String {
didSet {
UserDefaults.standard.set(self.voiceWakeLocaleID, forKey: voiceWakeLocaleKey)
if self.swabbleEnabled {
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
self.ifNotPreview {
UserDefaults.standard.set(self.voiceWakeLocaleID, forKey: voiceWakeLocaleKey)
if self.swabbleEnabled {
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
}
}
}
}
@Published var voiceWakeAdditionalLocaleIDs: [String] {
didSet { UserDefaults.standard.set(self.voiceWakeAdditionalLocaleIDs, forKey: voiceWakeAdditionalLocalesKey) }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeAdditionalLocaleIDs, forKey: voiceWakeAdditionalLocalesKey) } }
}
@Published var voiceWakeForwardEnabled: Bool {
didSet { UserDefaults.standard.set(self.voiceWakeForwardEnabled, forKey: voiceWakeForwardEnabledKey) }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeForwardEnabled, forKey: voiceWakeForwardEnabledKey) } }
}
@Published var voiceWakeForwardCommand: String {
didSet { UserDefaults.standard.set(self.voiceWakeForwardCommand, forKey: voiceWakeForwardCommandKey) }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeForwardCommand, forKey: voiceWakeForwardCommandKey) } }
}
@Published var voicePushToTalkEnabled: Bool {
didSet { UserDefaults.standard.set(self.voicePushToTalkEnabled, forKey: voicePushToTalkEnabledKey) }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voicePushToTalkEnabled, forKey: voicePushToTalkEnabledKey) } }
}
@Published var iconOverride: IconOverrideSelection {
didSet { UserDefaults.standard.set(self.iconOverride.rawValue, forKey: iconOverrideKey) }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.iconOverride.rawValue, forKey: iconOverrideKey) } }
}
@Published var isWorking: Bool = false
@@ -106,38 +123,41 @@ final class AppState: ObservableObject {
@Published var sendCelebrationTick: Int = 0
@Published var heartbeatsEnabled: Bool {
didSet {
UserDefaults.standard.set(self.heartbeatsEnabled, forKey: heartbeatsEnabledKey)
Task { _ = await AgentRPC.shared.setHeartbeatsEnabled(self.heartbeatsEnabled) }
self.ifNotPreview {
UserDefaults.standard.set(self.heartbeatsEnabled, forKey: heartbeatsEnabledKey)
Task { _ = await AgentRPC.shared.setHeartbeatsEnabled(self.heartbeatsEnabled) }
}
}
}
@Published var connectionMode: ConnectionMode {
didSet { UserDefaults.standard.set(self.connectionMode.rawValue, forKey: connectionModeKey) }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.connectionMode.rawValue, forKey: connectionModeKey) } }
}
@Published var webChatEnabled: Bool {
didSet { UserDefaults.standard.set(self.webChatEnabled, forKey: webChatEnabledKey) }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.webChatEnabled, forKey: webChatEnabledKey) } }
}
@Published var webChatPort: Int {
didSet { UserDefaults.standard.set(self.webChatPort, forKey: webChatPortKey) }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.webChatPort, forKey: webChatPortKey) } }
}
@Published var remoteTarget: String {
didSet { UserDefaults.standard.set(self.remoteTarget, forKey: remoteTargetKey) }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteTarget, forKey: remoteTargetKey) } }
}
@Published var remoteIdentity: String {
didSet { UserDefaults.standard.set(self.remoteIdentity, forKey: remoteIdentityKey) }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteIdentity, forKey: remoteIdentityKey) } }
}
@Published var remoteProjectRoot: String {
didSet { UserDefaults.standard.set(self.remoteProjectRoot, forKey: remoteProjectRootKey) }
didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteProjectRoot, forKey: remoteProjectRootKey) } }
}
private var earBoostTask: Task<Void, Never>?
init() {
init(preview: Bool = false) {
self.isPreview = preview
self.isPaused = UserDefaults.standard.bool(forKey: pauseDefaultsKey)
self.launchAtLogin = false
self.onboardingSeen = UserDefaults.standard.bool(forKey: "clawdis.onboardingSeen")
@@ -199,16 +219,20 @@ final class AppState: ObservableObject {
let storedPort = UserDefaults.standard.integer(forKey: webChatPortKey)
self.webChatPort = storedPort > 0 ? storedPort : 18788
Task.detached(priority: .utility) { [weak self] in
let current = await LaunchAgentManager.status()
await MainActor.run { [weak self] in self?.launchAtLogin = current }
if !self.isPreview {
Task.detached(priority: .utility) { [weak self] in
let current = await LaunchAgentManager.status()
await MainActor.run { [weak self] in self?.launchAtLogin = current }
}
}
if self.swabbleEnabled, !PermissionManager.voiceWakePermissionsGranted() {
self.swabbleEnabled = false
}
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
if !self.isPreview {
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
}
}
func triggerVoiceEars(ttl: TimeInterval? = 5) {
@@ -243,14 +267,15 @@ final class AppState: ObservableObject {
return
}
self.swabbleEnabled = enabled
guard !self.isPreview else { return }
if !enabled {
self.swabbleEnabled = false
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
return
}
if PermissionManager.voiceWakePermissionsGranted() {
self.swabbleEnabled = true
Task { await VoiceWakeRuntime.shared.refresh(state: self) }
return
}
@@ -280,6 +305,37 @@ final class AppState: ObservableObject {
}
}
extension AppState {
static var preview: AppState {
let state = AppState(preview: true)
state.isPaused = false
state.launchAtLogin = true
state.onboardingSeen = true
state.debugPaneEnabled = true
state.swabbleEnabled = true
state.swabbleTriggerWords = ["Claude", "Computer", "Jarvis"]
state.voiceWakeTriggerChime = .system(name: "Glass")
state.voiceWakeSendChime = .system(name: "Ping")
state.iconAnimationsEnabled = true
state.showDockIcon = true
state.voiceWakeMicID = "BuiltInMic"
state.voiceWakeLocaleID = Locale.current.identifier
state.voiceWakeAdditionalLocaleIDs = ["en-US", "de-DE"]
state.voiceWakeForwardEnabled = true
state.voiceWakeForwardCommand = defaultVoiceWakeForwardCommand
state.voicePushToTalkEnabled = false
state.iconOverride = .system
state.heartbeatsEnabled = true
state.connectionMode = .local
state.webChatEnabled = true
state.webChatPort = 18788
state.remoteTarget = "user@example.com"
state.remoteIdentity = "~/.ssh/id_ed25519"
state.remoteProjectRoot = "~/Projects/clawdis"
return state
}
}
@MainActor
enum AppStateStore {
static let shared = AppState()

View File

@@ -2,6 +2,7 @@ import SwiftUI
@MainActor
struct ConfigSettings: View {
private let isPreview = ProcessInfo.processInfo.isPreview
@State private var configModel: String = ""
@State private var customModel: String = ""
@State private var configSaving = false
@@ -131,6 +132,7 @@ struct ConfigSettings: View {
}
.task {
guard !self.hasLoaded else { return }
guard !self.isPreview else { return }
self.hasLoaded = true
self.loadConfig()
await self.loadModels()
@@ -247,3 +249,12 @@ struct ConfigSettings: View {
return "Context window: \(human) tokens"
}
}
#if DEBUG
struct ConfigSettings_Previews: PreviewProvider {
static var previews: some View {
ConfigSettings()
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -36,7 +36,7 @@ enum DebugActions {
_ = await NotificationManager().send(title: "Clawdis", body: "Test notification", sound: nil)
}
static func sendDebugVoice() async -> Result<String, String> {
static func sendDebugVoice() async -> Result<String, DebugActionError> {
let message = """
This is a debug test from the Mac app. Reply with "Debug test works (and a funny pun)" \
if you received that.
@@ -51,7 +51,7 @@ enum DebugActions {
return .success("Forwarded. Await reply.")
case let .failure(error):
let detail = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
return .failure("Forward failed: \(detail)")
return .failure(.message("Forward failed: \(detail)"))
}
}
@@ -75,11 +75,11 @@ enum DebugActions {
let detail = (reason?.isEmpty == false)
? reason!
: "No error returned. Check /tmp/clawdis.log or rpc output."
return .failure("Local send failed: \(detail)")
return .failure(.message("Local send failed: \(detail)"))
}
} catch {
let detail = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
return .failure("Local send failed: \(detail)")
return .failure(.message("Local send failed: \(detail)"))
}
}
@@ -91,7 +91,7 @@ enum DebugActions {
}
}
private static func pinoLogPath() -> String {
static func pinoLogPath() -> String {
let df = DateFormatter()
df.calendar = Calendar(identifier: .iso8601)
df.locale = Locale(identifier: "en_US_POSIX")
@@ -102,3 +102,13 @@ enum DebugActions {
return "/tmp/clawdis.log"
}
}
enum DebugActionError: LocalizedError {
case message(String)
var errorDescription: String? {
switch self {
case let .message(text): text
}
}
}

View File

@@ -3,6 +3,7 @@ import SwiftUI
import UniformTypeIdentifiers
struct DebugSettings: View {
private let isPreview = ProcessInfo.processInfo.isPreview
@AppStorage(modelCatalogPathKey) private var modelCatalogPath: String = ModelCatalogLoader.defaultPath
@AppStorage(modelCatalogReloadKey) private var modelCatalogReloadBump: Int = 0
@AppStorage(iconOverrideKey) private var iconOverrideRaw: String = IconOverrideSelection.system.rawValue
@@ -44,9 +45,9 @@ struct DebugSettings: View {
}
LabeledContent("PID") { Text("\(ProcessInfo.processInfo.processIdentifier)") }
LabeledContent("Log file") {
Button("Open pino log") { self.openLog() }
.help(self.pinoLogPath)
Text(self.pinoLogPath)
Button("Open pino log") { DebugActions.openLog() }
.help(DebugActions.pinoLogPath())
Text(DebugActions.pinoLogPath())
.font(.caption2.monospaced())
.foregroundStyle(.secondary)
.textSelection(.enabled)
@@ -205,6 +206,7 @@ struct DebugSettings: View {
.padding(.vertical, 8)
}
.task {
guard !self.isPreview else { return }
await self.reloadModels()
self.loadSessionStorePath()
}
@@ -256,9 +258,9 @@ struct DebugSettings: View {
case let .success(message):
self.debugSendStatus = message
self.debugSendError = nil
case let .failure(message):
self.debugSendStatus = message
self.debugSendError = nil
case let .failure(error):
self.debugSendStatus = nil
self.debugSendError = error.localizedDescription
}
}
}
@@ -348,3 +350,12 @@ struct DebugSettings: View {
.appendingPathComponent("clawdis.json")
}
}
#if DEBUG
struct DebugSettings_Previews: PreviewProvider {
static var previews: some View {
DebugSettings()
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -238,7 +238,7 @@ private actor GatewayChannelActor {
if let urlError = error as? URLError {
let desc = urlError.localizedDescription.isEmpty ? "cancelled" : urlError.localizedDescription
return NSError(
domain: urlError.errorDomain,
domain: URLError.errorDomain,
code: urlError.errorCode,
userInfo: [NSLocalizedDescriptionKey: "\(context): \(desc)"])
}

View File

@@ -13,6 +13,7 @@ struct GeneralSettings: View {
@State private var relayInstalling = false
@State private var remoteStatus: RemoteStatus = .idle
@State private var showRemoteAdvanced = false
private let isPreview = ProcessInfo.processInfo.isPreview
var body: some View {
ScrollView(.vertical) {
@@ -65,6 +66,7 @@ struct GeneralSettings: View {
.padding(.bottom, 16)
}
.onAppear {
guard !self.isPreview else { return }
self.refreshCLIStatus()
self.refreshRelayStatus()
}
@@ -527,3 +529,12 @@ private func healthAgeString(_ ms: Double?) -> String {
guard let ms else { return "unknown" }
return msToAge(ms)
}
#if DEBUG
struct GeneralSettings_Previews: PreviewProvider {
static var previews: some View {
GeneralSettings(state: .preview)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -1,7 +1,11 @@
import SwiftUI
struct InstancesSettings: View {
@StateObject private var store = InstancesStore.shared
@ObservedObject var store: InstancesStore
init(store: InstancesStore = .shared) {
self.store = store
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
@@ -87,3 +91,12 @@ struct InstancesSettings: View {
.font(.footnote)
}
}
#if DEBUG
struct InstancesSettings_Previews: PreviewProvider {
static var previews: some View {
InstancesSettings(store: .preview())
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -28,6 +28,7 @@ struct InstanceInfo: Identifiable, Codable {
@MainActor
final class InstancesStore: ObservableObject {
static let shared = InstancesStore()
let isPreview: Bool
@Published var instances: [InstanceInfo] = []
@Published var lastError: String?
@@ -39,7 +40,12 @@ final class InstancesStore: ObservableObject {
private let interval: TimeInterval = 30
private var observers: [NSObjectProtocol] = []
init(isPreview: Bool = false) {
self.isPreview = isPreview
}
func start() {
guard !self.isPreview else { return }
guard self.task == nil else { return }
self.observeGatewayEvents()
self.task = Task.detached { [weak self] in
@@ -295,3 +301,33 @@ final class InstancesStore: ObservableObject {
}
}
}
extension InstancesStore {
static func preview(instances: [InstanceInfo] = [
InstanceInfo(
id: "local",
host: "steipete-mac",
ip: "10.0.0.12",
version: "1.2.3",
lastInputSeconds: 12,
mode: "local",
reason: "preview",
text: "Local node: steipete-mac (10.0.0.12) · app 1.2.3",
ts: Date().timeIntervalSince1970 * 1000),
InstanceInfo(
id: "relay",
host: "relay",
ip: "100.64.0.2",
version: "1.2.3",
lastInputSeconds: 45,
mode: "remote",
reason: "preview",
text: "Relay node · tunnel ok",
ts: Date().timeIntervalSince1970 * 1000 - 45_000),
]) -> InstancesStore {
let store = InstancesStore(isPreview: true)
store.instances = instances
store.statusMessage = "Preview data"
return store
}
}

View File

@@ -118,3 +118,23 @@ struct PermissionRow: View {
}
}
}
#if DEBUG
struct PermissionsSettings_Previews: PreviewProvider {
static var previews: some View {
PermissionsSettings(
status: [
.appleScript: true,
.notifications: true,
.accessibility: false,
.screenRecording: false,
.microphone: true,
.speechRecognition: false,
],
refresh: {},
showOnboarding: {}
)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -95,6 +95,49 @@ struct SessionDefaults {
let contextTokens: Int
}
extension SessionRow {
static var previewRows: [SessionRow] {
[
SessionRow(
id: "direct-1",
key: "user@example.com",
kind: .direct,
updatedAt: Date().addingTimeInterval(-90),
sessionId: "sess-direct-1234",
thinkingLevel: "low",
verboseLevel: "info",
systemSent: false,
abortedLastRun: false,
tokens: SessionTokenStats(input: 320, output: 680, total: 1000, contextTokens: 200_000),
model: "claude-3.5-sonnet"),
SessionRow(
id: "group-1",
key: "group:engineering",
kind: .group,
updatedAt: Date().addingTimeInterval(-3600),
sessionId: "sess-group-4321",
thinkingLevel: "medium",
verboseLevel: nil,
systemSent: true,
abortedLastRun: true,
tokens: SessionTokenStats(input: 5000, output: 1200, total: 6200, contextTokens: 200_000),
model: "claude-opus-4-5"),
SessionRow(
id: "global",
key: "global",
kind: .global,
updatedAt: Date().addingTimeInterval(-86_400),
sessionId: nil,
thinkingLevel: nil,
verboseLevel: nil,
systemSent: false,
abortedLastRun: false,
tokens: SessionTokenStats(input: 150, output: 220, total: 370, contextTokens: 200_000),
model: "gpt-4.1-mini"),
]
}
}
struct ModelChoice: Identifiable, Hashable {
let id: String
let name: String

View File

@@ -3,13 +3,23 @@ import SwiftUI
@MainActor
struct SessionsSettings: View {
@State private var rows: [SessionRow] = []
private let isPreview: Bool
@State private var rows: [SessionRow]
@State private var storePath: String = SessionLoader.defaultStorePath
@State private var lastLoaded: Date?
@State private var errorMessage: String?
@State private var loading = false
@State private var hasLoaded = false
init(rows: [SessionRow]? = nil, isPreview: Bool = ProcessInfo.processInfo.isPreview) {
self._rows = State(initialValue: rows ?? [])
self.isPreview = isPreview
if isPreview {
self._lastLoaded = State(initialValue: Date())
self._hasLoaded = State(initialValue: true)
}
}
var body: some View {
VStack(alignment: .leading, spacing: 14) {
self.header
@@ -22,6 +32,7 @@ struct SessionsSettings: View {
.padding(.horizontal, 12)
.task {
guard !self.hasLoaded else { return }
guard !self.isPreview else { return }
self.hasLoaded = true
await self.refresh()
}
@@ -149,6 +160,7 @@ struct SessionsSettings: View {
private func refresh() async {
guard !self.loading else { return }
guard !self.isPreview else { return }
self.loading = true
self.errorMessage = nil
@@ -209,3 +221,12 @@ private struct Badge: View {
.clipShape(Capsule())
}
}
#if DEBUG
struct SessionsSettings_Previews: PreviewProvider {
static var previews: some View {
SessionsSettings(rows: SessionRow.previewRows, isPreview: true)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -6,6 +6,13 @@ struct SettingsRootView: View {
@State private var monitoringPermissions = false
@State private var selectedTab: SettingsTab = .general
let updater: UpdaterProviding?
private let isPreview = ProcessInfo.processInfo.isPreview
init(state: AppState, updater: UpdaterProviding?, initialTab: SettingsTab? = nil) {
self.state = state
self.updater = updater
self._selectedTab = State(initialValue: initialTab ?? .general)
}
var body: some View {
TabView(selection: self.$selectedTab) {
@@ -76,7 +83,10 @@ struct SettingsRootView: View {
self.updatePermissionMonitoring(for: newValue)
}
.onDisappear { self.stopPermissionMonitoring() }
.task { await self.refreshPerms() }
.task {
guard !self.isPreview else { return }
await self.refreshPerms()
}
}
private func validTab(for requested: SettingsTab) -> SettingsTab {
@@ -86,10 +96,12 @@ struct SettingsRootView: View {
@MainActor
private func refreshPerms() async {
guard !self.isPreview else { return }
await self.permissionMonitor.refreshNow()
}
private func updatePermissionMonitoring(for tab: SettingsTab) {
guard !self.isPreview else { return }
let shouldMonitor = tab == .permissions
if shouldMonitor, !self.monitoringPermissions {
self.monitoringPermissions = true
@@ -143,3 +155,15 @@ enum SettingsTabRouter {
extension Notification.Name {
static let clawdisSelectSettingsTab = Notification.Name("clawdisSelectSettingsTab")
}
#if DEBUG
struct SettingsRootView_Previews: PreviewProvider {
static var previews: some View {
ForEach(SettingsTab.allCases, id: \.self) { tab in
SettingsRootView(state: .preview, updater: DisabledUpdaterController(), initialTab: tab)
.previewDisplayName(tab.title)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
}
#endif

View File

@@ -386,3 +386,12 @@ private enum ToolInstaller {
}
}
}
#if DEBUG
struct ToolsSettings_Previews: PreviewProvider {
static var previews: some View {
ToolsSettings()
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif

View File

@@ -1,6 +1,12 @@
import AppKit
import Foundation
extension ProcessInfo {
var isPreview: Bool {
self.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
}
}
enum LaunchdManager {
private static func runLaunchctl(_ args: [String]) {
let process = Process()

View File

@@ -17,6 +17,7 @@ struct VoiceWakeSettings: View {
@State private var availableLocales: [Locale] = []
private let fieldLabelWidth: CGFloat = 120
private let controlWidth: CGFloat = 240
private let isPreview = ProcessInfo.processInfo.isPreview
private struct AudioInputDevice: Identifiable, Equatable {
let uid: String
@@ -83,13 +84,24 @@ struct VoiceWakeSettings: View {
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 12)
}
.task { await self.loadMicsIfNeeded() }
.task { await self.loadLocalesIfNeeded() }
.task { await self.restartMeter() }
.task {
guard !self.isPreview else { return }
await self.loadMicsIfNeeded()
}
.task {
guard !self.isPreview else { return }
await self.loadLocalesIfNeeded()
}
.task {
guard !self.isPreview else { return }
await self.restartMeter()
}
.onChange(of: self.state.voiceWakeMicID) { _, _ in
guard !self.isPreview else { return }
Task { await self.restartMeter() }
}
.onDisappear {
guard !self.isPreview else { return }
Task { await self.meter.stop() }
}
}
@@ -489,3 +501,12 @@ struct VoiceWakeSettings: View {
}
}
}
#if DEBUG
struct VoiceWakeSettings_Previews: PreviewProvider {
static var previews: some View {
VoiceWakeSettings(state: .preview)
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
#endif