Skip to content

Give the size of the terminal to the PTY of the processes - #921

Merged
lyrixx merged 1 commit into
mainfrom
pty-size
Oct 22, 2026
Merged

lyrixx merged 1 commit into
mainfrom
pty-size

Conversation

@lyrixx

@lyrixx lyrixx commented Oct 21, 2026

Copy link
Copy Markdown
Member

PHP creates the PTY of a process without a size (0x0), and has no API to change it. So every command run by Castor in a PTY (the default) saw a 0x0 terminal: PHPStan, PHP-CS-Fixer, etc. fell back on 80 columns, docker compose run gave a 0x0 terminal to the container, and Node tools saw 0 columns.

The command now runs through sh, which copies the size of the terminal of Castor (/dev/tty) to the PTY, then execs the command: same PID, signals and exit code. getCommandLine() still returns the command as asked, so logs, errors and notifications don't change. Without a terminal (CI, AI agents), the command is not wrapped at all. Only the size at the start of the command: following a resize would need the master side of the PTY, which is private in Symfony Process.

This code scares me a bit (a subclass of Symfony's Process, a shell around every command), but it's well tested. tests/PtySizeTest.php runs Castor in a pseudo terminal (script) and checks the size for array and string commands, the command line shown, the exit code, the failure message, and that nothing changes without a terminal (setsid). It costs about 3.5 ms per command in a PTY, only when Castor has a terminal. Not tested on macOS.

There is also a lighter alternative, which passes COLUMNS and LINES instead: see the end of this description.

By use case

pty-size is this PR, pty-columns the alternative.

Use case main pty-size pty-columns Measured by
PHP tools locally (castor phpstan in Castor) ❌ 80 columns ✅ 120 ✅ 120 me and my agent
PHP tools in Docker, docker-starter project (qotd: qa:phpstan, qa:twig-cs) ❌ 80 columns ✅ 120 ❌ 80 columns, unless the project adds -e COLUMNS -e LINES me and my agent
PHP tools in Docker with the castor-php/docker plugin (redirection.io: backend:qa:phpstan) ✅ 120 ✅ 120 ✅ 120 me
A task that runs Castor again (redirection.io: castor phpstan) ❌ 80 columns ✅ 120 ✅ 120 me
Node tools (npm, Vite, Jest…) ❌ 0 columns ✅ real size ❌ 0 columns: Node ignores COLUMNS my agent
TTY command in Docker (qotd: castor builder) ✅ ✅ ✅ (compose sets the size just after the start, for the three) me and my agent
TTY command locally ✅ ✅ ✅ me
Full screen without withTty() (vim, top) ❌ reported unusable ⚠️ right size at the start, does not follow resizing ⚠️ depends on the program, does not follow resizing either not measured
Resizing the terminal during a long task n/a ✅ each new command gets the new size ⚠️ the whole task keeps the size Castor started with from the code
CI, AI agent (no terminal) 80 columns unchanged unchanged tests
Output redirected from a terminal (castor phpstan > log) 80 columns width of the terminal in the file 80 columns from the code

On a docker-starter project, only pty-size fixes the width without changing each project.

Lighter alternative: COLUMNS and LINES

The PTY stays 0x0, but Castor gives the size of its terminal in COLUMNS and LINES, which most tools read first (Symfony Console, Python, tput, ls). Node ignores them, and they don't reach the containers unless the project passes them with -e. Known issue: buildRunnableCommand() reads getEnv(), so the error message of a failed command shows LINES='40' COLUMNS='150' 'false'. It could be fixed by passing the variables to start() instead.

diff --git a/src/Runner/ProcessRunner.php b/src/Runner/ProcessRunner.php
index 37e2a2dd..a85b56e7 100644
--- a/src/Runner/ProcessRunner.php
+++ b/src/Runner/ProcessRunner.php
@@ -18,6 +18,7 @@ use JoliCode\PhpOsHelper\OsHelper;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Console\Terminal;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\Process\Exception\ProcessFailedException;
 use Symfony\Component\Process\Exception\ProcessSignaledException;
@@ -117,6 +118,16 @@ class ProcessRunner
             } elseif ($context->pty) {
                 $process->setPty(true);
                 $process->setInput(\STDIN);
+
+                // PHP creates the PTY without a size (0x0): the tools would fall
+                // back on 80 columns, but most of them read COLUMNS and LINES first
+                if (stream_isatty(\STDOUT)) {
+                    $terminal = new Terminal();
+                    $process->setEnv($process->getEnv() + [
+                        'COLUMNS' => (string) $terminal->getWidth(),
+                        'LINES' => (string) $terminal->getHeight(),
+                    ]);
+                }
             }
         }
 

PHP creates the PTY without a size (0x0): the commands fell back on 80
columns, and a full screen one was unusable. The command now runs through
a shell which gives the PTY the size of the terminal of Castor, then runs
the command in its place. Only when Castor has a terminal, and only the
size at the start of the command.
@lyrixx
lyrixx merged commit 032af8c into main Oct 22, 2026
12 checks passed
@lyrixx
lyrixx deleted the pty-size branch October 22, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants