Fix add button. Fix hotkeys in settings. Add Workspace hotkeys
This commit is contained in:
Binary file not shown.
@@ -76,9 +76,7 @@ struct HotkeyRecorderField: NSViewRepresentable {
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: HotkeyNSView, context: Context) {
|
||||
nsView.currentLabel = binding.displayString
|
||||
nsView.showRecording = isRecording
|
||||
nsView.needsDisplay = true
|
||||
nsView.update(currentLabel: binding.displayString, isRecording: isRecording)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,9 +97,7 @@ struct OptionalHotkeyRecorderField: NSViewRepresentable {
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: HotkeyNSView, context: Context) {
|
||||
nsView.currentLabel = binding?.displayString ?? "Not set"
|
||||
nsView.showRecording = isRecording
|
||||
nsView.needsDisplay = true
|
||||
nsView.update(currentLabel: binding?.displayString ?? "Not set", isRecording: isRecording)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +179,12 @@ class HotkeyNSView: NSView {
|
||||
updateLabelAppearance()
|
||||
}
|
||||
|
||||
func update(currentLabel: String, isRecording: Bool) {
|
||||
self.currentLabel = currentLabel
|
||||
showRecording = isRecording
|
||||
updateLabelAppearance()
|
||||
}
|
||||
|
||||
private func updateLabelAppearance() {
|
||||
label.stringValue = showRecording ? "Press keys..." : currentLabel
|
||||
label.textColor = showRecording ? .controlAccentColor : .labelColor
|
||||
|
||||
@@ -9,17 +9,6 @@ struct TabBar: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 2) {
|
||||
ForEach(Array(workspace.tabs.enumerated()), id: \.element.id) { index, tab in
|
||||
tabButton(for: tab, at: index)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
workspace.newTab()
|
||||
} label: {
|
||||
@@ -31,6 +20,15 @@ struct TabBar: View {
|
||||
.accessibilityIdentifier("notch.new-tab")
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 2) {
|
||||
ForEach(Array(workspace.tabs.enumerated()), id: \.element.id) { index, tab in
|
||||
tabButton(for: tab, at: index)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
.frame(height: 28)
|
||||
.background(.black)
|
||||
|
||||
@@ -5,7 +5,7 @@ import Combine
|
||||
/// Manages global and local hotkeys.
|
||||
///
|
||||
/// The toggle hotkey uses Carbon's `RegisterEventHotKey` which works
|
||||
/// system-wide without Accessibility permission. Tab-level hotkeys
|
||||
/// system-wide without Accessibility permission. Notch-scoped hotkeys
|
||||
/// use a local `NSEvent` monitor (only fires when our app is active).
|
||||
@MainActor
|
||||
class HotkeyManager {
|
||||
@@ -19,21 +19,29 @@ class HotkeyManager {
|
||||
var onCloseTab: (() -> Void)?
|
||||
var onNextTab: (() -> Void)?
|
||||
var onPreviousTab: (() -> Void)?
|
||||
var onNextWorkspace: (() -> Void)?
|
||||
var onPreviousWorkspace: (() -> Void)?
|
||||
var onDetachTab: (() -> Void)?
|
||||
var onApplySizePreset: ((TerminalSizePreset) -> Void)?
|
||||
var onSwitchToTab: ((Int) -> Void)?
|
||||
var onSwitchToWorkspace: ((WorkspaceID) -> Void)?
|
||||
|
||||
/// Tab-level hotkeys only fire when the notch is open.
|
||||
/// Notch-scoped hotkeys only fire when the notch is open.
|
||||
var isNotchOpen: Bool = false
|
||||
|
||||
private var hotKeyRef: EventHotKeyRef?
|
||||
private var eventHandlerRef: EventHandlerRef?
|
||||
private var localMonitor: Any?
|
||||
private let settingsProvider: TerminalSessionConfigurationProviding
|
||||
private let workspaceRegistry: WorkspaceRegistry
|
||||
private var settingsCancellable: AnyCancellable?
|
||||
|
||||
init(settingsProvider: TerminalSessionConfigurationProviding? = nil) {
|
||||
init(
|
||||
settingsProvider: TerminalSessionConfigurationProviding? = nil,
|
||||
workspaceRegistry: WorkspaceRegistry? = nil
|
||||
) {
|
||||
self.settingsProvider = settingsProvider ?? AppSettingsController.shared
|
||||
self.workspaceRegistry = workspaceRegistry ?? WorkspaceRegistry.shared
|
||||
}
|
||||
|
||||
// MARK: - Resolved bindings from typed runtime settings
|
||||
@@ -53,6 +61,12 @@ class HotkeyManager {
|
||||
private var prevTabBinding: HotkeyBinding {
|
||||
settingsProvider.hotkeySettings.previousTab
|
||||
}
|
||||
private var nextWorkspaceBinding: HotkeyBinding {
|
||||
settingsProvider.hotkeySettings.nextWorkspace
|
||||
}
|
||||
private var previousWorkspaceBinding: HotkeyBinding {
|
||||
settingsProvider.hotkeySettings.previousWorkspace
|
||||
}
|
||||
private var detachBinding: HotkeyBinding {
|
||||
settingsProvider.hotkeySettings.detachTab
|
||||
}
|
||||
@@ -173,7 +187,7 @@ class HotkeyManager {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Local monitor (tab-level hotkeys, only when our app is active)
|
||||
// MARK: - Local monitor (notch-level hotkeys, only when our app is active)
|
||||
|
||||
private func installLocalMonitor() {
|
||||
localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
|
||||
@@ -189,9 +203,9 @@ class HotkeyManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles tab-level hotkeys. Returns true if the event was consumed.
|
||||
/// Handles notch-scoped hotkeys. Returns true if the event was consumed.
|
||||
private func handleLocalKeyEvent(_ event: NSEvent) -> Bool {
|
||||
// Tab hotkeys only when the notch is open and focused
|
||||
// Local shortcuts only fire when the notch is open and focused.
|
||||
guard isNotchOpen else { return false }
|
||||
|
||||
if newTabBinding.matches(event) {
|
||||
@@ -210,10 +224,25 @@ class HotkeyManager {
|
||||
onPreviousTab?()
|
||||
return true
|
||||
}
|
||||
if nextWorkspaceBinding.matches(event) {
|
||||
onNextWorkspace?()
|
||||
return true
|
||||
}
|
||||
if previousWorkspaceBinding.matches(event) {
|
||||
onPreviousWorkspace?()
|
||||
return true
|
||||
}
|
||||
if detachBinding.matches(event) {
|
||||
onDetachTab?()
|
||||
return true
|
||||
}
|
||||
for summary in workspaceRegistry.workspaceSummaries {
|
||||
guard let binding = summary.hotkey else { continue }
|
||||
if binding.matches(event) {
|
||||
onSwitchToWorkspace?(summary.id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
for preset in sizePresets {
|
||||
guard let binding = preset.hotkey else { continue }
|
||||
if binding.matches(event) {
|
||||
|
||||
@@ -9,6 +9,7 @@ final class ScreenManager: ObservableObject {
|
||||
static let shared = ScreenManager()
|
||||
|
||||
private let screenRegistry = ScreenRegistry.shared
|
||||
private let workspaceRegistry = WorkspaceRegistry.shared
|
||||
private let windowCoordinator = WindowCoordinator()
|
||||
private lazy var orchestrator = NotchOrchestrator(screenRegistry: screenRegistry, host: self)
|
||||
|
||||
@@ -55,6 +56,12 @@ final class ScreenManager: ObservableObject {
|
||||
hotkeyManager.onPreviousTab = { [weak self] in
|
||||
MainActor.assumeIsolated { self?.activeWorkspace().previousTab() }
|
||||
}
|
||||
hotkeyManager.onNextWorkspace = { [weak self] in
|
||||
MainActor.assumeIsolated { self?.switchWorkspace(offset: 1) }
|
||||
}
|
||||
hotkeyManager.onPreviousWorkspace = { [weak self] in
|
||||
MainActor.assumeIsolated { self?.switchWorkspace(offset: -1) }
|
||||
}
|
||||
hotkeyManager.onDetachTab = { [weak self] in
|
||||
MainActor.assumeIsolated { self?.detachActiveTab() }
|
||||
}
|
||||
@@ -64,6 +71,9 @@ final class ScreenManager: ObservableObject {
|
||||
hotkeyManager.onSwitchToTab = { [weak self] index in
|
||||
MainActor.assumeIsolated { self?.activeWorkspace().switchToTab(at: index) }
|
||||
}
|
||||
hotkeyManager.onSwitchToWorkspace = { [weak self] workspaceID in
|
||||
MainActor.assumeIsolated { self?.switchActiveScreen(to: workspaceID) }
|
||||
}
|
||||
|
||||
hotkeyManager.start()
|
||||
}
|
||||
@@ -92,6 +102,33 @@ final class ScreenManager: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func switchWorkspace(offset: Int) {
|
||||
guard let screenID = screenRegistry.activeScreenID() else { return }
|
||||
let currentWorkspaceID = screenRegistry.screenContext(for: screenID)?.workspaceID ?? workspaceRegistry.defaultWorkspaceID
|
||||
let nextWorkspaceID = offset >= 0
|
||||
? workspaceRegistry.nextWorkspaceID(after: currentWorkspaceID)
|
||||
: workspaceRegistry.previousWorkspaceID(before: currentWorkspaceID)
|
||||
|
||||
guard let nextWorkspaceID else { return }
|
||||
switchScreen(screenID, to: nextWorkspaceID)
|
||||
}
|
||||
|
||||
private func switchActiveScreen(to workspaceID: WorkspaceID) {
|
||||
guard let screenID = screenRegistry.activeScreenID() else { return }
|
||||
switchScreen(screenID, to: workspaceID)
|
||||
}
|
||||
|
||||
private func switchScreen(_ screenID: ScreenID, to workspaceID: WorkspaceID) {
|
||||
screenRegistry.assignWorkspace(workspaceID, to: screenID)
|
||||
|
||||
guard let context = screenRegistry.screenContext(for: screenID),
|
||||
context.notchState == .open else {
|
||||
return
|
||||
}
|
||||
|
||||
orchestrator.open(screenID: screenID)
|
||||
}
|
||||
|
||||
func applySizePreset(_ preset: TerminalSizePreset) {
|
||||
guard let context = screenRegistry.allScreens().first(where: { $0.notchState == .open }) else {
|
||||
AppSettingsController.shared.update {
|
||||
|
||||
@@ -56,6 +56,8 @@ struct AppSettings: Equatable, Codable {
|
||||
closeTab: .cmdW,
|
||||
nextTab: .cmdShiftRB,
|
||||
previousTab: .cmdShiftLB,
|
||||
nextWorkspace: .cmdShiftDown,
|
||||
previousWorkspace: .cmdShiftUp,
|
||||
detachTab: .cmdD
|
||||
)
|
||||
)
|
||||
@@ -121,6 +123,8 @@ extension AppSettings {
|
||||
var closeTab: HotkeyBinding
|
||||
var nextTab: HotkeyBinding
|
||||
var previousTab: HotkeyBinding
|
||||
var nextWorkspace: HotkeyBinding
|
||||
var previousWorkspace: HotkeyBinding
|
||||
var detachTab: HotkeyBinding
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,8 @@ struct UserDefaultsAppSettingsStore: AppSettingsStoreType {
|
||||
closeTab: hotkey(NotchSettings.Keys.hotkeyCloseTab, default: .cmdW),
|
||||
nextTab: hotkey(NotchSettings.Keys.hotkeyNextTab, default: .cmdShiftRB),
|
||||
previousTab: hotkey(NotchSettings.Keys.hotkeyPreviousTab, default: .cmdShiftLB),
|
||||
nextWorkspace: hotkey(NotchSettings.Keys.hotkeyNextWorkspace, default: .cmdShiftDown),
|
||||
previousWorkspace: hotkey(NotchSettings.Keys.hotkeyPreviousWorkspace, default: .cmdShiftUp),
|
||||
detachTab: hotkey(NotchSettings.Keys.hotkeyDetachTab, default: .cmdD)
|
||||
)
|
||||
)
|
||||
@@ -106,6 +108,8 @@ struct UserDefaultsAppSettingsStore: AppSettingsStoreType {
|
||||
defaults.set(settings.hotkeys.closeTab.toJSON(), forKey: NotchSettings.Keys.hotkeyCloseTab)
|
||||
defaults.set(settings.hotkeys.nextTab.toJSON(), forKey: NotchSettings.Keys.hotkeyNextTab)
|
||||
defaults.set(settings.hotkeys.previousTab.toJSON(), forKey: NotchSettings.Keys.hotkeyPreviousTab)
|
||||
defaults.set(settings.hotkeys.nextWorkspace.toJSON(), forKey: NotchSettings.Keys.hotkeyNextWorkspace)
|
||||
defaults.set(settings.hotkeys.previousWorkspace.toJSON(), forKey: NotchSettings.Keys.hotkeyPreviousWorkspace)
|
||||
defaults.set(settings.hotkeys.detachTab.toJSON(), forKey: NotchSettings.Keys.hotkeyDetachTab)
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,8 @@ struct HotkeyBinding: Codable, Equatable, Hashable {
|
||||
static let cmdW = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 13)
|
||||
static let cmdShiftRB = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 30) // ]
|
||||
static let cmdShiftLB = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 33) // [
|
||||
static let cmdShiftDown = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 125)
|
||||
static let cmdShiftUp = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 126)
|
||||
static let cmdD = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 2)
|
||||
|
||||
static func cmdShiftDigit(_ digit: Int) -> HotkeyBinding? {
|
||||
|
||||
@@ -57,6 +57,8 @@ enum NotchSettings {
|
||||
static let hotkeyCloseTab = "hotkey_closeTab"
|
||||
static let hotkeyNextTab = "hotkey_nextTab"
|
||||
static let hotkeyPreviousTab = "hotkey_previousTab"
|
||||
static let hotkeyNextWorkspace = "hotkey_nextWorkspace"
|
||||
static let hotkeyPreviousWorkspace = "hotkey_previousWorkspace"
|
||||
static let hotkeyDetachTab = "hotkey_detachTab"
|
||||
}
|
||||
|
||||
@@ -104,6 +106,8 @@ enum NotchSettings {
|
||||
static let hotkeyCloseTab: String = HotkeyBinding.cmdW.toJSON()
|
||||
static let hotkeyNextTab: String = HotkeyBinding.cmdShiftRB.toJSON()
|
||||
static let hotkeyPreviousTab: String = HotkeyBinding.cmdShiftLB.toJSON()
|
||||
static let hotkeyNextWorkspace: String = HotkeyBinding.cmdShiftDown.toJSON()
|
||||
static let hotkeyPreviousWorkspace: String = HotkeyBinding.cmdShiftUp.toJSON()
|
||||
static let hotkeyDetachTab: String = HotkeyBinding.cmdD.toJSON()
|
||||
}
|
||||
|
||||
@@ -151,6 +155,8 @@ enum NotchSettings {
|
||||
Keys.hotkeyCloseTab: Defaults.hotkeyCloseTab,
|
||||
Keys.hotkeyNextTab: Defaults.hotkeyNextTab,
|
||||
Keys.hotkeyPreviousTab: Defaults.hotkeyPreviousTab,
|
||||
Keys.hotkeyNextWorkspace: Defaults.hotkeyNextWorkspace,
|
||||
Keys.hotkeyPreviousWorkspace: Defaults.hotkeyPreviousWorkspace,
|
||||
Keys.hotkeyDetachTab: Defaults.hotkeyDetachTab,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ final class WorkspaceController: ObservableObject {
|
||||
let createdAt: Date
|
||||
|
||||
@Published private(set) var name: String
|
||||
@Published private(set) var hotkey: HotkeyBinding?
|
||||
@Published private(set) var tabs: [TerminalSession] = []
|
||||
@Published private(set) var activeTabIndex: Int = 0
|
||||
|
||||
@@ -34,6 +35,7 @@ final class WorkspaceController: ObservableObject {
|
||||
self.id = summary.id
|
||||
self.name = summary.name
|
||||
self.createdAt = summary.createdAt
|
||||
self.hotkey = summary.hotkey
|
||||
self.sessionFactory = sessionFactory
|
||||
self.settingsProvider = settingsProvider
|
||||
|
||||
@@ -51,7 +53,7 @@ final class WorkspaceController: ObservableObject {
|
||||
}
|
||||
|
||||
var summary: WorkspaceSummary {
|
||||
WorkspaceSummary(id: id, name: name, createdAt: createdAt)
|
||||
WorkspaceSummary(id: id, name: name, createdAt: createdAt, hotkey: hotkey)
|
||||
}
|
||||
|
||||
var state: WorkspaceState {
|
||||
@@ -78,6 +80,11 @@ final class WorkspaceController: ObservableObject {
|
||||
name = trimmed
|
||||
}
|
||||
|
||||
func updateHotkey(_ updatedHotkey: HotkeyBinding?) {
|
||||
guard hotkey != updatedHotkey else { return }
|
||||
hotkey = updatedHotkey
|
||||
}
|
||||
|
||||
func newTab() {
|
||||
let config = settingsProvider.terminalSessionConfiguration
|
||||
let session = sessionFactory.makeSession(
|
||||
|
||||
@@ -104,6 +104,37 @@ final class WorkspaceRegistry: ObservableObject {
|
||||
persistWorkspaceSummaries()
|
||||
}
|
||||
|
||||
func updateWorkspaceHotkey(id: WorkspaceID, to hotkey: HotkeyBinding?) {
|
||||
guard let index = workspaceSummaries.firstIndex(where: { $0.id == id }) else { return }
|
||||
guard workspaceSummaries[index].hotkey != hotkey else { return }
|
||||
|
||||
workspaceSummaries[index].hotkey = hotkey
|
||||
controllers[id]?.updateHotkey(hotkey)
|
||||
persistWorkspaceSummaries()
|
||||
}
|
||||
|
||||
func nextWorkspaceID(after id: WorkspaceID) -> WorkspaceID? {
|
||||
guard !workspaceSummaries.isEmpty else { return nil }
|
||||
guard let index = workspaceSummaries.firstIndex(where: { $0.id == id }) else {
|
||||
return workspaceSummaries.first?.id
|
||||
}
|
||||
|
||||
let nextIndex = workspaceSummaries.index(after: index)
|
||||
return workspaceSummaries[nextIndex == workspaceSummaries.endIndex ? workspaceSummaries.startIndex : nextIndex].id
|
||||
}
|
||||
|
||||
func previousWorkspaceID(before id: WorkspaceID) -> WorkspaceID? {
|
||||
guard !workspaceSummaries.isEmpty else { return nil }
|
||||
guard let index = workspaceSummaries.firstIndex(where: { $0.id == id }) else {
|
||||
return workspaceSummaries.last?.id
|
||||
}
|
||||
|
||||
let previousIndex = index == workspaceSummaries.startIndex
|
||||
? workspaceSummaries.index(before: workspaceSummaries.endIndex)
|
||||
: workspaceSummaries.index(before: index)
|
||||
return workspaceSummaries[previousIndex].id
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func deleteWorkspace(id: WorkspaceID) -> Bool {
|
||||
guard canDeleteWorkspace(id: id) else { return false }
|
||||
|
||||
@@ -6,11 +6,13 @@ struct WorkspaceSummary: Identifiable, Equatable, Codable {
|
||||
var id: WorkspaceID
|
||||
var name: String
|
||||
var createdAt: Date
|
||||
var hotkey: HotkeyBinding?
|
||||
|
||||
init(id: WorkspaceID = UUID(), name: String, createdAt: Date = Date()) {
|
||||
init(id: WorkspaceID = UUID(), name: String, createdAt: Date = Date(), hotkey: HotkeyBinding? = nil) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.createdAt = createdAt
|
||||
self.hotkey = hotkey
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,13 @@ struct HotkeySettingsView: View {
|
||||
HotkeyRecorderView(label: "Detach tab", binding: settingsController.binding(\.hotkeys.detachTab))
|
||||
}
|
||||
|
||||
Section("Workspaces (active when notch is open)") {
|
||||
HotkeyRecorderView(label: "Next workspace", binding: settingsController.binding(\.hotkeys.nextWorkspace))
|
||||
HotkeyRecorderView(label: "Previous workspace", binding: settingsController.binding(\.hotkeys.previousWorkspace))
|
||||
}
|
||||
|
||||
Section {
|
||||
Text("⌘1–9 always switch to tab by number. Size preset hotkeys are configured in Terminal > Size Presets.")
|
||||
Text("⌘1–9 always switch to tab by number. Size preset hotkeys are configured in Terminal > Size Presets. Per-workspace jump hotkeys are configured in Workspaces.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
@@ -2,21 +2,7 @@ import SwiftUI
|
||||
|
||||
struct TerminalSettingsView: View {
|
||||
@ObservedObject private var settingsController = AppSettingsController.shared
|
||||
|
||||
private var sizePresetsBinding: Binding<[TerminalSizePreset]> {
|
||||
Binding(
|
||||
get: {
|
||||
TerminalSizePresetStore.decodePresets(
|
||||
from: settingsController.settings.terminal.sizePresetsJSON
|
||||
) ?? TerminalSizePresetStore.loadDefaults()
|
||||
},
|
||||
set: { newValue in
|
||||
settingsController.update {
|
||||
$0.terminal.sizePresetsJSON = TerminalSizePresetStore.encodePresets(newValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@State private var sizePresets: [TerminalSizePreset] = []
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
@@ -55,7 +41,7 @@ struct TerminalSettingsView: View {
|
||||
}
|
||||
|
||||
Section("Size Presets") {
|
||||
ForEach(sizePresetsBinding) { $preset in
|
||||
ForEach($sizePresets) { $preset in
|
||||
TerminalSizePresetEditor(
|
||||
preset: $preset,
|
||||
currentOpenWidth: settingsController.settings.display.openWidth,
|
||||
@@ -67,20 +53,18 @@ struct TerminalSettingsView: View {
|
||||
|
||||
HStack {
|
||||
Button("Add Preset") {
|
||||
var presets = sizePresetsBinding.wrappedValue
|
||||
presets.append(
|
||||
sizePresets.append(
|
||||
TerminalSizePreset(
|
||||
name: "Preset \(presets.count + 1)",
|
||||
name: "Preset \(sizePresets.count + 1)",
|
||||
width: settingsController.settings.display.openWidth,
|
||||
height: settingsController.settings.display.openHeight,
|
||||
hotkey: TerminalSizePresetStore.suggestedHotkey(for: presets)
|
||||
hotkey: TerminalSizePresetStore.suggestedHotkey(for: sizePresets)
|
||||
)
|
||||
)
|
||||
sizePresetsBinding.wrappedValue = presets
|
||||
}
|
||||
|
||||
Button("Reset Presets") {
|
||||
sizePresetsBinding.wrappedValue = TerminalSizePresetStore.loadDefaults()
|
||||
sizePresets = TerminalSizePresetStore.loadDefaults()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,10 +74,24 @@ struct TerminalSettingsView: View {
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.onAppear {
|
||||
synchronizePresetsFromSettings()
|
||||
}
|
||||
.onChange(of: settingsController.settings.terminal.sizePresetsJSON) { _, _ in
|
||||
synchronizePresetsFromSettings()
|
||||
}
|
||||
.onChange(of: sizePresets) { _, newValue in
|
||||
let encoded = TerminalSizePresetStore.encodePresets(newValue)
|
||||
guard encoded != settingsController.settings.terminal.sizePresetsJSON else { return }
|
||||
|
||||
settingsController.update {
|
||||
$0.terminal.sizePresetsJSON = encoded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deletePreset(id: UUID) {
|
||||
sizePresetsBinding.wrappedValue.removeAll { $0.id == id }
|
||||
sizePresets.removeAll { $0.id == id }
|
||||
}
|
||||
|
||||
private func applyPreset(_ preset: TerminalSizePreset) {
|
||||
@@ -103,6 +101,15 @@ struct TerminalSettingsView: View {
|
||||
}
|
||||
ScreenManager.shared.applySizePreset(preset)
|
||||
}
|
||||
|
||||
private func synchronizePresetsFromSettings() {
|
||||
let decoded = TerminalSizePresetStore.decodePresets(
|
||||
from: settingsController.settings.terminal.sizePresetsJSON
|
||||
) ?? TerminalSizePresetStore.loadDefaults()
|
||||
|
||||
guard decoded != sizePresets else { return }
|
||||
sizePresets = decoded
|
||||
}
|
||||
}
|
||||
|
||||
private struct TerminalSizePresetEditor: View {
|
||||
|
||||
@@ -75,6 +75,11 @@ struct WorkspacesSettingsView: View {
|
||||
renameSelectedWorkspace()
|
||||
}
|
||||
|
||||
OptionalHotkeyRecorderView(
|
||||
label: "Jump Hotkey",
|
||||
binding: workspaceHotkeyBinding(for: summary.id)
|
||||
)
|
||||
|
||||
HStack {
|
||||
Button("Save Name") {
|
||||
renameSelectedWorkspace()
|
||||
@@ -86,6 +91,10 @@ struct WorkspacesSettingsView: View {
|
||||
}
|
||||
.accessibilityIdentifier("settings.workspaces.new")
|
||||
}
|
||||
|
||||
Text("Workspace jump hotkeys are active when the notch is open and switch the current screen to this workspace.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Section("Usage") {
|
||||
@@ -256,6 +265,17 @@ struct WorkspacesSettingsView: View {
|
||||
renameDraft = workspaceRegistry.summary(for: workspaceID)?.name ?? ""
|
||||
}
|
||||
|
||||
private func workspaceHotkeyBinding(for workspaceID: WorkspaceID) -> Binding<HotkeyBinding?> {
|
||||
Binding(
|
||||
get: {
|
||||
workspaceRegistry.summary(for: workspaceID)?.hotkey
|
||||
},
|
||||
set: { newValue in
|
||||
workspaceRegistry.updateWorkspaceHotkey(id: workspaceID, to: newValue)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func deleteSelectedWorkspace() {
|
||||
guard let effectiveSelectedWorkspaceID,
|
||||
let fallbackWorkspaceID = screenRegistry.deleteWorkspace(
|
||||
|
||||
@@ -74,6 +74,30 @@ final class WorkspaceRegistryTests: XCTestCase {
|
||||
XCTAssertEqual(store.savedSummaries.map(\.name), ["Main"])
|
||||
}
|
||||
|
||||
func testUpdateWorkspaceHotkeyPersistsAndUpdatesSummary() {
|
||||
let store = InMemoryWorkspaceStore()
|
||||
let registry = makeRegistry(store: store)
|
||||
let docsID = registry.createWorkspace(named: "Docs")
|
||||
let hotkey = HotkeyBinding.cmdShiftDigit(4)
|
||||
|
||||
registry.updateWorkspaceHotkey(id: docsID, to: hotkey)
|
||||
|
||||
XCTAssertEqual(registry.summary(for: docsID)?.hotkey, hotkey)
|
||||
XCTAssertEqual(store.savedSummaries.last?.hotkey, hotkey)
|
||||
}
|
||||
|
||||
func testNextAndPreviousWorkspaceWrapAroundRegistryOrder() {
|
||||
let registry = makeRegistry()
|
||||
let mainID = registry.defaultWorkspaceID
|
||||
let docsID = registry.createWorkspace(named: "Docs")
|
||||
let reviewID = registry.createWorkspace(named: "Review")
|
||||
|
||||
XCTAssertEqual(registry.nextWorkspaceID(after: mainID), docsID)
|
||||
XCTAssertEqual(registry.nextWorkspaceID(after: reviewID), mainID)
|
||||
XCTAssertEqual(registry.previousWorkspaceID(before: mainID), reviewID)
|
||||
XCTAssertEqual(registry.previousWorkspaceID(before: docsID), mainID)
|
||||
}
|
||||
|
||||
private func makeRegistry(
|
||||
initialWorkspaces: [WorkspaceSummary]? = [],
|
||||
store: (any WorkspaceStoreType)? = nil
|
||||
|
||||
@@ -9,7 +9,11 @@ final class WorkspaceStoreTests: XCTestCase {
|
||||
|
||||
let store = UserDefaultsWorkspaceStore(defaults: defaults)
|
||||
let summaries = [
|
||||
WorkspaceSummary(id: UUID(uuidString: "11111111-1111-1111-1111-111111111111")!, name: "Main"),
|
||||
WorkspaceSummary(
|
||||
id: UUID(uuidString: "11111111-1111-1111-1111-111111111111")!,
|
||||
name: "Main",
|
||||
hotkey: HotkeyBinding.cmdShiftDigit(4)
|
||||
),
|
||||
WorkspaceSummary(id: UUID(uuidString: "22222222-2222-2222-2222-222222222222")!, name: "Docs")
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user