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}