-
Notifications
You must be signed in to change notification settings - Fork 3
feat(sessions): link tabs and focus panes under tmux #160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
a716960
feat(sessions): link tabs and focus panes under tmux
StuBehan 7c80e6d
feat(sessions): surface iTerm2 -CC tab by matching pane title
StuBehan 0823d37
fix(sessions): match iTerm2 -CC tab title unicode-safely
StuBehan 1169fa5
chore(sessions): debug-log tmux focus resolution behind panel flag
StuBehan 693146f
fix(sessions): force UTF-8 locale for tmux title read under launchd
StuBehan d0f40ae
fix(sessions): harden tmux focus (host/fall-through/tab-id/threading)
StuBehan 1c440f4
fix(sessions): keep empty fields when parsing TMUX server id
StuBehan 8fe8d74
fix(sessions): strip animated spinner from tmux title match
StuBehan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import XCTest | ||
|
|
||
| @testable import StackNudgePanelCore | ||
|
|
||
| // Pure-parse tests for the tmux focus resolver. The live path (`target`) needs | ||
| // `ps eww` against a real tmux pane, but `parse` is where the extraction rules | ||
| // live and is fully pure. | ||
| final class TmuxFocusTests: XCTestCase { | ||
|
|
||
| func test_parse_extractsPaneSocketAndHost() { | ||
| let raw = "99028 /bin/zsh TMUX=/private/tmp/tmux-502/default,12390,0 TMUX_PANE=%4 LC_TERMINAL=iTerm2" | ||
| let target = TmuxFocus.parse(psOutput: raw, pid: 99028) | ||
| XCTAssertEqual(target?.pane, "%4") | ||
| // TMUX is "<socket>,<serverPID>,<sessionN>" — only the socket path. | ||
| XCTAssertEqual(target?.socket, "/private/tmp/tmux-502/default") | ||
| XCTAssertEqual(target?.hostBundleID, "com.googlecode.iterm2") | ||
| } | ||
|
|
||
| func test_parse_nilWhenNotInTmux() { | ||
| // No TMUX_PANE → the process isn't inside tmux. | ||
| let raw = "99028 /bin/zsh TERM_PROGRAM=iTerm.app ITERM_SESSION_ID=w0t1p0:ABC" | ||
| XCTAssertNil(TmuxFocus.parse(psOutput: raw, pid: 99028)) | ||
| } | ||
|
|
||
| func test_parse_socketNilWhenTmuxUnset() { | ||
| // A pane var with no TMUX socket (unusual, but must not crash): socket | ||
| // is nil and focus falls back to the default socket. | ||
| let raw = "42 /bin/zsh TMUX_PANE=%1 LC_TERMINAL=Apple_Terminal" | ||
| let target = TmuxFocus.parse(psOutput: raw, pid: 42) | ||
| XCTAssertEqual(target?.pane, "%1") | ||
| XCTAssertNil(target?.socket) | ||
| XCTAssertEqual(target?.hostBundleID, "com.apple.Terminal") | ||
| } | ||
|
|
||
| func test_hostBundleID_knownHosts() { | ||
| XCTAssertEqual(TmuxFocus.hostBundleID(forLCTerminal: "iTerm2"), "com.googlecode.iterm2") | ||
| XCTAssertEqual(TmuxFocus.hostBundleID(forLCTerminal: "Apple_Terminal"), "com.apple.Terminal") | ||
| } | ||
|
|
||
| func test_hostBundleID_unknownOrNilIsNil() { | ||
| XCTAssertNil(TmuxFocus.hostBundleID(forLCTerminal: "WezTerm")) | ||
| XCTAssertNil(TmuxFocus.hostBundleID(forLCTerminal: nil)) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import Foundation | ||
|
|
||
| // Resolves a tmux-hosted agent to the values AppActivator needs to focus its | ||
| // pane. tmux severs the process tree from the host terminal — the agent runs | ||
| // under the tmux server (parented to launchd), so none of the usual terminal | ||
| // enrichment reaches iTerm2/Terminal. Instead we read the agent process's live | ||
| // environment (TMUX socket, TMUX_PANE, LC_TERMINAL) at focus time. Reading it | ||
| // live rather than storing it keeps custom sockets and the host terminal | ||
| // current, and a dead pid simply yields nil (focus becomes a no-op). | ||
| enum TmuxFocus { | ||
|
|
||
| struct Target: Equatable { | ||
| let pane: String // TMUX_PANE, e.g. "%4" | ||
| let socket: String? // tmux server socket path; nil → default socket | ||
| let hostBundleID: String? // app to raise; nil → rely on -CC tab surfacing | ||
| } | ||
|
|
||
| // iTerm2 and Terminal.app propagate LC_TERMINAL through tmux/ssh. Only map | ||
| // the hosts we can actually raise; anything else leaves hostBundleID nil, | ||
| // so focus still selects the pane and (under iTerm2 `-CC`) the mapped tab | ||
| // still surfaces. | ||
| static func hostBundleID(forLCTerminal lcTerminal: String?) -> String? { | ||
| switch lcTerminal { | ||
| case "iTerm2": return "com.googlecode.iterm2" | ||
| case "Apple_Terminal": return "com.apple.Terminal" | ||
| default: return nil | ||
| } | ||
| } | ||
|
|
||
| // Live resolve: read the agent pid's environment and pull the tmux identity. | ||
| static func target(agentPID: Int) -> Target? { | ||
| let raw = ProcessOutput.read( | ||
| "/bin/ps", ["eww", "-o", "pid=,command=", "-p", String(agentPID)]) | ||
| return parse(psOutput: raw, pid: agentPID) | ||
| } | ||
|
|
||
| // Pure: given `ps eww` output and the pid, extract the tmux target. Returns | ||
| // nil when the process isn't inside tmux (no TMUX_PANE). Reuses the generic | ||
| // env-var parser so the extraction rules stay in one place. | ||
| static func parse(psOutput raw: String, pid: Int) -> Target? { | ||
| let panes = EnvVarTerminalIntegration.parseEnvValues(raw, envVar: "TMUX_PANE") | ||
| guard let pane = panes[pid], !pane.isEmpty else { return nil } | ||
| // TMUX is "<socket>,<serverPID>,<sessionN>" — the socket is the part | ||
| // before the first comma; tmux -S wants just that path. | ||
| let socket = EnvVarTerminalIntegration.parseEnvValues(raw, envVar: "TMUX")[pid] | ||
| .flatMap { $0.split(separator: ",").first.map(String.init) } | ||
| let host = hostBundleID(forLCTerminal: | ||
| EnvVarTerminalIntegration.parseEnvValues(raw, envVar: "LC_TERMINAL")[pid]) | ||
| return Target(pane: pane, socket: socket, hostBundleID: host) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.