client_toast.go RawHistoryFolder

1package client
2
3import (
4	"fmt"
5	"io"
6	"strings"
7
8	renderer "github.com/bnema/vev-vt"
9	"github.com/bnema/vev/internal/domain"
10	"github.com/bnema/vev/internal/usecase/picker"
11	"github.com/bnema/vev/internal/usecase/ui"
12)
13
14// drawClientToast draws a client-local toast without changing terminal state.
15// The attach main loop owns both this write and the later daemon-frame
16// reconciliation; input pumps must only publish a request for it.
17// The anchor places it: transitions stay centered, notices sit top-right so
18// they never cover the picker list. The border is colored by severity.
19func drawClientToast(out io.Writer, size domain.Size, message string, anchor domain.Anchor, severity domain.NoticeSeverity) (domain.Rect, error) {
20	bounds := ui.ToastBounds(size, ui.Toast{Message: message, Anchor: anchor})
21	if bounds.Width <= 0 || bounds.Height <= 0 {
22		return domain.Rect{}, nil
23	}
24	return bounds, writeClientToast(out, bounds, clientToastLines(bounds, message, toastBorderSGR(severity)))
25}
26
27// toastBorderSGR is the border color per severity, in the same fixed
28// xterm-256 colors as the picker's problem dots. Info keeps the terminal's
29// default color.
30func toastBorderSGR(severity domain.NoticeSeverity) string {
31	switch severity {
32	case domain.NoticeError:
33		return fmt.Sprintf("\x1b[38;5;%dm", picker.ColorProblemError)
34	case domain.NoticeWarn:
35		return fmt.Sprintf("\x1b[38;5;%dm", picker.ColorProblemWarn)
36	default:
37		return ""
38	}
39}
40
41// clientToastLines renders the toast box as one string per row, exactly
42// bounds.Width columns wide.
43func clientToastLines(bounds domain.Rect, message, borderSGR string) []string {
44	if bounds.Width <= 0 || bounds.Height <= 0 {
45		return nil
46	}
47	frame := renderer.NewFrame(bounds.Width, bounds.Height)
48	ui.DrawToast(frame, ui.Toast{Message: message, Anchor: domain.AnchorCenter}, ui.ToastStyles{
49		Text: renderer.DefaultStyle(),
50		Box:  renderer.DefaultStyle(),
51	})
52	lines := make([]string, bounds.Height)
53	for y := range bounds.Height {
54		var b strings.Builder
55		b.Grow(bounds.Width)
56		colored := false
57		for x := range bounds.Width {
58			border := y == 0 || y == bounds.Height-1 || x == 0 || x == bounds.Width-1
59			if borderSGR != "" && border != colored {
60				if border {
61					b.WriteString(borderSGR)
62				} else {
63					b.WriteString("\x1b[39m")
64				}
65				colored = border
66			}
67			cell := frame.At(x, y)
68			if cell.Continuation {
69				// The wide rune before it already covers this column.
70				continue
71			}
72			if cell.Rune == 0 {
73				b.WriteRune(' ')
74				continue
75			}
76			b.WriteRune(cell.Rune)
77		}
78		if colored {
79			b.WriteString("\x1b[39m")
80		}
81		lines[y] = b.String()
82	}
83	return lines
84}
85
86// blankToastLines erases a toast box. The session under it belongs to the
87// daemon, so blank cells are the best the client can restore.
88func blankToastLines(bounds domain.Rect) []string {
89	lines := make([]string, max(0, bounds.Height))
90	for i := range lines {
91		lines[i] = strings.Repeat(" ", max(0, bounds.Width))
92	}
93	return lines
94}
95
96func writeClientToast(out io.Writer, bounds domain.Rect, lines []string) error {
97	if _, err := io.WriteString(out, "\x1b[s"); err != nil {
98		return err
99	}
100	for i, line := range lines {
101		if _, err := fmt.Fprintf(out, "\x1b[%d;%dH\x1b[0m%s", bounds.Y+i+1, bounds.X+1, line); err != nil {
102			return err
103		}
104	}
105	_, err := io.WriteString(out, "\x1b[0m\x1b[u")
106	return err
107}

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