* Issue #155: remap bonsplit tooltips and add browser split shortcuts * Fix split button mousedown feedback regression * Match split button sizing with main
This commit is contained in:
parent
573cec4a75
commit
df9ba6dcd9
8 changed files with 219 additions and 5 deletions
|
|
@ -187,6 +187,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
private var workspaceObserver: NSObjectProtocol?
|
||||
private var windowKeyObserver: NSObjectProtocol?
|
||||
private var shortcutMonitor: Any?
|
||||
private var shortcutDefaultsObserver: NSObjectProtocol?
|
||||
private var ghosttyConfigObserver: NSObjectProtocol?
|
||||
private var ghosttyGotoSplitLeftShortcut: StoredShortcut?
|
||||
private var ghosttyGotoSplitRightShortcut: StoredShortcut?
|
||||
|
|
@ -336,6 +337,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
installWindowKeyEquivalentSwizzle()
|
||||
installBrowserAddressBarFocusObservers()
|
||||
installShortcutMonitor()
|
||||
installShortcutDefaultsObserver()
|
||||
NSApp.servicesProvider = self
|
||||
#if DEBUG
|
||||
UpdateTestSupport.applyIfNeeded(to: updateController.viewModel)
|
||||
|
|
@ -1460,6 +1462,31 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
}
|
||||
}
|
||||
|
||||
private func installShortcutDefaultsObserver() {
|
||||
guard shortcutDefaultsObserver == nil else { return }
|
||||
shortcutDefaultsObserver = NotificationCenter.default.addObserver(
|
||||
forName: UserDefaults.didChangeNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.refreshSplitButtonTooltipsAcrossWorkspaces()
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshSplitButtonTooltipsAcrossWorkspaces() {
|
||||
var refreshedManagers: Set<ObjectIdentifier> = []
|
||||
if let manager = tabManager {
|
||||
manager.refreshSplitButtonTooltips()
|
||||
refreshedManagers.insert(ObjectIdentifier(manager))
|
||||
}
|
||||
for context in mainWindowContexts.values {
|
||||
let manager = context.tabManager
|
||||
let identifier = ObjectIdentifier(manager)
|
||||
guard refreshedManagers.insert(identifier).inserted else { continue }
|
||||
manager.refreshSplitButtonTooltips()
|
||||
}
|
||||
}
|
||||
|
||||
private func installGhosttyConfigObserver() {
|
||||
guard ghosttyConfigObserver == nil else { return }
|
||||
ghosttyConfigObserver = NotificationCenter.default.addObserver(
|
||||
|
|
@ -1861,6 +1888,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
return true
|
||||
}
|
||||
|
||||
if matchShortcut(event: event, shortcut: KeyboardShortcutSettings.shortcut(for: .splitBrowserRight)) {
|
||||
_ = performBrowserSplitShortcut(direction: .right)
|
||||
return true
|
||||
}
|
||||
|
||||
if matchShortcut(event: event, shortcut: KeyboardShortcutSettings.shortcut(for: .splitBrowserDown)) {
|
||||
_ = performBrowserSplitShortcut(direction: .down)
|
||||
return true
|
||||
}
|
||||
|
||||
// Surface navigation (legacy Ctrl+Tab support)
|
||||
if matchTabShortcut(event: event, shortcut: StoredShortcut(key: "\t", command: false, shift: false, option: false, control: true)) {
|
||||
tabManager?.selectNextSurface()
|
||||
|
|
@ -2041,6 +2078,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
return true
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func performBrowserSplitShortcut(direction: SplitDirection) -> Bool {
|
||||
guard let panelId = tabManager?.createBrowserSplit(direction: direction) else { return false }
|
||||
_ = focusBrowserAddressBar(panelId: panelId)
|
||||
return true
|
||||
}
|
||||
|
||||
/// Allow AppKit-backed browser surfaces (WKWebView) to route non-menu shortcuts
|
||||
/// through the same app-level shortcut handler used by the local key monitor.
|
||||
@discardableResult
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ enum KeyboardShortcutSettings {
|
|||
case focusDown
|
||||
case splitRight
|
||||
case splitDown
|
||||
case splitBrowserRight
|
||||
case splitBrowserDown
|
||||
|
||||
// Panels
|
||||
case openBrowser
|
||||
|
|
@ -51,6 +53,8 @@ enum KeyboardShortcutSettings {
|
|||
case .focusDown: return "Focus Pane Down"
|
||||
case .splitRight: return "Split Right"
|
||||
case .splitDown: return "Split Down"
|
||||
case .splitBrowserRight: return "Split Browser Right"
|
||||
case .splitBrowserDown: return "Split Browser Down"
|
||||
case .openBrowser: return "Open Browser"
|
||||
}
|
||||
}
|
||||
|
|
@ -71,6 +75,8 @@ enum KeyboardShortcutSettings {
|
|||
case .focusDown: return "shortcut.focusDown"
|
||||
case .splitRight: return "shortcut.splitRight"
|
||||
case .splitDown: return "shortcut.splitDown"
|
||||
case .splitBrowserRight: return "shortcut.splitBrowserRight"
|
||||
case .splitBrowserDown: return "shortcut.splitBrowserDown"
|
||||
case .nextSurface: return "shortcut.nextSurface"
|
||||
case .prevSurface: return "shortcut.prevSurface"
|
||||
case .newSurface: return "shortcut.newSurface"
|
||||
|
|
@ -108,6 +114,10 @@ enum KeyboardShortcutSettings {
|
|||
return StoredShortcut(key: "d", command: true, shift: false, option: false, control: false)
|
||||
case .splitDown:
|
||||
return StoredShortcut(key: "d", command: true, shift: true, option: false, control: false)
|
||||
case .splitBrowserRight:
|
||||
return StoredShortcut(key: "d", command: true, shift: false, option: true, control: false)
|
||||
case .splitBrowserDown:
|
||||
return StoredShortcut(key: "d", command: true, shift: true, option: true, control: false)
|
||||
case .nextSurface:
|
||||
return StoredShortcut(key: "]", command: true, shift: true, option: false, control: false)
|
||||
case .prevSurface:
|
||||
|
|
@ -176,6 +186,8 @@ enum KeyboardShortcutSettings {
|
|||
|
||||
static func splitRightShortcut() -> StoredShortcut { shortcut(for: .splitRight) }
|
||||
static func splitDownShortcut() -> StoredShortcut { shortcut(for: .splitDown) }
|
||||
static func splitBrowserRightShortcut() -> StoredShortcut { shortcut(for: .splitBrowserRight) }
|
||||
static func splitBrowserDownShortcut() -> StoredShortcut { shortcut(for: .splitBrowserDown) }
|
||||
|
||||
static func nextSurfaceShortcut() -> StoredShortcut { shortcut(for: .nextSurface) }
|
||||
static func prevSurfaceShortcut() -> StoredShortcut { shortcut(for: .prevSurface) }
|
||||
|
|
|
|||
|
|
@ -1253,6 +1253,28 @@ class TabManager: ObservableObject {
|
|||
_ = newSplit(tabId: selectedTabId, surfaceId: focusedPanelId, direction: direction)
|
||||
}
|
||||
|
||||
/// Create a new browser split from the currently focused panel.
|
||||
@discardableResult
|
||||
func createBrowserSplit(direction: SplitDirection, url: URL? = nil) -> UUID? {
|
||||
guard let selectedTabId,
|
||||
let tab = tabs.first(where: { $0.id == selectedTabId }),
|
||||
let focusedPanelId = tab.focusedPanelId else { return nil }
|
||||
return newBrowserSplit(
|
||||
tabId: selectedTabId,
|
||||
fromPanelId: focusedPanelId,
|
||||
orientation: direction.orientation,
|
||||
insertFirst: direction.insertFirst,
|
||||
url: url
|
||||
)
|
||||
}
|
||||
|
||||
/// Refresh Bonsplit right-side action button tooltips for all workspaces.
|
||||
func refreshSplitButtonTooltips() {
|
||||
for workspace in tabs {
|
||||
workspace.refreshSplitButtonTooltips()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Pane Focus Navigation
|
||||
|
||||
/// Move focus to an adjacent pane in the specified direction
|
||||
|
|
@ -1393,9 +1415,20 @@ class TabManager: ObservableObject {
|
|||
// MARK: - Browser Panel Operations
|
||||
|
||||
/// Create a new browser panel in a split
|
||||
func newBrowserSplit(tabId: UUID, fromPanelId: UUID, orientation: SplitOrientation, url: URL? = nil) -> UUID? {
|
||||
func newBrowserSplit(
|
||||
tabId: UUID,
|
||||
fromPanelId: UUID,
|
||||
orientation: SplitOrientation,
|
||||
insertFirst: Bool = false,
|
||||
url: URL? = nil
|
||||
) -> UUID? {
|
||||
guard let tab = tabs.first(where: { $0.id == tabId }) else { return nil }
|
||||
return tab.newBrowserSplit(from: fromPanelId, orientation: orientation, url: url)?.id
|
||||
return tab.newBrowserSplit(
|
||||
from: fromPanelId,
|
||||
orientation: orientation,
|
||||
insertFirst: insertFirst,
|
||||
url: url
|
||||
)?.id
|
||||
}
|
||||
|
||||
/// Create a new browser surface in a pane
|
||||
|
|
|
|||
|
|
@ -105,12 +105,22 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
|
||||
// MARK: - Initialization
|
||||
|
||||
private static func currentSplitButtonTooltips() -> BonsplitConfiguration.SplitButtonTooltips {
|
||||
BonsplitConfiguration.SplitButtonTooltips(
|
||||
newTerminal: KeyboardShortcutSettings.Action.newSurface.tooltip("New Terminal"),
|
||||
newBrowser: KeyboardShortcutSettings.Action.openBrowser.tooltip("New Browser"),
|
||||
splitRight: KeyboardShortcutSettings.Action.splitRight.tooltip("Split Right"),
|
||||
splitDown: KeyboardShortcutSettings.Action.splitDown.tooltip("Split Down")
|
||||
)
|
||||
}
|
||||
|
||||
private static func bonsplitAppearance(from config: GhosttyConfig) -> BonsplitConfiguration.Appearance {
|
||||
bonsplitAppearance(from: config.backgroundColor)
|
||||
}
|
||||
|
||||
private static func bonsplitAppearance(from backgroundColor: NSColor) -> BonsplitConfiguration.Appearance {
|
||||
BonsplitConfiguration.Appearance(
|
||||
splitButtonTooltips: Self.currentSplitButtonTooltips(),
|
||||
enableAnimations: false,
|
||||
chromeColors: .init(backgroundHex: backgroundColor.hexString())
|
||||
)
|
||||
|
|
@ -208,6 +218,12 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func refreshSplitButtonTooltips() {
|
||||
var configuration = bonsplitController.configuration
|
||||
configuration.appearance.splitButtonTooltips = Self.currentSplitButtonTooltips()
|
||||
bonsplitController.configuration = configuration
|
||||
}
|
||||
|
||||
// MARK: - Surface ID to Panel ID Mapping
|
||||
|
||||
/// Mapping from bonsplit TabID (surface ID) to panel UUID
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ struct cmuxApp: App {
|
|||
@AppStorage(SocketControlSettings.appStorageKey) private var socketControlMode = SocketControlSettings.defaultMode.rawValue
|
||||
@AppStorage(KeyboardShortcutSettings.Action.splitRight.defaultsKey) private var splitRightShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.splitDown.defaultsKey) private var splitDownShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.splitBrowserRight.defaultsKey) private var splitBrowserRightShortcutData = Data()
|
||||
@AppStorage(KeyboardShortcutSettings.Action.splitBrowserDown.defaultsKey) private var splitBrowserDownShortcutData = Data()
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
||||
|
||||
init() {
|
||||
|
|
@ -463,6 +465,14 @@ struct cmuxApp: App {
|
|||
performSplitFromMenu(direction: .down)
|
||||
}
|
||||
|
||||
splitCommandButton(title: "Split Browser Right", shortcut: splitBrowserRightMenuShortcut) {
|
||||
performBrowserSplitFromMenu(direction: .right)
|
||||
}
|
||||
|
||||
splitCommandButton(title: "Split Browser Down", shortcut: splitBrowserDownMenuShortcut) {
|
||||
performBrowserSplitFromMenu(direction: .down)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// Cmd+1 through Cmd+9 for workspace selection (9 = last workspace)
|
||||
|
|
@ -545,6 +555,20 @@ struct cmuxApp: App {
|
|||
decodeShortcut(from: splitDownShortcutData, fallback: KeyboardShortcutSettings.Action.splitDown.defaultShortcut)
|
||||
}
|
||||
|
||||
private var splitBrowserRightMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(
|
||||
from: splitBrowserRightShortcutData,
|
||||
fallback: KeyboardShortcutSettings.Action.splitBrowserRight.defaultShortcut
|
||||
)
|
||||
}
|
||||
|
||||
private var splitBrowserDownMenuShortcut: StoredShortcut {
|
||||
decodeShortcut(
|
||||
from: splitBrowserDownShortcutData,
|
||||
fallback: KeyboardShortcutSettings.Action.splitBrowserDown.defaultShortcut
|
||||
)
|
||||
}
|
||||
|
||||
private var notificationMenuSnapshot: NotificationMenuSnapshot {
|
||||
NotificationMenuSnapshotBuilder.make(notifications: notificationStore.notifications)
|
||||
}
|
||||
|
|
@ -577,6 +601,13 @@ struct cmuxApp: App {
|
|||
tabManager.createSplit(direction: direction)
|
||||
}
|
||||
|
||||
private func performBrowserSplitFromMenu(direction: SplitDirection) {
|
||||
if AppDelegate.shared?.performBrowserSplitShortcut(direction: direction) == true {
|
||||
return
|
||||
}
|
||||
_ = tabManager.createBrowserSplit(direction: direction)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func splitCommandButton(title: String, shortcut: StoredShortcut, action: @escaping () -> Void) -> some View {
|
||||
if let key = keyEquivalent(for: shortcut) {
|
||||
|
|
|
|||
|
|
@ -225,6 +225,74 @@ final class ShortcutHintDebugSettingsTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
final class KeyboardShortcutSettingsTests: XCTestCase {
|
||||
func testBrowserSplitShortcutDefaults() {
|
||||
let keys = [
|
||||
KeyboardShortcutSettings.Action.splitBrowserRight.defaultsKey,
|
||||
KeyboardShortcutSettings.Action.splitBrowserDown.defaultsKey
|
||||
]
|
||||
let defaults = UserDefaults.standard
|
||||
let previousValues = keys.map { key in (key, defaults.data(forKey: key)) }
|
||||
defer {
|
||||
for (key, value) in previousValues {
|
||||
if let value {
|
||||
defaults.set(value, forKey: key)
|
||||
} else {
|
||||
defaults.removeObject(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
keys.forEach { defaults.removeObject(forKey: $0) }
|
||||
|
||||
XCTAssertEqual(KeyboardShortcutSettings.shortcut(for: .splitBrowserRight).displayString, "⌥⌘D")
|
||||
XCTAssertEqual(KeyboardShortcutSettings.shortcut(for: .splitBrowserDown).displayString, "⌥⇧⌘D")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testWorkspaceConfiguresSplitButtonTooltipsWithEffectiveShortcuts() throws {
|
||||
let keys = [
|
||||
KeyboardShortcutSettings.Action.newSurface.defaultsKey,
|
||||
KeyboardShortcutSettings.Action.openBrowser.defaultsKey,
|
||||
KeyboardShortcutSettings.Action.splitRight.defaultsKey,
|
||||
KeyboardShortcutSettings.Action.splitDown.defaultsKey
|
||||
]
|
||||
let defaults = UserDefaults.standard
|
||||
let previousValues = keys.map { key in (key, defaults.data(forKey: key)) }
|
||||
defer {
|
||||
for (key, value) in previousValues {
|
||||
if let value {
|
||||
defaults.set(value, forKey: key)
|
||||
} else {
|
||||
defaults.removeObject(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let customPairs: [(KeyboardShortcutSettings.Action, StoredShortcut)] = [
|
||||
(.newSurface, StoredShortcut(key: "1", command: true, shift: false, option: false, control: false)),
|
||||
(.openBrowser, StoredShortcut(key: "2", command: true, shift: false, option: false, control: false)),
|
||||
(.splitRight, StoredShortcut(key: "3", command: true, shift: false, option: false, control: false)),
|
||||
(.splitDown, StoredShortcut(key: "4", command: true, shift: false, option: false, control: false)),
|
||||
]
|
||||
|
||||
for (action, shortcut) in customPairs {
|
||||
guard let data = try? JSONEncoder().encode(shortcut) else {
|
||||
XCTFail("Failed to encode shortcut for \(action.rawValue)")
|
||||
return
|
||||
}
|
||||
defaults.set(data, forKey: action.defaultsKey)
|
||||
}
|
||||
|
||||
let workspace = Workspace(title: "Tooltip Test")
|
||||
let tooltips = workspace.bonsplitController.configuration.appearance.splitButtonTooltips
|
||||
|
||||
XCTAssertEqual(tooltips.newTerminal, "New Terminal (⌘1)")
|
||||
XCTAssertEqual(tooltips.newBrowser, "New Browser (⌘2)")
|
||||
XCTAssertEqual(tooltips.splitRight, "Split Right (⌘3)")
|
||||
XCTAssertEqual(tooltips.splitDown, "Split Down (⌘4)")
|
||||
}
|
||||
}
|
||||
|
||||
final class ShortcutHintLanePlannerTests: XCTestCase {
|
||||
func testAssignLanesKeepsSeparatedIntervalsOnSingleLane() {
|
||||
let intervals: [ClosedRange<CGFloat>] = [0...20, 28...40, 48...64]
|
||||
|
|
|
|||
2
vendor/bonsplit
vendored
2
vendor/bonsplit
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit ae234a227cb77cc4f34e28098a565e987ca23d87
|
||||
Subproject commit 6ac667d3a9c359b84f920eac4a2ffb027e3bf745
|
||||
|
|
@ -80,6 +80,16 @@ const CATEGORIES: ShortcutCategory[] = [
|
|||
combos: [["⌥", "⌘", "←/→/↑/↓"]],
|
||||
description: "Focus pane directionally",
|
||||
},
|
||||
{
|
||||
id: "sp-browser-right",
|
||||
combos: [["⌥", "⌘", "D"]],
|
||||
description: "Split browser right",
|
||||
},
|
||||
{
|
||||
id: "sp-browser-down",
|
||||
combos: [["⌥", "⌘", "⇧", "D"]],
|
||||
description: "Split browser down",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -88,8 +98,8 @@ const CATEGORIES: ShortcutCategory[] = [
|
|||
shortcuts: [
|
||||
{
|
||||
id: "br-open",
|
||||
combos: [["⌘", "⇧", "B"]],
|
||||
description: "Open browser in split",
|
||||
combos: [["⌘", "⇧", "L"]],
|
||||
description: "Open browser surface",
|
||||
},
|
||||
{ id: "br-addr", combos: [["⌘", "L"]], description: "Focus address bar" },
|
||||
{ id: "br-forward", combos: [["⌘", "]"]], description: "Forward" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue