1package copy
2
3import (
4 "encoding/base64"
5 "math"
6 "slices"
7 "strconv"
8 "strings"
9 "sync"
10 "unicode"
11
12 vt "github.com/bnema/vev-vt"
13 renderer "github.com/bnema/vev-vt/ansi"
14)
15
16const OSC52MaxPayloadBytes = 75_000
17
18// Snapshot is the immutable scrollback document: sealed history plus a cloned
19// screen. screenBounds and screenRowIDs are parallel to screen rows, exactly as
20// the history view's own metadata is parallel to its chunk rows.
21type Snapshot struct {
22 history vt.HistoryView
23 screen renderer.Frame
24 screenBounds []vt.LineBound
25 screenRowIDs []vt.RowID
26 Width, Height int
27}
28
29func NewSnapshot(historySource *vt.History, screen vt.CellSource, bounds []vt.LineBound, rowIDs []vt.RowID) Snapshot {
30 return newSnapshot(historySource, screen, bounds, append([]vt.RowID(nil), rowIDs...))
31}
32
33// NewSnapshotFromScreen captures a screen while its owner holds the Screen
34// lock. Screen.RowIDs returns the one owned copy required for immutable
35// snapshot state; newSnapshot takes that slice directly without duplicating
36// it. The live screen's rowIDs cannot be transferred or shared because later
37// scroll, resize, and clear mutations update that storage.
38func NewSnapshotFromScreen(historySource *vt.History, screen *vt.Screen) Snapshot {
39 if screen == nil {
40 return NewSnapshot(historySource, renderer.Frame{}, nil, nil)
41 }
42 return newSnapshot(historySource, screen, screen.LineBounds(), screen.RowIDs())
43}
44
45func newSnapshot(historySource *vt.History, screen vt.CellSource, bounds []vt.LineBound, screenRowIDs []vt.RowID) Snapshot {
46 owned := renderer.NewFrame(screen.Columns(), screen.Rows())
47 for y := range screen.Rows() {
48 for x := range screen.Columns() {
49 owned.Set(x, y, screen.Cell(x, y))
50 }
51 }
52 var history vt.HistoryView
53 if historySource != nil {
54 history = historySource.SealAndView()
55 }
56 return Snapshot{
57 history: history,
58 screen: owned,
59 screenBounds: append([]vt.LineBound(nil), bounds...),
60 screenRowIDs: screenRowIDs,
61 Width: screen.Columns(),
62 Height: screen.Rows(),
63 }
64}
65
66// NewSnapshotFromLines builds a snapshot from raw rows and their bounds. It
67// exists for tests; production code goes through NewSnapshot.
68func NewSnapshotFromLines(rows [][]renderer.Cell, bounds []vt.LineBound, width, height int) Snapshot {
69 history := vt.NewHistory(vt.HistoryConfig{
70 MaxRows: max(len(rows), 1),
71 MaxBytes: math.MaxUint64, // Input is already owned and bounded by the caller.
72 ChunkRows: 256,
73 })
74 for i, row := range rows {
75 bound := vt.LineBound{End: len(row)}
76 if i < len(bounds) {
77 bound = bounds[i]
78 }
79 if err := history.Append(row, bound); err != nil {
80 panic("copy snapshot: configured history rejected supplied row")
81 }
82 }
83 return Snapshot{history: history.SealAndView(), Width: width, Height: height}
84}
85
86// NewSnapshotFromRows builds a snapshot whose rows are all hard logical lines.
87func NewSnapshotFromRows(rows [][]renderer.Cell, width, height int) Snapshot {
88 return NewSnapshotFromLines(rows, nil, width, height)
89}
90
91func (s Snapshot) Len() int { return s.history.Len() + s.screen.Height }
92func (s Snapshot) Row(i int) []renderer.Cell {
93 if i < 0 {
94 return nil
95 }
96 if i < s.history.Len() {
97 return s.history.Row(i)
98 }
99 i -= s.history.Len()
100 if i >= s.screen.Height {
101 return nil
102 }
103 return s.screen.Row(i)
104}
105
106func (s Snapshot) RowWidth(y int) int {
107 if y < 0 {
108 return 0
109 }
110 if y < s.history.Len() {
111 return s.history.RowWidth(y)
112 }
113 y -= s.history.Len()
114 if y >= s.screen.Height {
115 return 0
116 }
117 return s.screen.Width
118}
119
120func (s Snapshot) Cell(x, y int) renderer.Cell {
121 if y < 0 || x < 0 {
122 return renderer.BlankCell()
123 }
124 if y < s.history.Len() {
125 return s.history.Cell(x, y)
126 }
127 y -= s.history.Len()
128 if y >= s.screen.Height || x >= s.screen.Width {
129 return renderer.BlankCell()
130 }
131 return s.screen.Cell(x, y)
132}
133
134func (s Snapshot) CopyRow(y int, dst []renderer.Cell) int {
135 if y < 0 {
136 return 0
137 }
138 if y < s.history.Len() {
139 return s.history.CopyRow(y, dst)
140 }
141 y -= s.history.Len()
142 if y >= s.screen.Height {
143 return 0
144 }
145 n := min(len(dst), s.screen.Width)
146 for x := range n {
147 dst[x] = s.screen.Cell(x, y)
148 }
149 return n
150}
151
152// Bound returns the logical extent of row i, dispatching between sealed history
153// and the live screen exactly as Row does.
154func (s Snapshot) Bound(i int) vt.LineBound {
155 if i < 0 {
156 return vt.LineBound{}
157 }
158 if i < s.history.Len() {
159 return s.history.Bound(i)
160 }
161 i -= s.history.Len()
162 if i >= s.screen.Height || i >= len(s.screenBounds) {
163 return vt.LineBound{}
164 }
165 return s.screenBounds[i]
166}
167
168// RowIDs returns an owned copy of physical row identities in document order.
169func (s Snapshot) RowIDs() []vt.RowID {
170 ids := make([]vt.RowID, s.Len())
171 for i := range ids {
172 ids[i] = s.RowID(i)
173 }
174 return ids
175}
176
177// RowID returns the identity of document row i, or zero when it is unavailable.
178func (s Snapshot) RowID(i int) vt.RowID {
179 if i < 0 {
180 return 0
181 }
182 if i < s.history.Len() {
183 return s.history.RowID(i)
184 }
185 i -= s.history.Len()
186 if i >= s.screen.Height || i >= len(s.screenRowIDs) {
187 return 0
188 }
189 return s.screenRowIDs[i]
190}
191
192// FindRowID returns the document row containing id, or -1 when it is absent.
193func (s Snapshot) FindRowID(id vt.RowID) int {
194 if id == 0 {
195 return -1
196 }
197 if row := s.history.FindRowID(id); row >= 0 {
198 return row
199 }
200 offset := s.history.Len()
201 for i, candidate := range s.screenRowIDs {
202 if i >= s.screen.Height {
203 break
204 }
205 if candidate == id {
206 return offset + i
207 }
208 }
209 return -1
210}
211
212// textRows streams every row as text: one rune per non-continuation cell
213// (zero runes read as spaces) plus each rune's cell column and the row width.
214// The slices are only valid during the callback.
215type textRows func(yield func(row int, runes []rune, columns []int, width int) bool) error
216
217func (s Snapshot) rangeText(yield func(int, []rune, []int, int) bool) error {
218 stopped := false
219 err := s.history.RangeText(func(i int, runes []rune, columns []int, width int) bool {
220 if !yield(i, runes, columns, width) {
221 stopped = true
222 return false
223 }
224 return true
225 })
226 if err != nil || stopped {
227 return err
228 }
229 offset := s.history.Len()
230 var runes []rune
231 var columns []int
232 for y := range s.screen.Height {
233 runes, columns = s.screen.AppendRowText(runes[:0], columns[:0], y)
234 if !yield(offset+y, runes, columns, s.screen.Width) {
235 return nil
236 }
237 }
238 return nil
239}
240
241type SearchMatch struct {
242 Row, Start, End int
243 Text string
244} // End is exclusive display-cell offset.
245
246type Mode struct {
247 document *Document
248 navigator Navigator
249 selection Selection
250 ViewportTop int
251 SearchQuery string
252 Searches []SearchMatch
253 SearchIndex int
254}
255
256// rowPool recycles the semantic rows lent to RenderRowsRange callbacks.
257// Renderers snapshot Mode by value, so the buffer cannot live on Mode.
258var rowPool sync.Pool
259
260func NewMode(doc *Document) *Mode {
261 m := &Mode{document: doc, SearchIndex: -1}
262 if doc != nil && doc.Len() > 0 {
263 m.navigator = NewNavigator(Pos{Row: doc.Len() - 1})
264 m.navigator.Set(doc, m.navigator.Pos)
265 m.ViewportTop = max(doc.Len()-max(doc.Height(), 1), 0)
266 }
267 return m
268}
269func (m *Mode) Document() *Document {
270 if m == nil {
271 return nil
272 }
273 return m.document
274}
275func (m *Mode) Cursor() Pos {
276 if m == nil {
277 return Pos{}
278 }
279 return m.navigator.Pos
280}
281func (m *Mode) Selection() Selection {
282 if m == nil {
283 return Selection{}
284 }
285 return m.selection
286}
287func (m *Mode) SetPosition(p Pos) bool {
288 if m == nil {
289 return false
290 }
291 changed := m.navigator.Set(m.document, p)
292 m.adjustViewport()
293 return changed
294}
295func (m *Mode) move(op func(*Document) bool, stream bool) bool {
296 if m == nil || m.document == nil {
297 return false
298 }
299 if stream && m.selection.Enabled && m.selection.Granularity == Line {
300 m.selection.AsCharacter()
301 }
302 changed := op(m.document)
303 if changed {
304 if m.selection.Enabled {
305 m.selection.Extend(m.navigator.Pos)
306 }
307 m.adjustViewport()
308 }
309 return changed
310}
311func (m *Mode) Left() bool { return m.move(m.navigator.Left, true) }
312func (m *Mode) Right() bool { return m.move(m.navigator.Right, true) }
313func (m *Mode) Up() bool { return m.move(m.navigator.Up, false) }
314func (m *Mode) Down() bool { return m.move(m.navigator.Down, false) }
315func (m *Mode) WordNext() bool { return m.move(m.navigator.WordNext, true) }
316func (m *Mode) WordBackward() bool { return m.move(m.navigator.WordBackward, true) }
317func (m *Mode) WordEnd() bool { return m.move(m.navigator.WordEnd, true) }
318func (m *Mode) MoveRows(delta int) bool {
319 if delta == 0 {
320 return false
321 }
322 changed := false
323 op := m.Up
324 if delta > 0 {
325 op = m.Down
326 }
327 for range abs(delta) {
328 if !op() {
329 break
330 }
331 changed = true
332 }
333 return changed
334}
335
336// ScrollRows moves the viewport and cursor together for mouse-wheel input.
337// Unlike keyboard navigation, reversing direction scrolls immediately rather
338// than first walking the cursor to the opposite edge of the viewport.
339func (m *Mode) ScrollRows(delta int) bool {
340 if m == nil || m.document == nil || m.document.Len() == 0 || delta == 0 {
341 return false
342 }
343 d := m.document
344 oldTop := m.ViewportTop
345 maxTop := max(d.Len()-max(d.Height(), 1), 0)
346 top := oldTop + min(max(delta, -oldTop), maxTop-oldTop)
347 row := m.navigator.Pos.Row
348 row += min(max(delta, -row), d.Len()-1-row)
349 changed := m.move(func(d *Document) bool { return m.navigator.moveVertical(d, row) }, false)
350 m.ViewportTop = top
351 return changed || oldTop != top
352}
353
354func (m *Mode) Page(pages int) bool {
355 if m == nil || m.document == nil {
356 return false
357 }
358 return m.move(func(d *Document) bool { return m.navigator.Page(d, pages*max(d.Height(), 1)) }, false)
359}
360func (m *Mode) Top() bool { return m.move(m.navigator.Top, false) }
361func (m *Mode) Bottom() bool { return m.move(m.navigator.Bottom, false) }
362func (m *Mode) AtBottom() bool {
363 if m == nil || m.document == nil || m.document.Len() == 0 {
364 return m != nil && m.ViewportTop == 0
365 }
366 return m.navigator.Pos.Row == m.document.Len()-1 && m.ViewportTop == max(m.document.Len()-max(m.document.Height(), 1), 0)
367}
368func (m *Mode) ToggleLineSelection() {
369 if m.selection.Enabled {
370 m.selection = Selection{}
371 return
372 }
373 m.selection = NewLineSelection(m.navigator.Pos)
374}
375func (m *Mode) StartCharacterSelection(p Pos) bool {
376 if m == nil || m.document == nil {
377 return false
378 }
379 p, ok := m.document.Normalize(p)
380 if !ok {
381 return false
382 }
383 m.navigator.Set(m.document, p)
384 m.selection = Selection{Anchor: p, Active: p, Granularity: Character, Enabled: true}
385 m.adjustViewport()
386 return true
387}
388func (m *Mode) ExtendCharacterSelection(p Pos) bool {
389 if m == nil || !m.selection.Enabled {
390 return false
391 }
392 p, ok := m.document.Normalize(p)
393 if !ok {
394 return false
395 }
396 m.selection.AsCharacter()
397 m.selection.Extend(p)
398 m.navigator.Set(m.document, p)
399 m.adjustViewport()
400 return true
401}
402func (m *Mode) SelectWordAt(p Pos) bool {
403 if m == nil {
404 return false
405 }
406 s, ok := NewWordSelection(m.document, p)
407 if !ok {
408 return false
409 }
410 m.selection = s
411 m.navigator.Set(m.document, s.Active)
412 m.adjustViewport()
413 return true
414}
415func (m *Mode) ExtendWordSelection(p Pos) bool {
416 if m == nil || !m.selection.Enabled {
417 return false
418 }
419 _, end, ok := m.document.WordBounds(p)
420 if !ok {
421 return false
422 }
423 m.selection.Granularity = Word
424 m.selection.Extend(end)
425 m.navigator.Set(m.document, end)
426 m.adjustViewport()
427 return true
428}
429func (m *Mode) Search(query string) bool {
430 if m == nil {
431 return false
432 }
433 return m.SetSearchMatches(query, FindMatches(m.document, query), 0)
434}
435func (m *Mode) SetSearchMatches(query string, matches []SearchMatch, index int) bool {
436 if m == nil {
437 return false
438 }
439 m.SearchQuery = strings.TrimSpace(query)
440 if len(matches) == 0 {
441 m.Searches = nil
442 m.SearchIndex = -1
443 return false
444 }
445 m.Searches = append(m.Searches[:0], matches...)
446 m.SearchIndex = min(max(index, 0), len(matches)-1)
447 m.moveToSearchMatch(matches[m.SearchIndex])
448 return true
449}
450func (m *Mode) NextSearchMatch(delta int) bool {
451 if m == nil || len(m.Searches) == 0 || delta == 0 {
452 return false
453 }
454 m.SearchIndex = (m.SearchIndex + delta) % len(m.Searches)
455 if m.SearchIndex < 0 {
456 m.SearchIndex += len(m.Searches)
457 }
458 match := m.Searches[m.SearchIndex]
459 m.moveToSearchMatch(match)
460 return true
461}
462func (m *Mode) moveToSearchMatch(match SearchMatch) {
463 if m == nil || m.document == nil {
464 return
465 }
466 pos, ok := m.document.Normalize(Pos{Row: match.Row, Col: match.Start})
467 if !ok {
468 return
469 }
470 m.navigator.Set(m.document, pos)
471 if m.selection.Enabled {
472 m.selection.Extend(m.navigator.Pos)
473 }
474 m.adjustViewport()
475}
476func (m *Mode) SelectedText() string {
477 if m == nil {
478 return ""
479 }
480 return m.selection.Text(m.document)
481}
482func (m *Mode) adjustViewport() {
483 if m == nil || m.document == nil || m.document.Len() == 0 {
484 return
485 }
486 rows := max(m.document.Height(), 1)
487 if m.navigator.Pos.Row < m.ViewportTop {
488 m.ViewportTop = m.navigator.Pos.Row
489 }
490 if m.navigator.Pos.Row >= m.ViewportTop+rows {
491 m.ViewportTop = m.navigator.Pos.Row - rows + 1
492 }
493 m.ViewportTop = min(max(m.ViewportTop, 0), max(m.document.Len()-rows, 0))
494}
495
496func FindMatches(doc *Document, query string) []SearchMatch {
497 query = strings.TrimSpace(query)
498 if doc == nil || query == "" {
499 return nil
500 }
501 return findMatches(query, doc.Snapshot().rangeText)
502}
503
504// RefineMatches returns FindMatches(doc, query) given previous, the complete
505// result for a query that query extends. Any row matching the longer query also
506// matched the shorter one, so only rows listed in previous are scanned again.
507func RefineMatches(doc *Document, previous []SearchMatch, query string) []SearchMatch {
508 query = strings.TrimSpace(query)
509 if doc == nil || query == "" {
510 return nil
511 }
512 snapshot := doc.Snapshot()
513 return findMatches(query, func(yield func(int, []rune, []int, int) bool) error {
514 var cells []renderer.Cell
515 var runes []rune
516 var columns []int
517 last := -1
518 for _, match := range previous {
519 if match.Row == last {
520 continue
521 }
522 last = match.Row
523 width := snapshot.RowWidth(match.Row)
524 cells = slices.Grow(cells[:0], width)[:width]
525 cells = cells[:snapshot.CopyRow(match.Row, cells)]
526 runes, columns = runes[:0], columns[:0]
527 for x, cell := range cells {
528 if cell.Continuation {
529 continue
530 }
531 r := cell.Rune
532 if r == 0 {
533 r = ' '
534 }
535 runes = append(runes, r)
536 columns = append(columns, x)
537 }
538 if !yield(match.Row, runes, columns, len(cells)) {
539 return nil
540 }
541 }
542 return nil
543 })
544}
545
546func findMatches(query string, rows textRows) []SearchMatch {
547 needle := lowerRunes(query)
548 matches := []SearchMatch{}
549 var hay []rune
550 err := rows(func(row int, runes []rune, columns []int, width int) bool {
551 hay = hay[:0]
552 for _, r := range runes {
553 hay = append(hay, unicode.ToLower(r))
554 }
555 text := ""
556 for start := 0; start+len(needle) <= len(hay); {
557 if !slices.Equal(hay[start:start+len(needle)], needle) {
558 start++
559 continue
560 }
561 if text == "" {
562 // Same text as Document.LineText, built from the runes in hand.
563 text = strings.TrimRight(string(runes), " ")
564 }
565 end := start + len(needle)
566 cellEnd := width
567 if end < len(columns) {
568 cellEnd = columns[end]
569 }
570 matches = append(matches, SearchMatch{Row: row, Start: columns[start], End: cellEnd, Text: text})
571 start += len(needle)
572 }
573 return true
574 })
575 if err != nil {
576 // Discard partial results without taking down the daemon on corrupt history.
577 return nil
578 }
579 return matches
580}
581func lowerRunes(s string) []rune {
582 rs := []rune(s)
583 for i, r := range rs {
584 rs[i] = unicode.ToLower(r)
585 }
586 return rs
587}
588
589func (m *Mode) Render(styles ...renderer.Style) renderer.Frame {
590 if m == nil || m.document == nil {
591 return renderer.NewFrame(0, 0)
592 }
593 frame := renderer.NewFrame(m.document.Width(), m.document.Height()+1)
594 m.RenderRows(func(y int, row []renderer.Cell) { frame.WriteRow(y, 0, row) }, styles...)
595 return frame
596}
597
598// RenderRows paints viewport rows followed by the status row at Document.Height().
599// The callback borrows a reusable semantic row only until it returns. Rendering
600// directly into a compositor avoids building and then decoding a compact frame.
601func (m *Mode) RenderRows(paint func(int, []renderer.Cell), styles ...renderer.Style) {
602 if m == nil || m.document == nil {
603 return
604 }
605 m.RenderRowsRange(0, m.document.Height(), paint, styles...)
606}
607
608// RenderRowsRange paints viewport rows in [start, end), then the status row.
609// Row coordinates remain viewport-relative and the callback borrows each row.
610func (m *Mode) RenderRowsRange(start, end int, paint func(int, []renderer.Cell), styles ...renderer.Style) {
611 if m == nil || m.document == nil {
612 return
613 }
614 d := m.document
615 selectionBounds, hasSelectionBounds := m.selection.bounds(d)
616 selection, hasSelection := optionalStyle(styles, 1)
617 cursor, cursorValid := d.Normalize(m.navigator.Pos)
618 buf, _ := rowPool.Get().(*[]renderer.Cell)
619 if buf == nil || cap(*buf) < d.Width() {
620 row := make([]renderer.Cell, d.Width())
621 buf = &row
622 }
623 defer rowPool.Put(buf)
624 row := (*buf)[:d.Width()]
625 for y := max(start, 0); y < min(end, d.Height()); y++ {
626 src := m.ViewportTop + y
627 for x := range row {
628 row[x] = renderer.BlankCell()
629 }
630 if src >= d.Len() {
631 paint(y, row)
632 continue
633 }
634 n := d.snapshot.CopyRow(src, row)
635 if match, ok := m.currentSearchMatchForRow(src); ok {
636 for x := max(match.Start, 0); x < min(match.End, len(row)); x++ {
637 applySelectionStyle(&row[x].Style, selection, hasSelection)
638 }
639 }
640 cursorCovered := false
641 if hasSelectionBounds {
642 bound := d.snapshot.Bound(src)
643 width := d.RowWidth(src)
644 cells, rangeCells := row[:n], row
645 if n < width {
646 cells = d.Row(src)
647 rangeCells = cells
648 }
649 limit := contentEndOf(cells, bound)
650 r, ok := selectionBounds.rangeForCells(src, rangeCells, limit)
651 if ok {
652 for x := max(r.Start, 0); x <= min(r.End, len(row)-1); x++ {
653 toggleInverse(&row[x].Style)
654 }
655 if col, ok := selectionBounds.newlineMarkerAt(src, limit, width, bound.Soft); ok && col < len(row) {
656 toggleInverse(&row[col].Style)
657 if cursorValid && cursor.Row == src && cursor.Col == col {
658 cursorCovered = true
659 }
660 }
661 if cursorValid && r.Row == cursor.Row && cursor.Col >= r.Start && cursor.Col <= r.End {
662 cursorCovered = true
663 }
664 }
665 }
666 if src == cursor.Row && cursorValid && !cursorCovered && len(row) > 0 {
667 cursorCol := min(cursor.Col, len(row)-1)
668 applySelectionStyle(&row[cursorCol].Style, selection, hasSelection)
669 }
670 paint(y, row)
671 }
672 status := inverseStyle()
673 if len(styles) > 0 {
674 status = styles[0]
675 }
676 drawCopyStatus(row, m, d.Len(), status)
677 paint(d.Height(), row)
678}
679func (m *Mode) currentSearchMatchForRow(row int) (SearchMatch, bool) {
680 if m == nil || m.SearchIndex < 0 || m.SearchIndex >= len(m.Searches) {
681 return SearchMatch{}, false
682 }
683 v := m.Searches[m.SearchIndex]
684 return v, v.Row == row
685}
686func optionalStyle(styles []renderer.Style, idx int) (renderer.Style, bool) {
687 if idx >= len(styles) {
688 return renderer.Style{}, false
689 }
690 return styles[idx], true
691}
692func toggleInverse(dst *renderer.Style) { dst.Inverse = !dst.Inverse }
693
694func applySelectionStyle(dst *renderer.Style, style renderer.Style, ok bool) {
695 if !ok || style.Equal(inverseStyle()) {
696 dst.Inverse = true
697 return
698 }
699 *dst = style
700}
701func inverseStyle() renderer.Style { s := renderer.DefaultStyle(); s.Inverse = true; return s }
702func drawCopyStatus(row []renderer.Cell, m *Mode, total int, style renderer.Style) {
703 for i := range row {
704 row[i] = renderer.Cell{Rune: ' ', Style: style}
705 }
706 col := 0
707 put := func(text string) {
708 for _, r := range text {
709 if col >= len(row) {
710 return
711 }
712 row[col] = renderer.Cell{Rune: r, Style: style}
713 col++
714 }
715 }
716 var digits [20]byte
717 putInt := func(n int) {
718 for _, b := range strconv.AppendInt(digits[:0], int64(n), 10) {
719 if col >= len(row) {
720 return
721 }
722 row[col] = renderer.Cell{Rune: rune(b), Style: style}
723 col++
724 }
725 }
726 if m.selection.Enabled {
727 put(" [SELECT] ")
728 } else {
729 put(" [SCROLL] ")
730 }
731 if total > 0 {
732 putInt(m.navigator.Pos.Row + 1)
733 put("/")
734 putInt(total)
735 put(" ")
736 } else {
737 put("0/0 ")
738 }
739 if m.SearchQuery != "" {
740 if len(m.Searches) > 0 && m.SearchIndex >= 0 && m.SearchIndex < len(m.Searches) {
741 putInt(m.SearchIndex + 1)
742 put("/")
743 putInt(len(m.Searches))
744 put(" ")
745 }
746 put("/")
747 put(m.SearchQuery)
748 put(" ")
749 }
750}
751
752func OSC52(text string) [][]byte {
753 if len([]byte(text)) > OSC52MaxPayloadBytes {
754 return nil
755 }
756 return [][]byte{[]byte("\x1b]52;c;" + base64.StdEncoding.EncodeToString([]byte(text)) + "\x07")}
757}
758func OSC52FromBase64(b64 string) []byte {
759 decoded, err := base64.StdEncoding.DecodeString(b64)
760 if err != nil || len(decoded) > OSC52MaxPayloadBytes {
761 return nil
762 }
763 return []byte("\x1b]52;c;" + b64 + "\x07")
764}
765func abs(n int) int {
766 if n < 0 {
767 return -n
768 }
769 return n
770}