Skip to content

Commit 86ec7ce

Browse files
authored
Fix nested tmux selection handling (#104)
1 parent 144ac07 commit 86ec7ce

6 files changed

Lines changed: 50 additions & 5 deletions

File tree

cmd/okdev-sshd/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ func terminalBootstrapScript() string {
181181

182182
func devTmuxBootstrapScript() string {
183183
return strings.Join([]string{
184-
`if command -v tmux >/dev/null 2>&1; then if [ -n "${SSH_AUTH_SOCK:-}" ]; then tmux set-environment -g SSH_AUTH_SOCK "$SSH_AUTH_SOCK" >/dev/null 2>&1 || true; else tmux set-environment -gu SSH_AUTH_SOCK >/dev/null 2>&1 || true; fi; if [ -f /var/okdev/dev.tmux.conf ]; then exec tmux -f /var/okdev/dev.tmux.conf new-session -A -s okdev; fi; exec tmux new-session -A -s okdev; fi`,
184+
`if command -v tmux >/dev/null 2>&1; then if [ -f /var/okdev/dev.tmux.conf ]; then tmux source-file /var/okdev/dev.tmux.conf >/dev/null 2>&1 || true; fi; if ! tmux has-session -t okdev >/dev/null 2>&1; then if [ -f /var/okdev/dev.tmux.conf ]; then tmux -f /var/okdev/dev.tmux.conf new-session -d -s okdev >/dev/null 2>&1 || tmux new-session -d -s okdev >/dev/null 2>&1; else tmux new-session -d -s okdev >/dev/null 2>&1; fi; fi; if [ -n "${SSH_AUTH_SOCK:-}" ]; then tmux set-environment -g SSH_AUTH_SOCK "$SSH_AUTH_SOCK" >/dev/null 2>&1 || true; else tmux set-environment -gu SSH_AUTH_SOCK >/dev/null 2>&1 || true; fi; if [ "${OKDEV_NESTED_TMUX:-0}" = "1" ]; then tmux set -g mouse off >/dev/null 2>&1 || true; tmux set -g set-clipboard off >/dev/null 2>&1 || true; else tmux set -g mouse on >/dev/null 2>&1 || true; tmux set -g set-clipboard on >/dev/null 2>&1 || true; fi; exec tmux attach-session -t okdev; fi`,
185185
`echo 'warning: tmux not available in dev container; continuing without tmux' >&2`,
186186
}, "; ")
187187
}

cmd/okdev-sshd/main_test.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,17 @@ func TestBuildInteractiveLoginScriptWithoutWorkspaceOrTmux(t *testing.T) {
157157

158158
func TestDevTmuxBootstrapScriptIncludesFallbackWarning(t *testing.T) {
159159
script := devTmuxBootstrapScript()
160-
for _, want := range []string{"tmux", "warning: tmux not available", "set-environment -g SSH_AUTH_SOCK"} {
160+
for _, want := range []string{
161+
"tmux",
162+
"warning: tmux not available",
163+
"set-environment -g SSH_AUTH_SOCK",
164+
"has-session -t okdev",
165+
"attach-session -t okdev",
166+
"source-file /var/okdev/dev.tmux.conf",
167+
`"${OKDEV_NESTED_TMUX:-0}" = "1"`,
168+
"set -g mouse off",
169+
"set -g mouse on",
170+
} {
161171
if !strings.Contains(script, want) {
162172
t.Fatalf("expected tmux bootstrap script to contain %q: %s", want, script)
163173
}

docs/command-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
- Opens a shell or runs a command in one or more session pods.
7171
- Without `-- command...`, opens an interactive shell on the pinned target pod.
7272
- With `-- command...`, runs the command across all running pods by default, with output prefixed by short pod name.
73+
- The `-- command...` path is non-interactive fanout execution, not a terminal session. TTY-dependent programs such as `watch`, `top`, `htop`, or full-screen TUIs are not expected to work there; use `okdev ssh` or `okdev exec` without `-- command...` to open an interactive shell first.
7374
- `--pod`: target specific pods by name (repeatable or comma-separated).
7475
- `--role`: target pods by `okdev.io/workload-role` label (case-insensitive).
7576
- `--label`: target pods by arbitrary label `key=value` (repeatable, AND logic).

docs/quickstart.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ okdev resolves the target cluster in this order:
209209

210210
- SSH connects to the `dev` container via `okdev-sshd` on port 2222.
211211
- `okdev ssh` uses tmux by default with a built-in okdev profile (history, mouse, vi-copy, status bar) using the standard `Ctrl-b` prefix.
212+
- When you launch `okdev ssh` from inside a local tmux session, the inner okdev tmux disables mouse mode so normal drag-selection still works in the outer terminal/tmux.
213+
- That nested-tmux mouse setting is shared per remote okdev tmux session. Separate okdev sessions do not affect each other, but if one client attaches from local tmux and another attaches to the same remote session from a plain terminal, the most recent attach decides whether inner tmux mouse mode is on or off.
212214
- The generated `ssh okdev-<session>` host entry bypasses tmux by default for a plain shell.
213215
- Use `okdev ssh --no-tmux` to bypass tmux for a single `okdev ssh` connection, or set `spec.ssh.persistentSession: false` to disable it globally.
214216
- The managed SSH host entry uses tight proxy keepalive settings so hung sessions fail fast instead of freezing.

internal/cli/ssh.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,7 @@ func newSSHCmd(opts *Options) *cobra.Command {
169169
KeepAliveTimeout: time.Duration(cc.cfg.Spec.SSH.KeepAliveTimeout) * time.Second,
170170
KeepAliveCountMax: cc.cfg.Spec.SSH.KeepAliveCountMax,
171171
}
172-
if noTmux {
173-
tm.Env = map[string]string{"OKDEV_NO_TMUX": "1"}
174-
}
172+
tm.Env = buildSSHSessionEnv(noTmux)
175173
tmRef.Store(tm)
176174
tm.SetReconnectTargetProvider(func(_ context.Context) (string, int, error) {
177175
pfMu.Lock()
@@ -823,6 +821,21 @@ func shouldReconnectShell(err error, connected bool) bool {
823821
strings.Contains(msg, "i/o timeout")
824822
}
825823

824+
func buildSSHSessionEnv(noTmux bool) map[string]string {
825+
env := map[string]string{}
826+
if noTmux {
827+
env["OKDEV_NO_TMUX"] = "1"
828+
return env
829+
}
830+
if strings.TrimSpace(os.Getenv("TMUX")) != "" {
831+
env["OKDEV_NESTED_TMUX"] = "1"
832+
}
833+
if len(env) == 0 {
834+
return nil
835+
}
836+
return env
837+
}
838+
826839
type sshSessionProber interface {
827840
ExecContext(ctx context.Context, cmd string) ([]byte, error)
828841
}

internal/cli/ssh_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,22 @@ func TestResolveLocalSSHAgentSocket(t *testing.T) {
4242
t.Fatal("expected non-socket SSH_AUTH_SOCK error")
4343
}
4444
}
45+
46+
func TestBuildSSHSessionEnv(t *testing.T) {
47+
t.Setenv("TMUX", "")
48+
if got := buildSSHSessionEnv(false); got != nil {
49+
t.Fatalf("expected nil env outside tmux, got %#v", got)
50+
}
51+
52+
t.Setenv("TMUX", "/tmp/tmux-1000/default,123,0")
53+
if got := buildSSHSessionEnv(false); got["OKDEV_NESTED_TMUX"] != "1" {
54+
t.Fatalf("expected nested tmux marker, got %#v", got)
55+
}
56+
57+
if got := buildSSHSessionEnv(true); got["OKDEV_NO_TMUX"] != "1" {
58+
t.Fatalf("expected no-tmux marker, got %#v", got)
59+
}
60+
if got := buildSSHSessionEnv(true); got["OKDEV_NESTED_TMUX"] != "" {
61+
t.Fatalf("did not expect nested marker when tmux is disabled, got %#v", got)
62+
}
63+
}

0 commit comments

Comments
 (0)