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) {
|
func updateNSView(_ nsView: HotkeyNSView, context: Context) {
|
||||||
nsView.currentLabel = binding.displayString
|
nsView.update(currentLabel: binding.displayString, isRecording: isRecording)
|
||||||
nsView.showRecording = isRecording
|
|
||||||
nsView.needsDisplay = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,9 +97,7 @@ struct OptionalHotkeyRecorderField: NSViewRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateNSView(_ nsView: HotkeyNSView, context: Context) {
|
func updateNSView(_ nsView: HotkeyNSView, context: Context) {
|
||||||
nsView.currentLabel = binding?.displayString ?? "Not set"
|
nsView.update(currentLabel: binding?.displayString ?? "Not set", isRecording: isRecording)
|
||||||
nsView.showRecording = isRecording
|
|
||||||
nsView.needsDisplay = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +179,12 @@ class HotkeyNSView: NSView {
|
|||||||
updateLabelAppearance()
|
updateLabelAppearance()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func update(currentLabel: String, isRecording: Bool) {
|
||||||
|
self.currentLabel = currentLabel
|
||||||
|
showRecording = isRecording
|
||||||
|
updateLabelAppearance()
|
||||||
|
}
|
||||||
|
|
||||||
private func updateLabelAppearance() {
|
private func updateLabelAppearance() {
|
||||||
label.stringValue = showRecording ? "Press keys..." : currentLabel
|
label.stringValue = showRecording ? "Press keys..." : currentLabel
|
||||||
label.textColor = showRecording ? .controlAccentColor : .labelColor
|
label.textColor = showRecording ? .controlAccentColor : .labelColor
|
||||||
|
|||||||
@@ -9,17 +9,6 @@ struct TabBar: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 0) {
|
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 {
|
Button {
|
||||||
workspace.newTab()
|
workspace.newTab()
|
||||||
} label: {
|
} label: {
|
||||||
@@ -31,6 +20,15 @@ struct TabBar: View {
|
|||||||
.accessibilityIdentifier("notch.new-tab")
|
.accessibilityIdentifier("notch.new-tab")
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.padding(.horizontal, 8)
|
.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)
|
.frame(height: 28)
|
||||||
.background(.black)
|
.background(.black)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Combine
|
|||||||
/// Manages global and local hotkeys.
|
/// Manages global and local hotkeys.
|
||||||
///
|
///
|
||||||
/// The toggle hotkey uses Carbon's `RegisterEventHotKey` which works
|
/// 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).
|
/// use a local `NSEvent` monitor (only fires when our app is active).
|
||||||
@MainActor
|
@MainActor
|
||||||
class HotkeyManager {
|
class HotkeyManager {
|
||||||
@@ -19,21 +19,29 @@ class HotkeyManager {
|
|||||||
var onCloseTab: (() -> Void)?
|
var onCloseTab: (() -> Void)?
|
||||||
var onNextTab: (() -> Void)?
|
var onNextTab: (() -> Void)?
|
||||||
var onPreviousTab: (() -> Void)?
|
var onPreviousTab: (() -> Void)?
|
||||||
|
var onNextWorkspace: (() -> Void)?
|
||||||
|
var onPreviousWorkspace: (() -> Void)?
|
||||||
var onDetachTab: (() -> Void)?
|
var onDetachTab: (() -> Void)?
|
||||||
var onApplySizePreset: ((TerminalSizePreset) -> Void)?
|
var onApplySizePreset: ((TerminalSizePreset) -> Void)?
|
||||||
var onSwitchToTab: ((Int) -> 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
|
var isNotchOpen: Bool = false
|
||||||
|
|
||||||
private var hotKeyRef: EventHotKeyRef?
|
private var hotKeyRef: EventHotKeyRef?
|
||||||
private var eventHandlerRef: EventHandlerRef?
|
private var eventHandlerRef: EventHandlerRef?
|
||||||
private var localMonitor: Any?
|
private var localMonitor: Any?
|
||||||
private let settingsProvider: TerminalSessionConfigurationProviding
|
private let settingsProvider: TerminalSessionConfigurationProviding
|
||||||
|
private let workspaceRegistry: WorkspaceRegistry
|
||||||
private var settingsCancellable: AnyCancellable?
|
private var settingsCancellable: AnyCancellable?
|
||||||
|
|
||||||
init(settingsProvider: TerminalSessionConfigurationProviding? = nil) {
|
init(
|
||||||
|
settingsProvider: TerminalSessionConfigurationProviding? = nil,
|
||||||
|
workspaceRegistry: WorkspaceRegistry? = nil
|
||||||
|
) {
|
||||||
self.settingsProvider = settingsProvider ?? AppSettingsController.shared
|
self.settingsProvider = settingsProvider ?? AppSettingsController.shared
|
||||||
|
self.workspaceRegistry = workspaceRegistry ?? WorkspaceRegistry.shared
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Resolved bindings from typed runtime settings
|
// MARK: - Resolved bindings from typed runtime settings
|
||||||
@@ -53,6 +61,12 @@ class HotkeyManager {
|
|||||||
private var prevTabBinding: HotkeyBinding {
|
private var prevTabBinding: HotkeyBinding {
|
||||||
settingsProvider.hotkeySettings.previousTab
|
settingsProvider.hotkeySettings.previousTab
|
||||||
}
|
}
|
||||||
|
private var nextWorkspaceBinding: HotkeyBinding {
|
||||||
|
settingsProvider.hotkeySettings.nextWorkspace
|
||||||
|
}
|
||||||
|
private var previousWorkspaceBinding: HotkeyBinding {
|
||||||
|
settingsProvider.hotkeySettings.previousWorkspace
|
||||||
|
}
|
||||||
private var detachBinding: HotkeyBinding {
|
private var detachBinding: HotkeyBinding {
|
||||||
settingsProvider.hotkeySettings.detachTab
|
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() {
|
private func installLocalMonitor() {
|
||||||
localMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
|
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 {
|
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 }
|
guard isNotchOpen else { return false }
|
||||||
|
|
||||||
if newTabBinding.matches(event) {
|
if newTabBinding.matches(event) {
|
||||||
@@ -210,10 +224,25 @@ class HotkeyManager {
|
|||||||
onPreviousTab?()
|
onPreviousTab?()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if nextWorkspaceBinding.matches(event) {
|
||||||
|
onNextWorkspace?()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if previousWorkspaceBinding.matches(event) {
|
||||||
|
onPreviousWorkspace?()
|
||||||
|
return true
|
||||||
|
}
|
||||||
if detachBinding.matches(event) {
|
if detachBinding.matches(event) {
|
||||||
onDetachTab?()
|
onDetachTab?()
|
||||||
return true
|
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 {
|
for preset in sizePresets {
|
||||||
guard let binding = preset.hotkey else { continue }
|
guard let binding = preset.hotkey else { continue }
|
||||||
if binding.matches(event) {
|
if binding.matches(event) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ final class ScreenManager: ObservableObject {
|
|||||||
static let shared = ScreenManager()
|
static let shared = ScreenManager()
|
||||||
|
|
||||||
private let screenRegistry = ScreenRegistry.shared
|
private let screenRegistry = ScreenRegistry.shared
|
||||||
|
private let workspaceRegistry = WorkspaceRegistry.shared
|
||||||
private let windowCoordinator = WindowCoordinator()
|
private let windowCoordinator = WindowCoordinator()
|
||||||
private lazy var orchestrator = NotchOrchestrator(screenRegistry: screenRegistry, host: self)
|
private lazy var orchestrator = NotchOrchestrator(screenRegistry: screenRegistry, host: self)
|
||||||
|
|
||||||
@@ -55,6 +56,12 @@ final class ScreenManager: ObservableObject {
|
|||||||
hotkeyManager.onPreviousTab = { [weak self] in
|
hotkeyManager.onPreviousTab = { [weak self] in
|
||||||
MainActor.assumeIsolated { self?.activeWorkspace().previousTab() }
|
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
|
hotkeyManager.onDetachTab = { [weak self] in
|
||||||
MainActor.assumeIsolated { self?.detachActiveTab() }
|
MainActor.assumeIsolated { self?.detachActiveTab() }
|
||||||
}
|
}
|
||||||
@@ -64,6 +71,9 @@ final class ScreenManager: ObservableObject {
|
|||||||
hotkeyManager.onSwitchToTab = { [weak self] index in
|
hotkeyManager.onSwitchToTab = { [weak self] index in
|
||||||
MainActor.assumeIsolated { self?.activeWorkspace().switchToTab(at: index) }
|
MainActor.assumeIsolated { self?.activeWorkspace().switchToTab(at: index) }
|
||||||
}
|
}
|
||||||
|
hotkeyManager.onSwitchToWorkspace = { [weak self] workspaceID in
|
||||||
|
MainActor.assumeIsolated { self?.switchActiveScreen(to: workspaceID) }
|
||||||
|
}
|
||||||
|
|
||||||
hotkeyManager.start()
|
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) {
|
func applySizePreset(_ preset: TerminalSizePreset) {
|
||||||
guard let context = screenRegistry.allScreens().first(where: { $0.notchState == .open }) else {
|
guard let context = screenRegistry.allScreens().first(where: { $0.notchState == .open }) else {
|
||||||
AppSettingsController.shared.update {
|
AppSettingsController.shared.update {
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ struct AppSettings: Equatable, Codable {
|
|||||||
closeTab: .cmdW,
|
closeTab: .cmdW,
|
||||||
nextTab: .cmdShiftRB,
|
nextTab: .cmdShiftRB,
|
||||||
previousTab: .cmdShiftLB,
|
previousTab: .cmdShiftLB,
|
||||||
|
nextWorkspace: .cmdShiftDown,
|
||||||
|
previousWorkspace: .cmdShiftUp,
|
||||||
detachTab: .cmdD
|
detachTab: .cmdD
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -121,6 +123,8 @@ extension AppSettings {
|
|||||||
var closeTab: HotkeyBinding
|
var closeTab: HotkeyBinding
|
||||||
var nextTab: HotkeyBinding
|
var nextTab: HotkeyBinding
|
||||||
var previousTab: HotkeyBinding
|
var previousTab: HotkeyBinding
|
||||||
|
var nextWorkspace: HotkeyBinding
|
||||||
|
var previousWorkspace: HotkeyBinding
|
||||||
var detachTab: HotkeyBinding
|
var detachTab: HotkeyBinding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ struct UserDefaultsAppSettingsStore: AppSettingsStoreType {
|
|||||||
closeTab: hotkey(NotchSettings.Keys.hotkeyCloseTab, default: .cmdW),
|
closeTab: hotkey(NotchSettings.Keys.hotkeyCloseTab, default: .cmdW),
|
||||||
nextTab: hotkey(NotchSettings.Keys.hotkeyNextTab, default: .cmdShiftRB),
|
nextTab: hotkey(NotchSettings.Keys.hotkeyNextTab, default: .cmdShiftRB),
|
||||||
previousTab: hotkey(NotchSettings.Keys.hotkeyPreviousTab, default: .cmdShiftLB),
|
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)
|
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.closeTab.toJSON(), forKey: NotchSettings.Keys.hotkeyCloseTab)
|
||||||
defaults.set(settings.hotkeys.nextTab.toJSON(), forKey: NotchSettings.Keys.hotkeyNextTab)
|
defaults.set(settings.hotkeys.nextTab.toJSON(), forKey: NotchSettings.Keys.hotkeyNextTab)
|
||||||
defaults.set(settings.hotkeys.previousTab.toJSON(), forKey: NotchSettings.Keys.hotkeyPreviousTab)
|
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)
|
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 cmdW = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 13)
|
||||||
static let cmdShiftRB = HotkeyBinding(modifiers: NSEvent.ModifierFlags([.command, .shift]).rawValue, keyCode: 30) // ]
|
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 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 let cmdD = HotkeyBinding(modifiers: NSEvent.ModifierFlags.command.rawValue, keyCode: 2)
|
||||||
|
|
||||||
static func cmdShiftDigit(_ digit: Int) -> HotkeyBinding? {
|
static func cmdShiftDigit(_ digit: Int) -> HotkeyBinding? {
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ enum NotchSettings {
|
|||||||
static let hotkeyCloseTab = "hotkey_closeTab"
|
static let hotkeyCloseTab = "hotkey_closeTab"
|
||||||
static let hotkeyNextTab = "hotkey_nextTab"
|
static let hotkeyNextTab = "hotkey_nextTab"
|
||||||
static let hotkeyPreviousTab = "hotkey_previousTab"
|
static let hotkeyPreviousTab = "hotkey_previousTab"
|
||||||
|
static let hotkeyNextWorkspace = "hotkey_nextWorkspace"
|
||||||
|
static let hotkeyPreviousWorkspace = "hotkey_previousWorkspace"
|
||||||
static let hotkeyDetachTab = "hotkey_detachTab"
|
static let hotkeyDetachTab = "hotkey_detachTab"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +106,8 @@ enum NotchSettings {
|
|||||||
static let hotkeyCloseTab: String = HotkeyBinding.cmdW.toJSON()
|
static let hotkeyCloseTab: String = HotkeyBinding.cmdW.toJSON()
|
||||||
static let hotkeyNextTab: String = HotkeyBinding.cmdShiftRB.toJSON()
|
static let hotkeyNextTab: String = HotkeyBinding.cmdShiftRB.toJSON()
|
||||||
static let hotkeyPreviousTab: String = HotkeyBinding.cmdShiftLB.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()
|
static let hotkeyDetachTab: String = HotkeyBinding.cmdD.toJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +155,8 @@ enum NotchSettings {
|
|||||||
Keys.hotkeyCloseTab: Defaults.hotkeyCloseTab,
|
Keys.hotkeyCloseTab: Defaults.hotkeyCloseTab,
|
||||||
Keys.hotkeyNextTab: Defaults.hotkeyNextTab,
|
Keys.hotkeyNextTab: Defaults.hotkeyNextTab,
|
||||||
Keys.hotkeyPreviousTab: Defaults.hotkeyPreviousTab,
|
Keys.hotkeyPreviousTab: Defaults.hotkeyPreviousTab,
|
||||||
|
Keys.hotkeyNextWorkspace: Defaults.hotkeyNextWorkspace,
|
||||||
|
Keys.hotkeyPreviousWorkspace: Defaults.hotkeyPreviousWorkspace,
|
||||||
Keys.hotkeyDetachTab: Defaults.hotkeyDetachTab,
|
Keys.hotkeyDetachTab: Defaults.hotkeyDetachTab,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ final class WorkspaceController: ObservableObject {
|
|||||||
let createdAt: Date
|
let createdAt: Date
|
||||||
|
|
||||||
@Published private(set) var name: String
|
@Published private(set) var name: String
|
||||||
|
@Published private(set) var hotkey: HotkeyBinding?
|
||||||
@Published private(set) var tabs: [TerminalSession] = []
|
@Published private(set) var tabs: [TerminalSession] = []
|
||||||
@Published private(set) var activeTabIndex: Int = 0
|
@Published private(set) var activeTabIndex: Int = 0
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ final class WorkspaceController: ObservableObject {
|
|||||||
self.id = summary.id
|
self.id = summary.id
|
||||||
self.name = summary.name
|
self.name = summary.name
|
||||||
self.createdAt = summary.createdAt
|
self.createdAt = summary.createdAt
|
||||||
|
self.hotkey = summary.hotkey
|
||||||
self.sessionFactory = sessionFactory
|
self.sessionFactory = sessionFactory
|
||||||
self.settingsProvider = settingsProvider
|
self.settingsProvider = settingsProvider
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ final class WorkspaceController: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var summary: WorkspaceSummary {
|
var summary: WorkspaceSummary {
|
||||||
WorkspaceSummary(id: id, name: name, createdAt: createdAt)
|
WorkspaceSummary(id: id, name: name, createdAt: createdAt, hotkey: hotkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
var state: WorkspaceState {
|
var state: WorkspaceState {
|
||||||
@@ -78,6 +80,11 @@ final class WorkspaceController: ObservableObject {
|
|||||||
name = trimmed
|
name = trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateHotkey(_ updatedHotkey: HotkeyBinding?) {
|
||||||
|
guard hotkey != updatedHotkey else { return }
|
||||||
|
hotkey = updatedHotkey
|
||||||
|
}
|
||||||
|
|
||||||
func newTab() {
|
func newTab() {
|
||||||
let config = settingsProvider.terminalSessionConfiguration
|
let config = settingsProvider.terminalSessionConfiguration
|
||||||
let session = sessionFactory.makeSession(
|
let session = sessionFactory.makeSession(
|
||||||
|
|||||||
@@ -104,6 +104,37 @@ final class WorkspaceRegistry: ObservableObject {
|
|||||||
persistWorkspaceSummaries()
|
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
|
@discardableResult
|
||||||
func deleteWorkspace(id: WorkspaceID) -> Bool {
|
func deleteWorkspace(id: WorkspaceID) -> Bool {
|
||||||
guard canDeleteWorkspace(id: id) else { return false }
|
guard canDeleteWorkspace(id: id) else { return false }
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ struct WorkspaceSummary: Identifiable, Equatable, Codable {
|
|||||||
var id: WorkspaceID
|
var id: WorkspaceID
|
||||||
var name: String
|
var name: String
|
||||||
var createdAt: Date
|
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.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
|
self.hotkey = hotkey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,13 @@ struct HotkeySettingsView: View {
|
|||||||
HotkeyRecorderView(label: "Detach tab", binding: settingsController.binding(\.hotkeys.detachTab))
|
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 {
|
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)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct TerminalSettingsView: View {
|
struct TerminalSettingsView: View {
|
||||||
@ObservedObject private var settingsController = AppSettingsController.shared
|
@ObservedObject private var settingsController = AppSettingsController.shared
|
||||||
|
@State private var sizePresets: [TerminalSizePreset] = []
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
@@ -55,7 +41,7 @@ struct TerminalSettingsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Section("Size Presets") {
|
Section("Size Presets") {
|
||||||
ForEach(sizePresetsBinding) { $preset in
|
ForEach($sizePresets) { $preset in
|
||||||
TerminalSizePresetEditor(
|
TerminalSizePresetEditor(
|
||||||
preset: $preset,
|
preset: $preset,
|
||||||
currentOpenWidth: settingsController.settings.display.openWidth,
|
currentOpenWidth: settingsController.settings.display.openWidth,
|
||||||
@@ -67,20 +53,18 @@ struct TerminalSettingsView: View {
|
|||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Button("Add Preset") {
|
Button("Add Preset") {
|
||||||
var presets = sizePresetsBinding.wrappedValue
|
sizePresets.append(
|
||||||
presets.append(
|
|
||||||
TerminalSizePreset(
|
TerminalSizePreset(
|
||||||
name: "Preset \(presets.count + 1)",
|
name: "Preset \(sizePresets.count + 1)",
|
||||||
width: settingsController.settings.display.openWidth,
|
width: settingsController.settings.display.openWidth,
|
||||||
height: settingsController.settings.display.openHeight,
|
height: settingsController.settings.display.openHeight,
|
||||||
hotkey: TerminalSizePresetStore.suggestedHotkey(for: presets)
|
hotkey: TerminalSizePresetStore.suggestedHotkey(for: sizePresets)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
sizePresetsBinding.wrappedValue = presets
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("Reset Presets") {
|
Button("Reset Presets") {
|
||||||
sizePresetsBinding.wrappedValue = TerminalSizePresetStore.loadDefaults()
|
sizePresets = TerminalSizePresetStore.loadDefaults()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,10 +74,24 @@ struct TerminalSettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.formStyle(.grouped)
|
.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) {
|
private func deletePreset(id: UUID) {
|
||||||
sizePresetsBinding.wrappedValue.removeAll { $0.id == id }
|
sizePresets.removeAll { $0.id == id }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyPreset(_ preset: TerminalSizePreset) {
|
private func applyPreset(_ preset: TerminalSizePreset) {
|
||||||
@@ -103,6 +101,15 @@ struct TerminalSettingsView: View {
|
|||||||
}
|
}
|
||||||
ScreenManager.shared.applySizePreset(preset)
|
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 {
|
private struct TerminalSizePresetEditor: View {
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ struct WorkspacesSettingsView: View {
|
|||||||
renameSelectedWorkspace()
|
renameSelectedWorkspace()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OptionalHotkeyRecorderView(
|
||||||
|
label: "Jump Hotkey",
|
||||||
|
binding: workspaceHotkeyBinding(for: summary.id)
|
||||||
|
)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Button("Save Name") {
|
Button("Save Name") {
|
||||||
renameSelectedWorkspace()
|
renameSelectedWorkspace()
|
||||||
@@ -86,6 +91,10 @@ struct WorkspacesSettingsView: View {
|
|||||||
}
|
}
|
||||||
.accessibilityIdentifier("settings.workspaces.new")
|
.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") {
|
Section("Usage") {
|
||||||
@@ -256,6 +265,17 @@ struct WorkspacesSettingsView: View {
|
|||||||
renameDraft = workspaceRegistry.summary(for: workspaceID)?.name ?? ""
|
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() {
|
private func deleteSelectedWorkspace() {
|
||||||
guard let effectiveSelectedWorkspaceID,
|
guard let effectiveSelectedWorkspaceID,
|
||||||
let fallbackWorkspaceID = screenRegistry.deleteWorkspace(
|
let fallbackWorkspaceID = screenRegistry.deleteWorkspace(
|
||||||
|
|||||||
@@ -74,6 +74,30 @@ final class WorkspaceRegistryTests: XCTestCase {
|
|||||||
XCTAssertEqual(store.savedSummaries.map(\.name), ["Main"])
|
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(
|
private func makeRegistry(
|
||||||
initialWorkspaces: [WorkspaceSummary]? = [],
|
initialWorkspaces: [WorkspaceSummary]? = [],
|
||||||
store: (any WorkspaceStoreType)? = nil
|
store: (any WorkspaceStoreType)? = nil
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ final class WorkspaceStoreTests: XCTestCase {
|
|||||||
|
|
||||||
let store = UserDefaultsWorkspaceStore(defaults: defaults)
|
let store = UserDefaultsWorkspaceStore(defaults: defaults)
|
||||||
let summaries = [
|
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")
|
WorkspaceSummary(id: UUID(uuidString: "22222222-2222-2222-2222-222222222222")!, name: "Docs")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user