tui-panel-select
Panel-scoped mouse text selection and clipboard copy for ratatui apps.
A terminal's own click-drag selection can't be confined to one panel — it spans the full terminal row, sweeping up borders and neighbouring panels. This crate lets your app capture the mouse itself and implement a selection that is:
- Confined to a single panel's rectangle — never spills into other panels or the border.
- Natural "stream" selection — first line from the click column to its end, full lines in between, last line up to the release column (never a rectangular block).
- Stable across resize / rewrap / scroll — selections are stored as logical
(line, column)positions, not stale screen cells, so the same characters stay selected when the panel is resized or rewrapped. - Cheap on huge content — only the rows actually on screen are ever wrapped or painted, so a multi-megabyte body (or one enormous unbroken line) stays responsive.
- Copy that works locally and remotely — on mouse-up the text is copied via
a local clipboard tool (
wl-copy/xclip/xsel/pbcopy/clip.exe) when available, falling back to an OSC 52 escape sequence for SSH/tmux sessions.
Quick start — the batteries-included API
use Arc;
use Rect;
use SelectablePanel;
let mut panel = new;
// Each frame, before drawing, give the panel its text and inner width.
panel.set_content;
// The panel's inner text area on screen, and its scroll offset (wrapped rows).
let area = new;
let scroll = 0;
// Mouse down starts a selection; drag extends it; up copies it.
panel.begin_selection; // click at 'h'
panel.extend_selection; // drag to 'o'
assert_eq!;
panel.copy_selection; // -> system clipboard
Rendering each frame:
# use SelectablePanel;
# use Rect;
# let panel = new;
# let area = new;
# let scroll = 0u16;
// 1. Draw the visible wrapped rows into your panel:
let rows = panel.visible_rows;
// 2. Paint the highlight over the selected cells:
for in panel.highlight_cells
Wire these into your event loop: capture the mouse (EnableMouseCapture), then
call begin_selection on MouseEventKind::Down, extend_selection on Drag,
and copy_selection on Up. Call set_content every frame — it only rebuilds
its cache when the text (by Arc identity) or width actually changed.
Opt-in mouse handler
If you'd rather not wire the three events up yourself, handle_mouse does the
common "drag to select, release to copy" flow in one call. Behaviour is
configured per-application via MouseConfig (e.g. copy_on_release), and the
low-level methods above stay available:
# use ;
# use Rect;
# use MouseEvent;
#
Panic-safe terminal guard (feature terminal-guard, on by default)
Enabling panel selection means turning on the terminal's mouse-tracking mode,
which must be undone on exit and on any panic — otherwise the user's shell is
left spewing tracking escape sequences. TerminalGuard centralises that: it
enables mouse capture (and, optionally, the keyboard-enhancement protocol),
wraps the panic hook, and restores everything on drop:
#
Disable the terminal-guard feature (default-features = false) if you only
want the pure selection/wrapping logic without the process-global panic hook.
Low-level primitives
If your app already owns its selection state (e.g. multiple simultaneous
selections, keyboard-extended selection, excluding decorative characters from
the copied text), skip SelectablePanel and use the stateless building blocks
directly:
- [
wrapcache::PanelWrap] / [wrapcache::TextPos] — the line/wrap cache and logical positions, with conversions between screen and logical space. - [
selection] — pure functions:point_to_textpos,extract_text,highlight_cells,strip_positions. - [
clipboard::copy_to_clipboard] — local tool + OSC 52 fallback. - [
wrap] — the underlying character-exact line-wrapping helpers.
License
MIT