v0.7.0 4c16dd8

notice.go RawHistoryFolder

1package ui
2
3import (
4	"fmt"
5	"strings"
6
7	renderer "github.com/bnema/vev-vt"
8	"github.com/bnema/vev/internal/domain"
9)
10
11const (
12	noticeMargin   = 1
13	noticeMaxWidth = 60
14	noticeMinWidth = 24
15	noticeMaxLines = 3
16)
17
18// NoticeView is a plain rendering view of one notification.
19type NoticeView struct {
20	Severity domain.NoticeSeverity
21	Title    string
22	Message  string
23	Count    int
24}
25
26// NoticeStyles selects severity-colored styles for each box's border and title.
27// Text styles the wrapped message body and overflow indicator; it excludes the
28// title, which is drawn with the box's severity style.
29type NoticeStyles struct {
30	Text     renderer.Style
31	BoxInfo  renderer.Style
32	BoxWarn  renderer.Style
33	BoxError renderer.Style
34}
35
36// ComposeNotices stacks notice boxes top-right of frame, newest first. Each
37// box's width starts at frame.Width*2/5, is capped at noticeMaxWidth (60),
38// then floored at noticeMinWidth (24), then clamped down to frame.Width so it
39// never exceeds the frame — on frames narrower than the floor, that final
40// clamp overrides the floor. Each box is titled with the notice's slug and an
41// optional " ×N" count suffix, and its message is word-wrapped to at most 3
42// lines. Boxes stack downward with one blank row between them; once the
43// count behind overflow is nonzero, a right-aligned "+N more" line follows
44// the last box.
45func ComposeNotices(frame renderer.Frame, notices []NoticeView, overflow int, styles NoticeStyles) []domain.Rect {
46	if frame.Width <= 0 || frame.Height <= 0 || (len(notices) == 0 && overflow <= 0) {
47		return nil
48	}
49
50	footprints := make([]domain.Rect, 0, len(notices)+1)
51
52	width := frame.Width * 2 / 5
53	if width > noticeMaxWidth {
54		width = noticeMaxWidth
55	}
56	if width < noticeMinWidth {
57		width = noticeMinWidth
58	}
59	if width > frame.Width {
60		width = frame.Width
61	}
62	if width <= 0 {
63		return nil
64	}
65	x := frame.Width - noticeMargin - width
66	y := noticeMargin
67
68	for _, n := range notices {
69		if y >= frame.Height {
70			break
71		}
72		innerWidth := width - 2
73		lines := wrapNoticeText(n.Message, innerWidth, noticeMaxLines)
74		if len(lines) == 0 {
75			lines = []string{""}
76		}
77		height := len(lines) + 2
78		bounds := domain.Rect{X: x, Y: y, Width: width, Height: height}
79		footprints = append(footprints, bounds)
80		box := noticeBoxStyle(styles, n.Severity)
81		DrawBox(frame, bounds, box)
82
83		title := n.Title
84		if n.Count > 1 {
85			title = fmt.Sprintf("%s ×%d", n.Title, n.Count)
86		}
87		if title != "" && bounds.Width > 2 {
88			left := bounds.X + 1
89			right := bounds.X + bounds.Width - 1
90			start := max(left, bounds.X+(bounds.Width-textWidth(title))/2)
91			DrawText(frame, start, bounds.Y, right, title, box)
92		}
93
94		inner := domain.Rect{X: bounds.X + 1, Y: bounds.Y + 1, Width: max(0, bounds.Width-2), Height: max(0, bounds.Height-2)}
95		if inner.Width > 0 && inner.Height > 0 {
96			fill := renderer.BlankCell()
97			fill.Style = styles.Text
98			FillRect(frame, inner, fill)
99			for i, line := range lines {
100				if i >= inner.Height {
101					break
102				}
103				DrawText(frame, inner.X, inner.Y+i, inner.X+inner.Width, line, styles.Text)
104			}
105		}
106
107		y += height + 1
108	}
109
110	if overflow > 0 && y < frame.Height {
111		text := fmt.Sprintf("+%d more", overflow)
112		start := x + width - textWidth(text)
113		if start < x {
114			start = x
115		}
116		DrawText(frame, start, y, x+width, text, styles.Text)
117		footprints = append(footprints, domain.Rect{X: start, Y: y, Width: x + width - start, Height: 1})
118	}
119	return footprints
120}
121
122func noticeBoxStyle(styles NoticeStyles, severity domain.NoticeSeverity) renderer.Style {
123	switch severity {
124	case domain.NoticeWarn:
125		return styles.BoxWarn
126	case domain.NoticeError:
127		return styles.BoxError
128	default:
129		return styles.BoxInfo
130	}
131}
132
133// wrapNoticeText greedily fills lines up to width, then caps at maxLines by
134// collapsing any remainder into the last line and truncating it, so an
135// overlong message is clipped visibly rather than silently dropped mid-word.
136func wrapNoticeText(message string, width, maxLines int) []string {
137	if width <= 0 || maxLines <= 0 {
138		return nil
139	}
140	words := strings.Fields(message)
141	if len(words) == 0 {
142		return nil
143	}
144
145	lines := make([]string, 0, maxLines)
146	current := words[0]
147	for _, w := range words[1:] {
148		candidate := current + " " + w
149		if textWidth(candidate) <= width {
150			current = candidate
151			continue
152		}
153		lines = append(lines, current)
154		current = w
155	}
156	lines = append(lines, current)
157
158	if len(lines) <= maxLines {
159		return lines
160	}
161	rest := strings.Join(lines[maxLines-1:], " ")
162	lines = lines[:maxLines-1]
163	return append(lines, TruncateText(rest, width))
164}

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