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}