v0.3.0 da0887c

layout.go RawHistoryFolder

1// Package layout turns the ANSI frames of modules into one row of cells.
2package layout
3
4import (
5	vt "github.com/bnema/vev-vt"
6
7	"github.com/bnema/neferbar/internal/glyph"
8	"github.com/bnema/neferbar/internal/gpu"
9	"github.com/bnema/neferbar/internal/module"
10)
11
12// Source is one module and the cells parsed from its latest frame.
13type Source struct {
14	M *module.Module
15
16	screen *vt.Screen
17	frame  []byte // raw ANSI of the latest frame
18	input  []byte // reset + erase + frame, reused for each Write
19	cells  []gpu.Cell
20	failed bool
21
22	// start and shown are where Compose put the module's cells in the row, and
23	// how many of them survived clipping.
24	start, shown int
25}
26
27// Span is the part of the row a module occupies after Compose.
28type Span struct {
29	M            *module.Module
30	Start, Width int
31}
32
33// Layout owns the sources and the composed row.
34type Layout struct {
35	cols    int
36	fg, bg  [3]uint8
37	pal     [16][3]uint8
38	sources []*Source
39	row     []gpu.Cell
40}
41
42// New builds a layout for a row of cols cells.
43func New(cols int, fg, bg [3]uint8, pal [16][3]uint8, mods []*module.Module) *Layout {
44	l := &Layout{cols: cols, fg: fg, bg: bg, pal: pal, row: make([]gpu.Cell, cols)}
45	l.SetModules(mods)
46	return l
47}
48
49func newSource(m *module.Module, cols int) *Source {
50	s := &Source{
51		M:      m,
52		screen: vt.NewScreen(cols, 1),
53		frame:  make([]byte, 0, module.MaxFrame),
54		input:  make([]byte, 0, module.MaxFrame+16),
55		cells:  make([]gpu.Cell, 0, cols),
56	}
57	s.screen.Write([]byte("\x1b[?7l")) // no autowrap: long text clips
58	return s
59}
60
61// SetModules replaces the module list. A module that is already shown keeps
62// its parsed text; new ones start empty. The order of mods is the order
63// within each zone.
64func (l *Layout) SetModules(mods []*module.Module) {
65	old := make(map[*module.Module]*Source, len(l.sources))
66	for _, s := range l.sources {
67		old[s.M] = s
68	}
69	l.sources = make([]*Source, 0, len(mods))
70	for _, m := range mods {
71		if s, ok := old[m]; ok {
72			l.sources = append(l.sources, s)
73		} else {
74			l.sources = append(l.sources, newSource(m, l.cols))
75		}
76	}
77}
78
79// SetColors changes the default colors and the 16-color palette, and reparses
80// every module's last frame.
81func (l *Layout) SetColors(fg, bg [3]uint8, pal [16][3]uint8) {
82	l.fg, l.bg, l.pal = fg, bg, pal
83	for _, s := range l.sources {
84		if s.failed {
85			s.cells = l.marker(s.cells[:0], s.M.Name)
86		} else if len(s.frame) > 0 {
87			s.parse(fg, bg, l.pal)
88		}
89	}
90}
91
92// Resize changes the row width and reparses the last frame of every module.
93func (l *Layout) Resize(cols int) {
94	l.cols = cols
95	l.row = make([]gpu.Cell, cols)
96	for _, s := range l.sources {
97		s.screen = vt.NewScreen(cols, 1)
98		s.screen.Write([]byte("\x1b[?7l"))
99		s.cells = make([]gpu.Cell, 0, cols)
100		if s.failed {
101			s.cells = l.marker(s.cells, s.M.Name)
102		} else if len(s.frame) > 0 {
103			s.parse(l.fg, l.bg, l.pal)
104		}
105	}
106}
107
108// Update pulls new frames from every module and reparses the ones that
109// changed. It reports whether the composed row may have changed.
110func (l *Layout) Update() bool {
111	changed := false
112	for _, s := range l.sources {
113		var ch, failed bool
114		s.frame, ch, failed = s.M.Take(s.frame)
115		if !ch {
116			continue
117		}
118		changed = true
119		s.failed = failed
120		if failed {
121			s.cells = l.marker(s.cells[:0], s.M.Name)
122			continue
123		}
124		s.parse(l.fg, l.bg, l.pal)
125	}
126	return changed
127}
128
129// marker writes a short red "[name!]" for a module whose script is down.
130func (l *Layout) marker(dst []gpu.Cell, name string) []gpu.Cell {
131	red := [3]uint8{0xf3, 0x8b, 0xa8}
132	add := func(r rune) { dst = append(dst, gpu.Cell{Rune: r, FG: red, BG: l.bg}) }
133	add('[')
134	for _, r := range name {
135		add(r)
136	}
137	add('!')
138	add(']')
139	return dst
140}
141
142func (s *Source) parse(fg, bg [3]uint8, pal [16][3]uint8) {
143	s.input = append(s.input[:0], "\x1b[0m\r\x1b[2K"...)
144	s.input = append(s.input, s.frame...)
145	// A frame that ends inside an escape sequence (a script bug, or the frame
146	// cap cutting one) would leave the parser waiting, and it would swallow the
147	// next frames. ESC \ is the string terminator: it closes any such sequence,
148	// and is harmless after a complete one.
149	s.input = append(s.input, "\x1b\\"...)
150	s.screen.Write(s.input)
151	cols := s.screen.Columns()
152	// Trim trailing blank default cells so modules do not claim empty space.
153	end := cols
154	for end > 0 {
155		c := s.screen.Cell(end-1, 0)
156		if (c.Rune == ' ' || c.Rune == 0) && c.Style.Background < 0 && !c.Style.HasBackgroundRGB && !c.Style.Inverse {
157			end--
158			continue
159		}
160		break
161	}
162	s.cells = s.cells[:0]
163	for x := 0; x < end; x++ {
164		s.cells = append(s.cells, convert(s.screen.Cell(x, 0), fg, bg, &pal))
165	}
166}
167
168func convert(c vt.Cell, defFG, defBG [3]uint8, pal *[16][3]uint8) gpu.Cell {
169	st := &c.Style
170	fg, bg := defFG, defBG
171	switch {
172	case st.HasForegroundRGB:
173		fg = [3]uint8{st.ForegroundRGB.R, st.ForegroundRGB.G, st.ForegroundRGB.B}
174	case st.Foreground >= 0:
175		fg = palette(st.Foreground, pal)
176	}
177	switch {
178	case st.HasBackgroundRGB:
179		bg = [3]uint8{st.BackgroundRGB.R, st.BackgroundRGB.G, st.BackgroundRGB.B}
180	case st.Background >= 0:
181		bg = palette(st.Background, pal)
182	}
183	if st.Inverse {
184		fg, bg = bg, fg
185	}
186	if st.Attrs&vt.AttrDim != 0 {
187		for i := range fg {
188			fg[i] = uint8((int(fg[i])*2 + int(bg[i])) / 3)
189		}
190	}
191	r := c.Rune
192	if r == 0 || c.Continuation {
193		r = ' '
194	}
195	var style glyph.Style
196	if st.Bold {
197		style |= glyph.Bold
198	}
199	if st.Italic {
200		style |= glyph.Italic
201	}
202	if st.Attrs&vt.AttrUnderline != 0 || st.UnderlineStyle != vt.UnderlineNone {
203		style |= glyph.Underline
204	}
205	if st.Attrs&vt.AttrStrikethrough != 0 {
206		style |= glyph.Strike
207	}
208	return gpu.Cell{Rune: r, FG: fg, BG: bg, Style: style}
209}
210
211// Compose lays the zones out in one row: left from the left edge, right to
212// the right edge, center in the gap between them. Overlap is cut from the
213// center first, then from the left.
214func (l *Layout) Compose() []gpu.Cell {
215	blank := gpu.Cell{Rune: ' ', FG: l.fg, BG: l.bg}
216	for i := range l.row {
217		l.row[i] = blank
218	}
219	var left, center, right int
220	for _, s := range l.sources {
221		s.start, s.shown = 0, 0
222		n := len(s.cells)
223		switch s.M.Zone {
224		case module.Left:
225			left += n
226		case module.Center:
227			center += n
228		default:
229			right += n
230		}
231	}
232	right = min(right, l.cols)
233	left = min(left, l.cols-right)
234	// Right zone, packed against the right edge.
235	x := l.cols - right
236	for _, s := range l.sources {
237		if s.M.Zone == module.Right {
238			s.start = x
239			s.shown = copy(l.row[x:], s.cells[:min(len(s.cells), l.cols-x)])
240			x += s.shown
241		}
242	}
243	// Left zone from the left edge, clipped to its budget.
244	x, limit := 0, left
245	for _, s := range l.sources {
246		if s.M.Zone == module.Left && x < limit {
247			s.start = x
248			s.shown = copy(l.row[x:limit], s.cells)
249			x += s.shown
250		}
251	}
252	// Center zone: on the middle of the bar, and only as far from it as the
253	// left and right zones force. Centering it in the space they leave would
254	// shift it whenever the two zones differ in width.
255	gap := l.cols - right - left
256	if center = min(center, gap); center > 0 {
257		start := (l.cols - center) / 2          // exactly on the middle
258		start = max(start, left)                // not over the left zone
259		start = min(start, l.cols-right-center) // not over the right zone
260		x, limit = start, start+center
261		for _, s := range l.sources {
262			if s.M.Zone == module.Center && x < limit {
263				s.start = x
264				s.shown = copy(l.row[x:limit], s.cells)
265				x += s.shown
266			}
267		}
268	}
269	return l.row
270}
271
272// Spans appends the span of every module that has cells in the row composed
273// last, in module order.
274func (l *Layout) Spans(dst []Span) []Span {
275	for _, s := range l.sources {
276		if s.shown > 0 {
277			dst = append(dst, Span{M: s.M, Start: s.start, Width: s.shown})
278		}
279	}
280	return dst
281}
282
283// At returns the module shown at column col of the row composed last, and the
284// column's offset inside the module.
285func (l *Layout) At(col int) (m *module.Module, offset int, ok bool) {
286	for _, s := range l.sources {
287		if col >= s.start && col < s.start+s.shown {
288			return s.M, col - s.start, true
289		}
290	}
291	return nil, 0, false
292}

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 5

Tags 4

↑↓ moveenter openesc close