Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue-2401-vscode-web-local-folders

This commit is contained in:
austinpower1258 2026-03-31 00:14:46 -07:00
commit a1352e0bfb
11 changed files with 60 additions and 1028 deletions

View file

@ -61,7 +61,6 @@ _CMUX_ASYNC_JOB_TIMEOUT="${_CMUX_ASYNC_JOB_TIMEOUT:-20}"
_CMUX_PORTS_LAST_RUN="${_CMUX_PORTS_LAST_RUN:-0}"
_CMUX_SHELL_ACTIVITY_LAST="${_CMUX_SHELL_ACTIVITY_LAST:-}"
_CMUX_TMUX_STATE_SIGNATURE_LAST="${_CMUX_TMUX_STATE_SIGNATURE_LAST:-}"
_CMUX_TTY_NAME="${_CMUX_TTY_NAME:-}"
_CMUX_TTY_REPORTED="${_CMUX_TTY_REPORTED:-0}"
_CMUX_TMUX_PUSH_SIGNATURE="${_CMUX_TMUX_PUSH_SIGNATURE:-}"
@ -265,47 +264,6 @@ _cmux_report_shell_activity_state() {
} >/dev/null 2>&1 & disown
}
_cmux_report_tmux_state_payload() {
[[ -n "$CMUX_TAB_ID" ]] || return 0
local state="outside"
[[ -n "$TMUX" ]] && state="inside"
local payload="report_tmux_state $state --tab=$CMUX_TAB_ID"
if [[ -n "$TMUX" ]]; then
[[ -n "$_CMUX_TTY_NAME" ]] && payload+=" --tty=$_CMUX_TTY_NAME"
else
[[ -n "$CMUX_PANEL_ID" ]] || return 0
payload+=" --panel=$CMUX_PANEL_ID"
fi
printf '%s\n' "$payload"
}
_cmux_tmux_state_report_signature() {
local payload="$1"
[[ -n "$payload" ]] || return 0
[[ -n "$CMUX_SOCKET_PATH" ]] || return 0
printf '%s\037%s\n' "$CMUX_SOCKET_PATH" "$payload"
}
_cmux_report_tmux_state() {
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
local payload=""
payload="$(_cmux_report_tmux_state_payload)"
[[ -n "$payload" ]] || return 0
local signature=""
signature="$(_cmux_tmux_state_report_signature "$payload")"
[[ -n "$signature" ]] || return 0
[[ "$_CMUX_TMUX_STATE_SIGNATURE_LAST" == "$signature" ]] && return 0
_CMUX_TMUX_STATE_SIGNATURE_LAST="$signature"
{
_cmux_send "$payload"
} >/dev/null 2>&1 & disown
}
_cmux_ports_kick() {
# Lightweight: just tell the app to run a batched scan for this panel.
# The app coalesces kicks across all panels and runs a single ps+lsof.
@ -543,6 +501,7 @@ _cmux_preexec_command() {
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
if [[ -z "$_CMUX_TTY_NAME" ]]; then
local t
@ -551,12 +510,8 @@ _cmux_preexec_command() {
[[ -n "$t" && "$t" != "not a tty" ]] && _CMUX_TTY_NAME="$t"
fi
if [[ -n "$CMUX_PANEL_ID" ]]; then
_cmux_report_shell_activity_state running
fi
_cmux_report_tmux_state
_cmux_report_shell_activity_state running
_cmux_report_tty_once
[[ -n "$CMUX_PANEL_ID" ]] || return 0
_cmux_ports_kick
_cmux_stop_pr_poll_loop
}
@ -570,21 +525,8 @@ _cmux_prompt_command() {
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
if [[ -z "$_CMUX_TTY_NAME" ]]; then
local t
t="$(tty 2>/dev/null || true)"
t="${t##*/}"
[[ "$t" != "not a tty" ]] && _CMUX_TTY_NAME="$t"
fi
if [[ -n "$CMUX_PANEL_ID" ]]; then
_cmux_report_shell_activity_state prompt
fi
_cmux_report_tmux_state
_cmux_report_tty_once
[[ -n "$CMUX_PANEL_ID" ]] || return 0
_cmux_report_shell_activity_state prompt
local now=$SECONDS
local pwd="$PWD"
@ -601,6 +543,16 @@ _cmux_prompt_command() {
fi
fi
# Resolve TTY name once.
if [[ -z "$_CMUX_TTY_NAME" ]]; then
local t
t="$(tty 2>/dev/null || true)"
t="${t##*/}"
[[ "$t" != "not a tty" ]] && _CMUX_TTY_NAME="$t"
fi
_cmux_report_tty_once
# CWD: keep the app in sync with the actual shell directory.
if [[ "$pwd" != "$_CMUX_PWD_LAST_PWD" ]]; then
_CMUX_PWD_LAST_PWD="$pwd"

View file

@ -74,7 +74,6 @@ typeset -g _CMUX_ASYNC_JOB_TIMEOUT=20
typeset -g _CMUX_PORTS_LAST_RUN=0
typeset -g _CMUX_CMD_START=0
typeset -g _CMUX_SHELL_ACTIVITY_LAST=""
typeset -g _CMUX_TMUX_STATE_SIGNATURE_LAST=""
typeset -g _CMUX_TTY_NAME=""
typeset -g _CMUX_TTY_REPORTED=0
typeset -g _CMUX_GHOSTTY_SEMANTIC_PATCHED=0
@ -370,45 +369,6 @@ _cmux_report_shell_activity_state() {
_cmux_send_bg "report_shell_state $state --tab=$CMUX_TAB_ID --panel=$CMUX_PANEL_ID"
}
_cmux_report_tmux_state_payload() {
[[ -n "$CMUX_TAB_ID" ]] || return 0
local state="outside"
[[ -n "$TMUX" ]] && state="inside"
local payload="report_tmux_state $state --tab=$CMUX_TAB_ID"
if [[ -n "$TMUX" ]]; then
[[ -n "$_CMUX_TTY_NAME" ]] && payload+=" --tty=$_CMUX_TTY_NAME"
else
[[ -n "$CMUX_PANEL_ID" ]] || return 0
payload+=" --panel=$CMUX_PANEL_ID"
fi
print -r -- "$payload"
}
_cmux_tmux_state_report_signature() {
local payload="$1"
[[ -n "$payload" ]] || return 0
[[ -n "$CMUX_SOCKET_PATH" ]] || return 0
print -r -- "${CMUX_SOCKET_PATH}"$'\x1f'"${payload}"
}
_cmux_report_tmux_state() {
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
local payload=""
payload="$(_cmux_report_tmux_state_payload)"
[[ -n "$payload" ]] || return 0
local signature=""
signature="$(_cmux_tmux_state_report_signature "$payload")"
[[ -n "$signature" ]] || return 0
[[ "$_CMUX_TMUX_STATE_SIGNATURE_LAST" == "$signature" ]] && return 0
_CMUX_TMUX_STATE_SIGNATURE_LAST="$signature"
_cmux_send_bg "$payload"
}
_cmux_ports_kick() {
# Lightweight: just tell the app to run a batched scan for this panel.
# The app coalesces kicks across all panels and runs a single ps+lsof.
@ -708,7 +668,6 @@ _cmux_preexec() {
_CMUX_CMD_START=$EPOCHSECONDS
_cmux_report_shell_activity_state running
_cmux_report_tmux_state
# Heuristic: commands that may change git branch/dirty state without changing $PWD.
local cmd="${1## }"
@ -732,6 +691,8 @@ _cmux_precmd() {
# Skip if socket doesn't exist yet
[[ -S "$CMUX_SOCKET_PATH" ]] || return 0
[[ -n "$CMUX_TAB_ID" ]] || return 0
[[ -n "$CMUX_PANEL_ID" ]] || return 0
_cmux_report_shell_activity_state prompt
# Handle cases where Ghostty integration initializes after this file.
(( _CMUX_GHOSTTY_SEMANTIC_PATCHED )) || _cmux_patch_ghostty_semantic_redraw
@ -743,14 +704,8 @@ _cmux_precmd() {
[[ -n "$t" && "$t" != "not a tty" ]] && _CMUX_TTY_NAME="$t"
fi
if [[ -n "$CMUX_PANEL_ID" ]]; then
_cmux_report_shell_activity_state prompt
fi
_cmux_report_tmux_state
_cmux_report_tty_once
[[ -n "$CMUX_PANEL_ID" ]] || return 0
local now=$EPOCHSECONDS
local pwd="$PWD"
local cmd_start="$_CMUX_CMD_START"

View file

@ -980,7 +980,6 @@ private final class WindowCommandPaletteOverlayController: NSObject {
private var focusLockTimer: DispatchSourceTimer?
private var scheduledFocusWorkItem: DispatchWorkItem?
private var isPaletteVisible = false
private var currentRenderFingerprint: Int?
private var windowDidBecomeKeyObserver: NSObjectProtocol?
private var windowDidResignKeyObserver: NSObjectProtocol?
@ -1231,7 +1230,7 @@ private final class WindowCommandPaletteOverlayController: NSObject {
editor.setSelectedRange(NSRange(location: length, length: 0))
}
func update(rootView: AnyView, isVisible: Bool, renderFingerprint: Int) {
func update(rootView: AnyView, isVisible: Bool) {
guard ensureInstalled() else { return }
let shouldPromote = CommandPaletteOverlayPromotionPolicy.shouldPromote(
previouslyVisible: isPaletteVisible,
@ -1239,10 +1238,7 @@ private final class WindowCommandPaletteOverlayController: NSObject {
)
isPaletteVisible = isVisible
if isVisible {
if currentRenderFingerprint != renderFingerprint {
hostingView.rootView = rootView
currentRenderFingerprint = renderFingerprint
}
hostingView.rootView = rootView
containerView.capturesMouseEvents = true
containerView.isHidden = false
containerView.alphaValue = 1
@ -1256,7 +1252,6 @@ private final class WindowCommandPaletteOverlayController: NSObject {
_ = window.makeFirstResponder(nil)
}
hostingView.rootView = AnyView(EmptyView())
currentRenderFingerprint = nil
containerView.capturesMouseEvents = false
containerView.alphaValue = 0
containerView.isHidden = true
@ -1732,53 +1727,12 @@ struct ContentView: View {
let shortcutHint: String?
let kindLabel: String?
let keywords: [String]
let liveTitleWorkspace: Workspace?
let dismissOnRun: Bool
let action: () -> Void
init(
id: String,
rank: Int,
title: String,
subtitle: String,
shortcutHint: String?,
kindLabel: String?,
keywords: [String],
liveTitleWorkspace: Workspace? = nil,
dismissOnRun: Bool,
action: @escaping () -> Void
) {
self.id = id
self.rank = rank
self.title = title
self.subtitle = subtitle
self.shortcutHint = shortcutHint
self.kindLabel = kindLabel
self.keywords = keywords
self.liveTitleWorkspace = liveTitleWorkspace
self.dismissOnRun = dismissOnRun
self.action = action
}
var searchableTexts: [String] {
[title, subtitle] + keywords
}
func displayTitle() -> String {
guard let liveTitleWorkspace else { return title }
return ContentView.commandPaletteWorkspaceDisplayName(liveTitleWorkspace)
}
func displayTitleMatchIndices(
matchingQuery: String,
fallbackIndices: Set<Int>
) -> Set<Int> {
guard liveTitleWorkspace != nil else { return fallbackIndices }
return CommandPaletteFuzzyMatcher.matchCharacterIndices(
query: matchingQuery,
candidate: displayTitle()
)
}
}
private struct CommandPaletteUsageEntry: Codable, Sendable {
@ -2032,39 +1986,6 @@ struct ContentView: View {
var id: String { command.id }
}
private struct CommandPaletteLiveWorkspaceResultLabel: View {
@ObservedObject private var workspace: Workspace
private let command: CommandPaletteCommand
private let matchingQuery: String
private let fallbackMatchIndices: Set<Int>
private let trailingLabel: CommandPaletteTrailingLabel?
init(
workspace: Workspace,
command: CommandPaletteCommand,
matchingQuery: String,
fallbackMatchIndices: Set<Int>,
trailingLabel: CommandPaletteTrailingLabel?
) {
_workspace = ObservedObject(wrappedValue: workspace)
self.command = command
self.matchingQuery = matchingQuery
self.fallbackMatchIndices = fallbackMatchIndices
self.trailingLabel = trailingLabel
}
var body: some View {
ContentView.commandPaletteResultLabelContent(
title: command.displayTitle(),
matchedIndices: command.displayTitleMatchIndices(
matchingQuery: matchingQuery,
fallbackIndices: fallbackMatchIndices
),
trailingLabel: trailingLabel
)
}
}
private struct CommandPaletteResolvedSearchMatch: Sendable {
let commandID: String
let score: Int
@ -2080,6 +2001,7 @@ struct ContentView: View {
struct CommandPaletteSwitcherFingerprintWorkspace: Sendable {
let id: UUID
let displayName: String
let metadata: CommandPaletteSwitcherSearchMetadata
let surfaces: [CommandPaletteSwitcherFingerprintSurface]
}
@ -3114,29 +3036,10 @@ struct ContentView: View {
let tmuxOverlayController = tmuxWorkspacePaneWindowOverlayController(for: window)
tmuxOverlayController.update(state: tmuxWorkspacePaneWindowOverlayState(for: window))
let overlayController = commandPaletteWindowOverlayController(for: window)
overlayController.update(
rootView: AnyView(commandPaletteOverlay),
isVisible: isCommandPalettePresented,
renderFingerprint: commandPaletteOverlayRenderFingerprint
)
overlayController.update(rootView: AnyView(commandPaletteOverlay), isVisible: isCommandPalettePresented)
}
}))
view = AnyView(view.onChange(of: commandPaletteCurrentSearchFingerprint) { _ in
guard isCommandPalettePresented, case .commands = commandPaletteMode else { return }
Task { @MainActor in
// Let the query-state transition settle first so the forced corpus refresh
// cannot rebuild the old command list after deleting the ">" prefix.
await Task.yield()
scheduleCommandPaletteResultsRefresh(
query: commandPaletteQuery,
forceSearchCorpusRefresh: true
)
updateCommandPaletteScrollTarget(resultCount: commandPaletteVisibleResults.count, animated: false)
syncCommandPaletteDebugStateForObservedWindow()
}
})
view = AnyView(view.onChange(of: bgGlassTintHex) { _ in
updateWindowGlassTint()
})
@ -3775,23 +3678,11 @@ struct ContentView: View {
Button {
runCommandPaletteResult(commandID: result.id)
} label: {
Group {
if let liveTitleWorkspace = result.command.liveTitleWorkspace {
CommandPaletteLiveWorkspaceResultLabel(
workspace: liveTitleWorkspace,
command: result.command,
matchingQuery: commandPaletteQueryForMatching,
fallbackMatchIndices: result.titleMatchIndices,
trailingLabel: trailingLabel
)
} else {
Self.commandPaletteResultLabelContent(
title: result.command.title,
matchedIndices: result.titleMatchIndices,
trailingLabel: trailingLabel
)
}
}
Self.commandPaletteResultLabelContent(
title: result.command.title,
matchedIndices: result.titleMatchIndices,
trailingLabel: trailingLabel
)
.padding(.horizontal, 9)
.padding(.vertical, 2)
.frame(maxWidth: .infinity, alignment: .leading)
@ -3863,6 +3754,19 @@ struct ContentView: View {
updateCommandPaletteScrollTarget(resultCount: commandPaletteVisibleResults.count, animated: false)
syncCommandPaletteDebugStateForObservedWindow()
}
.onChange(of: commandPaletteCurrentSearchFingerprint) { _ in
Task { @MainActor in
// Let the query-state transition settle first so the forced corpus refresh
// cannot rebuild the old command list after deleting the ">" prefix.
await Task.yield()
scheduleCommandPaletteResultsRefresh(
query: commandPaletteQuery,
forceSearchCorpusRefresh: true
)
updateCommandPaletteScrollTarget(resultCount: commandPaletteVisibleResults.count, animated: false)
syncCommandPaletteDebugStateForObservedWindow()
}
}
.onChange(of: commandPaletteResultsRevision) { _ in
let resultIDs = cachedCommandPaletteResults.map(\.id)
commandPaletteSelectedResultIndex = Self.commandPaletteResolvedSelectionIndex(
@ -4235,78 +4139,6 @@ struct ContentView: View {
)
}
private var commandPaletteOverlayRenderFingerprint: Int {
var hasher = Hasher()
switch commandPaletteMode {
case .commands:
hasher.combine("commands")
case .renameInput(let target):
hasher.combine("renameInput")
combineCommandPaletteRenameTarget(target, into: &hasher)
case .renameConfirm(let target, let proposedName):
hasher.combine("renameConfirm")
combineCommandPaletteRenameTarget(target, into: &hasher)
hasher.combine(proposedName)
}
hasher.combine(commandPaletteQuery)
hasher.combine(commandPaletteRenameDraft)
hasher.combine(commandPaletteListScope.rawValue)
hasher.combine(commandPaletteSearchAllSurfaces)
hasher.combine(commandPaletteSelectedResultIndex)
hasher.combine(commandPaletteSelectionAnchorCommandID)
hasher.combine(commandPaletteHoveredResultIndex)
hasher.combine(commandPaletteScrollTargetIndex)
hasher.combine(commandPaletteScrollTargetAnchor?.x)
hasher.combine(commandPaletteScrollTargetAnchor?.y)
hasher.combine(commandPaletteVisibleResultsScope?.rawValue)
hasher.combine(commandPaletteVisibleResultsFingerprint)
hasher.combine(cachedCommandPaletteScope?.rawValue)
hasher.combine(cachedCommandPaletteFingerprint)
hasher.combine(isCommandPaletteSearchPending)
hasher.combine(commandPaletteSearchRequestID)
hasher.combine(commandPaletteResolvedSearchRequestID)
hasher.combine(commandPaletteResolvedSearchScope?.rawValue)
hasher.combine(commandPaletteResolvedSearchFingerprint)
hasher.combine(commandPaletteResolvedMatchingQuery)
hasher.combine(commandPaletteResultsRevision)
combineCommandPalettePendingActivation(commandPalettePendingActivation, into: &hasher)
return hasher.finalize()
}
private func combineCommandPaletteRenameTarget(
_ target: CommandPaletteRenameTarget,
into hasher: inout Hasher
) {
switch target.kind {
case .workspace(let workspaceId):
hasher.combine("workspace")
hasher.combine(workspaceId)
case .tab(let workspaceId, let panelId):
hasher.combine("tab")
hasher.combine(workspaceId)
hasher.combine(panelId)
}
}
private func combineCommandPalettePendingActivation(
_ activation: CommandPalettePendingActivation?,
into hasher: inout Hasher
) {
switch activation {
case .selected(let requestID, let fallbackSelectedIndex, let preferredCommandID):
hasher.combine("selected")
hasher.combine(requestID)
hasher.combine(fallbackSelectedIndex)
hasher.combine(preferredCommandID)
case .command(let requestID, let commandID):
hasher.combine("command")
hasher.combine(requestID)
hasher.combine(commandID)
case nil:
hasher.combine("none")
}
}
nonisolated private static func commandPaletteListScope(for query: String) -> CommandPaletteListScope {
if query.hasPrefix(Self.commandPaletteCommandsPrefix) {
return .commands
@ -4686,6 +4518,7 @@ struct ContentView: View {
}
return currentMatchingQuery == resolvedMatchingQuery
|| currentMatchingQuery.hasPrefix(resolvedMatchingQuery)
}
private func scheduleCommandPaletteResultsRefresh(
@ -4853,6 +4686,7 @@ struct ContentView: View {
workspaces: commandPaletteOrderedSwitcherWorkspaces(for: context).map { workspace in
CommandPaletteSwitcherFingerprintWorkspace(
id: workspace.id,
displayName: workspaceDisplayName(workspace),
metadata: commandPaletteWorkspaceSearchMetadata(for: workspace),
surfaces: includeSurfaces
? commandPaletteOrderedSwitcherPanels(for: workspace).compactMap { panelId in
@ -5005,7 +4839,6 @@ struct ContentView: View {
shortcutHint: nil,
kindLabel: String(localized: "commandPalette.kind.workspace", defaultValue: "Workspace"),
keywords: workspaceKeywords,
liveTitleWorkspace: workspace,
dismissOnRun: true,
action: {
focusCommandPaletteSwitcherTarget(
@ -6722,7 +6555,7 @@ struct ContentView: View {
hasher.combine(context.workspaces.count)
for workspace in context.workspaces {
hasher.combine(workspace.id)
// Keep animated workspace titles from invalidating the live switcher corpus.
hasher.combine(workspace.displayName)
combineCommandPaletteSwitcherSearchMetadata(workspace.metadata, into: &hasher)
hasher.combine(workspace.surfaces.count)
for surface in workspace.surfaces {
@ -7061,7 +6894,7 @@ struct ContentView: View {
let rows = Array(commandPaletteVisibleResults.prefix(20)).map { result in
CommandPaletteDebugResultRow(
commandId: result.command.id,
title: result.command.displayTitle(),
title: result.command.title,
shortcutHint: result.command.shortcutHint,
trailingLabel: commandPaletteTrailingLabel(for: result.command)?.text,
score: result.score

View file

@ -949,7 +949,6 @@ class GhosttyApp {
private var backgroundEventCounter: UInt64 = 0
private var defaultBackgroundUpdateScope: GhosttyDefaultBackgroundUpdateScope = .unscoped
private var defaultBackgroundScopeSource: String = "initialize"
private(set) var userConfigDefinesShiftEnterBinding = false
private var lastAppearanceColorScheme: GhosttyConfig.ColorSchemePreference?
private lazy var defaultBackgroundNotificationDispatcher: GhosttyDefaultBackgroundNotificationDispatcher =
// Theme chrome should track terminal theme changes in the same frame.
@ -1394,7 +1393,6 @@ class GhosttyApp {
loadLegacyGhosttyConfigIfNeeded(config)
ghostty_config_load_recursive_files(config)
loadCmuxAppSupportGhosttyConfigIfNeeded(config)
userConfigDefinesShiftEnterBinding = Self.userConfigDefinesShiftEnterBinding()
loadCopyOnSelectOverride(config)
loadCJKFontFallbackIfNeeded(config)
// cmux provides the terminal background via backgroundView (CALayer)
@ -1600,12 +1598,6 @@ class GhosttyApp {
) != nil
}
static func userConfigDefinesShiftEnterBinding(
configPaths: [String] = loadedCJKScanPaths()
) -> Bool {
userShiftEnterConfigSummary(configPaths: configPaths).containsExplicitShiftEnterDirective
}
private static func configuredCTFont(
named name: String,
size: CGFloat = 12
@ -1683,55 +1675,6 @@ class GhosttyApp {
return summary
}
private struct UserShiftEnterConfigSummary {
var containsExplicitShiftEnterDirective = false
mutating func recordKeybind(_ value: String) {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if trimmed.isEmpty || trimmed == "clear" {
containsExplicitShiftEnterDirective = false
return
}
if GhosttyApp.keybindDirectiveTargetsShiftEnter(value) {
containsExplicitShiftEnterDirective = true
}
}
}
private static func userShiftEnterConfigSummary(
configPaths: [String] = loadedCJKScanPaths()
) -> UserShiftEnterConfigSummary {
var summary = UserShiftEnterConfigSummary()
var recursiveConfigPaths: [String] = []
for path in configPaths.map({ NSString(string: $0).expandingTildeInPath }) {
scanShiftEnterConfigFile(
atPath: path,
summary: &summary,
recursiveConfigPaths: &recursiveConfigPaths
)
}
var loadedRecursivePaths = Set<String>()
var index = 0
while index < recursiveConfigPaths.count {
let path = NSString(string: recursiveConfigPaths[index]).expandingTildeInPath
index += 1
let resolved = (path as NSString).standardizingPath
guard !loadedRecursivePaths.contains(resolved) else { continue }
loadedRecursivePaths.insert(resolved)
scanShiftEnterConfigFile(
atPath: path,
summary: &summary,
recursiveConfigPaths: &recursiveConfigPaths
)
}
return summary
}
/// Returns the top-level config paths that cmux will actually load before
/// recursive `config-file` processing.
static func loadedCJKScanPaths(
@ -1816,37 +1759,6 @@ class GhosttyApp {
}
}
private static func scanShiftEnterConfigFile(
atPath path: String,
summary: inout UserShiftEnterConfigSummary,
recursiveConfigPaths: inout [String]
) {
let resolved = (path as NSString).standardizingPath
guard let contents = try? String(contentsOfFile: resolved, encoding: .utf8) else {
return
}
let parentDir = (resolved as NSString).deletingLastPathComponent
for line in contents.components(separatedBy: .newlines) {
guard let entry = parsedConfigEntry(from: line) else { continue }
switch entry.key {
case "keybind":
guard let value = entry.value else { continue }
summary.recordKeybind(value)
case "config-file":
guard let value = entry.value else { continue }
applyConfigFileDirective(
value,
parentDir: parentDir,
recursiveConfigPaths: &recursiveConfigPaths
)
default:
continue
}
}
}
private static func parsedConfigEntry(
from rawLine: String
) -> (key: String, value: String?)? {
@ -1899,63 +1811,6 @@ class GhosttyApp {
recursiveConfigPaths.append(absolute)
}
private static func keybindDirectiveTargetsShiftEnter(_ value: String) -> Bool {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty,
trimmed.lowercased() != "clear",
let separatorIndex = trimmed.firstIndex(of: "=") else {
return false
}
let triggerExpression = String(trimmed[..<separatorIndex])
for rawPart in triggerExpression.split(separator: ">") {
var part = String(rawPart).trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !part.isEmpty else { continue }
if let slashIndex = part.lastIndex(of: "/") {
part = String(part[part.index(after: slashIndex)...])
}
for prefix in ["all:", "global:", "unconsumed:", "performable:"] {
while part.hasPrefix(prefix) {
part.removeFirst(prefix.count)
}
}
let tokens = part
.split(separator: "+")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
guard tokens.count == 2, tokens.contains("shift") else { continue }
guard let keyToken = tokens.first(where: { $0 != "shift" }) else { continue }
switch keyToken {
case "enter", "return", "kp_enter", "physical:enter", "physical:return", "physical:kp_enter":
return true
default:
continue
}
}
return false
}
static func shouldRemapShiftEnterForTmux(
keyCode: UInt16,
modifierFlags: NSEvent.ModifierFlags,
isInsideTmux: Bool,
userConfigDefinesShiftEnterBinding: Bool,
hasMarkedText: Bool
) -> Bool {
guard isInsideTmux else { return false }
guard !userConfigDefinesShiftEnterBinding else { return false }
guard !hasMarkedText else { return false }
let normalizedModifiers = terminalKeyboardCopyModeNormalizedModifiers(modifierFlags)
guard normalizedModifiers == [.shift] else { return false }
return keyCode == 36 || keyCode == 76
}
static func shouldLoadLegacyGhosttyConfig(
newConfigFileSize: Int?,
legacyConfigFileSize: Int?
@ -5773,7 +5628,16 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
#endif
// Check if this event matches a Ghostty keybinding.
let bindingFlags = ghosttyBindingFlags(for: event, surface: surface)
let bindingFlags: ghostty_binding_flags_e? = {
var keyEvent = ghosttyKeyEvent(for: event, surface: surface)
let text = textForKeyEvent(event).flatMap { shouldSendText($0) ? $0 : nil } ?? ""
var flags = ghostty_binding_flags_e(0)
let isBinding = text.withCString { ptr in
keyEvent.text = ptr
return ghostty_surface_key_is_binding(surface, keyEvent, &flags)
}
return isBinding ? flags : nil
}()
if let bindingFlags {
let isConsumed = (bindingFlags.rawValue & GHOSTTY_BINDING_FLAGS_CONSUMED.rawValue) != 0
@ -5928,10 +5792,6 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
#if DEBUG
keyboardCopyModeMs = (ProcessInfo.processInfo.systemUptime - keyboardCopyModeStart) * 1000.0
#endif
if shouldRemapShiftEnterForTmux(event: event, surface: surface) {
terminalSurface?.sendText("\n")
return
}
#if DEBUG
recordKeyLatency(path: "keyDown", event: event)
#endif
@ -6279,64 +6139,6 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
return ghostty_surface_key(surface, keyEvent)
}
private func ghosttyBindingFlags(
for event: NSEvent,
surface: ghostty_surface_t
) -> ghostty_binding_flags_e? {
var keyEvent = ghosttyKeyEvent(for: event, surface: surface)
let text = textForKeyEvent(event).flatMap { shouldSendText($0) ? $0 : nil } ?? ""
var flags = ghostty_binding_flags_e(0)
let isBinding = text.withCString { ptr in
keyEvent.text = ptr
return ghostty_surface_key_is_binding(surface, keyEvent, &flags)
}
return isBinding ? flags : nil
}
private func shouldRemapShiftEnterForTmux(
event: NSEvent,
surface: ghostty_surface_t
) -> Bool {
let userConfigDefinesShiftEnterBinding = GhosttyApp.shared.userConfigDefinesShiftEnterBinding
guard !userConfigDefinesShiftEnterBinding else { return false }
let normalizedModifiers = terminalKeyboardCopyModeNormalizedModifiers(event.modifierFlags)
guard normalizedModifiers == [.shift] else { return false }
guard event.keyCode == 36 || event.keyCode == 76 else { return false }
guard let terminalSurface else { return false }
let tabId = terminalSurface.tabId
let panelId = terminalSurface.id
guard let tab = AppDelegate.shared?
.tabManagerFor(tabId: tabId)?
.tabs
.first(where: { $0.id == tabId }) else {
return false
}
let reportedInsideTmux = tab.panelIsInsideTmux(panelId: panelId)
// Shell-side tmux telemetry can lag behind pane focus changes, so fall back to
// the current foreground process on the pane TTY before deciding whether to remap.
let detectedInsideTmux = tab.surfaceTTYNames[panelId].map {
TerminalSSHSessionDetector.isInsideTmux(forTTY: $0)
} ?? false
let isInsideTmux = reportedInsideTmux || detectedInsideTmux
if detectedInsideTmux != reportedInsideTmux {
AppDelegate.shared?
.tabManagerFor(tabId: tabId)?
.updateSurfaceTmuxState(
tabId: tabId,
surfaceId: panelId,
isInsideTmux: detectedInsideTmux
)
}
let shouldRemap = GhosttyApp.shouldRemapShiftEnterForTmux(
keyCode: event.keyCode,
modifierFlags: event.modifierFlags,
isInsideTmux: isInsideTmux,
userConfigDefinesShiftEnterBinding: userConfigDefinesShiftEnterBinding,
hasMarkedText: hasMarkedText()
)
return shouldRemap
}
#if DEBUG
@discardableResult
private func sendTimedGhosttyKey(

View file

@ -2598,15 +2598,6 @@ class TabManager: ObservableObject {
tab.updatePanelShellActivityState(panelId: surfaceId, state: state)
}
func updateSurfaceTmuxState(
tabId: UUID,
surfaceId: UUID,
isInsideTmux: Bool
) {
guard let tab = tabs.first(where: { $0.id == tabId }) else { return }
tab.updatePanelTmuxState(panelId: surfaceId, isInsideTmux: isInsideTmux)
}
private func normalizeDirectory(_ directory: String) -> String {
let trimmed = directory.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return directory }

View file

@ -440,10 +440,8 @@ class TerminalController {
private let queue = DispatchQueue(label: "com.cmux.socket-fast-path")
private var lastReportedDirectories: [SocketSurfaceKey: String] = [:]
private var lastReportedShellStates: [SocketSurfaceKey: Workspace.PanelShellActivityState] = [:]
private var lastReportedTmuxStates: [SocketSurfaceKey: Bool] = [:]
private let maxTrackedDirectories = 4096
private let maxTrackedShellStates = 4096
private let maxTrackedTmuxStates = 4096
func shouldPublishDirectory(workspaceId: UUID, panelId: UUID, directory: String) -> Bool {
let key = SocketSurfaceKey(workspaceId: workspaceId, panelId: panelId)
@ -476,24 +474,6 @@ class TerminalController {
return true
}
}
func shouldPublishTmuxState(
workspaceId: UUID,
panelId: UUID,
isInsideTmux: Bool
) -> Bool {
let key = SocketSurfaceKey(workspaceId: workspaceId, panelId: panelId)
return queue.sync {
if lastReportedTmuxStates[key] == isInsideTmux {
return false
}
if lastReportedTmuxStates.count >= maxTrackedTmuxStates {
lastReportedTmuxStates.removeAll(keepingCapacity: true)
}
lastReportedTmuxStates[key] = isInsideTmux
return true
}
}
}
private static let socketFastPathState = SocketFastPathState()
@ -520,42 +500,6 @@ class TerminalController {
return trimmed
}
nonisolated static func normalizedReportedTTYName(_ raw: String?) -> String? {
guard let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines),
!trimmed.isEmpty,
trimmed != "not a tty" else {
return nil
}
let components = trimmed.split(separator: "/")
if let last = components.last, !last.isEmpty {
return String(last)
}
return trimmed
}
static func resolvePanelIdByTTY(_ ttyName: String?, in tab: Tab) -> UUID? {
guard let ttyName = normalizedReportedTTYName(ttyName) else {
return nil
}
if let focusedPanelId = tab.focusedPanelId,
normalizedReportedTTYName(tab.surfaceTTYNames[focusedPanelId]) == ttyName,
tab.panels[focusedPanelId] != nil {
return focusedPanelId
}
let matches = tab.surfaceTTYNames.compactMap { (panelId, candidateTTY) -> UUID? in
guard tab.panels[panelId] != nil,
normalizedReportedTTYName(candidateTTY) == ttyName else {
return nil
}
return panelId
}
guard matches.count == 1 else { return nil }
return matches[0]
}
nonisolated static func normalizedExportedScreenPath(_ raw: String?) -> String? {
guard let raw else { return nil }
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
@ -601,19 +545,6 @@ class TerminalController {
}
}
nonisolated static func parseReportedTmuxState(
_ rawState: String
) -> Bool? {
switch rawState.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
case "inside", "tmux", "active", "1", "true", "yes":
return true
case "outside", "none", "inactive", "0", "false", "no", "clear":
return false
default:
return nil
}
}
/// Update which window's TabManager receives socket commands.
/// This is used when the user switches between multiple terminal windows.
func setActiveTabManager(_ tabManager: TabManager?) {
@ -1873,9 +1804,6 @@ class TerminalController {
case "report_shell_state":
return reportShellState(args)
case "report_tmux_state":
return reportTmuxState(args)
case "report_pwd":
return reportPwd(args)
@ -11189,7 +11117,6 @@ class TerminalController {
report_tty <tty_name> [--tab=X] [--panel=Y] - Register TTY for batched port scanning
ports_kick [--tab=X] [--panel=Y] - Request batched port scan for panel
report_shell_state <prompt|running> [--tab=X] [--panel=Y] - Report whether the shell is idle at a prompt or running a command
report_tmux_state <inside|outside> [--tab=X] [--panel=Y] [--tty=Z] - Report whether the shell is currently inside tmux
report_pwd <path> [--tab=X] [--panel=Y] - Report current working directory
clear_ports [--tab=X] [--panel=Y] - Clear listening ports
sidebar_state [--tab=X] - Dump sidebar metadata
@ -15240,80 +15167,6 @@ class TerminalController {
return result
}
private func reportTmuxState(_ args: String) -> String {
let parsed = parseOptions(args)
guard let rawState = parsed.positional.first, !rawState.isEmpty else {
return "ERROR: Missing tmux state — usage: report_tmux_state <inside|outside> [--tab=X] [--panel=Y] [--tty=Z]"
}
guard let isInsideTmux = Self.parseReportedTmuxState(rawState) else {
return "ERROR: Invalid tmux state '\(rawState)' — expected inside or outside"
}
let panelArg = parsed.options["panel"] ?? parsed.options["surface"]
let fallbackPanelId: UUID?
if let panelArg {
if panelArg.isEmpty {
return "ERROR: Missing panel id — usage: report_tmux_state <inside|outside> [--tab=X] [--panel=Y] [--tty=Z]"
}
guard let parsedId = UUID(uuidString: panelArg) else {
return "ERROR: Invalid panel id '\(panelArg)'"
}
fallbackPanelId = parsedId
} else {
fallbackPanelId = nil
}
let reportedTTYName = Self.normalizedReportedTTYName(parsed.options["tty"])
if let scope = Self.explicitSocketScope(options: parsed.options) {
guard Self.socketFastPathState.shouldPublishTmuxState(
workspaceId: scope.workspaceId,
panelId: scope.panelId,
isInsideTmux: isInsideTmux
) else {
return "OK"
}
DispatchQueue.main.async {
guard let tabManager = AppDelegate.shared?.tabManagerFor(tabId: scope.workspaceId) else { return }
tabManager.updateSurfaceTmuxState(
tabId: scope.workspaceId,
surfaceId: scope.panelId,
isInsideTmux: isInsideTmux
)
}
return "OK"
}
let tabResolution = resolveTabIdForSidebarMutation(reportArgs: args, options: parsed.options)
guard let targetTabId = tabResolution.tabId else {
return tabResolution.error ?? "ERROR: No tab selected"
}
DispatchQueue.main.async { [weak self] in
guard let self,
let tab = self.tabForSidebarMutation(id: targetTabId) else {
return
}
let validSurfaceIds = Set(tab.panels.keys)
tab.pruneSurfaceMetadata(validSurfaceIds: validSurfaceIds)
let surfaceId = fallbackPanelId
?? Self.resolvePanelIdByTTY(reportedTTYName, in: tab)
?? tab.focusedPanelId
guard let surfaceId, validSurfaceIds.contains(surfaceId) else { return }
guard Self.socketFastPathState.shouldPublishTmuxState(
workspaceId: tab.id,
panelId: surfaceId,
isInsideTmux: isInsideTmux
) else {
return
}
tab.updatePanelTmuxState(panelId: surfaceId, isInsideTmux: isInsideTmux)
}
return "OK"
}
private func clearPorts(_ args: String) -> String {
let parsed = parseOptions(args)
var result = "OK"

View file

@ -420,24 +420,6 @@ enum TerminalSSHSessionDetector {
)
}
static func isInsideTmux(forTTY ttyName: String) -> Bool {
let normalizedTTY = normalizeTTYName(ttyName)
guard !normalizedTTY.isEmpty else { return false }
return isInsideTmuxForTesting(
ttyName: normalizedTTY,
processes: processSnapshots(forTTY: normalizedTTY)
)
}
static func isInsideTmuxForTesting(
ttyName: String,
processes: [ProcessSnapshot]
) -> Bool {
let normalizedTTY = normalizeTTYName(ttyName)
guard !normalizedTTY.isEmpty else { return false }
return processes.contains { isForegroundProcess($0, ttyName: normalizedTTY, executableName: "tmux") }
}
static func detectForTesting(
ttyName: String,
processes: [ProcessSnapshot],
@ -492,16 +474,8 @@ enum TerminalSSHSessionDetector {
}
private static func isForegroundSSHProcess(_ process: ProcessSnapshot, ttyName: String) -> Bool {
isForegroundProcess(process, ttyName: ttyName, executableName: "ssh")
}
private static func isForegroundProcess(
_ process: ProcessSnapshot,
ttyName: String,
executableName: String
) -> Bool {
normalizeTTYName(process.tty) == normalizeTTYName(ttyName) &&
process.executableName == executableName &&
process.executableName == "ssh" &&
process.pgid > 0 &&
process.tpgid > 0 &&
process.pgid == process.tpgid

View file

@ -5578,7 +5578,6 @@ final class Workspace: Identifiable, ObservableObject {
}()
nonisolated(unsafe) static var runSSHControlMasterCommandOverrideForTesting: (([String]) -> Void)?
private var panelShellActivityStates: [UUID: PanelShellActivityState] = [:]
private var panelTmuxStates: [UUID: Bool] = [:]
/// PIDs associated with agent status entries (e.g. claude_code), keyed by status key.
/// Used for stale-session detection: if the PID is dead, the status entry is cleared.
var agentPIDs: [String: pid_t] = [:]
@ -6435,24 +6434,6 @@ final class Workspace: Identifiable, ObservableObject {
#endif
}
func updatePanelTmuxState(panelId: UUID, isInsideTmux: Bool) {
guard panels[panelId] != nil else { return }
let previousState = panelTmuxStates[panelId] ?? false
guard previousState != isInsideTmux else { return }
panelTmuxStates[panelId] = isInsideTmux
#if DEBUG
dlog(
"surface.tmuxState workspace=\(id.uuidString.prefix(5)) " +
"panel=\(panelId.uuidString.prefix(5)) from=\(previousState ? "inside" : "outside") " +
"to=\(isInsideTmux ? "inside" : "outside")"
)
#endif
}
func panelIsInsideTmux(panelId: UUID) -> Bool {
panelTmuxStates[panelId] ?? false
}
func panelNeedsConfirmClose(panelId: UUID, fallbackNeedsConfirmClose: Bool) -> Bool {
Self.resolveCloseConfirmation(
shellActivityState: panelShellActivityStates[panelId],
@ -6652,7 +6633,6 @@ final class Workspace: Identifiable, ObservableObject {
surfaceListeningPorts = surfaceListeningPorts.filter { validSurfaceIds.contains($0.key) }
surfaceTTYNames = surfaceTTYNames.filter { validSurfaceIds.contains($0.key) }
panelShellActivityStates = panelShellActivityStates.filter { validSurfaceIds.contains($0.key) }
panelTmuxStates = panelTmuxStates.filter { validSurfaceIds.contains($0.key) }
panelPullRequests = panelPullRequests.filter { validSurfaceIds.contains($0.key) }
recomputeListeningPorts()
}
@ -10417,7 +10397,6 @@ extension Workspace: BonsplitDelegate {
manualUnreadMarkedAt.removeValue(forKey: panelId)
panelSubscriptions.removeValue(forKey: panelId)
panelShellActivityStates.removeValue(forKey: panelId)
panelTmuxStates.removeValue(forKey: panelId)
surfaceTTYNames.removeValue(forKey: panelId)
restoredTerminalScrollbackByPanelId.removeValue(forKey: panelId)
PortScanner.shared.unregisterPanel(workspaceId: id, panelId: panelId)
@ -10570,7 +10549,6 @@ extension Workspace: BonsplitDelegate {
manualUnreadPanelIds.remove(panelId)
panelSubscriptions.removeValue(forKey: panelId)
panelShellActivityStates.removeValue(forKey: panelId)
panelTmuxStates.removeValue(forKey: panelId)
surfaceTTYNames.removeValue(forKey: panelId)
surfaceListeningPorts.removeValue(forKey: panelId)
restoredTerminalScrollbackByPanelId.removeValue(forKey: panelId)

View file

@ -663,7 +663,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase {
XCTAssertNotEqual(base, renamed)
}
func testSwitcherFingerprintIgnoresWorkspaceDisplayNameChurn() {
func testSwitcherFingerprintTracksMetadataValuesAtSameCardinality() {
let windowID = UUID()
let workspaceID = UUID()
let base = ContentView.commandPaletteSwitcherFingerprint(
@ -675,6 +675,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase {
workspaces: [
ContentView.CommandPaletteSwitcherFingerprintWorkspace(
id: workspaceID,
displayName: "Workspace Alpha",
metadata: CommandPaletteSwitcherSearchMetadata(
directories: ["/Users/example/dev/cmuxterm"],
branches: ["feature/search-speed"],
@ -695,6 +696,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase {
workspaces: [
ContentView.CommandPaletteSwitcherFingerprintWorkspace(
id: workspaceID,
displayName: "Workspace Alpha",
metadata: CommandPaletteSwitcherSearchMetadata(
directories: ["/Users/example/dev/other"],
branches: ["feature/search-speed"],
@ -715,6 +717,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase {
workspaces: [
ContentView.CommandPaletteSwitcherFingerprintWorkspace(
id: workspaceID,
displayName: "Workspace Beta",
metadata: CommandPaletteSwitcherSearchMetadata(
directories: ["/Users/example/dev/cmuxterm"],
branches: ["feature/search-speed"],
@ -727,8 +730,8 @@ final class CommandPaletteSearchEngineTests: XCTestCase {
]
)
XCTAssertEqual(base, changedDisplayName)
XCTAssertNotEqual(base, changedMetadata)
XCTAssertNotEqual(base, changedDisplayName)
}
func testSwitcherFingerprintTracksSurfaceValuesAtSameCardinality() {
@ -745,6 +748,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase {
workspaces: [
ContentView.CommandPaletteSwitcherFingerprintWorkspace(
id: workspaceID,
displayName: "Workspace Alpha",
metadata: CommandPaletteSwitcherSearchMetadata(),
surfaces: [
ContentView.CommandPaletteSwitcherFingerprintSurface(
@ -772,6 +776,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase {
workspaces: [
ContentView.CommandPaletteSwitcherFingerprintWorkspace(
id: workspaceID,
displayName: "Workspace Alpha",
metadata: CommandPaletteSwitcherSearchMetadata(),
surfaces: [
ContentView.CommandPaletteSwitcherFingerprintSurface(
@ -799,6 +804,7 @@ final class CommandPaletteSearchEngineTests: XCTestCase {
workspaces: [
ContentView.CommandPaletteSwitcherFingerprintWorkspace(
id: workspaceID,
displayName: "Workspace Alpha",
metadata: CommandPaletteSwitcherSearchMetadata(),
surfaces: [
ContentView.CommandPaletteSwitcherFingerprintSurface(

View file

@ -2495,150 +2495,6 @@ final class GhosttyMouseFocusTests: XCTestCase {
}
}
func testUserConfigDefinesShiftEnterBindingDetectsDirectBinding() throws {
try withTempConfig("keybind = shift+enter=text:\\x0a\n") { path in
XCTAssertTrue(
GhosttyApp.userConfigDefinesShiftEnterBinding(configPaths: [path])
)
}
}
func testUserConfigDefinesShiftEnterBindingDetectsUnbindInIncludedFile() throws {
let dir = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-test-shift-enter-unbind-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: dir) }
let included = dir.appendingPathComponent("bindings.conf")
try "keybind = shift+enter=unbind\n"
.write(to: included, atomically: true, encoding: .utf8)
let main = dir.appendingPathComponent("config")
try "config-file = \(included.path)\n"
.write(to: main, atomically: true, encoding: .utf8)
XCTAssertTrue(
GhosttyApp.userConfigDefinesShiftEnterBinding(configPaths: [main.path])
)
}
func testUserConfigDefinesShiftEnterBindingHonorsLaterClearInIncludedFile() throws {
let dir = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-test-shift-enter-clear-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: dir) }
let included = dir.appendingPathComponent("bindings.conf")
try "keybind = clear\n"
.write(to: included, atomically: true, encoding: .utf8)
let main = dir.appendingPathComponent("config")
try """
keybind = shift+enter=text:\\x0a
config-file = \(included.path)
"""
.write(to: main, atomically: true, encoding: .utf8)
XCTAssertFalse(
GhosttyApp.userConfigDefinesShiftEnterBinding(configPaths: [main.path])
)
}
func testUserConfigDefinesShiftEnterBindingIgnoresOtherModifierCombinations() throws {
try withTempConfig("keybind = cmd+shift+enter=text:\\x0a\n") { path in
XCTAssertFalse(
GhosttyApp.userConfigDefinesShiftEnterBinding(configPaths: [path])
)
}
}
func testShouldRemapShiftEnterForTmuxOnlyWhenScopedToTmuxWithoutOverrides() {
XCTAssertTrue(
GhosttyApp.shouldRemapShiftEnterForTmux(
keyCode: 36,
modifierFlags: [.shift],
isInsideTmux: true,
userConfigDefinesShiftEnterBinding: false,
hasMarkedText: false
)
)
XCTAssertFalse(
GhosttyApp.shouldRemapShiftEnterForTmux(
keyCode: 36,
modifierFlags: [.shift],
isInsideTmux: false,
userConfigDefinesShiftEnterBinding: false,
hasMarkedText: false
)
)
XCTAssertFalse(
GhosttyApp.shouldRemapShiftEnterForTmux(
keyCode: 36,
modifierFlags: [.shift],
isInsideTmux: true,
userConfigDefinesShiftEnterBinding: true,
hasMarkedText: false
)
)
XCTAssertFalse(
GhosttyApp.shouldRemapShiftEnterForTmux(
keyCode: 36,
modifierFlags: [.shift, .command],
isInsideTmux: true,
userConfigDefinesShiftEnterBinding: false,
hasMarkedText: false
)
)
}
func testForegroundTmuxProcessOnTTYIsDetected() {
let processes = [
TerminalSSHSessionDetector.ProcessSnapshot(
pid: 47486,
pgid: 47486,
tpgid: 48365,
tty: "ttys089",
executableName: "login"
),
TerminalSSHSessionDetector.ProcessSnapshot(
pid: 47487,
pgid: 47487,
tpgid: 48365,
tty: "ttys089",
executableName: "zsh"
),
TerminalSSHSessionDetector.ProcessSnapshot(
pid: 48365,
pgid: 48365,
tpgid: 48365,
tty: "ttys089",
executableName: "tmux"
),
]
XCTAssertTrue(
TerminalSSHSessionDetector.isInsideTmuxForTesting(
ttyName: "ttys089",
processes: processes
)
)
XCTAssertFalse(
TerminalSSHSessionDetector.isInsideTmuxForTesting(
ttyName: "ttys090",
processes: processes
)
)
XCTAssertFalse(
TerminalSSHSessionDetector.isInsideTmuxForTesting(
ttyName: "ttys089",
processes: processes.filter { $0.executableName != "tmux" }
)
)
}
func testLoadedCJKScanPathsSkipsReleaseAppSupportWhenTaggedConfigExists() throws {
let appSupport = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-test-cjk-app-support-\(UUID().uuidString)")
@ -3027,123 +2883,6 @@ final class ZshShellIntegrationHandoffTests: XCTestCase {
XCTAssertEqual(output, "report_tty ttys999 --tab=11111111-1111-1111-1111-111111111111")
}
func testShellIntegrationReportsTmuxStatePayload() throws {
let output = try runInteractiveZsh(
cmuxLoadGhosttyIntegration: false,
cmuxLoadShellIntegration: true,
command: """
_CMUX_TTY_NAME=ttys999
print -r -- "$(_cmux_report_tmux_state_payload)"
""",
extraEnvironment: [
"TMUX": "/tmp/tmux-current,123,0",
"CMUX_TAB_ID": "11111111-1111-1111-1111-111111111111",
"CMUX_PANEL_ID": "99999999-9999-9999-9999-999999999999",
]
)
XCTAssertEqual(
output,
"report_tmux_state inside --tab=11111111-1111-1111-1111-111111111111 --tty=ttys999"
)
}
func testShellIntegrationResendsTmuxStateWhenSocketTargetChanges() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
.appendingPathComponent("cmux-zsh-tmux-state-resend-\(UUID().uuidString)")
try fileManager.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: root) }
let socketA = root.appendingPathComponent("cmux-a.sock").path
let socketB = root.appendingPathComponent("cmux-b.sock").path
let output = try runInteractiveZsh(
cmuxLoadGhosttyIntegration: false,
cmuxLoadShellIntegration: true,
command: """
python3 -c 'import os, socket, sys, time; path = sys.argv[1]; \
os.path.exists(path) and os.unlink(path); \
s = socket.socket(socket.AF_UNIX); s.bind(path); s.listen(1); time.sleep(3)' "$CMUX_SOCKET_PATH" &
server_a=$!
sleep 0.1
functions[_cmux_send_bg]='print -r -- "$1"'
_CMUX_TTY_NAME=ttys999
_CMUX_TMUX_STATE_SIGNATURE_LAST=""
_cmux_report_tmux_state
kill $server_a >/dev/null 2>&1
wait $server_a >/dev/null 2>&1
export CMUX_SOCKET_PATH="\(socketB)"
python3 -c 'import os, socket, sys, time; path = sys.argv[1]; \
os.path.exists(path) and os.unlink(path); \
s = socket.socket(socket.AF_UNIX); s.bind(path); s.listen(1); time.sleep(3)' "$CMUX_SOCKET_PATH" &
server_b=$!
sleep 0.1
_cmux_report_tmux_state
kill $server_b >/dev/null 2>&1
wait $server_b >/dev/null 2>&1
""",
extraEnvironment: [
"TMUX": "/tmp/tmux-current,123,0",
"CMUX_SOCKET_PATH": socketA,
"CMUX_TAB_ID": "11111111-1111-1111-1111-111111111111",
"CMUX_PANEL_ID": "99999999-9999-9999-9999-999999999999",
]
)
XCTAssertEqual(
output,
"""
report_tmux_state inside --tab=11111111-1111-1111-1111-111111111111 --tty=ttys999
report_tmux_state inside --tab=11111111-1111-1111-1111-111111111111 --tty=ttys999
"""
)
}
func testShellIntegrationPrecmdReportsTmuxStateWithoutPanelScope() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
.appendingPathComponent("cmux-zsh-precmd-tmux-state-\(UUID().uuidString)")
try fileManager.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: root) }
let socketPath = root.appendingPathComponent("cmux.sock").path
let output = try runInteractiveZsh(
cmuxLoadGhosttyIntegration: false,
cmuxLoadShellIntegration: true,
command: """
python3 -c 'import os, socket, sys, time; path = sys.argv[1]; \
os.path.exists(path) and os.unlink(path); \
s = socket.socket(socket.AF_UNIX); s.bind(path); s.listen(1); time.sleep(3)' "$CMUX_SOCKET_PATH" &
server_pid=$!
sleep 0.1
functions[_cmux_send_bg]='print -r -- "$1"'
unset CMUX_PANEL_ID
_CMUX_TTY_NAME=ttys999
_CMUX_TTY_REPORTED=0
_CMUX_TMUX_STATE_SIGNATURE_LAST=""
_cmux_precmd
kill $server_pid >/dev/null 2>&1
wait $server_pid >/dev/null 2>&1
""",
extraEnvironment: [
"TMUX": "/tmp/tmux-current,123,0",
"CMUX_SOCKET_PATH": socketPath,
"CMUX_TAB_ID": "11111111-1111-1111-1111-111111111111",
]
)
XCTAssertEqual(
output,
"""
report_tmux_state inside --tab=11111111-1111-1111-1111-111111111111 --tty=ttys999
report_tty ttys999 --tab=11111111-1111-1111-1111-111111111111
"""
)
}
private func runInteractiveZsh(cmuxLoadGhosttyIntegration: Bool) throws -> String {
try runInteractiveZsh(
cmuxLoadGhosttyIntegration: cmuxLoadGhosttyIntegration,

View file

@ -254,41 +254,6 @@ final class TerminalControllerSocketSecurityTests: XCTestCase {
XCTAssertTrue(manager.tabs.contains(where: { $0.id == pinnedWorkspace.id }))
}
func testReportTmuxStateResolvesPanelByTTY() throws {
let socketPath = makeSocketPath("tmux-tty")
let manager = TabManager()
let workspace = manager.addWorkspace(select: true)
guard let focusedPanelId = workspace.focusedPanelId else {
XCTFail("Expected selected workspace with a focused panel")
return
}
guard let targetPanel = workspace.newTerminalSplit(from: focusedPanelId, orientation: .horizontal) else {
XCTFail("Expected split panel to be created")
return
}
workspace.focusPanel(focusedPanelId)
workspace.surfaceTTYNames[targetPanel.id] = "/dev/ttys777"
TerminalController.shared.start(
tabManager: manager,
socketPath: socketPath,
accessMode: .allowAll
)
try waitForSocket(at: socketPath)
let responses = try sendCommands(
["report_tmux_state inside --tab=\(workspace.id.uuidString) --tty=ttys777"],
to: socketPath
)
XCTAssertEqual(responses, ["OK"])
try waitForCondition("tmux state routed by tty") {
workspace.panelIsInsideTmux(panelId: targetPanel.id)
}
XCTAssertFalse(workspace.panelIsInsideTmux(panelId: focusedPanelId))
}
private func waitForSocket(at path: String, timeout: TimeInterval = 5.0) throws {
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate { _, _ in
@ -303,22 +268,6 @@ final class TerminalControllerSocketSecurityTests: XCTestCase {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(ETIMEDOUT))
}
private func waitForCondition(
_ description: String,
timeout: TimeInterval = 5.0,
condition: @escaping () -> Bool
) throws {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if condition() {
return
}
_ = RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01))
}
XCTFail("Timed out waiting for \(description)")
throw NSError(domain: NSPOSIXErrorDomain, code: Int(ETIMEDOUT))
}
private func socketMode(at path: String) throws -> UInt16 {
var fileInfo = stat()
guard lstat(path, &fileInfo) == 0 else {