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](https://docs.rs/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

```rust
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:

```rust,no_run
# 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:

```rust,no_run
# 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:

```rust,no_run
# 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