1package daemon
2
3import (
4 "errors"
5
6 renderer "github.com/bnema/vev-vt"
7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/protocol"
9)
10
11var errRemotePreviewNoSuchTarget = errors.New("daemon: remote preview target does not exist")
12
13// remotePreviewSession resolves the exact live session lifecycle a preview
14// names, or nil once that lifecycle is gone.
15func (d *Daemon) remotePreviewSession(name string, lifecycle domain.SessionLifecycleID) *session {
16 d.mu.Lock()
17 defer d.mu.Unlock()
18 sess := d.findByNameLocked(name)
19 if sess == nil || sess.incarnation != lifecycle {
20 return nil
21 }
22 return sess
23}
24
25// captureRemotePreview crops the focused pane of the exact target tab. It
26// never touches VT damage or attachment render state.
27func (d *Daemon) captureRemotePreview(request protocol.RemotePreviewRequest) (protocol.RemotePreview, error) {
28 target := request.Target
29 sess := d.remotePreviewSession(target.SessionName, target.LifecycleID)
30 if sess == nil {
31 return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
32 }
33 index, ok := remoteTargetTabIndexLocked(sess, protocol.SessionAttachTargetFromRemote(target))
34 if !ok {
35 return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
36 }
37
38 sess.mu.Lock()
39 if index < 0 || index >= len(sess.tabs) {
40 sess.mu.Unlock()
41 return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
42 }
43 tab := sess.tabs[index]
44 if tab == nil || domain.TabStableID(tab.stableID) != target.LiveTabID {
45 sess.mu.Unlock()
46 return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
47 }
48 sess.mu.Unlock()
49 tab.mu.Lock()
50 pane := tab.focusedPane()
51 if pane == nil {
52 tab.mu.Unlock()
53 return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
54 }
55 pane.mu.Lock()
56 if pane.screen == nil {
57 pane.mu.Unlock()
58 tab.mu.Unlock()
59 return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
60 }
61 source := pane.screen.Snapshot()
62 revision := pane.syncGen
63 pane.mu.Unlock()
64 tab.mu.Unlock()
65 if revision == 0 {
66 revision = 1
67 }
68
69 width := min(int(request.Width), source.Columns())
70 height := min(int(request.Height), source.Rows())
71 if width <= 0 || height <= 0 {
72 return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
73 }
74 startY, endY := source.Rows()-height, source.Rows()
75 if last := remotePreviewLastContentRow(source, width, startY, endY); last >= startY {
76 endY = last + 1
77 } else if last := remotePreviewLastContentRow(source, width, 0, startY); last >= 0 {
78 startY = max(0, last-height+1)
79 endY = last + 1
80 }
81 height = endY - startY
82 cells := make([]renderer.Cell, 0, width*height)
83 for y := 0; y < height; y++ {
84 row := source.Row(startY + y)
85 for x := 0; x < width; x++ {
86 cell, _ := cropRemotePreviewCell(row[x], x, width)
87 cells = append(cells, cell)
88 }
89 }
90 preview := protocol.RemotePreview{
91 Version: protocol.RemotePreviewSchemaVersion,
92 Status: protocol.RemotePreviewOK,
93 LifecycleID: target.LifecycleID,
94 TabID: target.LiveTabID,
95 Revision: revision,
96 Width: uint16(width),
97 Height: uint16(height),
98 Cells: cells,
99 }
100 if err := protocol.ValidateRemotePreview(preview); err != nil {
101 // A crop must not split a wide cell. Return a safe unavailable response
102 // instead of exposing a malformed cell stream.
103 return protocol.RemotePreview{}, err
104 }
105 return preview, nil
106}
107
108// remotePreviewLastContentRow finds the final row with a visible glyph inside
109// the bounded viewport crop. Remote terminal frames retain their full PTY
110// height even when the only output is near the top; compacting the blank tail lets the
111// picker bottom-anchor short output instead of clipping it away.
112func remotePreviewLastContentRow(frame renderer.CellSource, width, start, end int) int {
113 for y := end - 1; y >= start; y-- {
114 for x := range width {
115 cell, splitWideRune := cropRemotePreviewCell(frame.Cell(x, y), x, width)
116 if !splitWideRune && remotePreviewCellVisible(cell) {
117 return y
118 }
119 }
120 }
121 return -1
122}
123
124// cropRemotePreviewCell converts a left wide-rune half at the right crop edge
125// to the styled blank that the picker can safely render. The flag distinguishes
126// that replacement from an actual styled blank in the source frame.
127func cropRemotePreviewCell(cell renderer.Cell, x, width int) (renderer.Cell, bool) {
128 if x == width-1 && !cell.Continuation && renderer.RuneWidth(cell.Rune) == 2 {
129 return renderer.Cell{Rune: ' ', Style: cell.Style}, true
130 }
131 return cell, false
132}
133
134func remotePreviewCellVisible(cell renderer.Cell) bool {
135 return !cell.Continuation &&
136 (cell.Rune != 0 && cell.Rune != ' ' || !cell.Style.Equal(renderer.DefaultStyle()))
137}