main ece7682

client_toast.go RawHistoryFolder

1package client
2
3import (
4	"fmt"
5	"io"
6	"strings"
7
8	renderer "github.com/bnema/vev-vt"
9	ansirenderer "github.com/bnema/vev-vt/ansi"
10	"github.com/bnema/vev/internal/domain"
11	"github.com/bnema/vev/internal/usecase/picker"
12	"github.com/bnema/vev/internal/usecase/ui"
13)
14
15// drawClientToast draws a client-local toast without changing terminal state.
16// The attach main loop owns both this write and the later daemon-frame
17// reconciliation; input pumps must only publish a request for it.
18// The anchor places it: transitions stay centered, notices sit top-right so
19// they never cover the picker list. The border is colored by severity.
20//
21// The border uses fixed xterm-256 colors and does not follow the attachment
22// color profile, so production callers (the transition notice) pass only
23// domain.NoticeInfo, which emits no color. Toasts with a severity that must
24// honor 16-color or monochrome terminals go through toastBorderSGRFor, as the
25// picker does.
26func drawClientToast(out io.Writer, size domain.Size, message string, anchor domain.Anchor, severity domain.NoticeSeverity) (domain.Rect, error) {
27	bounds := ui.ToastBounds(size, ui.Toast{Message: message, Anchor: anchor})
28	if bounds.Width <= 0 || bounds.Height <= 0 {
29		return domain.Rect{}, nil
30	}
31	return bounds, writeClientToast(out, bounds, clientToastLines(bounds, message, toastBorderSGR(severity)))
32}
33
34// toastBorderSGR is the border color per severity, in the same fixed
35// xterm-256 colors as the picker's problem dots. Info keeps the terminal's
36// default color.
37func toastBorderSGR(severity domain.NoticeSeverity) string {
38	switch severity {
39	case domain.NoticeError:
40		return fmt.Sprintf("\x1b[38;5;%dm", picker.ColorProblemError)
41	case domain.NoticeWarn:
42		return fmt.Sprintf("\x1b[38;5;%dm", picker.ColorProblemWarn)
43	default:
44		return ""
45	}
46}
47
48// toastBorderSGRFor is toastBorderSGR for one renderer color profile. The
49// toast bypasses the renderer, so it must follow the profile itself: 16-color
50// terminals get basic red/yellow, monochrome terminals get bold for problems.
51// TrueColor and 256-color output is unchanged.
52func toastBorderSGRFor(severity domain.NoticeSeverity, profile ansirenderer.ColorProfile) string {
53	switch profile {
54	case ansirenderer.ColorProfileANSI16:
55		switch severity {
56		case domain.NoticeError:
57			return "\x1b[31m"
58		case domain.NoticeWarn:
59			return "\x1b[33m"
60		}
61		return ""
62	case ansirenderer.ColorProfileMonochrome:
63		if severity == domain.NoticeError || severity == domain.NoticeWarn {
64			return "\x1b[1m"
65		}
66		return ""
67	default:
68		return toastBorderSGR(severity)
69	}
70}
71
72// toastBorderReset undoes toastBorderSGR's attribute: default foreground for
73// colors, normal intensity for the monochrome bold border.
74func toastBorderReset(borderSGR string) string {
75	if borderSGR == "\x1b[1m" {
76		return "\x1b[22m"
77	}
78	return "\x1b[39m"
79}
80
81// clientToastLines renders the toast box as one string per row, exactly
82// bounds.Width columns wide.
83func clientToastLines(bounds domain.Rect, message, borderSGR string) []string {
84	if bounds.Width <= 0 || bounds.Height <= 0 {
85		return nil
86	}
87	frame := renderer.NewFrame(bounds.Width, bounds.Height)
88	ui.DrawToast(frame, ui.Toast{Message: message, Anchor: domain.AnchorCenter}, ui.ToastStyles{
89		Text: renderer.DefaultStyle(),
90		Box:  renderer.DefaultStyle(),
91	})
92	lines := make([]string, bounds.Height)
93	for y := range bounds.Height {
94		var b strings.Builder
95		b.Grow(bounds.Width)
96		colored := false
97		for x := range bounds.Width {
98			border := y == 0 || y == bounds.Height-1 || x == 0 || x == bounds.Width-1
99			if borderSGR != "" && border != colored {
100				if border {
101					b.WriteString(borderSGR)
102				} else {
103					b.WriteString(toastBorderReset(borderSGR))
104				}
105				colored = border
106			}
107			cell := frame.At(x, y)
108			if cell.Continuation {
109				// The wide rune before it already covers this column.
110				continue
111			}
112			if cell.Rune == 0 {
113				b.WriteRune(' ')
114				continue
115			}
116			b.WriteRune(cell.Rune)
117		}
118		if colored {
119			b.WriteString(toastBorderReset(borderSGR))
120		}
121		lines[y] = b.String()
122	}
123	return lines
124}
125
126// blankToastLines erases a toast box. The session under it belongs to the
127// daemon, so blank cells are the best the client can restore.
128func blankToastLines(bounds domain.Rect) []string {
129	lines := make([]string, max(0, bounds.Height))
130	for i := range lines {
131		lines[i] = strings.Repeat(" ", max(0, bounds.Width))
132	}
133	return lines
134}
135
136func writeClientToast(out io.Writer, bounds domain.Rect, lines []string) error {
137	if _, err := io.WriteString(out, "\x1b[s"); err != nil {
138		return err
139	}
140	for i, line := range lines {
141		if _, err := fmt.Fprintf(out, "\x1b[%d;%dH\x1b[0m%s", bounds.Y+i+1, bounds.X+1, line); err != nil {
142			return err
143		}
144	}
145	_, err := io.WriteString(out, "\x1b[0m\x1b[u")
146	return err
147}

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