Revert "Merge pull request #239 from manaflow-ai/issue-151-ssh-remote-port-proxying"
This reverts commit78e4bd32ba, reversing changes made tocf75da8f8a.
This commit is contained in:
parent
78e4bd32ba
commit
f7cbbad434
60 changed files with 1250 additions and 17140 deletions
|
|
@ -74,47 +74,6 @@ func cmuxAccentColor() -> Color {
|
|||
Color(nsColor: cmuxAccentNSColor())
|
||||
}
|
||||
|
||||
struct SidebarRemoteErrorCopyEntry: Equatable {
|
||||
let workspaceTitle: String
|
||||
let target: String
|
||||
let detail: String
|
||||
}
|
||||
|
||||
enum SidebarRemoteErrorCopySupport {
|
||||
static func menuLabel(for entries: [SidebarRemoteErrorCopyEntry]) -> String? {
|
||||
guard !entries.isEmpty else { return nil }
|
||||
if entries.count == 1 {
|
||||
return String(localized: "contextMenu.copyError", defaultValue: "Copy Error")
|
||||
}
|
||||
return String(localized: "contextMenu.copyErrors", defaultValue: "Copy Errors")
|
||||
}
|
||||
|
||||
static func clipboardText(for entries: [SidebarRemoteErrorCopyEntry]) -> String? {
|
||||
guard !entries.isEmpty else { return nil }
|
||||
if entries.count == 1, let entry = entries.first {
|
||||
return "SSH error (\(entry.target)): \(entry.detail)"
|
||||
}
|
||||
|
||||
return entries.enumerated().map { index, entry in
|
||||
"\(index + 1). \(entry.workspaceTitle) (\(entry.target)): \(entry.detail)"
|
||||
}.joined(separator: "\n")
|
||||
}
|
||||
|
||||
static func parsedTargetAndDetail(from value: String, fallbackTarget: String? = nil) -> (target: String, detail: String)? {
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard trimmed.hasPrefix("SSH error") else { return nil }
|
||||
|
||||
if let match = trimmed.firstMatch(of: /^SSH error \((.+?)\):\s*(.+)$/) {
|
||||
return (String(match.1), String(match.2))
|
||||
}
|
||||
if let match = trimmed.firstMatch(of: /^SSH error:\s*(.+)$/) {
|
||||
guard let fallbackTarget, !fallbackTarget.isEmpty else { return nil }
|
||||
return (fallbackTarget, String(match.1))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func sidebarSelectedWorkspaceBackgroundNSColor(for colorScheme: ColorScheme) -> NSColor {
|
||||
cmuxAccentNSColor(for: colorScheme)
|
||||
}
|
||||
|
|
@ -1970,7 +1929,6 @@ struct ContentView: View {
|
|||
lastSidebarSelectionIndex: $lastSidebarSelectionIndex
|
||||
)
|
||||
.frame(width: sidebarWidth)
|
||||
.frame(maxHeight: .infinity, alignment: .topLeading)
|
||||
}
|
||||
|
||||
/// Space at top of content area for the titlebar. This must be at least the actual titlebar
|
||||
|
|
@ -7338,7 +7296,6 @@ struct VerticalTabsSidebar: View {
|
|||
#endif
|
||||
draggedTabId = nil
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
}
|
||||
|
||||
private func debugShortSidebarTabId(_ id: UUID?) -> String {
|
||||
|
|
@ -9532,7 +9489,6 @@ private struct TabItemView: View, Equatable {
|
|||
@AppStorage("sidebarShowPullRequest") private var sidebarShowPullRequest = true
|
||||
@AppStorage(BrowserLinkOpenSettings.openSidebarPullRequestLinksInCmuxBrowserKey)
|
||||
private var openSidebarPullRequestLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenSidebarPullRequestLinksInCmuxBrowser
|
||||
@AppStorage("sidebarShowSSH") private var sidebarShowSSH = true
|
||||
@AppStorage("sidebarShowPorts") private var sidebarShowPorts = true
|
||||
@AppStorage("sidebarShowLog") private var sidebarShowLog = true
|
||||
@AppStorage("sidebarShowProgress") private var sidebarShowProgress = true
|
||||
|
|
@ -9635,84 +9591,12 @@ private struct TabItemView: View, Equatable {
|
|||
)
|
||||
}
|
||||
|
||||
private var remoteWorkspaceSidebarText: String? {
|
||||
guard tab.hasActiveRemoteTerminalSessions else { return nil }
|
||||
let trimmedTarget = tab.remoteDisplayTarget?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let trimmedTarget, !trimmedTarget.isEmpty {
|
||||
return trimmedTarget
|
||||
}
|
||||
return String(localized: "sidebar.remote.subtitleFallback", defaultValue: "SSH workspace")
|
||||
}
|
||||
|
||||
private var copyableSidebarSSHError: String? {
|
||||
let trimmedDetail = tab.remoteConnectionDetail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if tab.remoteConnectionState == .error, let trimmedDetail, !trimmedDetail.isEmpty {
|
||||
let target = tab.remoteDisplayTarget ?? "unknown"
|
||||
return "SSH error (\(target)): \(trimmedDetail)"
|
||||
}
|
||||
if let statusValue = tab.statusEntries["remote.error"]?.value
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!statusValue.isEmpty {
|
||||
return statusValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private var remoteConnectionStatusText: String {
|
||||
switch tab.remoteConnectionState {
|
||||
case .connected:
|
||||
return String(localized: "remote.status.connected", defaultValue: "Connected")
|
||||
case .connecting:
|
||||
return String(localized: "remote.status.connecting", defaultValue: "Connecting")
|
||||
case .error:
|
||||
return String(localized: "remote.status.error", defaultValue: "Error")
|
||||
case .disconnected:
|
||||
return String(localized: "remote.status.disconnected", defaultValue: "Disconnected")
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var remoteWorkspaceSection: some View {
|
||||
if sidebarShowSSH, let remoteWorkspaceSidebarText {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(String(localized: "sidebar.remote.badge", defaultValue: "SSH"))
|
||||
.font(.system(size: 9, weight: .semibold))
|
||||
.foregroundColor(activeSecondaryColor(0.62))
|
||||
.textCase(.uppercase)
|
||||
|
||||
HStack(spacing: 6) {
|
||||
Text(remoteWorkspaceSidebarText)
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
.foregroundColor(activeSecondaryColor(0.8))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Text(remoteConnectionStatusText)
|
||||
.font(.system(size: 9, weight: .medium))
|
||||
.foregroundColor(activeSecondaryColor(0.58))
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.padding(.top, latestNotificationText == nil ? 1 : 2)
|
||||
.safeHelp(remoteStateHelpText)
|
||||
}
|
||||
}
|
||||
|
||||
private func copyTextToPasteboard(_ text: String) {
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.setString(text, forType: .string)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let closeWorkspaceTooltip = String(localized: "sidebar.closeWorkspace.tooltip", defaultValue: "Close Workspace")
|
||||
let accessibilityHintText = String(localized: "sidebar.workspace.accessibilityHint", defaultValue: "Activate to focus this workspace. Drag to reorder, or use Move Up and Move Down actions.")
|
||||
let moveUpActionText = String(localized: "sidebar.workspace.moveUpAction", defaultValue: "Move Up")
|
||||
let moveDownActionText = String(localized: "sidebar.workspace.moveDownAction", defaultValue: "Move Down")
|
||||
let latestNotificationSubtitle = latestNotificationText
|
||||
let effectiveSubtitle = latestNotificationSubtitle
|
||||
let orderedPanelIds: [UUID]? = (sidebarShowBranchDirectory || sidebarShowPullRequest)
|
||||
? tab.sidebarOrderedPanelIds()
|
||||
: nil
|
||||
|
|
@ -9816,7 +9700,7 @@ private struct TabItemView: View, Equatable {
|
|||
.frame(width: workspaceHintSlotWidth, height: 16, alignment: .trailing)
|
||||
}
|
||||
|
||||
if let subtitle = effectiveSubtitle {
|
||||
if let subtitle = latestNotificationSubtitle {
|
||||
Text(subtitle)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(activeSecondaryColor(0.8))
|
||||
|
|
@ -9825,8 +9709,6 @@ private struct TabItemView: View, Equatable {
|
|||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
|
||||
remoteWorkspaceSection
|
||||
|
||||
if sidebarShowMetadata {
|
||||
let metadataEntries = tab.sidebarStatusEntriesInDisplayOrder()
|
||||
let metadataBlocks = tab.sidebarMetadataBlocksInDisplayOrder()
|
||||
|
|
@ -10086,16 +9968,6 @@ private struct TabItemView: View, Equatable {
|
|||
let isMulti = targetIds.count > 1
|
||||
let tabColorPalette = WorkspaceTabColorSettings.palette()
|
||||
let shouldPin = !tab.isPinned
|
||||
let targetWorkspaces = targetIds.compactMap { id in tabManager.tabs.first(where: { $0.id == id }) }
|
||||
let remoteTargetWorkspaces = targetWorkspaces.filter { $0.isRemoteWorkspace }
|
||||
let reconnectLabel = contextMenuLabel(
|
||||
multi: String(localized: "contextMenu.reconnectWorkspaces", defaultValue: "Reconnect Workspaces"),
|
||||
single: String(localized: "contextMenu.reconnectWorkspace", defaultValue: "Reconnect Workspace"),
|
||||
isMulti: isMulti)
|
||||
let disconnectLabel = contextMenuLabel(
|
||||
multi: String(localized: "contextMenu.disconnectWorkspaces", defaultValue: "Disconnect Workspaces"),
|
||||
single: String(localized: "contextMenu.disconnectWorkspace", defaultValue: "Disconnect Workspace"),
|
||||
isMulti: isMulti)
|
||||
let pinLabel = shouldPin
|
||||
? contextMenuLabel(
|
||||
multi: String(localized: "contextMenu.pinWorkspaces", defaultValue: "Pin Workspaces"),
|
||||
|
|
@ -10145,24 +10017,6 @@ private struct TabItemView: View, Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
if !remoteTargetWorkspaces.isEmpty {
|
||||
Divider()
|
||||
|
||||
Button(reconnectLabel) {
|
||||
for workspace in remoteTargetWorkspaces {
|
||||
workspace.reconnectRemoteConnection()
|
||||
}
|
||||
}
|
||||
.disabled(remoteTargetWorkspaces.allSatisfy { $0.remoteConnectionState == .connecting })
|
||||
|
||||
Button(disconnectLabel) {
|
||||
for workspace in remoteTargetWorkspaces {
|
||||
workspace.disconnectRemoteConnection(clearConfiguration: false)
|
||||
}
|
||||
}
|
||||
.disabled(remoteTargetWorkspaces.allSatisfy { $0.remoteConnectionState == .disconnected })
|
||||
}
|
||||
|
||||
Menu(String(localized: "contextMenu.workspaceColor", defaultValue: "Workspace Color")) {
|
||||
if tab.customColor != nil {
|
||||
Button {
|
||||
|
|
@ -10195,12 +10049,6 @@ private struct TabItemView: View, Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
if let copyableSidebarSSHError {
|
||||
Button(String(localized: "contextMenu.copySshError", defaultValue: "Copy SSH Error")) {
|
||||
copyTextToPasteboard(copyableSidebarSSHError)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button(String(localized: "contextMenu.moveUp", defaultValue: "Move Up")) {
|
||||
|
|
@ -10476,62 +10324,6 @@ private struct TabItemView: View, Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
private var remoteStateHelpText: String {
|
||||
let target = tab.remoteDisplayTarget ?? String(
|
||||
localized: "sidebar.remote.help.targetFallback",
|
||||
defaultValue: "remote host"
|
||||
)
|
||||
let detail = tab.remoteConnectionDetail?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
switch tab.remoteConnectionState {
|
||||
case .connected:
|
||||
return String(
|
||||
format: String(
|
||||
localized: "sidebar.remote.help.connected",
|
||||
defaultValue: "SSH connected to %@"
|
||||
),
|
||||
locale: .current,
|
||||
target
|
||||
)
|
||||
case .connecting:
|
||||
return String(
|
||||
format: String(
|
||||
localized: "sidebar.remote.help.connecting",
|
||||
defaultValue: "SSH connecting to %@"
|
||||
),
|
||||
locale: .current,
|
||||
target
|
||||
)
|
||||
case .error:
|
||||
if let detail, !detail.isEmpty {
|
||||
return String(
|
||||
format: String(
|
||||
localized: "sidebar.remote.help.errorWithDetail",
|
||||
defaultValue: "SSH error for %@: %@"
|
||||
),
|
||||
locale: .current,
|
||||
target,
|
||||
detail
|
||||
)
|
||||
}
|
||||
return String(
|
||||
format: String(
|
||||
localized: "sidebar.remote.help.error",
|
||||
defaultValue: "SSH error for %@"
|
||||
),
|
||||
locale: .current,
|
||||
target
|
||||
)
|
||||
case .disconnected:
|
||||
return String(
|
||||
format: String(
|
||||
localized: "sidebar.remote.help.disconnected",
|
||||
defaultValue: "SSH disconnected from %@"
|
||||
),
|
||||
locale: .current,
|
||||
target
|
||||
)
|
||||
}
|
||||
}
|
||||
private func moveWorkspaces(_ workspaceIds: [UUID], toWindow windowId: UUID) {
|
||||
guard let app = AppDelegate.shared else { return }
|
||||
let orderedWorkspaceIds = tabManager.tabs.compactMap { workspaceIds.contains($0.id) ? $0.id : nil }
|
||||
|
|
@ -10733,18 +10525,6 @@ private struct TabItemView: View, Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
private func shortenPath(_ path: String, home: String) -> String {
|
||||
let trimmed = path.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return path }
|
||||
if trimmed == home {
|
||||
return "~"
|
||||
}
|
||||
if trimmed.hasPrefix(home + "/") {
|
||||
return "~" + trimmed.dropFirst(home.count)
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
private struct PullRequestStatusIcon: View {
|
||||
let status: SidebarPullRequestStatus
|
||||
let color: Color
|
||||
|
|
|
|||
|
|
@ -2333,8 +2333,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
private let surfaceContext: ghostty_surface_context_e
|
||||
private let configTemplate: ghostty_surface_config_s?
|
||||
private let workingDirectory: String?
|
||||
private let initialCommand: String?
|
||||
private let initialEnvironmentOverrides: [String: String]
|
||||
private let additionalEnvironment: [String: String]
|
||||
let hostedView: GhosttySurfaceScrollView
|
||||
private let surfaceView: GhosttyNSView
|
||||
private var lastPixelWidth: UInt32 = 0
|
||||
|
|
@ -2402,8 +2401,6 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
context: ghostty_surface_context_e,
|
||||
configTemplate: ghostty_surface_config_s?,
|
||||
workingDirectory: String? = nil,
|
||||
initialCommand: String? = nil,
|
||||
initialEnvironmentOverrides: [String: String] = [:],
|
||||
additionalEnvironment: [String: String] = [:]
|
||||
) {
|
||||
self.id = UUID()
|
||||
|
|
@ -2411,12 +2408,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
self.surfaceContext = context
|
||||
self.configTemplate = configTemplate
|
||||
self.workingDirectory = workingDirectory?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let trimmedCommand = initialCommand?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.initialCommand = (trimmedCommand?.isEmpty == false) ? trimmedCommand : nil
|
||||
self.initialEnvironmentOverrides = Self.mergedNormalizedEnvironment(
|
||||
base: additionalEnvironment,
|
||||
overrides: initialEnvironmentOverrides
|
||||
)
|
||||
self.additionalEnvironment = additionalEnvironment
|
||||
// Match Ghostty's own SurfaceView: ensure a non-zero initial frame so the backing layer
|
||||
// has non-zero bounds and the renderer can initialize without presenting a blank/stretched
|
||||
// intermediate frame on the first real resize.
|
||||
|
|
@ -2434,25 +2426,6 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
surfaceView.tabId = newTabId
|
||||
}
|
||||
|
||||
private static func mergedNormalizedEnvironment(
|
||||
base: [String: String],
|
||||
overrides: [String: String]
|
||||
) -> [String: String] {
|
||||
var merged: [String: String] = [:]
|
||||
merged.reserveCapacity(base.count + overrides.count)
|
||||
for (rawKey, value) in base {
|
||||
let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !key.isEmpty else { continue }
|
||||
merged[key] = value
|
||||
}
|
||||
for (rawKey, value) in overrides {
|
||||
let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !key.isEmpty else { continue }
|
||||
merged[key] = value
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func isAttached(to view: GhosttyNSView) -> Bool {
|
||||
attachedView === view && surface != nil
|
||||
}
|
||||
|
|
@ -2811,10 +2784,6 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
env["CMUX_PANEL_ID"] = id.uuidString
|
||||
env["CMUX_TAB_ID"] = tabId.uuidString
|
||||
env["CMUX_SOCKET_PATH"] = SocketControlSettings.socketPath()
|
||||
if let bundledCLIPath = Bundle.main.resourceURL?.appendingPathComponent("bin/cmux").path,
|
||||
!bundledCLIPath.isEmpty {
|
||||
env["CMUX_BUNDLED_CLI_PATH"] = bundledCLIPath
|
||||
}
|
||||
if let bundleId = Bundle.main.bundleIdentifier, !bundleId.isEmpty {
|
||||
env["CMUX_BUNDLE_ID"] = bundleId
|
||||
}
|
||||
|
|
@ -2882,8 +2851,8 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
if !initialEnvironmentOverrides.isEmpty {
|
||||
for (key, value) in initialEnvironmentOverrides {
|
||||
if !additionalEnvironment.isEmpty {
|
||||
for (key, value) in additionalEnvironment where !key.isEmpty && !value.isEmpty {
|
||||
env[key] = value
|
||||
}
|
||||
}
|
||||
|
|
@ -2911,31 +2880,15 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
let createWithCommandAndWorkingDirectory = { [self] in
|
||||
if let initialCommand, !initialCommand.isEmpty {
|
||||
initialCommand.withCString { cCommand in
|
||||
surfaceConfig.command = cCommand
|
||||
if let workingDirectory, !workingDirectory.isEmpty {
|
||||
workingDirectory.withCString { cWorkingDir in
|
||||
surfaceConfig.working_directory = cWorkingDir
|
||||
createSurface()
|
||||
}
|
||||
} else {
|
||||
createSurface()
|
||||
}
|
||||
}
|
||||
} else if let workingDirectory, !workingDirectory.isEmpty {
|
||||
workingDirectory.withCString { cWorkingDir in
|
||||
surfaceConfig.working_directory = cWorkingDir
|
||||
createSurface()
|
||||
}
|
||||
} else {
|
||||
if let workingDirectory, !workingDirectory.isEmpty {
|
||||
workingDirectory.withCString { cWorkingDir in
|
||||
surfaceConfig.working_directory = cWorkingDir
|
||||
createSurface()
|
||||
}
|
||||
} else {
|
||||
createSurface()
|
||||
}
|
||||
|
||||
createWithCommandAndWorkingDirectory()
|
||||
|
||||
if surface == nil {
|
||||
surfaceCallbackContext?.release()
|
||||
surfaceCallbackContext = nil
|
||||
|
|
@ -3085,7 +3038,6 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
dlog("forceRefresh: \(id) reason=\(reason) \(viewState)")
|
||||
#endif
|
||||
guard let view = attachedView,
|
||||
let surface,
|
||||
view.window != nil,
|
||||
view.bounds.width > 0,
|
||||
view.bounds.height > 0 else {
|
||||
|
|
@ -5685,7 +5637,6 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
private var activeDropZone: DropZone?
|
||||
private var pendingDropZone: DropZone?
|
||||
private var dropZoneOverlayAnimationGeneration: UInt64 = 0
|
||||
private var pendingAutomaticFirstResponderApply = false
|
||||
// Intentionally no focus retry loops: rely on AppKit first-responder and bonsplit selection.
|
||||
|
||||
/// Tracks whether keyboard focus should go to the search field or the terminal
|
||||
|
|
@ -6293,7 +6244,7 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
#if DEBUG
|
||||
dlog("find.window.didBecomeKey surface=\(self.surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil") searchActive=\(searchActive) focusTarget=\(self.searchFocusTarget) firstResponder=\(String(describing: self.window?.firstResponder))")
|
||||
#endif
|
||||
self.scheduleAutomaticFirstResponderApply(reason: "didBecomeKey")
|
||||
self.applyFirstResponderIfNeeded()
|
||||
})
|
||||
windowObservers.append(NotificationCenter.default.addObserver(
|
||||
forName: NSWindow.didResignKeyNotification,
|
||||
|
|
@ -6316,9 +6267,7 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
#endif
|
||||
}
|
||||
})
|
||||
if window.isKeyWindow {
|
||||
scheduleAutomaticFirstResponderApply(reason: "viewDidMoveToWindow")
|
||||
}
|
||||
if window.isKeyWindow { applyFirstResponderIfNeeded() }
|
||||
}
|
||||
|
||||
func attachSurface(_ terminalSurface: TerminalSurface) {
|
||||
|
|
@ -6735,7 +6684,7 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
window.makeFirstResponder(nil)
|
||||
}
|
||||
} else {
|
||||
scheduleAutomaticFirstResponderApply(reason: "setVisibleInUI")
|
||||
applyFirstResponderIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6762,7 +6711,7 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
}
|
||||
#endif
|
||||
if active {
|
||||
scheduleAutomaticFirstResponderApply(reason: "setActive")
|
||||
applyFirstResponderIfNeeded()
|
||||
} else {
|
||||
resignOwnedFirstResponderIfNeeded(reason: "setActive(false)")
|
||||
}
|
||||
|
|
@ -7124,20 +7073,6 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
return fr === surfaceView || fr.isDescendant(of: surfaceView)
|
||||
}
|
||||
|
||||
private func scheduleAutomaticFirstResponderApply(reason: String) {
|
||||
guard !pendingAutomaticFirstResponderApply else { return }
|
||||
pendingAutomaticFirstResponderApply = true
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
self.pendingAutomaticFirstResponderApply = false
|
||||
#if DEBUG
|
||||
let surfaceShort = self.surfaceView.terminalSurface?.id.uuidString.prefix(5) ?? "nil"
|
||||
dlog("find.applyFirstResponder.defer surface=\(surfaceShort) reason=\(reason)")
|
||||
#endif
|
||||
self.applyFirstResponderIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private func reassertTerminalSurfaceFocus(reason: String) {
|
||||
guard let terminalSurface = surfaceView.terminalSurface else { return }
|
||||
#if DEBUG
|
||||
|
|
@ -7713,15 +7648,35 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
/// regions such as scrollbar space) when telling libghostty the terminal size.
|
||||
@discardableResult
|
||||
private func synchronizeCoreSurface() -> Bool {
|
||||
// Reserving extra overlay-scroller gutter here causes AppKit and libghostty to fight
|
||||
// over terminal columns during split churn. The width can flap by one scrollbar gutter,
|
||||
// which redraws the shell prompt multiple times on Cmd+D. Favor stable columns.
|
||||
let width = max(0, scrollView.contentSize.width)
|
||||
let width = max(0, scrollView.contentSize.width - overlayScrollbarInsetWidth())
|
||||
let height = surfaceView.frame.height
|
||||
guard width > 0, height > 0 else { return false }
|
||||
return surfaceView.pushTargetSurfaceSize(CGSize(width: width, height: height))
|
||||
}
|
||||
|
||||
/// Reserve overlay scrollbar gutter so wrapped text never sits underneath a visible scroller.
|
||||
private func overlayScrollbarInsetWidth() -> CGFloat {
|
||||
guard scrollView.hasVerticalScroller, scrollView.scrollerStyle == .overlay else { return 0 }
|
||||
|
||||
// If AppKit already reserved non-content width in `contentSize`, avoid double-subtraction.
|
||||
let alreadyReserved = max(0, scrollView.bounds.width - scrollView.contentSize.width)
|
||||
if alreadyReserved > 0.5 { return 0 }
|
||||
|
||||
let fallback = NSScroller.scrollerWidth(for: .regular, scrollerStyle: .overlay)
|
||||
guard let verticalScroller = scrollView.verticalScroller else { return fallback }
|
||||
|
||||
let measuredWidth = verticalScroller.frame.width
|
||||
if measuredWidth > 0 {
|
||||
return max(measuredWidth, fallback)
|
||||
}
|
||||
|
||||
let controlSizeWidth = NSScroller.scrollerWidth(
|
||||
for: verticalScroller.controlSize,
|
||||
scrollerStyle: .overlay
|
||||
)
|
||||
return max(controlSizeWidth, fallback)
|
||||
}
|
||||
|
||||
private func updateNotificationRingPath() {
|
||||
updateOverlayRingPath(
|
||||
layer: notificationRingLayer,
|
||||
|
|
@ -8235,12 +8190,6 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
}
|
||||
let portalExpectedSurfaceId = terminalSurface.id
|
||||
let portalExpectedGeneration = terminalSurface.portalBindingGeneration()
|
||||
func portalBindingStillLive() -> Bool {
|
||||
terminalSurface.canAcceptPortalBinding(
|
||||
expectedSurfaceId: portalExpectedSurfaceId,
|
||||
expectedGeneration: portalExpectedGeneration
|
||||
)
|
||||
}
|
||||
let forwardedDropZone = isVisibleInUI ? paneDropZone : nil
|
||||
#if DEBUG
|
||||
if coordinator.lastPaneDropZone != paneDropZone {
|
||||
|
|
@ -8279,7 +8228,6 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
reason: "didMoveToWindow"
|
||||
) else { return }
|
||||
guard host.window != nil else { return }
|
||||
guard portalBindingStillLive() else { return }
|
||||
TerminalWindowPortalRegistry.bind(
|
||||
hostedView: hostedView,
|
||||
to: host,
|
||||
|
|
@ -8303,7 +8251,6 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
bounds: host.bounds,
|
||||
reason: "geometryChanged"
|
||||
) else { return }
|
||||
guard portalBindingStillLive() else { return }
|
||||
let hostId = ObjectIdentifier(host)
|
||||
if host.window != nil,
|
||||
(coordinator.lastBoundHostId != hostId ||
|
||||
|
|
@ -8333,7 +8280,6 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
}
|
||||
|
||||
if host.window != nil, hostOwnsPortalNow {
|
||||
let portalBindingLive = portalBindingStillLive()
|
||||
let hostId = ObjectIdentifier(host)
|
||||
let geometryRevision = host.geometryRevision
|
||||
let portalEntryMissing = !TerminalWindowPortalRegistry.isHostedView(hostedView, boundTo: host)
|
||||
|
|
@ -8344,7 +8290,7 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
previousDesiredIsVisibleInUI != isVisibleInUI ||
|
||||
previousDesiredShowsUnreadNotificationRing != showsUnreadNotificationRing ||
|
||||
previousDesiredPortalZPriority != portalZPriority
|
||||
if portalBindingLive && shouldBindNow {
|
||||
if shouldBindNow {
|
||||
#if DEBUG
|
||||
if portalEntryMissing {
|
||||
dlog(
|
||||
|
|
@ -8364,11 +8310,11 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
)
|
||||
coordinator.lastBoundHostId = hostId
|
||||
coordinator.lastSynchronizedHostGeometryRevision = geometryRevision
|
||||
} else if portalBindingLive && coordinator.lastSynchronizedHostGeometryRevision != geometryRevision {
|
||||
} else if coordinator.lastSynchronizedHostGeometryRevision != geometryRevision {
|
||||
TerminalWindowPortalRegistry.synchronizeForAnchor(host)
|
||||
coordinator.lastSynchronizedHostGeometryRevision = geometryRevision
|
||||
}
|
||||
} else if hostOwnsPortalNow, portalBindingStillLive() {
|
||||
} else if hostOwnsPortalNow {
|
||||
// Bind is deferred until host moves into a window. Update the
|
||||
// existing portal entry's visibleInUI now so that any portal sync
|
||||
// that runs before the deferred bind completes won't hide the view.
|
||||
|
|
@ -8398,7 +8344,7 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
isBoundToCurrentHost: isBoundToCurrentHost
|
||||
)
|
||||
|
||||
if portalBindingStillLive() && shouldApplyImmediateHostedState {
|
||||
if shouldApplyImmediateHostedState {
|
||||
hostedView.setVisibleInUI(isVisibleInUI)
|
||||
hostedView.setActive(isActive)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -3,19 +3,6 @@ import Combine
|
|||
import WebKit
|
||||
import AppKit
|
||||
import Bonsplit
|
||||
import Network
|
||||
|
||||
struct BrowserProxyEndpoint: Equatable {
|
||||
let host: String
|
||||
let port: Int
|
||||
}
|
||||
|
||||
struct BrowserRemoteWorkspaceStatus: Equatable {
|
||||
let target: String
|
||||
let connectionState: WorkspaceRemoteConnectionState
|
||||
let heartbeatCount: Int
|
||||
let lastHeartbeatAt: Date?
|
||||
}
|
||||
|
||||
enum GhosttyBackgroundTheme {
|
||||
static func clampedOpacity(_ opacity: Double) -> CGFloat {
|
||||
|
|
@ -1268,14 +1255,6 @@ final class BrowserPortalAnchorView: NSView {
|
|||
|
||||
@MainActor
|
||||
final class BrowserPanel: Panel, ObservableObject {
|
||||
private static let remoteLoopbackProxyAliasHost = "cmux-loopback.localtest.me"
|
||||
private static let remoteLoopbackHosts: Set<String> = [
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
"::1",
|
||||
"0.0.0.0",
|
||||
]
|
||||
|
||||
/// Shared process pool for cookie sharing across all browser panels
|
||||
private static let sharedProcessPool = WKProcessPool()
|
||||
|
||||
|
|
@ -1794,8 +1773,6 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
private var developerToolsRestoreRetryAttempt: Int = 0
|
||||
private let developerToolsRestoreRetryDelay: TimeInterval = 0.05
|
||||
private let developerToolsRestoreRetryMaxAttempts: Int = 40
|
||||
private var remoteProxyEndpoint: BrowserProxyEndpoint?
|
||||
@Published private(set) var remoteWorkspaceStatus: BrowserRemoteWorkspaceStatus?
|
||||
private let developerToolsDetachedOpenGracePeriod: TimeInterval = 0.35
|
||||
private var developerToolsDetachedOpenGraceDeadline: Date?
|
||||
private var developerToolsTransitionTargetVisible: Bool?
|
||||
|
|
@ -2038,24 +2015,15 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
return instanceID == webViewInstanceID
|
||||
}
|
||||
|
||||
init(
|
||||
workspaceId: UUID,
|
||||
initialURL: URL? = nil,
|
||||
bypassInsecureHTTPHostOnce: String? = nil,
|
||||
proxyEndpoint: BrowserProxyEndpoint? = nil,
|
||||
isRemoteWorkspace: Bool = false
|
||||
) {
|
||||
init(workspaceId: UUID, initialURL: URL? = nil, bypassInsecureHTTPHostOnce: String? = nil) {
|
||||
self.id = UUID()
|
||||
self.workspaceId = workspaceId
|
||||
self.insecureHTTPBypassHostOnce = BrowserInsecureHTTPSettings.normalizeHost(bypassInsecureHTTPHostOnce ?? "")
|
||||
self.remoteProxyEndpoint = proxyEndpoint
|
||||
self.browserThemeMode = BrowserThemeSettings.mode()
|
||||
|
||||
let webView = Self.makeWebView()
|
||||
self.webView = webView
|
||||
self.insecureHTTPAlertFactory = { NSAlert() }
|
||||
let _ = isRemoteWorkspace
|
||||
applyRemoteProxyConfigurationIfAvailable()
|
||||
|
||||
// Set up navigation delegate
|
||||
let navDelegate = BrowserNavigationDelegate()
|
||||
|
|
@ -2135,40 +2103,6 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func setRemoteProxyEndpoint(_ endpoint: BrowserProxyEndpoint?) {
|
||||
guard remoteProxyEndpoint != endpoint else { return }
|
||||
remoteProxyEndpoint = endpoint
|
||||
applyRemoteProxyConfigurationIfAvailable()
|
||||
}
|
||||
|
||||
func setRemoteWorkspaceStatus(_ status: BrowserRemoteWorkspaceStatus?) {
|
||||
guard remoteWorkspaceStatus != status else { return }
|
||||
remoteWorkspaceStatus = status
|
||||
}
|
||||
|
||||
private func applyRemoteProxyConfigurationIfAvailable() {
|
||||
guard #available(macOS 14.0, *) else { return }
|
||||
|
||||
let store = webView.configuration.websiteDataStore
|
||||
guard let endpoint = remoteProxyEndpoint else {
|
||||
store.proxyConfigurations = []
|
||||
return
|
||||
}
|
||||
|
||||
let host = endpoint.host.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !host.isEmpty,
|
||||
endpoint.port > 0 && endpoint.port <= 65535,
|
||||
let nwPort = NWEndpoint.Port(rawValue: UInt16(endpoint.port)) else {
|
||||
store.proxyConfigurations = []
|
||||
return
|
||||
}
|
||||
|
||||
let nwEndpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(host), port: nwPort)
|
||||
let socks = ProxyConfiguration(socksv5Proxy: nwEndpoint)
|
||||
let connect = ProxyConfiguration(httpCONNECTProxy: nwEndpoint)
|
||||
store.proxyConfigurations = [socks, connect]
|
||||
}
|
||||
|
||||
private func beginDownloadActivity() {
|
||||
let apply = {
|
||||
self.activeDownloadCount += 1
|
||||
|
|
@ -2665,7 +2599,6 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
if !preserveRestoredSessionHistory {
|
||||
abandonRestoredSessionHistoryIfNeeded()
|
||||
}
|
||||
let effectiveRequest = remoteProxyPreparedNavigationRequest(from: request)
|
||||
// Some installs can end up with a legacy Chrome UA override; keep this pinned.
|
||||
webView.customUserAgent = BrowserUserAgentSettings.safariUserAgent
|
||||
shouldRenderWebView = true
|
||||
|
|
@ -2673,35 +2606,7 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
BrowserHistoryStore.shared.recordTypedNavigation(url: url)
|
||||
}
|
||||
navigationDelegate?.lastAttemptedURL = url
|
||||
browserLoadRequest(effectiveRequest, in: webView)
|
||||
}
|
||||
|
||||
private func remoteProxyPreparedNavigationRequest(from request: URLRequest) -> URLRequest {
|
||||
guard remoteProxyEndpoint != nil else { return request }
|
||||
guard let url = request.url else { return request }
|
||||
guard let rewrittenURL = Self.remoteProxyLoopbackAliasURL(for: url) else { return request }
|
||||
|
||||
var rewrittenRequest = request
|
||||
rewrittenRequest.url = rewrittenURL
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.remoteProxy.rewrite " +
|
||||
"panel=\(id.uuidString.prefix(5)) " +
|
||||
"from=\(url.absoluteString) " +
|
||||
"to=\(rewrittenURL.absoluteString)"
|
||||
)
|
||||
#endif
|
||||
return rewrittenRequest
|
||||
}
|
||||
|
||||
private static func remoteProxyLoopbackAliasURL(for url: URL) -> URL? {
|
||||
guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else { return nil }
|
||||
guard let host = BrowserInsecureHTTPSettings.normalizeHost(url.host ?? "") else { return nil }
|
||||
guard remoteLoopbackHosts.contains(host) else { return nil }
|
||||
|
||||
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
components?.host = remoteLoopbackProxyAliasHost
|
||||
return components?.url
|
||||
browserLoadRequest(request, in: webView)
|
||||
}
|
||||
|
||||
/// Navigate with smart URL/search detection
|
||||
|
|
@ -3576,16 +3481,6 @@ extension BrowserPanel {
|
|||
applyPageZoom(1.0)
|
||||
}
|
||||
|
||||
func currentPageZoomFactor() -> CGFloat {
|
||||
webView.pageZoom
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setPageZoomFactor(_ pageZoom: CGFloat) -> Bool {
|
||||
let clamped = max(minPageZoom, min(maxPageZoom, pageZoom))
|
||||
return applyPageZoom(clamped)
|
||||
}
|
||||
|
||||
/// Take a snapshot of the web view
|
||||
func takeSnapshot(completion: @escaping (NSImage?) -> Void) {
|
||||
let config = WKSnapshotConfiguration()
|
||||
|
|
@ -4400,13 +4295,6 @@ extension BrowserPanel {
|
|||
return "webFrame=\(Self.debugRectDescription(webFrame)) webBounds=\(Self.debugRectDescription(webView.bounds)) webWin=\(webView.window?.windowNumber ?? -1) super=\(Self.debugObjectToken(container)) superType=\(containerType) superBounds=\(Self.debugRectDescription(containerBounds)) inspectorHApprox=\(String(format: "%.1f", inspectorHeightApprox)) inspectorInsets=\(String(format: "%.1f", inspectorInsets)) inspectorOverflow=\(String(format: "%.1f", inspectorOverflow)) inspectorSubviews=\(inspectorSubviews)"
|
||||
}
|
||||
|
||||
func hideBrowserPortalView(source: String) {
|
||||
BrowserWindowPortalRegistry.hide(
|
||||
webView: webView,
|
||||
source: source
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
|||
|
|
@ -84,45 +84,20 @@ final class TerminalPanel: Panel, ObservableObject {
|
|||
context: ghostty_surface_context_e = GHOSTTY_SURFACE_CONTEXT_SPLIT,
|
||||
configTemplate: ghostty_surface_config_s? = nil,
|
||||
workingDirectory: String? = nil,
|
||||
portOrdinal: Int = 0,
|
||||
initialCommand: String? = nil,
|
||||
initialEnvironmentOverrides: [String: String] = [:],
|
||||
additionalEnvironment: [String: String] = [:]
|
||||
additionalEnvironment: [String: String] = [:],
|
||||
portOrdinal: Int = 0
|
||||
) {
|
||||
let surface = TerminalSurface(
|
||||
tabId: workspaceId,
|
||||
context: context,
|
||||
configTemplate: configTemplate,
|
||||
workingDirectory: workingDirectory,
|
||||
initialCommand: initialCommand,
|
||||
initialEnvironmentOverrides: Self.mergedNormalizedEnvironment(
|
||||
base: additionalEnvironment,
|
||||
overrides: initialEnvironmentOverrides
|
||||
)
|
||||
additionalEnvironment: additionalEnvironment
|
||||
)
|
||||
surface.portOrdinal = portOrdinal
|
||||
self.init(workspaceId: workspaceId, surface: surface)
|
||||
}
|
||||
|
||||
private static func mergedNormalizedEnvironment(
|
||||
base: [String: String],
|
||||
overrides: [String: String]
|
||||
) -> [String: String] {
|
||||
var merged: [String: String] = [:]
|
||||
merged.reserveCapacity(base.count + overrides.count)
|
||||
for (rawKey, value) in base {
|
||||
let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !key.isEmpty else { continue }
|
||||
merged[key] = value
|
||||
}
|
||||
for (rawKey, value) in overrides {
|
||||
let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !key.isEmpty else { continue }
|
||||
merged[key] = value
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func updateTitle(_ newTitle: String) {
|
||||
let trimmed = newTitle.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty && title != trimmed {
|
||||
|
|
|
|||
|
|
@ -406,18 +406,6 @@ struct SocketControlSettings {
|
|||
) -> String {
|
||||
let fallback = defaultSocketPath(bundleIdentifier: bundleIdentifier, isDebugBuild: isDebugBuild)
|
||||
|
||||
if let taggedDebugPath = taggedDebugSocketPath(
|
||||
bundleIdentifier: bundleIdentifier,
|
||||
environment: environment
|
||||
) {
|
||||
if isTruthy(environment[allowSocketPathOverrideKey]),
|
||||
let override = environment["CMUX_SOCKET_PATH"],
|
||||
!override.isEmpty {
|
||||
return override
|
||||
}
|
||||
return taggedDebugPath
|
||||
}
|
||||
|
||||
guard let override = environment["CMUX_SOCKET_PATH"], !override.isEmpty else {
|
||||
return fallback
|
||||
}
|
||||
|
|
@ -434,9 +422,6 @@ struct SocketControlSettings {
|
|||
}
|
||||
|
||||
static func defaultSocketPath(bundleIdentifier: String?, isDebugBuild: Bool) -> String {
|
||||
if let taggedDebugPath = taggedDebugSocketPath(bundleIdentifier: bundleIdentifier, environment: [:]) {
|
||||
return taggedDebugPath
|
||||
}
|
||||
if bundleIdentifier == "com.cmuxterm.app.nightly" {
|
||||
return "/tmp/cmux-nightly.sock"
|
||||
}
|
||||
|
|
@ -469,37 +454,6 @@ struct SocketControlSettings {
|
|||
|| bundleIdentifier.hasPrefix("com.cmuxterm.app.debug.")
|
||||
}
|
||||
|
||||
static func taggedDebugSocketPath(
|
||||
bundleIdentifier: String?,
|
||||
environment: [String: String]
|
||||
) -> String? {
|
||||
let bundleId = bundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if bundleId.hasPrefix("\(baseDebugBundleIdentifier).") {
|
||||
let suffix = String(bundleId.dropFirst(baseDebugBundleIdentifier.count + 1))
|
||||
let slug = suffix
|
||||
.replacingOccurrences(of: ".", with: "-")
|
||||
.trimmingCharacters(in: CharacterSet(charactersIn: "-"))
|
||||
if !slug.isEmpty {
|
||||
return "/tmp/cmux-debug-\(slug).sock"
|
||||
}
|
||||
}
|
||||
|
||||
let tag = launchTag(environment: environment)?
|
||||
.lowercased()
|
||||
.replacingOccurrences(of: ".", with: "-")
|
||||
.replacingOccurrences(of: "_", with: "-")
|
||||
.components(separatedBy: CharacterSet.alphanumerics.inverted)
|
||||
.filter { !$0.isEmpty }
|
||||
.joined(separator: "-")
|
||||
|
||||
guard bundleId == baseDebugBundleIdentifier,
|
||||
let tag,
|
||||
!tag.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return "/tmp/cmux-debug-\(tag).sock"
|
||||
}
|
||||
|
||||
static func isStagingBundleIdentifier(_ bundleIdentifier: String?) -> Bool {
|
||||
guard let bundleIdentifier else { return false }
|
||||
return bundleIdentifier == "com.cmuxterm.app.staging"
|
||||
|
|
|
|||
|
|
@ -30,20 +30,11 @@ enum NewWorkspacePlacement: String, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .top:
|
||||
return String(
|
||||
localized: "workspace.placement.top.description",
|
||||
defaultValue: "Insert new workspaces at the top of the list."
|
||||
)
|
||||
return String(localized: "workspace.placement.top.description", defaultValue: "Insert new workspaces at the top of the list.")
|
||||
case .afterCurrent:
|
||||
return String(
|
||||
localized: "workspace.placement.afterCurrent.description",
|
||||
defaultValue: "Insert new workspaces directly after the active workspace."
|
||||
)
|
||||
return String(localized: "workspace.placement.afterCurrent.description", defaultValue: "Insert new workspaces directly after the active workspace.")
|
||||
case .end:
|
||||
return String(
|
||||
localized: "workspace.placement.end.description",
|
||||
defaultValue: "Append new workspaces to the bottom of the list."
|
||||
)
|
||||
return String(localized: "workspace.placement.end.description", defaultValue: "Append new workspaces to the bottom of the list.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -81,9 +72,9 @@ enum SidebarActiveTabIndicatorStyle: String, CaseIterable, Identifiable {
|
|||
var displayName: String {
|
||||
switch self {
|
||||
case .leftRail:
|
||||
return "Left Rail"
|
||||
return String(localized: "sidebar.indicator.leftRail", defaultValue: "Left Rail")
|
||||
case .solidFill:
|
||||
return "Solid Fill"
|
||||
return String(localized: "sidebar.indicator.solidFill", defaultValue: "Solid Fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -741,25 +732,36 @@ class TabManager: ObservableObject {
|
|||
}
|
||||
|
||||
var isFindVisible: Bool {
|
||||
selectedTerminalPanel?.searchState != nil || focusedBrowserPanel?.searchState != nil
|
||||
if selectedTerminalPanel?.searchState != nil { return true }
|
||||
if focusedBrowserPanel?.searchState != nil { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
var canUseSelectionForFind: Bool {
|
||||
selectedTerminalPanel?.hasSelection() == true
|
||||
if focusedBrowserPanel != nil { return false }
|
||||
return selectedTerminalPanel?.hasSelection() == true
|
||||
}
|
||||
|
||||
func startSearch() {
|
||||
if let panel = selectedTerminalPanel {
|
||||
if panel.searchState == nil {
|
||||
panel.searchState = TerminalSurface.SearchState()
|
||||
}
|
||||
NSLog("Find: startSearch workspace=%@ panel=%@", panel.workspaceId.uuidString, panel.id.uuidString)
|
||||
NotificationCenter.default.post(name: .ghosttySearchFocus, object: panel.surface)
|
||||
_ = panel.performBindingAction("start_search")
|
||||
if let browser = focusedBrowserPanel {
|
||||
browser.startFind()
|
||||
return
|
||||
}
|
||||
|
||||
focusedBrowserPanel?.startFind()
|
||||
guard let panel = selectedTerminalPanel else {
|
||||
#if DEBUG
|
||||
dlog("find.startSearch SKIPPED no selectedTerminalPanel")
|
||||
#endif
|
||||
return
|
||||
}
|
||||
let wasNil = panel.searchState == nil
|
||||
if wasNil {
|
||||
panel.searchState = TerminalSurface.SearchState()
|
||||
}
|
||||
#if DEBUG
|
||||
dlog("find.startSearch workspace=\(panel.workspaceId.uuidString.prefix(5)) panel=\(panel.id.uuidString.prefix(5)) created=\(wasNil ? "yes" : "no(reuse)") firstResponder=\(String(describing: panel.surface.hostedView.window?.firstResponder))")
|
||||
#endif
|
||||
NotificationCenter.default.post(name: .ghosttySearchFocus, object: panel.surface)
|
||||
_ = panel.performBindingAction("start_search")
|
||||
}
|
||||
|
||||
func searchSelection() {
|
||||
|
|
@ -767,27 +769,27 @@ class TabManager: ObservableObject {
|
|||
if panel.searchState == nil {
|
||||
panel.searchState = TerminalSurface.SearchState()
|
||||
}
|
||||
NSLog("Find: searchSelection workspace=%@ panel=%@", panel.workspaceId.uuidString, panel.id.uuidString)
|
||||
#if DEBUG
|
||||
dlog("find.searchSelection workspace=\(panel.workspaceId.uuidString.prefix(5)) panel=\(panel.id.uuidString.prefix(5))")
|
||||
#endif
|
||||
NotificationCenter.default.post(name: .ghosttySearchFocus, object: panel.surface)
|
||||
_ = panel.performBindingAction("search_selection")
|
||||
}
|
||||
|
||||
func findNext() {
|
||||
if let panel = selectedTerminalPanel {
|
||||
_ = panel.performBindingAction("search:next")
|
||||
if let browser = focusedBrowserPanel, browser.searchState != nil {
|
||||
browser.findNext()
|
||||
return
|
||||
}
|
||||
|
||||
focusedBrowserPanel?.findNext()
|
||||
_ = selectedTerminalPanel?.performBindingAction("search:next")
|
||||
}
|
||||
|
||||
func findPrevious() {
|
||||
if let panel = selectedTerminalPanel {
|
||||
_ = panel.performBindingAction("search:previous")
|
||||
if let browser = focusedBrowserPanel, browser.searchState != nil {
|
||||
browser.findPrevious()
|
||||
return
|
||||
}
|
||||
|
||||
focusedBrowserPanel?.findPrevious()
|
||||
_ = selectedTerminalPanel?.performBindingAction("search:previous")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
|
@ -797,26 +799,27 @@ class TabManager: ObservableObject {
|
|||
}
|
||||
|
||||
func hideFind() {
|
||||
if let panel = selectedTerminalPanel {
|
||||
panel.searchState = nil
|
||||
if let browser = focusedBrowserPanel, browser.searchState != nil {
|
||||
browser.hideFind()
|
||||
return
|
||||
}
|
||||
|
||||
focusedBrowserPanel?.hideFind()
|
||||
#if DEBUG
|
||||
dlog("find.hideFind panel=\(selectedTerminalPanel?.id.uuidString.prefix(5) ?? "nil")")
|
||||
#endif
|
||||
selectedTerminalPanel?.searchState = nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addWorkspace(
|
||||
workingDirectory overrideWorkingDirectory: String? = nil,
|
||||
initialTerminalCommand: String? = nil,
|
||||
initialTerminalEnvironment: [String: String] = [:],
|
||||
select: Bool = true,
|
||||
eagerLoadTerminal: Bool = false,
|
||||
placementOverride: NewWorkspacePlacement? = nil,
|
||||
autoWelcomeIfNeeded: Bool = true
|
||||
) -> Workspace {
|
||||
sentryBreadcrumb("workspace.create", data: ["tabCount": tabs.count + 1])
|
||||
let workingDirectory = normalizedWorkingDirectory(overrideWorkingDirectory) ?? preferredWorkingDirectoryForNewTab()
|
||||
let explicitWorkingDirectory = normalizedWorkingDirectory(overrideWorkingDirectory)
|
||||
let workingDirectory = explicitWorkingDirectory ?? preferredWorkingDirectoryForNewTab()
|
||||
let inheritedConfig = inheritedTerminalConfigForNewWorkspace()
|
||||
let ordinal = Self.nextPortOrdinal
|
||||
Self.nextPortOrdinal += 1
|
||||
|
|
@ -824,9 +827,7 @@ class TabManager: ObservableObject {
|
|||
title: "Terminal \(tabs.count + 1)",
|
||||
workingDirectory: workingDirectory,
|
||||
portOrdinal: ordinal,
|
||||
configTemplate: inheritedConfig,
|
||||
initialTerminalCommand: initialTerminalCommand,
|
||||
initialTerminalEnvironment: initialTerminalEnvironment
|
||||
configTemplate: inheritedConfig
|
||||
)
|
||||
wireClosedBrowserTracking(for: newWorkspace)
|
||||
let insertIndex = newTabInsertIndex(placementOverride: placementOverride)
|
||||
|
|
@ -835,8 +836,17 @@ class TabManager: ObservableObject {
|
|||
} else {
|
||||
tabs.append(newWorkspace)
|
||||
}
|
||||
if let explicitWorkingDirectory,
|
||||
let terminalPanel = newWorkspace.focusedTerminalPanel {
|
||||
scheduleInitialWorkspaceGitMetadataRefresh(
|
||||
workspaceId: newWorkspace.id,
|
||||
panelId: terminalPanel.id,
|
||||
directory: explicitWorkingDirectory
|
||||
)
|
||||
}
|
||||
if eagerLoadTerminal {
|
||||
newWorkspace.focusedTerminalPanel?.surface.requestBackgroundSurfaceStartIfNeeded()
|
||||
requestBackgroundWorkspaceLoad(for: newWorkspace.id)
|
||||
newWorkspace.requestBackgroundTerminalSurfaceStartIfNeeded()
|
||||
}
|
||||
if select {
|
||||
selectedTabId = newWorkspace.id
|
||||
|
|
@ -1152,6 +1162,16 @@ class TabManager: ObservableObject {
|
|||
tabs.insert(tab, at: insertIndex)
|
||||
}
|
||||
|
||||
func moveTabToTopForNotification(_ tabId: UUID) {
|
||||
guard let index = tabs.firstIndex(where: { $0.id == tabId }) else { return }
|
||||
let pinnedCount = tabs.filter { $0.isPinned }.count
|
||||
guard index != pinnedCount else { return }
|
||||
let tab = tabs[index]
|
||||
guard !tab.isPinned else { return }
|
||||
tabs.remove(at: index)
|
||||
tabs.insert(tab, at: pinnedCount)
|
||||
}
|
||||
|
||||
func moveTabsToTop(_ tabIds: Set<UUID>) {
|
||||
guard !tabIds.isEmpty else { return }
|
||||
let selectedTabs = tabs.filter { tabIds.contains($0.id) }
|
||||
|
|
@ -1164,16 +1184,6 @@ class TabManager: ObservableObject {
|
|||
tabs = selectedPinned + remainingPinned + selectedUnpinned + remainingUnpinned
|
||||
}
|
||||
|
||||
func moveTabToTopForNotification(_ tabId: UUID) {
|
||||
guard let index = tabs.firstIndex(where: { $0.id == tabId }) else { return }
|
||||
let pinnedCount = tabs.filter { $0.isPinned }.count
|
||||
guard index != pinnedCount else { return }
|
||||
let tab = tabs[index]
|
||||
guard !tab.isPinned else { return }
|
||||
tabs.remove(at: index)
|
||||
tabs.insert(tab, at: pinnedCount)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func reorderWorkspace(tabId: UUID, toIndex targetIndex: Int) -> Bool {
|
||||
guard let currentIndex = tabs.firstIndex(where: { $0.id == tabId }) else { return false }
|
||||
|
|
@ -1259,23 +1269,22 @@ class TabManager: ObservableObject {
|
|||
|
||||
func closeWorkspace(_ workspace: Workspace) {
|
||||
guard tabs.count > 1 else { return }
|
||||
guard let index = tabs.firstIndex(where: { $0.id == workspace.id }) else { return }
|
||||
sentryBreadcrumb("workspace.close", data: ["tabCount": tabs.count - 1])
|
||||
clearInitialWorkspaceGitProbe(workspaceId: workspace.id)
|
||||
|
||||
AppDelegate.shared?.notificationStore?.clearNotifications(forTabId: workspace.id)
|
||||
workspace.teardownAllPanels()
|
||||
workspace.teardownRemoteConnection()
|
||||
unwireClosedBrowserTracking(for: workspace)
|
||||
workspace.teardownAllPanels()
|
||||
|
||||
if let index = tabs.firstIndex(where: { $0.id == workspace.id }) {
|
||||
tabs.remove(at: index)
|
||||
tabs.remove(at: index)
|
||||
|
||||
if selectedTabId == workspace.id {
|
||||
// Keep the "focused index" stable when possible:
|
||||
// - If we closed workspace i and there is still a workspace at index i, focus it (the one that moved up).
|
||||
// - Otherwise (we closed the last workspace), focus the new last workspace (i-1).
|
||||
let newIndex = min(index, max(0, tabs.count - 1))
|
||||
selectedTabId = tabs[newIndex].id
|
||||
}
|
||||
if selectedTabId == workspace.id {
|
||||
// Keep the "focused index" stable when possible:
|
||||
// - If we closed workspace i and there is still a workspace at index i, focus it (the one that moved up).
|
||||
// - Otherwise (we closed the last workspace), focus the new last workspace (i-1).
|
||||
let newIndex = min(index, max(0, tabs.count - 1))
|
||||
selectedTabId = tabs[newIndex].id
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1284,6 +1293,7 @@ class TabManager: ObservableObject {
|
|||
@discardableResult
|
||||
func detachWorkspace(tabId: UUID) -> Workspace? {
|
||||
guard let index = tabs.firstIndex(where: { $0.id == tabId }) else { return nil }
|
||||
clearInitialWorkspaceGitProbe(workspaceId: tabId)
|
||||
|
||||
let removed = tabs.remove(at: index)
|
||||
unwireClosedBrowserTracking(for: removed)
|
||||
|
|
@ -1345,9 +1355,13 @@ class TabManager: ObservableObject {
|
|||
|
||||
let count = plan.panelIds.count
|
||||
let titleLines = plan.titles.map { "• \($0)" }.joined(separator: "\n")
|
||||
let message = "This is about to close \(count) tab\(count == 1 ? "" : "s") in this pane:\n\(titleLines)"
|
||||
let message = if count == 1 {
|
||||
String(localized: "dialog.closeOtherTabs.message.one", defaultValue: "This will close 1 tab in this pane:\n\(titleLines)")
|
||||
} else {
|
||||
String(localized: "dialog.closeOtherTabs.message.other", defaultValue: "This will close \(count) tabs in this pane:\n\(titleLines)")
|
||||
}
|
||||
guard confirmClose(
|
||||
title: "Close other tabs?",
|
||||
title: String(localized: "dialog.closeOtherTabs.title", defaultValue: "Close other tabs?"),
|
||||
message: message,
|
||||
acceptCmdD: false
|
||||
) else { return }
|
||||
|
|
@ -1387,8 +1401,8 @@ class TabManager: ObservableObject {
|
|||
alert.messageText = title
|
||||
alert.informativeText = message
|
||||
alert.alertStyle = .warning
|
||||
alert.addButton(withTitle: "Close")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.addButton(withTitle: String(localized: "common.close", defaultValue: "Close"))
|
||||
alert.addButton(withTitle: String(localized: "common.cancel", defaultValue: "Cancel"))
|
||||
|
||||
// macOS convention: Cmd+D = confirm destructive close (e.g. "Don't Save").
|
||||
// We only opt into this for the "close last workspace => close window" path to avoid
|
||||
|
|
@ -1449,15 +1463,15 @@ class TabManager: ObservableObject {
|
|||
if let collapsed, !collapsed.isEmpty {
|
||||
return collapsed
|
||||
}
|
||||
return "Untitled Tab"
|
||||
return String(localized: "tab.untitled", defaultValue: "Untitled Tab")
|
||||
}
|
||||
|
||||
private func closeWorkspaceIfRunningProcess(_ workspace: Workspace) {
|
||||
let willCloseWindow = tabs.count <= 1
|
||||
if workspaceNeedsConfirmClose(workspace),
|
||||
!confirmClose(
|
||||
title: "Close workspace?",
|
||||
message: "This will close the workspace and all of its panels.",
|
||||
title: String(localized: "dialog.closeWorkspace.title", defaultValue: "Close workspace?"),
|
||||
message: String(localized: "dialog.closeWorkspace.message", defaultValue: "This will close the workspace and all of its panels."),
|
||||
acceptCmdD: willCloseWindow
|
||||
) {
|
||||
return
|
||||
|
|
@ -1498,8 +1512,8 @@ class TabManager: ObservableObject {
|
|||
let needsConfirm = workspaceNeedsConfirmClose(tab)
|
||||
if needsConfirm {
|
||||
let message = willCloseWindow
|
||||
? "This will close the last tab and close the window."
|
||||
: "This will close the last tab and close its workspace."
|
||||
? String(localized: "dialog.closeLastTabWindow.message", defaultValue: "This will close the last tab and close the window.")
|
||||
: String(localized: "dialog.closeLastTabWorkspace.message", defaultValue: "This will close the last tab and close its workspace.")
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"surface.close.shortcut.confirm tab=\(tab.id.uuidString.prefix(5)) " +
|
||||
|
|
@ -1507,7 +1521,7 @@ class TabManager: ObservableObject {
|
|||
)
|
||||
#endif
|
||||
guard confirmClose(
|
||||
title: "Close tab?",
|
||||
title: String(localized: "dialog.closeTab.title", defaultValue: "Close tab?"),
|
||||
message: message,
|
||||
acceptCmdD: willCloseWindow
|
||||
) else {
|
||||
|
|
@ -1539,8 +1553,8 @@ class TabManager: ObservableObject {
|
|||
)
|
||||
#endif
|
||||
guard confirmClose(
|
||||
title: "Close tab?",
|
||||
message: "This will close the current tab.",
|
||||
title: String(localized: "dialog.closeTab.title", defaultValue: "Close tab?"),
|
||||
message: String(localized: "dialog.closeTab.message", defaultValue: "This will close the current tab."),
|
||||
acceptCmdD: false
|
||||
) else {
|
||||
#if DEBUG
|
||||
|
|
@ -1578,8 +1592,8 @@ class TabManager: ObservableObject {
|
|||
if let terminalPanel = tab.terminalPanel(for: surfaceId),
|
||||
terminalPanel.needsConfirmClose() {
|
||||
guard confirmClose(
|
||||
title: "Close tab?",
|
||||
message: "This will close the current tab.",
|
||||
title: String(localized: "dialog.closeTab.title", defaultValue: "Close tab?"),
|
||||
message: String(localized: "dialog.closeTab.message", defaultValue: "This will close the current tab."),
|
||||
acceptCmdD: false
|
||||
) else { return }
|
||||
}
|
||||
|
|
@ -1846,28 +1860,32 @@ class TabManager: ObservableObject {
|
|||
guard !shouldSuppressFlash else { return }
|
||||
guard AppFocusState.isAppActive() else { return }
|
||||
guard let panelId = focusedPanelId(for: tabId) else { return }
|
||||
markPanelReadOnFocusIfActive(tabId: tabId, panelId: panelId)
|
||||
_ = dismissNotificationIfActive(tabId: tabId, surfaceId: panelId, triggerFlash: true)
|
||||
}
|
||||
|
||||
private func markPanelReadOnFocusIfActive(tabId: UUID, panelId: UUID) {
|
||||
guard selectedTabId == tabId else { return }
|
||||
guard !suppressFocusFlash else { return }
|
||||
guard AppFocusState.isAppActive() else { return }
|
||||
guard let notificationStore = AppDelegate.shared?.notificationStore else { return }
|
||||
guard notificationStore.hasUnreadNotification(forTabId: tabId, surfaceId: panelId) else { return }
|
||||
if let tab = tabs.first(where: { $0.id == tabId }) {
|
||||
tab.triggerNotificationFocusFlash(panelId: panelId, requiresSplit: false, shouldFocus: false)
|
||||
}
|
||||
notificationStore.markRead(forTabId: tabId, surfaceId: panelId)
|
||||
_ = dismissNotificationIfActive(tabId: tabId, surfaceId: panelId, triggerFlash: true)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func dismissNotificationOnDirectInteraction(tabId: UUID, surfaceId: UUID?) -> Bool {
|
||||
dismissNotificationIfActive(tabId: tabId, surfaceId: surfaceId, triggerFlash: true)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func dismissNotificationIfActive(
|
||||
tabId: UUID,
|
||||
surfaceId: UUID?,
|
||||
triggerFlash: Bool
|
||||
) -> Bool {
|
||||
guard selectedTabId == tabId else { return false }
|
||||
guard AppFocusState.isAppActive() else { return false }
|
||||
guard let notificationStore = AppDelegate.shared?.notificationStore else { return false }
|
||||
guard notificationStore.hasUnreadNotification(forTabId: tabId, surfaceId: surfaceId) else { return false }
|
||||
if let panelId = surfaceId,
|
||||
if triggerFlash,
|
||||
let panelId = surfaceId,
|
||||
let tab = tabs.first(where: { $0.id == tabId }) {
|
||||
tab.triggerNotificationFocusFlash(panelId: panelId, requiresSplit: false, shouldFocus: false)
|
||||
}
|
||||
|
|
@ -2166,9 +2184,24 @@ class TabManager: ObservableObject {
|
|||
guard let selectedTabId,
|
||||
let tab = tabs.first(where: { $0.id == selectedTabId }),
|
||||
let focusedPanelId = tab.focusedPanelId else { return }
|
||||
#if DEBUG
|
||||
let directionLabel = direction.debugLabel
|
||||
dlog(
|
||||
"split.create.request kind=terminal dir=\(directionLabel) " +
|
||||
"tab=\(selectedTabId.uuidString.prefix(5)) panel=\(focusedPanelId.uuidString.prefix(5)) " +
|
||||
"panels=\(tab.panels.count) panes=\(tab.bonsplitController.allPaneIds.count)"
|
||||
)
|
||||
#endif
|
||||
tab.clearSplitZoom()
|
||||
sentryBreadcrumb("split.create", data: ["direction": String(describing: direction)])
|
||||
_ = newSplit(tabId: selectedTabId, surfaceId: focusedPanelId, direction: direction)
|
||||
let createdPanelId = newSplit(tabId: selectedTabId, surfaceId: focusedPanelId, direction: direction)
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"split.create.result kind=terminal dir=\(directionLabel) " +
|
||||
"created=\(createdPanelId?.uuidString.prefix(5) ?? "nil") " +
|
||||
"panels=\(tab.panels.count) panes=\(tab.bonsplitController.allPaneIds.count)"
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Create a new browser split from the currently focused panel.
|
||||
|
|
@ -2177,14 +2210,30 @@ class TabManager: ObservableObject {
|
|||
guard let selectedTabId,
|
||||
let tab = tabs.first(where: { $0.id == selectedTabId }),
|
||||
let focusedPanelId = tab.focusedPanelId else { return nil }
|
||||
#if DEBUG
|
||||
let directionLabel = direction.debugLabel
|
||||
dlog(
|
||||
"split.create.request kind=browser dir=\(directionLabel) " +
|
||||
"tab=\(selectedTabId.uuidString.prefix(5)) panel=\(focusedPanelId.uuidString.prefix(5)) " +
|
||||
"panels=\(tab.panels.count) panes=\(tab.bonsplitController.allPaneIds.count)"
|
||||
)
|
||||
#endif
|
||||
tab.clearSplitZoom()
|
||||
return newBrowserSplit(
|
||||
let createdPanelId = newBrowserSplit(
|
||||
tabId: selectedTabId,
|
||||
fromPanelId: focusedPanelId,
|
||||
orientation: direction.orientation,
|
||||
insertFirst: direction.insertFirst,
|
||||
url: url
|
||||
)
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"split.create.result kind=browser dir=\(directionLabel) " +
|
||||
"created=\(createdPanelId?.uuidString.prefix(5) ?? "nil") " +
|
||||
"panels=\(tab.panels.count) panes=\(tab.bonsplitController.allPaneIds.count)"
|
||||
)
|
||||
#endif
|
||||
return createdPanelId
|
||||
}
|
||||
|
||||
/// Refresh Bonsplit right-side action button tooltips for all workspaces.
|
||||
|
|
@ -2285,12 +2334,21 @@ class TabManager: ObservableObject {
|
|||
/// Returns the new panel's ID (which is also the surface ID for terminals)
|
||||
func newSplit(tabId: UUID, surfaceId: UUID, direction: SplitDirection, focus: Bool = true) -> UUID? {
|
||||
guard let tab = tabs.first(where: { $0.id == tabId }) else { return nil }
|
||||
return tab.newTerminalSplit(
|
||||
let createdPanel = tab.newTerminalSplit(
|
||||
from: surfaceId,
|
||||
orientation: direction.orientation,
|
||||
insertFirst: direction.insertFirst,
|
||||
focus: focus
|
||||
)?.id
|
||||
#if DEBUG
|
||||
let directionLabel = direction.debugLabel
|
||||
dlog(
|
||||
"split.newSurface result dir=\(directionLabel) " +
|
||||
"tab=\(tabId.uuidString.prefix(5)) source=\(surfaceId.uuidString.prefix(5)) " +
|
||||
"created=\(createdPanel?.uuidString.prefix(5) ?? "nil") focus=\(focus ? 1 : 0)"
|
||||
)
|
||||
#endif
|
||||
return createdPanel
|
||||
}
|
||||
|
||||
/// Move focus in the specified direction
|
||||
|
|
@ -2891,7 +2949,7 @@ class TabManager: ObservableObject {
|
|||
continue
|
||||
}
|
||||
terminal.hostedView.reconcileGeometryNow()
|
||||
terminal.surface.forceRefresh()
|
||||
terminal.surface.forceRefresh(reason: "tabManager.reconcileVisibleTerminalGeometry")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3869,6 +3927,15 @@ enum SplitDirection {
|
|||
var insertFirst: Bool {
|
||||
self == .left || self == .up
|
||||
}
|
||||
|
||||
var debugLabel: String {
|
||||
switch self {
|
||||
case .left: return "left"
|
||||
case .right: return "right"
|
||||
case .up: return "up"
|
||||
case .down: return "down"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize direction for backwards compatibility
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -94,7 +94,7 @@ final class WindowToolbarController: NSObject, NSToolbarDelegate {
|
|||
let text: String
|
||||
if let selectedId = tabManager.selectedTabId,
|
||||
let tab = tabManager.tabs.first(where: { $0.id == selectedId }) {
|
||||
let title = tab.title.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
let title = tab.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
text = title.isEmpty ? "Cmd: —" : "Cmd: \(title)"
|
||||
} else {
|
||||
text = "Cmd: —"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3086,7 +3086,6 @@ struct SettingsView: View {
|
|||
private var openSidebarPullRequestLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenSidebarPullRequestLinksInCmuxBrowser
|
||||
@AppStorage(ShortcutHintDebugSettings.showHintsOnCommandHoldKey)
|
||||
private var showShortcutHintsOnCommandHold = ShortcutHintDebugSettings.defaultShowHintsOnCommandHold
|
||||
@AppStorage("sidebarShowSSH") private var sidebarShowSSH = true
|
||||
@AppStorage("sidebarShowPorts") private var sidebarShowPorts = true
|
||||
@AppStorage("sidebarShowLog") private var sidebarShowLog = true
|
||||
@AppStorage("sidebarShowProgress") private var sidebarShowProgress = true
|
||||
|
|
@ -3698,17 +3697,6 @@ struct SettingsView: View {
|
|||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardRow(
|
||||
String(localized: "settings.app.showSSH", defaultValue: "Show SSH in Sidebar"),
|
||||
subtitle: String(localized: "settings.app.showSSH.subtitle", defaultValue: "Display the SSH target for remote workspaces in its own row.")
|
||||
) {
|
||||
Toggle("", isOn: $sidebarShowSSH)
|
||||
.labelsHidden()
|
||||
.controlSize(.small)
|
||||
}
|
||||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardRow(
|
||||
String(localized: "settings.app.showPorts", defaultValue: "Show Listening Ports in Sidebar"),
|
||||
subtitle: String(localized: "settings.app.showPorts.subtitle", defaultValue: "Display detected listening ports for the active workspace.")
|
||||
|
|
@ -4394,7 +4382,6 @@ struct SettingsView: View {
|
|||
sidebarShowPullRequest = true
|
||||
openSidebarPullRequestLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenSidebarPullRequestLinksInCmuxBrowser
|
||||
showShortcutHintsOnCommandHold = ShortcutHintDebugSettings.defaultShowHintsOnCommandHold
|
||||
sidebarShowSSH = true
|
||||
sidebarShowPorts = true
|
||||
sidebarShowLog = true
|
||||
sidebarShowProgress = true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue