Add KeyboardShortcutSettings to enable customizable shortcuts
The SettingsView was referencing KeyboardShortcutSettings but the file was not committed, causing CI build failures.
This commit is contained in:
parent
f622dff0a4
commit
cf6ae2b72d
2 changed files with 235 additions and 0 deletions
|
|
@ -23,6 +23,7 @@
|
|||
A50010A5 /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50010A1 /* SplitView.swift */; };
|
||||
A50010A6 /* TerminalSplitTreeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50010A2 /* TerminalSplitTreeView.swift */; };
|
||||
A50012F1 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50012F0 /* Backport.swift */; };
|
||||
A50012F3 /* KeyboardShortcutSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50012F2 /* KeyboardShortcutSettings.swift */; };
|
||||
A5001201 /* UpdateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001211 /* UpdateController.swift */; };
|
||||
A5001202 /* UpdateDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001212 /* UpdateDelegate.swift */; };
|
||||
A5001203 /* UpdateDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001213 /* UpdateDriver.swift */; };
|
||||
|
|
@ -46,6 +47,7 @@
|
|||
B9000012A1B2C3D4E5F60719 /* AutomationSocketUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000011A1B2C3D4E5F60719 /* AutomationSocketUITests.swift */; };
|
||||
B8F266236A1A3D9A45BD840F /* SidebarResizeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */; };
|
||||
C0B4D9B0A1B2C3D4E5F60718 /* UpdatePillUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */; };
|
||||
B9000014A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000013A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
|
|
@ -109,6 +111,7 @@
|
|||
A50010A1 /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Splits/SplitView.swift; sourceTree = "<group>"; };
|
||||
A50010A2 /* TerminalSplitTreeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Splits/TerminalSplitTreeView.swift; sourceTree = "<group>"; };
|
||||
A50012F0 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||
A50012F2 /* KeyboardShortcutSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcutSettings.swift; sourceTree = "<group>"; };
|
||||
A5001211 /* UpdateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateController.swift; sourceTree = "<group>"; };
|
||||
A5001212 /* UpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateDelegate.swift; sourceTree = "<group>"; };
|
||||
A5001213 /* UpdateDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateDriver.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -131,6 +134,7 @@
|
|||
B9000001A1B2C3D4E5F60719 /* cmuxterm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = cmuxterm.swift; sourceTree = "<group>"; };
|
||||
B9000004A1B2C3D4E5F60719 /* cmuxterm */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = cmuxterm; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B9000011A1B2C3D4E5F60719 /* AutomationSocketUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomationSocketUITests.swift; sourceTree = "<group>"; };
|
||||
B9000013A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JumpToUnreadUITests.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -221,6 +225,7 @@
|
|||
A5001011 /* cmuxApp.swift */,
|
||||
A5001012 /* ContentView.swift */,
|
||||
A50012F0 /* Backport.swift */,
|
||||
A50012F2 /* KeyboardShortcutSettings.swift */,
|
||||
A5001013 /* TabManager.swift */,
|
||||
A5001014 /* GhosttyConfig.swift */,
|
||||
A5001015 /* GhosttyTerminalView.swift */,
|
||||
|
|
@ -281,6 +286,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
B9000011A1B2C3D4E5F60719 /* AutomationSocketUITests.swift */,
|
||||
B9000013A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift */,
|
||||
818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */,
|
||||
C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */,
|
||||
);
|
||||
|
|
@ -391,6 +397,7 @@
|
|||
A5001001 /* cmuxApp.swift in Sources */,
|
||||
A5001002 /* ContentView.swift in Sources */,
|
||||
A50012F1 /* Backport.swift in Sources */,
|
||||
A50012F3 /* KeyboardShortcutSettings.swift in Sources */,
|
||||
A5001003 /* TabManager.swift in Sources */,
|
||||
A5001004 /* GhosttyConfig.swift in Sources */,
|
||||
A5001005 /* GhosttyTerminalView.swift in Sources */,
|
||||
|
|
@ -425,6 +432,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B9000012A1B2C3D4E5F60719 /* AutomationSocketUITests.swift in Sources */,
|
||||
B9000014A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift in Sources */,
|
||||
B8F266236A1A3D9A45BD840F /* SidebarResizeUITests.swift in Sources */,
|
||||
C0B4D9B0A1B2C3D4E5F60718 /* UpdatePillUITests.swift in Sources */,
|
||||
);
|
||||
|
|
|
|||
227
Sources/KeyboardShortcutSettings.swift
Normal file
227
Sources/KeyboardShortcutSettings.swift
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
/// Stores customizable keyboard shortcuts
|
||||
enum KeyboardShortcutSettings {
|
||||
static let showNotificationsKey = "shortcut.showNotifications"
|
||||
static let jumpToUnreadKey = "shortcut.jumpToUnread"
|
||||
|
||||
/// Default shortcut: Cmd+Shift+I
|
||||
static let showNotificationsDefault = StoredShortcut(key: "i", command: true, shift: true, option: false, control: false)
|
||||
/// Default shortcut: Cmd+Shift+U
|
||||
static let jumpToUnreadDefault = StoredShortcut(key: "u", command: true, shift: true, option: false, control: false)
|
||||
|
||||
static func showNotificationsShortcut() -> StoredShortcut {
|
||||
guard let data = UserDefaults.standard.data(forKey: showNotificationsKey),
|
||||
let shortcut = try? JSONDecoder().decode(StoredShortcut.self, from: data) else {
|
||||
return showNotificationsDefault
|
||||
}
|
||||
return shortcut
|
||||
}
|
||||
|
||||
static func setShowNotificationsShortcut(_ shortcut: StoredShortcut) {
|
||||
if let data = try? JSONEncoder().encode(shortcut) {
|
||||
UserDefaults.standard.set(data, forKey: showNotificationsKey)
|
||||
}
|
||||
}
|
||||
|
||||
static func jumpToUnreadShortcut() -> StoredShortcut {
|
||||
guard let data = UserDefaults.standard.data(forKey: jumpToUnreadKey),
|
||||
let shortcut = try? JSONDecoder().decode(StoredShortcut.self, from: data) else {
|
||||
return jumpToUnreadDefault
|
||||
}
|
||||
return shortcut
|
||||
}
|
||||
|
||||
static func setJumpToUnreadShortcut(_ shortcut: StoredShortcut) {
|
||||
if let data = try? JSONEncoder().encode(shortcut) {
|
||||
UserDefaults.standard.set(data, forKey: jumpToUnreadKey)
|
||||
}
|
||||
}
|
||||
|
||||
static func resetAll() {
|
||||
UserDefaults.standard.removeObject(forKey: showNotificationsKey)
|
||||
UserDefaults.standard.removeObject(forKey: jumpToUnreadKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// A keyboard shortcut that can be stored in UserDefaults
|
||||
struct StoredShortcut: Codable, Equatable {
|
||||
var key: String
|
||||
var command: Bool
|
||||
var shift: Bool
|
||||
var option: Bool
|
||||
var control: Bool
|
||||
|
||||
var displayString: String {
|
||||
var parts: [String] = []
|
||||
if control { parts.append("⌃") }
|
||||
if option { parts.append("⌥") }
|
||||
if shift { parts.append("⇧") }
|
||||
if command { parts.append("⌘") }
|
||||
parts.append(key.uppercased())
|
||||
return parts.joined()
|
||||
}
|
||||
|
||||
var modifierFlags: NSEvent.ModifierFlags {
|
||||
var flags: NSEvent.ModifierFlags = []
|
||||
if command { flags.insert(.command) }
|
||||
if shift { flags.insert(.shift) }
|
||||
if option { flags.insert(.option) }
|
||||
if control { flags.insert(.control) }
|
||||
return flags
|
||||
}
|
||||
|
||||
static func from(event: NSEvent) -> StoredShortcut? {
|
||||
guard let chars = event.charactersIgnoringModifiers?.lowercased(),
|
||||
let char = chars.first,
|
||||
char.isLetter || char.isNumber else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let flags = event.modifierFlags
|
||||
return StoredShortcut(
|
||||
key: String(char),
|
||||
command: flags.contains(.command),
|
||||
shift: flags.contains(.shift),
|
||||
option: flags.contains(.option),
|
||||
control: flags.contains(.control)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// View for recording a keyboard shortcut
|
||||
struct KeyboardShortcutRecorder: View {
|
||||
let label: String
|
||||
@Binding var shortcut: StoredShortcut
|
||||
@State private var isRecording = false
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(label)
|
||||
|
||||
Spacer()
|
||||
|
||||
ShortcutRecorderButton(shortcut: $shortcut, isRecording: $isRecording)
|
||||
.frame(width: 120)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ShortcutRecorderButton: NSViewRepresentable {
|
||||
@Binding var shortcut: StoredShortcut
|
||||
@Binding var isRecording: Bool
|
||||
|
||||
func makeNSView(context: Context) -> ShortcutRecorderNSButton {
|
||||
let button = ShortcutRecorderNSButton()
|
||||
button.shortcut = shortcut
|
||||
button.onShortcutRecorded = { newShortcut in
|
||||
shortcut = newShortcut
|
||||
isRecording = false
|
||||
}
|
||||
button.onRecordingChanged = { recording in
|
||||
isRecording = recording
|
||||
}
|
||||
return button
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: ShortcutRecorderNSButton, context: Context) {
|
||||
nsView.shortcut = shortcut
|
||||
nsView.updateTitle()
|
||||
}
|
||||
}
|
||||
|
||||
private class ShortcutRecorderNSButton: NSButton {
|
||||
var shortcut: StoredShortcut = KeyboardShortcutSettings.showNotificationsDefault
|
||||
var onShortcutRecorded: ((StoredShortcut) -> Void)?
|
||||
var onRecordingChanged: ((Bool) -> Void)?
|
||||
private var isRecording = false
|
||||
private var eventMonitor: Any?
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
setup()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setup()
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
bezelStyle = .rounded
|
||||
setButtonType(.momentaryPushIn)
|
||||
target = self
|
||||
action = #selector(buttonClicked)
|
||||
updateTitle()
|
||||
}
|
||||
|
||||
func updateTitle() {
|
||||
if isRecording {
|
||||
title = "Press shortcut…"
|
||||
} else {
|
||||
title = shortcut.displayString
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func buttonClicked() {
|
||||
if isRecording {
|
||||
stopRecording()
|
||||
} else {
|
||||
startRecording()
|
||||
}
|
||||
}
|
||||
|
||||
private func startRecording() {
|
||||
isRecording = true
|
||||
onRecordingChanged?(true)
|
||||
updateTitle()
|
||||
|
||||
eventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
|
||||
guard let self = self else { return event }
|
||||
|
||||
if event.keyCode == 53 { // Escape
|
||||
self.stopRecording()
|
||||
return nil
|
||||
}
|
||||
|
||||
if let newShortcut = StoredShortcut.from(event: event) {
|
||||
self.shortcut = newShortcut
|
||||
self.onShortcutRecorded?(newShortcut)
|
||||
self.stopRecording()
|
||||
return nil
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// Also stop recording if window loses focus
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(windowResigned),
|
||||
name: NSWindow.didResignKeyNotification,
|
||||
object: window
|
||||
)
|
||||
}
|
||||
|
||||
private func stopRecording() {
|
||||
isRecording = false
|
||||
onRecordingChanged?(false)
|
||||
updateTitle()
|
||||
|
||||
if let monitor = eventMonitor {
|
||||
NSEvent.removeMonitor(monitor)
|
||||
eventMonitor = nil
|
||||
}
|
||||
|
||||
NotificationCenter.default.removeObserver(self, name: NSWindow.didResignKeyNotification, object: window)
|
||||
}
|
||||
|
||||
@objc private func windowResigned() {
|
||||
stopRecording()
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopRecording()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue