v0.2.0 c616855

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

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