tui-panel-select 0.1.1

Panel-scoped mouse text selection and clipboard copy for ratatui apps
Documentation

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 std::sync::Arc;
use ratatui::layout::Rect;
use tui_panel_select::SelectablePanel;

let mut panel = SelectablePanel::new();

// Each frame, before drawing, give the panel its text and inner width.
panel.set_content(Arc::from("hello world\nsecond line"), 40);

// The panel's inner text area on screen, and its scroll offset (wrapped rows).
let area = Rect::new(1, 1, 40, 10);
let scroll = 0;

// Mouse down starts a selection; drag extends it; up copies it.
panel.begin_selection(area, scroll, (1, 1));   // click at 'h'
panel.extend_selection(area, scroll, (5, 1));  // drag to 'o'
assert_eq!(panel.selected_text().as_deref(), Some("hello"));
panel.copy_selection();                         // -> system clipboard

Rendering each frame:

# use tui_panel_select::SelectablePanel;
# use ratatui::layout::Rect;
# let panel = SelectablePanel::new();
# let area = Rect::new(0, 0, 40, 10);
# let scroll = 0u16;
// 1. Draw the visible wrapped rows into your panel:
let rows = panel.visible_rows(scroll, area.height);

// 2. Paint the highlight over the selected cells:
for (row, col_from, col_to) in panel.highlight_cells(area, scroll) {
    // invert/style cells [col_from, col_to) on terminal row `row`
}

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 tui_panel_select::{MouseConfig, SelectablePanel};
# use ratatui::layout::Rect;
# use ratatui::crossterm::event::MouseEvent;
# fn demo(panel: &mut SelectablePanel, area: Rect, scroll: u16, ev: MouseEvent) {
let cfg = MouseConfig::default(); // copy_on_release: true
let _action = panel.handle_mouse(ev, area, scroll, &cfg);
# }

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:

# fn main() -> std::io::Result<()> {
use tui_panel_select::TerminalGuard;

let mut terminal = ratatui::init();
let guard = TerminalGuard::install(true)?;
// ... run your event loop ...
drop(guard);           // restores mouse capture / keyboard flags
ratatui::restore();
# Ok(())
# }

Disable the terminal-guard feature (default-features = false) if you only want the pure selection/wrapping logic without the process-global panic hook.

Line layout: wrap or clip (WrapMode)

By default each raw line wider than the panel is wrapped onto multiple rows. For panels that show pre-formatted, column-aligned output (e.g. program output echoed verbatim), call set_wrap_mode(WrapMode::Clip) instead: every raw line then occupies exactly one screen row and anything past the right edge is clipped. Selection, copy and scrolling all follow suit (scrolling moves by whole lines, one row per line).

# use tui_panel_select::{SelectablePanel, WrapMode};
let mut panel = SelectablePanel::new();
panel.set_wrap_mode(WrapMode::Clip);

ANSI-coloured content (feature ansi, off by default)

If your panel text contains ANSI escape sequences (e.g. coloured program output), enable the ansi feature and feed it via set_ansi_content instead of set_content. Rendered rows (visible_rows) keep their colour, while selection, copy and all geometry operate on the plain, stripped text — so you don't have to maintain a second, un-coloured copy yourself:

tui-panel-select = { version = "0.1", features = ["ansi"] }
# use std::sync::Arc;
# use tui_panel_select::{SelectablePanel, WrapMode};
# fn demo(panel: &mut SelectablePanel, colored: Arc<str>, width: usize) {
panel.set_wrap_mode(WrapMode::Clip);      // often paired with clip for verbatim output
panel.set_ansi_content(colored, width);   // escapes parsed once, colour preserved
# }

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