Skip to content

Commit ff59792

Browse files
fix: sesh channel fixes and inherit TTY fds for interactive actions (#1052)
Closes joshmedeski/sesh#368 - Fix an interaction between \`tv\`'s execute actions and \`tmux attach-session\` on macOS that surfaces as \`open terminal failed: can't use /dev/tty\` when an action shells out to tmux from outside a running tmux client. - Let \`sesh\` (and other session managers) manage their own ordering by adding \`frecency = false\` / \`no_sort = true\` to the built-in \`sesh\` channel, and tidy a couple of formatting nits in \`cable/unix/sesh.toml\`. ## The TTY bug \`attach_to_tty\` in \`television/utils/command.rs\` unconditionally reopens \`/dev/tty\` and dup's the resulting FD onto the child's stdin/stdout/stderr before every execute/fork action. That behaviour is necessary when \`tv\` is invoked with piped streams (\`ls | tv --preview ...\`), but in the normal interactive case it hands the child a file descriptor whose kernel path is \`/dev/tty\`. On macOS, \`ttyname()\` on such an FD resolves back to \`/dev/tty\` rather than to the underlying pty device (e.g. \`/dev/ttys001\`). When the child eventually runs \`tmux attach-session\`, tmux's client code rejects that path in \`tty.c\` — it explicitly refuses \`/dev/tty\` because it needs an unambiguous, openable device — and exits 1. The user sees: \`\`\` open terminal failed: can't use /dev/tty ERROR Failed to attach to tmux session: exit status 1. \`\`\` Running the exact same \`sesh connect <session>\` command from the same shell (fish/zsh/bash) outside of \`tv\` works fine, because the shell's stdin is the real pty slave, not a dup of \`/dev/tty\`. That's the tell that the issue lives in \`attach_to_tty\`, not in sesh/tmux. This only surfaces now because previously the path most people hit was \`tv sesh\` from inside tmux, which takes the \`tmux switch-client\` branch in sesh — \`switch-client\` is a non-interactive tmux subcommand and tolerates any FD that \`isatty()\` accepts, so the bug stays hidden. ### The fix Check whether the inherited stdin/stdout/stderr are already TTYs (via \`std::io::IsTerminal\`, no new dependencies). If so, inherit them as-is so the child sees the real pty device path. Otherwise fall through to the existing \`/dev/tty\` fallback so piped invocations keep working. Before calling \`execute_action\`, \`tv\` already tears down its TUI (\`RenderingTask::Quit\`, \`disable_raw_mode\`, etc. in \`app.rs\`), so the inherited FDs are in cooked mode — safe to pass through. ## Channel config tweak The \`sesh\` channel now sets: \`\`\`toml frecency = false # handled by sesh no_sort = true # handled by sesh \`\`\` \`sesh list\` already returns sessions in the order users expect (recently-used tmux sessions first, then config/zoxide entries). Layering \`tv\`'s frecency and default sort on top of that reorders the list in confusing ways — better to defer to sesh. Also reflowed the \`command = [...]\` array and the \`ctrl-d\` binding to match the formatter's output. ## Test plan - [x] \`cargo check --lib\` clean - [x] \`cargo build\` clean (debug binary exercised manually) - [x] \`tv sesh\` from outside tmux (server running, no attached client) attaches to an existing session and returns cleanly on detach - [x] \`tv sesh\` from inside tmux still switches (unchanged path) - [ ] \`ls | tv\` style piped invocation — \`/dev/tty\` fallback still engaged; preview/actions still usable - [ ] Linux sanity check (macOS-specific symptom, but the fix is portable) The test suite wasn't run because \`libghostty-vt-sys\` in this checkout requires zig 0.15.2 while my local zig is 0.16.0 — unrelated to this change. --------- Co-authored-by: Alex Pasmantier <47638216+alexpasmantier@users.noreply.github.com>
1 parent ec0db0a commit ff59792

2 files changed

Lines changed: 22 additions & 6 deletions

File tree

cable/unix/sesh.toml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,23 @@ requirements = ["sesh", "fd"]
1010
# Multiple source commands for cycling through different modes
1111
# Press Ctrl+S to cycle between sources
1212
command = [
13-
"sesh list --icons", # all sessions (default)
14-
"sesh list -t --icons", # tmux sessions only
15-
"sesh list -c --icons", # config directories
16-
"sesh list -z --icons", # zoxide directories
17-
"fd -H -d 2 -t d -E .Trash . ~" # find directories
13+
"sesh list --icons", # all sessions (default)
14+
"sesh list -t --icons", # tmux sessions only
15+
"sesh list -c --icons", # config directories
16+
"sesh list -z --icons", # zoxide directories
17+
"fd -H -d 2 -t d -E .Trash . ~", # find directories
1818
]
1919
ansi = true
20+
frecency = false # handled by sesh
21+
no_sort = true # handled by sesh
2022
output = "{strip_ansi|split: :1..|join: }"
2123

2224
[preview]
2325
command = "sesh preview '{strip_ansi|split: :1..|join: }'"
2426

2527
[keybindings]
2628
enter = "actions:connect"
27-
ctrl-d = [ "actions:kill_session", "reload_source" ]
29+
ctrl-d = ["actions:kill_session", "reload_source"]
2830

2931
[actions.connect]
3032
description = "Connect to selected session"

television/utils/command.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,20 @@ pub fn execute_action(
204204

205205
#[cfg(unix)]
206206
fn attach_to_tty(cmd: &mut Command) -> Result<()> {
207+
use std::io::{IsTerminal, stderr, stdin, stdout};
208+
209+
// If stdin/stdout/stderr are already real TTYs (interactive invocation),
210+
// inherit them as-is. Reopening `/dev/tty` and dup'ing the resulting FD
211+
// produces a file descriptor whose ttyname resolves to `/dev/tty` on
212+
// macOS, which tmux rejects with "can't use /dev/tty" when attaching.
213+
// Inheriting the original pty FDs preserves the real device path.
214+
if stdin().is_terminal()
215+
&& stdout().is_terminal()
216+
&& stderr().is_terminal()
217+
{
218+
return Ok(());
219+
}
220+
207221
let Ok(tty) = OpenOptions::new().read(true).write(true).open("/dev/tty")
208222
else {
209223
return Ok(());

0 commit comments

Comments
 (0)