Search by

wachterjohannes / tui-mouse

wachterjohannes

Mouse support for symfony/tui, without touching vendor code

Package info

github.com/wachterjohannes/tui-mouse

pkg:composer/wachterjohannes/tui-mouse

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-09-29 08:23 UTC

This package is auto-updated.

Last update: 2026-09-29 08:34:06 UTC


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 (default false): use the alternate screen (?1049).
  • drag (default false): also report movement while a button is held (?1002).
  • signals (default true): 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): ?MouseEvent reads SGR (ESC [ < b ; x ; y M|m) and the old X10 form. Anything else gives null, including Kitty keyboard release events such as ESC [ 106;1:3 u.
  • MouseEvent has kind, button, x, y, shift, alt and ctrl.
  • MouseTracking writes the enable and disable sequences and restores on stop, exceptions, fatal errors and signals.
  • ClickTracker counts 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' or reset in 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 pcntl and posix. Without posix the process exits with 128 + signal and other handlers do not run. If your app has its own signal handlers that stop the Tui, pass signals => false and call disable() 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::Other or 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