remote_preview_watch.go RawHistoryFolder

1package daemon
2
3import (
4	"context"
5	"errors"
6	"slices"
7	"time"
8
9	renderer "github.com/bnema/vev-vt"
10	"github.com/bnema/vev/internal/ports"
11	"github.com/bnema/vev/internal/protocol"
12)
13
14// remotePreviewRevalidate bounds how long a quiet watch trusts its last frame
15// without a render wake: it re-captures to notice a moved tab or a dead target.
16const remotePreviewRevalidate = 2 * time.Second
17
18// remotePreviewWatch is one live preview stream. Its address keys the render
19// coordinator subscription; the coordinator only ever signals wake.
20type remotePreviewWatch struct {
21	wake chan struct{}
22	rc   *renderCoordinator
23}
24
25// notify is the coordinator callback: a non-blocking, lock-free signal. The
26// watch goroutine owns every capture and send.
27func (w *remotePreviewWatch) notify(renderWake) {
28	select {
29	case w.wake <- struct{}{}:
30	default:
31	}
32}
33
34// subscribe follows the target session's coordinator. A headless target gets
35// its own coordinator, which keeps the watched tab renderable while subscribed.
36func (w *remotePreviewWatch) subscribe(d *Daemon, target *session) {
37	var rc *renderCoordinator
38	if target != nil {
39		rc = d.ensureRenderCoordinator(target)
40	}
41	if rc == w.rc {
42		return
43	}
44	w.unsubscribe()
45	if rc != nil && rc.subscribePreviewFor(w, 1, w.notify) {
46		w.rc = rc
47	}
48}
49
50func (w *remotePreviewWatch) unsubscribe() {
51	if w.rc != nil {
52		w.rc.teardownPreviewFor(w, 1)
53		w.rc = nil
54	}
55}
56
57// serveRemotePreviewWatch streams the watched viewport until the client
58// closes the connection, the daemon shuts down, or the target dies. Frames are
59// captured on this goroutine, sent only when they changed, and spaced by at
60// least the requested interval; a dead target ends the stream with one
61// NoSuchTarget frame.
62func (d *Daemon) serveRemotePreviewWatch(tr ports.ServerConnection, watch protocol.RemotePreviewWatch) {
63	if protocol.ValidateRemotePreviewWatch(watch) != nil {
64		_ = d.sendRemotePreview(tr, protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed})
65		_ = tr.Close()
66		return
67	}
68	base := d.hardCtx
69	if base == nil {
70		base = context.Background()
71	}
72	ctx, cancel := context.WithCancel(base)
73	defer cancel()
74	// The client cancels by closing or by sending anything at all.
75	readerDone := make(chan struct{})
76	go func() {
77		defer close(readerDone)
78		_, _ = tr.ReceiveClient()
79		cancel()
80	}()
81	stopClose := context.AfterFunc(ctx, func() { _ = tr.Close() })
82	defer func() {
83		stopClose()
84		_ = tr.Close()
85		<-readerDone
86	}()
87
88	w := &remotePreviewWatch{wake: make(chan struct{}, 1)}
89	defer w.unsubscribe()
90	interval := min(max(watch.MinInterval, protocol.RemotePreviewWatchMinInterval), protocol.RemotePreviewWatchMaxInterval)
91	var last protocol.RemotePreview
92	for {
93		// Subscribe before capturing, so output racing the capture still wakes
94		// the next one.
95		w.subscribe(d, d.remotePreviewSession(watch.Request.Target.SessionName, watch.Request.Target.LifecycleID))
96		preview, err := d.captureRemotePreview(watch.Request)
97		if errors.Is(err, errRemotePreviewNoSuchTarget) {
98			_ = d.sendRemotePreview(tr, protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewNoSuchTarget})
99			return
100		}
101		if err != nil {
102			preview = protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}
103		}
104		if !sameRemotePreview(last, preview) {
105			if d.sendRemotePreview(tr, preview) != nil {
106				return
107			}
108			last = preview
109		}
110		if !d.waitRemotePreview(ctx, interval, nil) || !d.waitRemotePreview(ctx, remotePreviewRevalidate, w.wake) {
111			return
112		}
113	}
114}
115
116// waitRemotePreview blocks for d or until wake fires. It reports false once
117// the watch is cancelled.
118func (d *Daemon) waitRemotePreview(ctx context.Context, delay time.Duration, wake <-chan struct{}) bool {
119	timer := d.clock.NewTimer(delay)
120	defer timer.Stop()
121	select {
122	case <-ctx.Done():
123		return false
124	case <-wake:
125	case <-timer.C():
126	}
127	return ctx.Err() == nil
128}
129
130func (d *Daemon) sendRemotePreview(tr ports.ServerConnection, preview protocol.RemotePreview) error {
131	if protocol.ValidateRemotePreview(preview) != nil {
132		preview = protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed}
133	}
134	return d.boundedControlSend(tr, preview)
135}
136
137func sameRemotePreview(a, b protocol.RemotePreview) bool {
138	return a.Version == b.Version && a.Status == b.Status && a.LifecycleID == b.LifecycleID && a.TabID == b.TabID &&
139		a.Revision == b.Revision && a.Width == b.Width && a.Height == b.Height &&
140		slices.EqualFunc(a.Cells, b.Cells, renderer.Cell.Equal)
141}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close