first commit
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
|
||||
"github.com/smallnest/pigo/internal/cli/ui"
|
||||
)
|
||||
|
||||
// This file implements mouse text selection over the rendered shell. Because the
|
||||
// model paints the whole screen as one string (transcript + menu + input +
|
||||
// status), a selection is expressed in screen cells (0-based, top-left origin)
|
||||
// and spans any region uniformly — dragging across transcript output or the
|
||||
// input line both work. The left mouse button starts a selection at the press
|
||||
// cell and extends it on drag; the selection persists after release so Ctrl+C
|
||||
// can copy it. A plain click (no drag) leaves an empty selection, which clears
|
||||
// any prior highlight and lets Ctrl+C fall back to its interrupt/quit role.
|
||||
|
||||
// maxCol is a sentinel column that reaches past the end of any rendered row, so
|
||||
// a multi-row selection's interior rows select through to their line end.
|
||||
const maxCol = 1 << 30
|
||||
|
||||
// point is a screen cell: x is the column, y the row, both 0-based from the
|
||||
// top-left of the rendered shell.
|
||||
type point struct{ x, y int }
|
||||
|
||||
// selection is an in-progress or completed text selection. anchor is where the
|
||||
// drag began and cursor is the latest drag point; active is set between the
|
||||
// initial press and the next fresh press. It is a plain value so the Model can
|
||||
// hold and copy it cheaply.
|
||||
type selection struct {
|
||||
active bool
|
||||
anchor point
|
||||
cursor point
|
||||
}
|
||||
|
||||
// empty reports whether the selection covers no cells — either inactive or a
|
||||
// bare click where the cursor never moved off the anchor. Ctrl+C treats an empty
|
||||
// selection as "nothing to copy" and keeps its interrupt/quit behavior.
|
||||
func (s selection) empty() bool {
|
||||
return !s.active || s.anchor == s.cursor
|
||||
}
|
||||
|
||||
// ordered returns the selection endpoints in reading order (top-to-bottom, and
|
||||
// left-to-right within a row), so callers can walk rows start.y..end.y without
|
||||
// re-checking which of anchor/cursor came first.
|
||||
func (s selection) ordered() (start, end point) {
|
||||
a, c := s.anchor, s.cursor
|
||||
if a.y > c.y || (a.y == c.y && a.x > c.x) {
|
||||
return c, a
|
||||
}
|
||||
return a, c
|
||||
}
|
||||
|
||||
// rowRange computes the selected column span [c0, c1) on screen row y for a
|
||||
// selection running start..end. Interior rows of a multi-row selection run from
|
||||
// column 0 through the line end (maxCol); the first row starts at start.x and
|
||||
// the last ends at end.x. ok is false when y falls outside the selection.
|
||||
func rowRange(start, end point, y int) (c0, c1 int, ok bool) {
|
||||
if y < start.y || y > end.y {
|
||||
return 0, 0, false
|
||||
}
|
||||
c0, c1 = 0, maxCol
|
||||
if y == start.y {
|
||||
c0 = start.x
|
||||
}
|
||||
if y == end.y {
|
||||
c1 = end.x
|
||||
}
|
||||
if c0 < 0 {
|
||||
c0 = 0
|
||||
}
|
||||
if c1 < c0 {
|
||||
c1 = c0
|
||||
}
|
||||
return c0, c1, true
|
||||
}
|
||||
|
||||
// selectRow walks one rendered row (ANSI stripped to plain cells) and returns
|
||||
// both the row with the selected span visually highlighted and the selected
|
||||
// text itself. Columns are measured in display cells (ui.Width) so double-width
|
||||
// runes are never split. The highlight is applied over plain text — the row's
|
||||
// original coloring is dropped on the intersected row while a selection is live,
|
||||
// which keeps the overlay ANSI-safe without parsing the embedded escapes.
|
||||
func selectRow(row string, c0, c1 int, hi lipgloss.Style) (highlighted, text string) {
|
||||
plain := ansi.Strip(row)
|
||||
|
||||
var out, sel, run strings.Builder
|
||||
flush := func() {
|
||||
if run.Len() > 0 {
|
||||
out.WriteString(hi.Render(run.String()))
|
||||
run.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
col := 0
|
||||
for _, r := range plain {
|
||||
w := ui.Width(string(r))
|
||||
if col >= c0 && col < c1 {
|
||||
run.WriteRune(r)
|
||||
sel.WriteRune(r)
|
||||
} else {
|
||||
flush()
|
||||
out.WriteRune(r)
|
||||
}
|
||||
col += w
|
||||
}
|
||||
flush()
|
||||
return out.String(), sel.String()
|
||||
}
|
||||
Reference in New Issue
Block a user