v0.2.0 80c61e3

preview.go RawHistoryFolder

1package remote
2
3import (
4	"context"
5	"encoding/base64"
6	"errors"
7	"fmt"
8	"os/exec"
9	"time"
10
11	"github.com/bnema/vev/internal/adapters/sshstdio"
12	"github.com/bnema/vev/internal/domain"
13	"github.com/bnema/vev/internal/ports"
14)
15
16const (
17	remotePreviewCommandTimeout = 5 * time.Second
18	remotePreviewWaitDelay      = 500 * time.Millisecond
19	remotePreviewMaxDiagnostic  = 4 << 10
20)
21
22var (
23	errRemotePreviewSSH      = errors.New("remote preview: ssh command failed")
24	errRemotePreviewTooLarge = errors.New("remote preview: output exceeds size limit")
25	errRemotePreviewIdentity = errors.New("remote preview: response identity mismatch")
26)
27
28// PreviewClient invokes the hidden remote preview command over SSH. The
29// command receives only a base64url request and stdout is decoded as one
30// bounded binary response.
31type PreviewClient struct {
32	command func(ctx context.Context, name string, args ...string) *exec.Cmd
33	timeout time.Duration
34}
35
36var _ ports.RemotePreviewClient = (*PreviewClient)(nil)
37
38func NewPreviewClient() ports.RemotePreviewClient {
39	return &PreviewClient{command: exec.CommandContext}
40}
41
42func (c *PreviewClient) Preview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (ports.RemotePreview, error) {
43	request := ports.RemotePreviewRequest{Version: ports.RemotePreviewSchemaVersion, Target: target, Width: width, Height: height}
44	payload := ports.MarshalRemotePreviewRequest(request)
45	if payload == nil {
46		return ports.RemotePreview{}, ports.ErrInvalidRemotePreviewRequest
47	}
48	runCtx, cancel := context.WithTimeout(ctx, c.previewTimeout())
49	defer cancel()
50	command := c.command
51	if command == nil {
52		command = exec.CommandContext
53	}
54	encoded := base64.RawURLEncoding.EncodeToString(payload)
55	spec := sshstdio.BuildCommandForRemoteCommand(target.Endpoint, "vev", "_remote-preview", encoded)
56	cmd := command(runCtx, spec.Path, spec.Args...)
57	stdout := boundedBuffer{limit: ports.RemotePreviewMaxBytes}
58	stderr := boundedBuffer{limit: remotePreviewMaxDiagnostic}
59	cmd.Stdout = &stdout
60	cmd.Stderr = &stderr
61	cmd.WaitDelay = remotePreviewWaitDelay
62	if err := cmd.Run(); err != nil {
63		if ctxErr := runCtx.Err(); ctxErr != nil {
64			if parentErr := ctx.Err(); parentErr != nil {
65				return ports.RemotePreview{}, parentErr
66			}
67			return ports.RemotePreview{}, ports.ErrRemotePreviewTimeout
68		}
69		if stdout.overflow || stderr.overflow {
70			return ports.RemotePreview{}, errRemotePreviewTooLarge
71		}
72		if diagnostic := sanitizeCatalogDiagnostic(string(stderr.Bytes())); diagnostic != "" {
73			return ports.RemotePreview{}, fmt.Errorf("%w: %v: %s", errRemotePreviewSSH, err, diagnostic)
74		}
75		return ports.RemotePreview{}, fmt.Errorf("%w: %v", errRemotePreviewSSH, err)
76	}
77	if stdout.overflow || stderr.overflow {
78		return ports.RemotePreview{}, errRemotePreviewTooLarge
79	}
80	preview, err := ports.UnmarshalRemotePreview(stdout.Bytes())
81	if err != nil {
82		return ports.RemotePreview{}, err
83	}
84	if preview.Status != ports.RemotePreviewOK {
85		return ports.RemotePreview{}, fmt.Errorf("remote preview unavailable: status %d", preview.Status)
86	}
87	if preview.LifecycleID != target.LifecycleID || preview.TabID != target.LiveTabID {
88		return ports.RemotePreview{}, errRemotePreviewIdentity
89	}
90	return preview, nil
91}
92
93func (c *PreviewClient) previewTimeout() time.Duration {
94	if c != nil && c.timeout > 0 {
95		return c.timeout
96	}
97	return remotePreviewCommandTimeout
98}

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