tui-panel-select 0.1.0

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.

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