toast.go RawHistoryFolder

1package ui
2
3import (
4	renderer "github.com/bnema/vev-vt"
5	"github.com/bnema/vev/internal/domain"
6)
7
8const (
9	defaultToastMargin   = 2
10	defaultToastPaddingX = 1
11	defaultToastMaxWidth = 60
12)
13
14// Toast describes a transient message rendered over a frame.
15type Toast struct {
16	Message string
17	// Severity is carried for callers that style by it (client toast
18	// borders); DrawToast does not read it.
19	Severity domain.NoticeSeverity
20	Anchor   domain.Anchor
21	MinWidth int
22	MaxWidth int
23	PaddingX int
24	PaddingY int
25}
26
27// ToastStyles contains styles used when drawing a toast.
28type ToastStyles struct {
29	Text renderer.Style
30	Box  renderer.Style
31}
32
33// ToastBounds returns the toast rectangle positioned within base and clamped to base.
34func ToastBounds(base domain.Size, toast Toast) domain.Rect {
35	if base.Cols <= 0 || base.Rows <= 0 {
36		return domain.Rect{}
37	}
38
39	paddingX := toast.PaddingX
40	if paddingX == 0 {
41		paddingX = defaultToastPaddingX
42	}
43	const borderWidth = 2
44	const borderHeight = 2
45
46	maxWidth := toast.MaxWidth
47	if maxWidth <= 0 {
48		maxWidth = defaultToastMaxWidth
49	}
50	width := textWidth(toast.Message) + paddingX*2 + borderWidth
51	width = max(width, toast.MinWidth)
52	width = clamp(width, 0, maxWidth)
53	width = clamp(width, 0, base.Cols)
54
55	height := 1 + toast.PaddingY*2 + borderHeight
56	height = clamp(height, 0, base.Rows)
57
58	return Place(base, domain.Size{Cols: width, Rows: height}, toast.Anchor, Margins{
59		Top:    defaultToastMargin,
60		Right:  defaultToastMargin,
61		Bottom: defaultToastMargin,
62		Left:   defaultToastMargin,
63	})
64}
65
66// DrawToast draws one bordered toast at its anchor within frame.
67func DrawToast(frame renderer.Frame, toast Toast, styles ToastStyles) {
68	bounds := ToastBounds(domain.Size{Cols: frame.Width, Rows: frame.Height}, toast)
69	if bounds.Width <= 0 || bounds.Height <= 0 {
70		return
71	}
72
73	DrawBox(frame, bounds, styles.Box)
74	inner := domain.Rect{X: bounds.X + 1, Y: bounds.Y + 1, Width: max(0, bounds.Width-2), Height: max(0, bounds.Height-2)}
75	if inner.Width <= 0 || inner.Height <= 0 {
76		return
77	}
78
79	fill := renderer.BlankCell()
80	fill.Style = styles.Text
81	FillRect(frame, inner, fill)
82
83	paddingX := toast.PaddingX
84	if paddingX == 0 {
85		paddingX = defaultToastPaddingX
86	}
87	textY := inner.Y + toast.PaddingY
88	if textY >= inner.Y+inner.Height {
89		return
90	}
91	textLeft := inner.X + paddingX
92	textRight := inner.X + inner.Width - paddingX
93	if textRight <= textLeft {
94		return
95	}
96	DrawText(frame, textLeft, textY, textRight, truncateText(toast.Message, textRight-textLeft), styles.Text)
97}
98
99// TruncateText clips text to maxWidth cells, appending an ellipsis when cut.
100func TruncateText(text string, maxWidth int) string { return truncateText(text, maxWidth) }
101
102func truncateText(text string, maxWidth int) string {
103	if maxWidth <= 0 {
104		return ""
105	}
106	if textWidth(text) <= maxWidth {
107		return text
108	}
109	ellipsis := "…"
110	ellipsisWidth := textWidth(ellipsis)
111	if maxWidth < ellipsisWidth {
112		return ""
113	}
114	limit := maxWidth - ellipsisWidth
115	out := make([]rune, 0, len(text))
116	width := 0
117	for _, r := range text {
118		w := renderer.RuneWidth(r)
119		if w == 0 {
120			continue
121		}
122		if width+w > limit {
123			break
124		}
125		out = append(out, r)
126		width += w
127	}
128	return string(out) + ellipsis
129}

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