The low level details of the Linux TTY driver is a surprisingly deep subject. This very old page taught me a lot about it and a recent HN discussion reminded me to dig up the old link. Hopefully others find it as enlightening as I have.
Looking back over the times that the page has been submitted to Hacker News, it's almost an even split between with 'index.php' and without.
It's an interesting question for CIS URLs whether a url-path with an empty final component, that duplicates a url-path with an actual 'index' component (of some kind), is to be taken as the primary or the secondary form. When a site has 'index.php', 'index.html', 'index.gopher', 'index.gemini', and others, the question is whether the container is the same as the index file.
Frankly, the way the functionality needed to implement LF-to-CRLF conversion on output/CR-to-LF conversion on input/line editing/session and process management is split between the terminal itself, the kernel, and the application code is quite awkward.
And of course, the concerns of the serial line driver are thrown into the mix too, for the added entertainment.
The split-of-responsibilities point really shows up when you try to host a TUI inside a PTY you control. Spawning Claude Code, Codex, and other agents into terminal panes on macOS, I hit this chain of small surprises:
- SIGWINCH doesn't always fire on initial spawn — the TUI starts up thinking it has 0 columns and emits garbage until the first real resize. Fix: synthesize an ioctl(TIOCSWINSZ) before the first read, and re-send on focus events.
- xterm.js negotiates dimensions with the PTY backend over a non-obvious dance; off-by-one in the cell math wraps long prompts in the wrong place every time.
- Tools that detect "am I in a TTY" via isatty() behave differently from tools that stat() stdin; a few agents fall through to non-TUI mode if the PTY's mode bits aren't quite right.
None of that is reflected in the abstract "PTY is a virtual terminal" mental model. The kernel/terminal/application split is a leaky abstraction in practice — you only find out by hosting one inside the other.
> Tools that detect "am I in a TTY" via isatty() behave differently from tools that stat() stdin
Wait, how do you even use fstat's output to find out if the file is a tty?
Although in my experience the "funniest" part is deciding whether to use isatty() on stdin or on stdout. I mean, there is no much point enabling line editing/tab completion if stdin is a pipe/regular file, right?
Fair pushback — I was being sloppy. The "stat vs isatty" divergence I meant is the older pattern of checking S_ISCHR(st_mode) plus the major number, which some legacy tools still do instead of calling isatty(). Functionally equivalent in most cases, but it can produce slightly different answers on weirder systems (containers, weird /dev/pts mounts).
The stdin-vs-stdout split is where I see the most actual "is this a TTY" mistakes though. Tools that emit JSON-on-stdout-when-piped and TUI-when-not work fine until something stuffs them into a PTY with piped stdin — then they're in TUI mode but can't actually read the user input format they expect.
> The stdin-vs-stdout split is where I see the most actual "is this a TTY" mistakes though. Tools that emit JSON-on-stdout-when-piped and TUI-when-not work fine until something stuffs them into a PTY with piped stdin — then they're in TUI mode but can't actually read the user input format they expect.
Stuff like this is why a build script I used to maintain would redirect stdin from /dev/null when running commands that were intended to be non-interactive. You only need one script to hang forever waiting for a user to type in a password to decide that you'll force the issue going forward.
Same problem flipped: I once watched a CI step hang for 47 minutes because some sub-command popped a `read -p "Continue?"` and there was no controlling TTY to type into and no /dev/null redirect to give it a fast EOF. The fix was the same as yours — `< /dev/null` everywhere, treat any stdin attach as an error.
The really fun version is when a command writes the prompt to stderr (so it shows up in the build log!) and then reads from a stdin you didn't realize was still open. Took embarrassingly long to track down.
The low level details of the Linux TTY driver is a surprisingly deep subject. This very old page taught me a lot about it and a recent HN discussion reminded me to dig up the old link. Hopefully others find it as enlightening as I have.
The “index.php” in the URL is redundant.
Looking back over the times that the page has been submitted to Hacker News, it's almost an even split between with 'index.php' and without.
It's an interesting question for CIS URLs whether a url-path with an empty final component, that duplicates a url-path with an actual 'index' component (of some kind), is to be taken as the primary or the secondary form. When a site has 'index.php', 'index.html', 'index.gopher', 'index.gemini', and others, the question is whether the container is the same as the index file.
Frankly, the way the functionality needed to implement LF-to-CRLF conversion on output/CR-to-LF conversion on input/line editing/session and process management is split between the terminal itself, the kernel, and the application code is quite awkward.
And of course, the concerns of the serial line driver are thrown into the mix too, for the added entertainment.
The split-of-responsibilities point really shows up when you try to host a TUI inside a PTY you control. Spawning Claude Code, Codex, and other agents into terminal panes on macOS, I hit this chain of small surprises:
- SIGWINCH doesn't always fire on initial spawn — the TUI starts up thinking it has 0 columns and emits garbage until the first real resize. Fix: synthesize an ioctl(TIOCSWINSZ) before the first read, and re-send on focus events. - xterm.js negotiates dimensions with the PTY backend over a non-obvious dance; off-by-one in the cell math wraps long prompts in the wrong place every time. - Tools that detect "am I in a TTY" via isatty() behave differently from tools that stat() stdin; a few agents fall through to non-TUI mode if the PTY's mode bits aren't quite right.
None of that is reflected in the abstract "PTY is a virtual terminal" mental model. The kernel/terminal/application split is a leaky abstraction in practice — you only find out by hosting one inside the other.
> Tools that detect "am I in a TTY" via isatty() behave differently from tools that stat() stdin
Wait, how do you even use fstat's output to find out if the file is a tty?
Although in my experience the "funniest" part is deciding whether to use isatty() on stdin or on stdout. I mean, there is no much point enabling line editing/tab completion if stdin is a pipe/regular file, right?
Fair pushback — I was being sloppy. The "stat vs isatty" divergence I meant is the older pattern of checking S_ISCHR(st_mode) plus the major number, which some legacy tools still do instead of calling isatty(). Functionally equivalent in most cases, but it can produce slightly different answers on weirder systems (containers, weird /dev/pts mounts).
The stdin-vs-stdout split is where I see the most actual "is this a TTY" mistakes though. Tools that emit JSON-on-stdout-when-piped and TUI-when-not work fine until something stuffs them into a PTY with piped stdin — then they're in TUI mode but can't actually read the user input format they expect.
> The stdin-vs-stdout split is where I see the most actual "is this a TTY" mistakes though. Tools that emit JSON-on-stdout-when-piped and TUI-when-not work fine until something stuffs them into a PTY with piped stdin — then they're in TUI mode but can't actually read the user input format they expect.
Stuff like this is why a build script I used to maintain would redirect stdin from /dev/null when running commands that were intended to be non-interactive. You only need one script to hang forever waiting for a user to type in a password to decide that you'll force the issue going forward.
Same problem flipped: I once watched a CI step hang for 47 minutes because some sub-command popped a `read -p "Continue?"` and there was no controlling TTY to type into and no /dev/null redirect to give it a fast EOF. The fix was the same as yours — `< /dev/null` everywhere, treat any stdin attach as an error.
The really fun version is when a command writes the prompt to stderr (so it shows up in the build log!) and then reads from a stdin you didn't realize was still open. Took embarrassingly long to track down.