native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add repo-scoped custom command buttons and shortcut overrides

onevcat 76046bc0 56deb491

+936 -39
+50
supacode/App/OnevcatCustomShortcut+SwiftUI.swift
··· 1 + import AppKit 2 + import SwiftUI 3 + 4 + extension OnevcatCustomShortcut { 5 + var keyboardShortcut: KeyboardShortcut? { 6 + guard let keyEquivalent else { return nil } 7 + return KeyboardShortcut(keyEquivalent, modifiers: modifiers.eventModifiers) 8 + } 9 + 10 + var keyEquivalent: KeyEquivalent? { 11 + guard let character = normalizedKeyCharacter else { return nil } 12 + return KeyEquivalent(character) 13 + } 14 + 15 + func matches(event: NSEvent) -> Bool { 16 + guard let characters = event.charactersIgnoringModifiers else { return false } 17 + let normalized = characters.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() 18 + guard normalized.count == 1 else { return false } 19 + guard normalized == String(normalizedKeyCharacter ?? Character(" ")) else { return false } 20 + return event.modifierFlags.contains(.command) == modifiers.command 21 + && event.modifierFlags.contains(.shift) == modifiers.shift 22 + && event.modifierFlags.contains(.option) == modifiers.option 23 + && event.modifierFlags.contains(.control) == modifiers.control 24 + } 25 + 26 + private var normalizedKeyCharacter: Character? { 27 + let normalizedKey = key.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() 28 + guard normalizedKey.count == 1 else { return nil } 29 + return normalizedKey.first 30 + } 31 + } 32 + 33 + extension OnevcatCustomShortcutModifiers { 34 + var eventModifiers: EventModifiers { 35 + var modifiers: EventModifiers = [] 36 + if command { 37 + modifiers.insert(.command) 38 + } 39 + if shift { 40 + modifiers.insert(.shift) 41 + } 42 + if option { 43 + modifiers.insert(.option) 44 + } 45 + if control { 46 + modifiers.insert(.control) 47 + } 48 + return modifiers 49 + } 50 + }
+1
supacode/Clients/Terminal/TerminalClient.swift
··· 10 10 case createTabWithInput(Worktree, input: String, runSetupScriptIfNew: Bool) 11 11 case ensureInitialTab(Worktree, runSetupScriptIfNew: Bool, focusing: Bool) 12 12 case runScript(Worktree, script: String) 13 + case insertText(Worktree, text: String) 13 14 case stopRunScript(Worktree) 14 15 case closeFocusedTab(Worktree) 15 16 case closeFocusedSurface(Worktree)
+1 -5
supacode/Commands/UpdateCommands.swift
··· 9 9 Button("Check for Updates...") { 10 10 store.send(.checkForUpdates) 11 11 } 12 - .keyboardShortcut( 13 - AppShortcuts.checkForUpdates.keyEquivalent, 14 - modifiers: AppShortcuts.checkForUpdates.modifiers 15 - ) 16 - .help("Check for Updates (\(AppShortcuts.checkForUpdates.display))") 12 + .help("Check for updates") 17 13 } 18 14 } 19 15 }
+45
supacode/Commands/WorktreeCommands.swift
··· 18 18 19 19 var body: some Commands { 20 20 let repositories = store.repositories 21 + let hasActiveWorktree = repositories.worktree(for: repositories.selectedWorktreeID) != nil 21 22 let orderedRows = visibleHotkeyWorktreeRows ?? repositories.orderedWorktreeRows() 22 23 let pullRequestURL = selectedPullRequestURL 23 24 let githubIntegrationEnabled = store.settings.githubIntegrationEnabled 24 25 let deleteShortcut = KeyboardShortcut(.delete, modifiers: [.command, .shift]).display 26 + let customCommands = store.selectedCustomCommands 25 27 CommandMenu("Worktrees") { 26 28 Button("Select Next Worktree") { 27 29 store.send(.repositories(.selectNextWorktree)) ··· 48 50 } 49 51 } 50 52 CommandGroup(replacing: .newItem) { 53 + if !customCommands.isEmpty { 54 + ForEach(Array(customCommands.enumerated()), id: \.element.id) { index, command in 55 + customCommandButton( 56 + index: index, 57 + command: command, 58 + hasActiveWorktree: hasActiveWorktree 59 + ) 60 + } 61 + Divider() 62 + } 51 63 Button("Open Repository...", systemImage: "folder") { 52 64 store.send(.repositories(.setOpenPanelPresented(true))) 53 65 } ··· 170 182 guard let row else { return "Worktree \(index + 1)" } 171 183 let repositoryName = store.repositories.repositoryName(for: row.repositoryID) ?? "Repository" 172 184 return "\(repositoryName) — \(row.name)" 185 + } 186 + 187 + @ViewBuilder 188 + private func customCommandButton( 189 + index: Int, 190 + command: OnevcatCustomCommand, 191 + hasActiveWorktree: Bool 192 + ) -> some View { 193 + let title = command.resolvedTitle 194 + let helpText: String = 195 + if let shortcut = command.shortcut?.keyboardShortcut?.display { 196 + "\(title) (\(shortcut))" 197 + } else { 198 + title 199 + } 200 + Button(title, systemImage: command.resolvedSystemImage) { 201 + store.send(.runCustomCommand(index)) 202 + } 203 + .modifier(KeyboardShortcutModifier(shortcut: command.shortcut?.keyboardShortcut)) 204 + .help(helpText) 205 + .disabled(!hasActiveWorktree) 206 + } 207 + } 208 + 209 + private struct KeyboardShortcutModifier: ViewModifier { 210 + let shortcut: KeyboardShortcut? 211 + 212 + func body(content: Content) -> some View { 213 + if let shortcut { 214 + content.keyboardShortcut(shortcut) 215 + } else { 216 + content 217 + } 173 218 } 174 219 } 175 220
+78 -4
supacode/Features/App/Reducer/AppFeature.swift
··· 18 18 var commandPalette = CommandPaletteFeature.State() 19 19 var openActionSelection: OpenWorktreeAction = .finder 20 20 var selectedRunScript: String = "" 21 + var selectedCustomCommands: [OnevcatCustomCommand] = [] 21 22 var runScriptDraft: String = "" 22 23 var isRunScriptPromptPresented = false 23 24 var runScriptStatusByWorktreeID: [Worktree.ID: Bool] = [:] ··· 47 48 case commandPalette(CommandPaletteFeature.Action) 48 49 case openActionSelectionChanged(OpenWorktreeAction) 49 50 case worktreeSettingsLoaded(RepositorySettings, worktreeID: Worktree.ID) 51 + case worktreeOnevcatSettingsLoaded(OnevcatRepositorySettings, worktreeID: Worktree.ID) 50 52 case openSelectedWorktree 51 53 case openWorktree(OpenWorktreeAction) 52 54 case openWorktreeFailed(OpenActionError) 53 55 case requestQuit 54 56 case newTerminal 55 57 case runScript 58 + case runCustomCommand(Int) 56 59 case runScriptDraftChanged(String) 57 60 case runScriptPromptPresented(Bool) 58 61 case saveRunScriptAndRun ··· 135 138 guard let worktree else { 136 139 state.openActionSelection = .finder 137 140 state.selectedRunScript = "" 141 + state.selectedCustomCommands = [] 138 142 state.runScriptDraft = "" 139 143 state.isRunScriptPromptPresented = false 140 144 var effects: [Effect<Action>] = [ ··· 153 157 at: 0 154 158 ) 155 159 } 156 - return .merge(effects) 160 + return .merge( 161 + .merge(effects), 162 + .run { _ in 163 + await MainActor.run { 164 + OnevcatCustomShortcutRegistry.shared.setShortcuts([]) 165 + } 166 + } 167 + ) 157 168 } 158 169 let rootURL = worktree.repositoryRootURL 159 170 let worktreeID = worktree.id 171 + state.selectedCustomCommands = [] 160 172 state.runScriptDraft = "" 161 173 state.isRunScriptPromptPresented = false 162 174 @Shared(.repositorySettings(rootURL)) var repositorySettings 175 + @Shared(.onevcatRepositorySettings(rootURL)) var onevcatRepositorySettings 163 176 let settings = repositorySettings 177 + let onevcatSettings = onevcatRepositorySettings 164 178 return .merge( 165 179 .run { _ in 166 180 await repositoryPersistence.saveLastFocusedWorktreeID(lastFocusedWorktreeID) ··· 171 185 .run { _ in 172 186 await worktreeInfoWatcher.send(.setSelectedWorktreeID(worktree.id)) 173 187 }, 174 - .send(.worktreeSettingsLoaded(settings, worktreeID: worktreeID)) 188 + .run { _ in 189 + await MainActor.run { 190 + OnevcatCustomShortcutRegistry.shared.setShortcuts([]) 191 + } 192 + }, 193 + .concatenate( 194 + .send(.worktreeSettingsLoaded(settings, worktreeID: worktreeID)), 195 + .send(.worktreeOnevcatSettingsLoaded(onevcatSettings, worktreeID: worktreeID)) 196 + ) 175 197 ) 176 198 177 199 case .repositories(.delegate(.worktreeCreated(let worktree))): ··· 237 259 return .none 238 260 } 239 261 @Shared(.repositorySettings(repository.rootURL)) var repositorySettings 262 + @Shared(.onevcatRepositorySettings(repository.rootURL)) var onevcatRepositorySettings 240 263 state.settings.repositorySettings = RepositorySettingsFeature.State( 241 264 rootURL: repository.rootURL, 242 - settings: repositorySettings 265 + settings: repositorySettings, 266 + onevcatSettings: onevcatRepositorySettings 243 267 ) 244 268 case .general, .notifications, .worktree, .updates, .advanced, .github: 245 269 state.settings.repositorySettings = nil ··· 416 440 await terminalClient.send(.runScript(worktree, script: script)) 417 441 } 418 442 443 + case .runCustomCommand(let index): 444 + guard let worktree = state.repositories.worktree(for: state.repositories.selectedWorktreeID) else { 445 + return .none 446 + } 447 + guard state.selectedCustomCommands.indices.contains(index) else { 448 + return .none 449 + } 450 + let customCommand = state.selectedCustomCommands[index] 451 + guard customCommand.hasRunnableCommand else { 452 + return .none 453 + } 454 + let command = customCommand.command 455 + switch customCommand.execution { 456 + case .shellScript: 457 + return .run { _ in 458 + await terminalClient.send( 459 + .createTabWithInput( 460 + worktree, 461 + input: command, 462 + runSetupScriptIfNew: false 463 + ) 464 + ) 465 + } 466 + case .terminalInput: 467 + let input = command.hasSuffix("\n") ? command : "\(command)\n" 468 + return .run { _ in 469 + await terminalClient.send(.insertText(worktree, text: input)) 470 + } 471 + } 472 + 419 473 case .runScriptDraftChanged(let script): 420 474 state.runScriptDraft = script 421 475 return .none ··· 522 576 } 523 577 let worktreeID = selectedWorktree.id 524 578 @Shared(.repositorySettings(rootURL)) var repositorySettings 525 - return .send(.worktreeSettingsLoaded(repositorySettings, worktreeID: worktreeID)) 579 + @Shared(.onevcatRepositorySettings(rootURL)) var onevcatRepositorySettings 580 + return .concatenate( 581 + .send(.worktreeSettingsLoaded(repositorySettings, worktreeID: worktreeID)), 582 + .send(.worktreeOnevcatSettingsLoaded(onevcatRepositorySettings, worktreeID: worktreeID)) 583 + ) 526 584 527 585 case .worktreeSettingsLoaded(let settings, let worktreeID): 528 586 guard state.repositories.selectedWorktreeID == worktreeID else { ··· 538 596 ) 539 597 state.selectedRunScript = settings.runScript 540 598 return .none 599 + 600 + case .worktreeOnevcatSettingsLoaded(let settings, let worktreeID): 601 + guard state.repositories.selectedWorktreeID == worktreeID else { 602 + return .none 603 + } 604 + state.selectedCustomCommands = OnevcatRepositorySettings.normalizedCommands(settings.customCommands) 605 + .filter(\.hasRunnableCommand) 606 + let shortcuts: [OnevcatCustomShortcut] = state.selectedCustomCommands.compactMap { command in 607 + guard let shortcut = command.shortcut, shortcut.isValid else { return nil } 608 + return shortcut.normalized() 609 + } 610 + return .run { _ in 611 + await MainActor.run { 612 + OnevcatCustomShortcutRegistry.shared.setShortcuts(shortcuts) 613 + } 614 + } 541 615 542 616 case .systemNotificationsPermissionFailed(let errorMessage): 543 617 let message: String
+1 -1
supacode/Features/CommandPalette/CommandPaletteItem.swift
··· 91 91 var appShortcut: AppShortcut? { 92 92 switch kind { 93 93 case .checkForUpdates: 94 - return AppShortcuts.checkForUpdates 94 + return nil 95 95 case .openRepository: 96 96 return AppShortcuts.openRepository 97 97 case .openSettings:
+111 -19
supacode/Features/Repositories/Views/WorktreeDetailView.swift
··· 32 32 let openActionSelection = state.openActionSelection 33 33 let runScriptEnabled = hasActiveWorktree 34 34 let runScriptIsRunning = selectedWorktree.flatMap { state.runScriptStatusByWorktreeID[$0.id] } == true 35 + let customCommands = state.selectedCustomCommands 35 36 let notificationGroups = repositories.toolbarNotificationGroups(terminalManager: terminalManager) 36 37 let unseenNotificationWorktreeCount = notificationGroups.reduce(0) { count, repository in 37 38 count + repository.unseenWorktreeCount ··· 61 62 openActionSelection: openActionSelection, 62 63 showExtras: commandKeyObserver.isPressed, 63 64 runScriptEnabled: runScriptEnabled, 64 - runScriptIsRunning: runScriptIsRunning 65 + runScriptIsRunning: runScriptIsRunning, 66 + customCommands: customCommands 65 67 ) 66 68 WorktreeToolbarContent( 67 69 toolbarState: toolbarState, ··· 81 83 onSelectNotification: selectToolbarNotification, 82 84 onDismissAllNotifications: { dismissAllToolbarNotifications(in: notificationGroups) }, 83 85 onRunScript: { store.send(.runScript) }, 84 - onStopRunScript: { store.send(.stopRunScript) } 86 + onStopRunScript: { store.send(.stopRunScript) }, 87 + onRunCustomCommand: { index in 88 + store.send(.runCustomCommand(index)) 89 + } 85 90 ) 86 91 } 87 92 } ··· 247 252 let showExtras: Bool 248 253 let runScriptEnabled: Bool 249 254 let runScriptIsRunning: Bool 255 + let customCommands: [OnevcatCustomCommand] 250 256 251 257 var runScriptHelpText: String { 252 258 "Run Script (\(AppShortcuts.runScript.display))" ··· 267 273 let onDismissAllNotifications: () -> Void 268 274 let onRunScript: () -> Void 269 275 let onStopRunScript: () -> Void 276 + let onRunCustomCommand: (Int) -> Void 270 277 271 278 var body: some ToolbarContent { 272 279 ToolbarItem { ··· 307 314 ) 308 315 } 309 316 ToolbarSpacer(.fixed) 310 - 311 - if toolbarState.runScriptIsRunning || toolbarState.runScriptEnabled { 312 - ToolbarItem { 313 - RunScriptToolbarButton( 314 - isRunning: toolbarState.runScriptIsRunning, 315 - isEnabled: toolbarState.runScriptEnabled, 316 - runHelpText: toolbarState.runScriptHelpText, 317 - stopHelpText: toolbarState.stopRunScriptHelpText, 318 - runShortcut: AppShortcuts.runScript.display, 319 - stopShortcut: AppShortcuts.stopRunScript.display, 320 - runAction: onRunScript, 321 - stopAction: onStopRunScript 322 - ) 323 - } 324 - } 317 + commandToolbarItems 325 318 326 319 } 327 320 ··· 373 366 ? "\(action.title) (\(AppShortcuts.openFinder.display))" 374 367 : action.title 375 368 } 369 + 370 + @ToolbarContentBuilder 371 + private var commandToolbarItems: some ToolbarContent { 372 + if toolbarState.runScriptIsRunning || toolbarState.runScriptEnabled { 373 + ToolbarItem { 374 + RunScriptToolbarButton( 375 + isRunning: toolbarState.runScriptIsRunning, 376 + isEnabled: toolbarState.runScriptEnabled, 377 + runHelpText: toolbarState.runScriptHelpText, 378 + stopHelpText: toolbarState.stopRunScriptHelpText, 379 + runShortcut: AppShortcuts.runScript.display, 380 + stopShortcut: AppShortcuts.stopRunScript.display, 381 + runAction: onRunScript, 382 + stopAction: onStopRunScript 383 + ) 384 + } 385 + } 386 + 387 + if let command = customCommand(at: 0) { 388 + ToolbarItem { 389 + customCommandButton(command, index: 0) 390 + } 391 + } 392 + if let command = customCommand(at: 1) { 393 + ToolbarItem { 394 + customCommandButton(command, index: 1) 395 + } 396 + } 397 + if let command = customCommand(at: 2) { 398 + ToolbarItem { 399 + customCommandButton(command, index: 2) 400 + } 401 + } 402 + } 403 + 404 + private func customCommand(at index: Int) -> OnevcatCustomCommand? { 405 + guard toolbarState.customCommands.indices.contains(index) else { 406 + return nil 407 + } 408 + return toolbarState.customCommands[index] 409 + } 410 + 411 + private func customCommandButton(_ command: OnevcatCustomCommand, index: Int) -> some View { 412 + OnevcatCustomCommandToolbarButton( 413 + title: command.resolvedTitle, 414 + systemImage: command.resolvedSystemImage, 415 + shortcut: command.shortcut?.isValid == true ? command.shortcut?.display : nil, 416 + action: { 417 + onRunCustomCommand(index) 418 + } 419 + ) 420 + } 376 421 } 377 422 378 423 private func loadingInfo( ··· 542 587 } 543 588 } 544 589 590 + private struct OnevcatCustomCommandToolbarButton: View { 591 + let title: String 592 + let systemImage: String 593 + let shortcut: String? 594 + let action: () -> Void 595 + @Environment(CommandKeyObserver.self) private var commandKeyObserver 596 + 597 + var body: some View { 598 + Button { 599 + action() 600 + } label: { 601 + HStack(spacing: 6) { 602 + Image(systemName: systemImage) 603 + .accessibilityHidden(true) 604 + Text(title) 605 + if commandKeyObserver.isPressed, let shortcut { 606 + Text(shortcut) 607 + .font(.caption) 608 + .foregroundStyle(.secondary) 609 + } 610 + } 611 + } 612 + .font(.caption) 613 + .help(helpText) 614 + } 615 + 616 + private var helpText: String { 617 + if let shortcut { 618 + return "\(title) (\(shortcut))" 619 + } 620 + return title 621 + } 622 + } 623 + 545 624 @MainActor 546 625 private struct WorktreeToolbarPreview: View { 547 626 private let toolbarState: WorktreeDetailView.WorktreeToolbarState ··· 557 636 openActionSelection: .finder, 558 637 showExtras: false, 559 638 runScriptEnabled: true, 560 - runScriptIsRunning: false 639 + runScriptIsRunning: false, 640 + customCommands: [ 641 + OnevcatCustomCommand( 642 + title: "Test", 643 + systemImage: "checkmark.circle.fill", 644 + command: "swift test", 645 + execution: .shellScript, 646 + shortcut: OnevcatCustomShortcut( 647 + key: "u", 648 + modifiers: OnevcatCustomShortcutModifiers() 649 + ) 650 + ), 651 + ] 561 652 ) 562 653 let observer = CommandKeyObserver() 563 654 observer.isPressed = false ··· 579 670 onSelectNotification: { _, _ in }, 580 671 onDismissAllNotifications: {}, 581 672 onRunScript: {}, 582 - onStopRunScript: {} 673 + onStopRunScript: {}, 674 + onRunCustomCommand: { _ in } 583 675 ) 584 676 } 585 677 .environment(commandKeyObserver)
+10 -3
supacode/Features/RepositorySettings/Reducer/RepositorySettingsFeature.swift
··· 7 7 struct State: Equatable { 8 8 var rootURL: URL 9 9 var settings: RepositorySettings 10 + var onevcatSettings: OnevcatRepositorySettings 10 11 var isBareRepository = false 11 12 var branchOptions: [String] = [] 12 13 var defaultWorktreeBaseRef = "origin/main" ··· 15 16 16 17 enum Action: BindableAction { 17 18 case task 18 - case settingsLoaded(RepositorySettings, isBareRepository: Bool) 19 + case settingsLoaded(RepositorySettings, OnevcatRepositorySettings, isBareRepository: Bool) 19 20 case branchDataLoaded([String], defaultBaseRef: String) 20 21 case delegate(Delegate) 21 22 case binding(BindingAction<State>) ··· 35 36 case .task: 36 37 let rootURL = state.rootURL 37 38 @Shared(.repositorySettings(rootURL)) var repositorySettings 39 + @Shared(.onevcatRepositorySettings(rootURL)) var onevcatRepositorySettings 38 40 let settings = repositorySettings 41 + let onevcatSettings = onevcatRepositorySettings 39 42 let gitClient = gitClient 40 43 return .run { send in 41 44 let isBareRepository = (try? await gitClient.isBareRepository(rootURL)) ?? false 42 - await send(.settingsLoaded(settings, isBareRepository: isBareRepository)) 45 + await send(.settingsLoaded(settings, onevcatSettings, isBareRepository: isBareRepository)) 43 46 let branches: [String] 44 47 do { 45 48 branches = try await gitClient.branchRefs(rootURL) ··· 54 57 await send(.branchDataLoaded(branches, defaultBaseRef: defaultBaseRef)) 55 58 } 56 59 57 - case .settingsLoaded(let settings, let isBareRepository): 60 + case .settingsLoaded(let settings, let onevcatSettings, let isBareRepository): 58 61 var updatedSettings = settings 59 62 if isBareRepository { 60 63 updatedSettings.copyIgnoredOnWorktreeCreate = false 61 64 updatedSettings.copyUntrackedOnWorktreeCreate = false 62 65 } 63 66 state.settings = updatedSettings 67 + state.onevcatSettings = onevcatSettings.normalized() 64 68 state.isBareRepository = isBareRepository 65 69 guard isBareRepository, updatedSettings != settings else { return .none } 66 70 let rootURL = state.rootURL ··· 86 90 state.settings.copyIgnoredOnWorktreeCreate = false 87 91 state.settings.copyUntrackedOnWorktreeCreate = false 88 92 } 93 + state.onevcatSettings = state.onevcatSettings.normalized() 89 94 let rootURL = state.rootURL 90 95 @Shared(.repositorySettings(rootURL)) var repositorySettings 96 + @Shared(.onevcatRepositorySettings(rootURL)) var onevcatRepositorySettings 91 97 $repositorySettings.withLock { $0 = state.settings } 98 + $onevcatRepositorySettings.withLock { $0 = state.onevcatSettings } 92 99 return .send(.delegate(.settingsChanged(rootURL))) 93 100 94 101 case .delegate:
+86
supacode/Features/Settings/BusinessLogic/OnevcatRepositorySettingsKey.swift
··· 1 + import Dependencies 2 + import Foundation 3 + import Sharing 4 + 5 + nonisolated struct OnevcatRepositorySettingsKeyID: Hashable, Sendable { 6 + let repositoryID: String 7 + } 8 + 9 + nonisolated struct OnevcatRepositorySettingsKey: SharedKey { 10 + let repositoryID: String 11 + let rootURL: URL 12 + 13 + init(rootURL: URL) { 14 + self.rootURL = rootURL.standardizedFileURL 15 + repositoryID = self.rootURL.path(percentEncoded: false) 16 + } 17 + 18 + var id: OnevcatRepositorySettingsKeyID { 19 + OnevcatRepositorySettingsKeyID(repositoryID: repositoryID) 20 + } 21 + 22 + func load( 23 + context: LoadContext<OnevcatRepositorySettings>, 24 + continuation: LoadContinuation<OnevcatRepositorySettings> 25 + ) { 26 + @Dependency(\.repositoryLocalSettingsStorage) var repositoryLocalSettingsStorage 27 + let settingsURL = SupacodePaths.onevcatRepositorySettingsURL(for: rootURL) 28 + if let localData = try? repositoryLocalSettingsStorage.load(settingsURL) { 29 + let decoder = JSONDecoder() 30 + if let settings = try? decoder.decode(OnevcatRepositorySettings.self, from: localData) { 31 + continuation.resume(returning: settings.normalized()) 32 + return 33 + } 34 + let path = settingsURL.path(percentEncoded: false) 35 + SupaLogger("Settings").warning( 36 + "Unable to decode onevcat repository settings at \(path); using defaults." 37 + ) 38 + } 39 + 40 + let defaultSettings = (context.initialValue ?? .default).normalized() 41 + do { 42 + let encoder = JSONEncoder() 43 + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 44 + let data = try encoder.encode(defaultSettings) 45 + try repositoryLocalSettingsStorage.save(data, settingsURL) 46 + } catch { 47 + let path = settingsURL.path(percentEncoded: false) 48 + SupaLogger("Settings").warning( 49 + "Unable to write onevcat repository settings to \(path): \(error.localizedDescription)" 50 + ) 51 + } 52 + 53 + continuation.resume(returning: defaultSettings) 54 + } 55 + 56 + func subscribe( 57 + context _: LoadContext<OnevcatRepositorySettings>, 58 + subscriber _: SharedSubscriber<OnevcatRepositorySettings> 59 + ) -> SharedSubscription { 60 + SharedSubscription {} 61 + } 62 + 63 + func save( 64 + _ value: OnevcatRepositorySettings, 65 + context _: SaveContext, 66 + continuation: SaveContinuation 67 + ) { 68 + @Dependency(\.repositoryLocalSettingsStorage) var repositoryLocalSettingsStorage 69 + let settingsURL = SupacodePaths.onevcatRepositorySettingsURL(for: rootURL) 70 + do { 71 + let encoder = JSONEncoder() 72 + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 73 + let data = try encoder.encode(value.normalized()) 74 + try repositoryLocalSettingsStorage.save(data, settingsURL) 75 + continuation.resume() 76 + } catch { 77 + continuation.resume(throwing: error) 78 + } 79 + } 80 + } 81 + 82 + nonisolated extension SharedReaderKey where Self == OnevcatRepositorySettingsKey.Default { 83 + static func onevcatRepositorySettings(_ rootURL: URL) -> Self { 84 + Self[OnevcatRepositorySettingsKey(rootURL: rootURL), default: .default] 85 + } 86 + }
+164
supacode/Features/Settings/Models/OnevcatRepositorySettings.swift
··· 1 + import Foundation 2 + 3 + nonisolated struct OnevcatRepositorySettings: Codable, Equatable, Sendable { 4 + static let maxCustomCommands = 3 5 + 6 + var customCommands: [OnevcatCustomCommand] 7 + 8 + static let `default` = OnevcatRepositorySettings(customCommands: []) 9 + 10 + private enum CodingKeys: String, CodingKey { 11 + case customCommands 12 + } 13 + 14 + init(customCommands: [OnevcatCustomCommand]) { 15 + self.customCommands = Self.normalizedCommands(customCommands) 16 + } 17 + 18 + init(from decoder: Decoder) throws { 19 + let container = try decoder.container(keyedBy: CodingKeys.self) 20 + let commands = try container.decodeIfPresent([OnevcatCustomCommand].self, forKey: .customCommands) ?? [] 21 + customCommands = Self.normalizedCommands(commands) 22 + } 23 + 24 + func normalized() -> OnevcatRepositorySettings { 25 + OnevcatRepositorySettings(customCommands: customCommands) 26 + } 27 + 28 + static func normalizedCommands(_ commands: [OnevcatCustomCommand]) -> [OnevcatCustomCommand] { 29 + Array(commands.prefix(maxCustomCommands)).map { $0.normalized() } 30 + } 31 + } 32 + 33 + nonisolated struct OnevcatCustomCommand: Codable, Equatable, Sendable, Identifiable { 34 + var id: String 35 + var title: String 36 + var systemImage: String 37 + var command: String 38 + var execution: OnevcatCustomCommandExecution 39 + var shortcut: OnevcatCustomShortcut? 40 + 41 + init( 42 + id: String = UUID().uuidString, 43 + title: String, 44 + systemImage: String, 45 + command: String, 46 + execution: OnevcatCustomCommandExecution, 47 + shortcut: OnevcatCustomShortcut? 48 + ) { 49 + self.id = id 50 + self.title = title 51 + self.systemImage = systemImage 52 + self.command = command 53 + self.execution = execution 54 + self.shortcut = shortcut?.normalized() 55 + } 56 + 57 + static func `default`(index: Int) -> OnevcatCustomCommand { 58 + OnevcatCustomCommand( 59 + title: "Command \(index + 1)", 60 + systemImage: "terminal", 61 + command: "", 62 + execution: .shellScript, 63 + shortcut: nil 64 + ) 65 + } 66 + 67 + func normalized() -> OnevcatCustomCommand { 68 + OnevcatCustomCommand( 69 + id: id, 70 + title: title, 71 + systemImage: systemImage, 72 + command: command, 73 + execution: execution, 74 + shortcut: shortcut?.normalized() 75 + ) 76 + } 77 + 78 + var resolvedTitle: String { 79 + let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines) 80 + if trimmed.isEmpty { 81 + return "Command" 82 + } 83 + return trimmed 84 + } 85 + 86 + var resolvedSystemImage: String { 87 + let trimmed = systemImage.trimmingCharacters(in: .whitespacesAndNewlines) 88 + if trimmed.isEmpty { 89 + return "terminal" 90 + } 91 + return trimmed 92 + } 93 + 94 + var hasRunnableCommand: Bool { 95 + !command.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty 96 + } 97 + } 98 + 99 + nonisolated enum OnevcatCustomCommandExecution: String, Codable, CaseIterable, Identifiable, Sendable { 100 + case shellScript 101 + case terminalInput 102 + 103 + var id: String { rawValue } 104 + 105 + var title: String { 106 + switch self { 107 + case .shellScript: 108 + return "Shell Script" 109 + case .terminalInput: 110 + return "Terminal Input" 111 + } 112 + } 113 + } 114 + 115 + nonisolated struct OnevcatCustomShortcut: Codable, Equatable, Sendable { 116 + var key: String 117 + var modifiers: OnevcatCustomShortcutModifiers 118 + 119 + init(key: String, modifiers: OnevcatCustomShortcutModifiers) { 120 + self.key = key 121 + self.modifiers = modifiers 122 + } 123 + 124 + func normalized() -> OnevcatCustomShortcut { 125 + let scalar = key.trimmingCharacters(in: .whitespacesAndNewlines).first 126 + return OnevcatCustomShortcut( 127 + key: scalar.map { String($0).lowercased() } ?? "", 128 + modifiers: modifiers 129 + ) 130 + } 131 + 132 + var isValid: Bool { 133 + let normalizedKey = key.trimmingCharacters(in: .whitespacesAndNewlines) 134 + return normalizedKey.count == 1 135 + } 136 + 137 + var display: String { 138 + var parts: [String] = [] 139 + if modifiers.command { parts.append("⌘") } 140 + if modifiers.shift { parts.append("⇧") } 141 + if modifiers.option { parts.append("⌥") } 142 + if modifiers.control { parts.append("⌃") } 143 + parts.append(key.uppercased()) 144 + return parts.joined() 145 + } 146 + } 147 + 148 + nonisolated struct OnevcatCustomShortcutModifiers: Codable, Equatable, Sendable { 149 + var command: Bool 150 + var shift: Bool 151 + var option: Bool 152 + var control: Bool 153 + 154 + init(command: Bool = true, shift: Bool = false, option: Bool = false, control: Bool = false) { 155 + self.command = command 156 + self.shift = shift 157 + self.option = option 158 + self.control = control 159 + } 160 + 161 + var isEmpty: Bool { 162 + !command && !shift && !option && !control 163 + } 164 + }
+145
supacode/Features/Settings/Views/RepositorySettingsView.swift
··· 10 10 let baseRefOptions = 11 11 store.branchOptions.isEmpty ? [store.defaultWorktreeBaseRef] : store.branchOptions 12 12 let settings = $store.settings 13 + let onevcatSettings = $store.onevcatSettings 13 14 Form { 14 15 Section { 15 16 if store.isBranchDataLoaded { ··· 155 156 .foregroundStyle(.secondary) 156 157 } 157 158 } 159 + Section { 160 + ForEach(onevcatSettings.customCommands) { $command in 161 + OnevcatCustomCommandCard( 162 + command: $command, 163 + onRemove: { 164 + removeCustomCommand(id: command.id) 165 + } 166 + ) 167 + } 168 + if store.onevcatSettings.customCommands.count < OnevcatRepositorySettings.maxCustomCommands { 169 + Button { 170 + addCustomCommand() 171 + } label: { 172 + Label("Add Command", systemImage: "plus") 173 + } 174 + .help("Add a custom command") 175 + } 176 + } header: { 177 + VStack(alignment: .leading, spacing: 4) { 178 + Text("Custom Commands") 179 + Text("Onevcat-only commands shown after Run in the toolbar (up to 3)") 180 + .foregroundStyle(.secondary) 181 + } 182 + } 158 183 } 159 184 .formStyle(.grouped) 160 185 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 161 186 .task { 162 187 store.send(.task) 163 188 } 189 + } 190 + 191 + private func addCustomCommand() { 192 + let current = store.onevcatSettings.customCommands 193 + let next = current + [.default(index: current.count)] 194 + store.onevcatSettings.customCommands = OnevcatRepositorySettings.normalizedCommands(next) 195 + } 196 + 197 + private func removeCustomCommand(id: OnevcatCustomCommand.ID) { 198 + store.onevcatSettings.customCommands.removeAll { $0.id == id } 164 199 } 165 200 } 166 201 ··· 226 261 .onAppear { isSearchFocused = true } 227 262 } 228 263 } 264 + 265 + private struct OnevcatCustomCommandCard: View { 266 + @Binding var command: OnevcatCustomCommand 267 + let onRemove: () -> Void 268 + 269 + var body: some View { 270 + VStack(alignment: .leading, spacing: 10) { 271 + HStack(alignment: .firstTextBaseline, spacing: 12) { 272 + TextField("Name", text: $command.title) 273 + .textFieldStyle(.roundedBorder) 274 + TextField("SF Symbol", text: $command.systemImage) 275 + .textFieldStyle(.roundedBorder) 276 + Picker("Type", selection: $command.execution) { 277 + ForEach(OnevcatCustomCommandExecution.allCases) { execution in 278 + Text(execution.title) 279 + .tag(execution) 280 + } 281 + } 282 + .labelsHidden() 283 + .frame(maxWidth: 180) 284 + Button(role: .destructive) { 285 + onRemove() 286 + } label: { 287 + Image(systemName: "trash") 288 + .accessibilityLabel("Remove command") 289 + } 290 + .help("Remove this custom command") 291 + } 292 + .font(.caption) 293 + 294 + shortcutEditor 295 + 296 + ZStack(alignment: .topLeading) { 297 + PlainTextEditor( 298 + text: $command.command, 299 + isMonospaced: true 300 + ) 301 + .frame(minHeight: 100) 302 + if command.command.isEmpty { 303 + Text(placeholder) 304 + .foregroundStyle(.secondary) 305 + .padding(.leading, 6) 306 + .font(.body.monospaced()) 307 + .allowsHitTesting(false) 308 + } 309 + } 310 + } 311 + .padding(.vertical, 4) 312 + } 313 + 314 + @ViewBuilder 315 + private var shortcutEditor: some View { 316 + VStack(alignment: .leading, spacing: 6) { 317 + Toggle("Enable Shortcut", isOn: shortcutEnabled) 318 + if let shortcut = Binding($command.shortcut) { 319 + HStack(spacing: 12) { 320 + TextField("Key", text: shortcutKeyBinding(shortcut)) 321 + .textFieldStyle(.roundedBorder) 322 + .frame(width: 70) 323 + Toggle("⌘", isOn: shortcut.modifiers.command) 324 + Toggle("⇧", isOn: shortcut.modifiers.shift) 325 + Toggle("⌥", isOn: shortcut.modifiers.option) 326 + Toggle("⌃", isOn: shortcut.modifiers.control) 327 + Spacer(minLength: 0) 328 + Text(shortcut.wrappedValue.display) 329 + .font(.caption.monospaced()) 330 + .foregroundStyle(.secondary) 331 + } 332 + .font(.caption) 333 + } 334 + } 335 + } 336 + 337 + private var shortcutEnabled: Binding<Bool> { 338 + Binding( 339 + get: { command.shortcut != nil }, 340 + set: { enabled in 341 + if enabled { 342 + command.shortcut = 343 + command.shortcut 344 + ?? OnevcatCustomShortcut( 345 + key: "", 346 + modifiers: OnevcatCustomShortcutModifiers() 347 + ) 348 + } else { 349 + command.shortcut = nil 350 + } 351 + } 352 + ) 353 + } 354 + 355 + private func shortcutKeyBinding(_ shortcut: Binding<OnevcatCustomShortcut>) -> Binding<String> { 356 + Binding( 357 + get: { shortcut.wrappedValue.key }, 358 + set: { value in 359 + let scalar = value.trimmingCharacters(in: .whitespacesAndNewlines).first 360 + shortcut.wrappedValue.key = scalar.map { String($0).lowercased() } ?? "" 361 + } 362 + ) 363 + } 364 + 365 + private var placeholder: String { 366 + switch command.execution { 367 + case .shellScript: 368 + return "npm test && swift test" 369 + case .terminalInput: 370 + return "pnpm test --watch" 371 + } 372 + } 373 + }
+1 -1
supacode/Features/Settings/Views/UpdatesSettingsView.swift
··· 32 32 Button("Check for Updates Now") { 33 33 updatesStore.send(.checkForUpdates) 34 34 } 35 - .help("Check for Updates (\(AppShortcuts.checkForUpdates.display))") 35 + .help("Check for updates now") 36 36 Spacer() 37 37 } 38 38 .padding(.top)
+10
supacode/Features/Terminal/BusinessLogic/WorktreeTerminalManager.swift
··· 41 41 state.ensureInitialTab(focusing: focusing) 42 42 case .runScript(let worktree, let script): 43 43 _ = state(for: worktree).runScript(script) 44 + case .insertText(let worktree, let text): 45 + if !state(for: worktree).focusAndInsertText(text) { 46 + Task { 47 + createTabAsync( 48 + in: worktree, 49 + runSetupScriptIfNew: false, 50 + initialInput: text 51 + ) 52 + } 53 + } 44 54 case .stopRunScript(let worktree): 45 55 _ = state(for: worktree).stopRunScript() 46 56 case .closeFocusedTab(let worktree):
+4 -2
supacode/Features/Terminal/Models/WorktreeTerminalState.swift
··· 198 198 focusSurface(in: tabId) 199 199 } 200 200 201 - func focusAndInsertText(_ text: String) { 201 + @discardableResult 202 + func focusAndInsertText(_ text: String) -> Bool { 202 203 guard let tabId = tabManager.selectedTabId, 203 204 let focusedId = focusedSurfaceIdByTab[tabId], 204 205 let surface = surfaces[focusedId] 205 - else { return } 206 + else { return false } 206 207 surface.requestFocus() 207 208 surface.insertText(text, replacementRange: NSRange(location: 0, length: 0)) 209 + return true 208 210 } 209 211 210 212 func syncFocus(windowIsKey: Bool, windowIsVisible: Bool) {
+7
supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift
··· 799 799 guard let surface else { return false } 800 800 guard focused else { return false } 801 801 802 + if OnevcatCustomShortcutRegistry.shared.matches(event: event), 803 + let menu = NSApp.mainMenu, 804 + menu.performKeyEquivalent(with: event) 805 + { 806 + return true 807 + } 808 + 802 809 if let bindingFlags = bindingFlags(for: event, surface: surface) { 803 810 if shouldAttemptMenu(for: bindingFlags), 804 811 let menu = NSApp.mainMenu,
+21
supacode/Infrastructure/Ghostty/OnevcatCustomShortcutRegistry.swift
··· 1 + import AppKit 2 + 3 + @MainActor 4 + final class OnevcatCustomShortcutRegistry { 5 + static let shared = OnevcatCustomShortcutRegistry() 6 + 7 + private var shortcuts: [OnevcatCustomShortcut] = [] 8 + 9 + private init() {} 10 + 11 + func setShortcuts(_ shortcuts: [OnevcatCustomShortcut]) { 12 + self.shortcuts = shortcuts.compactMap { shortcut in 13 + let normalized = shortcut.normalized() 14 + return normalized.isValid ? normalized : nil 15 + } 16 + } 17 + 18 + func matches(event: NSEvent) -> Bool { 19 + shortcuts.contains { $0.matches(event: event) } 20 + } 21 + }
+4
supacode/Support/SupacodePaths.swift
··· 23 23 rootURL.standardizedFileURL.appending(path: "supacode.json", directoryHint: .notDirectory) 24 24 } 25 25 26 + static func onevcatRepositorySettingsURL(for rootURL: URL) -> URL { 27 + rootURL.standardizedFileURL.appending(path: "supacode.onevcat.json", directoryHint: .notDirectory) 28 + } 29 + 26 30 private static func repositoryDirectoryName(for rootURL: URL) -> String { 27 31 let repoName = rootURL.lastPathComponent 28 32 if repoName.isEmpty || repoName == ".bare" || repoName == ".git" {
+124
supacodeTests/AppFeatureCustomCommandTests.swift
··· 1 + import ComposableArchitecture 2 + import DependenciesTestSupport 3 + import Foundation 4 + import Testing 5 + 6 + @testable import supacode 7 + 8 + @MainActor 9 + struct AppFeatureCustomCommandTests { 10 + @Test(.dependencies) func shellScriptCommandCreatesTabWithInput() async { 11 + let worktree = makeWorktree() 12 + let sent = LockIsolated<[TerminalClient.Command]>([]) 13 + var state = AppFeature.State( 14 + repositories: makeRepositoriesState(worktree: worktree), 15 + settings: SettingsFeature.State() 16 + ) 17 + state.selectedCustomCommands = [ 18 + OnevcatCustomCommand( 19 + title: "Test", 20 + systemImage: "checkmark.circle", 21 + command: "swift test", 22 + execution: .shellScript, 23 + shortcut: nil 24 + ), 25 + ] 26 + 27 + let store = TestStore(initialState: state) { 28 + AppFeature() 29 + } withDependencies: { 30 + $0.terminalClient.send = { command in 31 + sent.withValue { $0.append(command) } 32 + } 33 + } 34 + 35 + await store.send(.runCustomCommand(0)) 36 + await store.finish() 37 + 38 + #expect( 39 + sent.value == [ 40 + .createTabWithInput(worktree, input: "swift test", runSetupScriptIfNew: false) 41 + ] 42 + ) 43 + } 44 + 45 + @Test(.dependencies) func terminalInputCommandInsertsTextWithNewline() async { 46 + let worktree = makeWorktree() 47 + let sent = LockIsolated<[TerminalClient.Command]>([]) 48 + var state = AppFeature.State( 49 + repositories: makeRepositoriesState(worktree: worktree), 50 + settings: SettingsFeature.State() 51 + ) 52 + state.selectedCustomCommands = [ 53 + OnevcatCustomCommand( 54 + title: "Watch", 55 + systemImage: "terminal", 56 + command: "pnpm test --watch", 57 + execution: .terminalInput, 58 + shortcut: nil 59 + ), 60 + ] 61 + 62 + let store = TestStore(initialState: state) { 63 + AppFeature() 64 + } withDependencies: { 65 + $0.terminalClient.send = { command in 66 + sent.withValue { $0.append(command) } 67 + } 68 + } 69 + 70 + await store.send(.runCustomCommand(0)) 71 + await store.finish() 72 + 73 + #expect( 74 + sent.value == [ 75 + .insertText(worktree, text: "pnpm test --watch\n") 76 + ] 77 + ) 78 + } 79 + 80 + @Test(.dependencies) func invalidCommandIndexDoesNothing() async { 81 + let worktree = makeWorktree() 82 + let sent = LockIsolated<[TerminalClient.Command]>([]) 83 + let state = AppFeature.State( 84 + repositories: makeRepositoriesState(worktree: worktree), 85 + settings: SettingsFeature.State() 86 + ) 87 + 88 + let store = TestStore(initialState: state) { 89 + AppFeature() 90 + } withDependencies: { 91 + $0.terminalClient.send = { command in 92 + sent.withValue { $0.append(command) } 93 + } 94 + } 95 + 96 + await store.send(.runCustomCommand(0)) 97 + await store.finish() 98 + 99 + #expect(sent.value.isEmpty) 100 + } 101 + 102 + private func makeWorktree() -> Worktree { 103 + Worktree( 104 + id: "/tmp/repo/wt-1", 105 + name: "wt-1", 106 + detail: "detail", 107 + workingDirectory: URL(fileURLWithPath: "/tmp/repo/wt-1"), 108 + repositoryRootURL: URL(fileURLWithPath: "/tmp/repo") 109 + ) 110 + } 111 + 112 + private func makeRepositoriesState(worktree: Worktree) -> RepositoriesFeature.State { 113 + let repository = Repository( 114 + id: "/tmp/repo", 115 + rootURL: URL(fileURLWithPath: "/tmp/repo"), 116 + name: "repo", 117 + worktrees: [worktree] 118 + ) 119 + var repositoriesState = RepositoriesFeature.State() 120 + repositoriesState.repositories = [repository] 121 + repositoriesState.selection = .worktree(worktree.id) 122 + return repositoriesState 123 + } 124 + }
+3
supacodeTests/AppFeatureDefaultEditorTests.swift
··· 34 34 35 35 await store.send(.repositories(.delegate(.selectedWorktreeChanged(worktree)))) 36 36 await store.receive(\.worktreeSettingsLoaded) 37 + await store.receive(\.worktreeOnevcatSettingsLoaded) 37 38 #expect(store.state.openActionSelection == .finder) 38 39 #expect(store.state.selectedRunScript == "") 39 40 await store.finish() ··· 90 91 $0.openActionSelection = .terminal 91 92 $0.selectedRunScript = "pnpm dev" 92 93 } 94 + await store.receive(\.worktreeOnevcatSettingsLoaded) 93 95 await store.finish() 94 96 } 95 97 ··· 123 125 await store.receive(\.worktreeSettingsLoaded) { 124 126 $0.openActionSelection = expectedOpenActionSelection 125 127 } 128 + await store.receive(\.worktreeOnevcatSettingsLoaded) 126 129 await store.finish() 127 130 128 131 #expect(watcherCommands.value == [.setSelectedWorktreeID(worktree.id)])
+4 -2
supacodeTests/AppFeatureSettingsSelectionTests.swift
··· 26 26 $0.settings.selection = .repository(repository.id) 27 27 $0.settings.repositorySettings = RepositorySettingsFeature.State( 28 28 rootURL: repository.rootURL, 29 - settings: .default 29 + settings: .default, 30 + onevcatSettings: .default 30 31 ) 31 32 } 32 33 } ··· 61 62 state.settings.selection = .repository(repository.id) 62 63 state.settings.repositorySettings = RepositorySettingsFeature.State( 63 64 rootURL: repository.rootURL, 64 - settings: .default 65 + settings: .default, 66 + onevcatSettings: .default 65 67 ) 66 68 let store = TestStore(initialState: state) { 67 69 AppFeature()
+62
supacodeTests/OnevcatRepositorySettingsKeyTests.swift
··· 1 + import Dependencies 2 + import DependenciesTestSupport 3 + import Foundation 4 + import Sharing 5 + import Testing 6 + 7 + @testable import supacode 8 + 9 + struct OnevcatRepositorySettingsKeyTests { 10 + @Test(.dependencies) func loadMissingFileReturnsDefaultAndCreatesLocalFile() throws { 11 + let localStorage = RepositoryLocalSettingsTestStorage() 12 + let rootURL = URL(fileURLWithPath: "/tmp/repo") 13 + let localURL = SupacodePaths.onevcatRepositorySettingsURL(for: rootURL) 14 + 15 + let loaded = withDependencies { 16 + $0.repositoryLocalSettingsStorage = localStorage.storage 17 + } operation: { 18 + @Shared(.onevcatRepositorySettings(rootURL)) var settings: OnevcatRepositorySettings 19 + return settings 20 + } 21 + 22 + #expect(loaded == .default) 23 + 24 + let localData = try #require(localStorage.data(at: localURL)) 25 + let decoded = try JSONDecoder().decode(OnevcatRepositorySettings.self, from: localData) 26 + #expect(decoded == .default) 27 + } 28 + 29 + @Test(.dependencies) func savePersistsCustomCommandsToOnevcatFile() throws { 30 + let localStorage = RepositoryLocalSettingsTestStorage() 31 + let rootURL = URL(fileURLWithPath: "/tmp/repo") 32 + let localURL = SupacodePaths.onevcatRepositorySettingsURL(for: rootURL) 33 + 34 + let customSettings = OnevcatRepositorySettings( 35 + customCommands: [ 36 + OnevcatCustomCommand( 37 + title: "Test", 38 + systemImage: "checkmark.circle", 39 + command: "swift test", 40 + execution: .shellScript, 41 + shortcut: OnevcatCustomShortcut( 42 + key: "u", 43 + modifiers: OnevcatCustomShortcutModifiers(command: true) 44 + ) 45 + ), 46 + ] 47 + ) 48 + 49 + withDependencies { 50 + $0.repositoryLocalSettingsStorage = localStorage.storage 51 + } operation: { 52 + @Shared(.onevcatRepositorySettings(rootURL)) var settings: OnevcatRepositorySettings 53 + $settings.withLock { 54 + $0 = customSettings 55 + } 56 + } 57 + 58 + let localData = try #require(localStorage.data(at: localURL)) 59 + let decoded = try JSONDecoder().decode(OnevcatRepositorySettings.self, from: localData) 60 + #expect(decoded == customSettings) 61 + } 62 + }
+4 -2
supacodeTests/SettingsFeatureTests.swift
··· 148 148 state.selection = selection 149 149 state.repositorySettings = RepositorySettingsFeature.State( 150 150 rootURL: rootURL, 151 - settings: .default 151 + settings: .default, 152 + onevcatSettings: .default 152 153 ) 153 154 let store = TestStore(initialState: state) { 154 155 SettingsFeature() ··· 193 194 $0.selection = selection 194 195 $0.repositorySettings = RepositorySettingsFeature.State( 195 196 rootURL: rootURL, 196 - settings: .default 197 + settings: .default, 198 + onevcatSettings: .default 197 199 ) 198 200 } 199 201 await store.receive(\.delegate.settingsChanged)