wachterjohannes / tui-mouse
Mouse support for symfony/tui, without touching vendor code
Requires
- php: >=8.4
- revolt/event-loop: ^1.0
- symfony/tui: ^8.1
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^12.0
Suggests
- ext-pcntl: Restores the terminal on SIGTERM, SIGINT and SIGHUP
- ext-posix: Lets the signal handler pass the signal on after the restore
Provides
None
Conflicts
None
Replaces
None
README
Mouse support for symfony/tui. It adds clicks, double clicks, the wheel and drag events without changing any vendor code.
Tui hands every byte from the terminal to an InputEvent. This package listens to that event, turns mouse reports into MouseEvent objects and stops the event. Focused widgets never see the escape sequence as typed text.
Installation
composer require wachterjohannes/tui-mouse
PHP 8.4 or newer and symfony/tui ^8.1. The extensions pcntl and posix are optional. With them, the terminal is also restored on SIGTERM, SIGINT and SIGHUP.
Example
use Symfony\Component\Tui\Tui; use WachterJohannes\TuiMouse\ClickTracker; use WachterJohannes\TuiMouse\MouseEvent; use WachterJohannes\TuiMouse\MouseEventKind; use WachterJohannes\TuiMouse\MouseSupport; $tui = new Tui(); // ... add your widgets ... $clicks = new ClickTracker(); $tracking = MouseSupport::attach($tui, function (MouseEvent $e) use ($clicks): void { if (MouseEventKind::WheelDown === $e->kind) { // scroll } if (2 === $clicks->track($e)) { // double click on cell ($e->x, $e->y) } }, ['alternate_screen' => true]); try { $tui->run(); } finally { $tracking->disable(); }
attach() switches mouse reporting on right away and returns the MouseTracking object. Call disable() after run(). It is safe to call twice.
Options:
alternate_screen(defaultfalse): use the alternate screen (?1049).drag(defaultfalse): also report movement while a button is held (?1002).signals(defaulttrue): restore on SIGTERM, SIGINT and SIGHUP, then pass the signal on.
Coordinates and widgets
MouseEvent::$x and $y are 0-indexed terminal cells. The top left cell is 0, 0. This is the same convention as WidgetRect inside Tui.
Tui 8.1 has no public hit-test. Tui::getWidgetRect() does not exist. The renderer that tracks widget positions is private and marked internal. So you map clicks yourself. You know your layout. A widget at screen row $top with $height rows and column $left with $width columns is hit when:
$e->y >= $top && $e->y < $top + $height && $e->x >= $left && $e->x < $left + $width
This works when the Tui renders from the first screen row. Use alternate_screen => true for that. In the default inline mode Tui draws from the cursor and the package cannot know which screen row is the first line of your UI. Clicks then do not match widgets.
What is in the box
MouseParser::parse(string): ?MouseEventreads SGR (ESC [ < b ; x ; y M|m) and the old X10 form. Anything else givesnull, including Kitty keyboard release events such asESC [ 106;1:3 u.MouseEventhaskind,button,x,y,shift,altandctrl.MouseTrackingwrites the enable and disable sequences and restores on stop, exceptions, fatal errors and signals.ClickTrackercounts quick clicks on the same cell. The clock is injectable.
Limits
- Inline mode does not work for clicks, see above. Use the alternate screen.
- The package needs a terminal that speaks SGR or X10 mouse reporting. Modern terminals do. Some multiplexers filter reports unless their own mouse option is on.
- Stops that no code can catch (SIGKILL, a crash of the interpreter, a lost SSH link) leave reporting on. Run
printf '\e[?1000l\e[?1002l\e[?1006l'orresetin that case. - Signal handling uses the Revolt event loop, so it only fires while the loop runs. Code that blocks for a long time delays the restore. It needs
pcntlandposix. Withoutposixthe process exits with128 + signaland other handlers do not run. If your app has its own signal handlers that stop the Tui, passsignals => falseand calldisable()yourself after the Tui has stopped. - One report per input event is assumed. Tui splits the stream, but a terminal that sends reports glued together in one read would be ignored by the parser and reach the widgets.
- Wheel release, horizontal wheel and extra buttons are reported as
MouseButton::Otheror dropped. Move events need a terminal mode (?1003) that this package does not enable. - Mouse selection of text by the terminal is off while reporting is on. Most terminals let you hold Shift to select.
- Focus follows the keyboard only. A click does not focus a widget. You do that in your handler.
- The package is tested against Tui 8.1, which is marked experimental. The event API can change.
Development
composer test
composer phpstan
License
MIT