1package ui
2
3import (
4 "fmt"
5 "strings"
6 "sync"
7 "time"
8
9 "git.sr.ht/~rockorager/vaxis"
10 "github.com/delthas/go-localeinfo"
11 "github.com/rivo/uniseg"
12)
13
14var asciiStringCache []string
15
16var FormattingChars = map[rune]rune{
17 '*': '\x02', // bold
18 '^': '\x03', // custom color (terminal color, 0-99)
19 '#': '\x04', // custom color (hexadecimal color)
20 '-': '\x0F', // reset all attributes
21 '`': '\x11', // monospace text
22 '|': '\x16', // reverse color
23 '_': '\x1D', // italics
24 '~': '\x1E', // strikethrough
25 '=': '\x1F', // underline
26}
27
28// allow for reverse lookup
29var formattingRunes map[rune]rune
30
31func init() {
32 formattingRunes = make(map[rune]rune, len(FormattingChars))
33 for c, r := range FormattingChars {
34 formattingRunes[r] = c
35 }
36
37 asciiStringCache = make([]string, 0x80)
38 for i := range asciiStringCache {
39 asciiStringCache[i] = string(rune(i))
40
41 if f, ok := formattingRunes[rune(i)]; ok {
42 asciiStringCache[i] = string(f)
43 }
44 }
45}
46
47var runeWidthMap = make(map[rune]int)
48
49func runeWidth(vx *Vaxis, r rune) int {
50 if vx == nil { // For tests only
51 return 1
52 }
53 if r == '\n' {
54 r = '↲'
55 }
56 if r <= 0x1F {
57 if f, ok := formattingRunes[r]; ok {
58 r = f
59 return 1
60 }
61 return 0
62 }
63 if r <= 0x7F {
64 return 1
65 }
66 if n, ok := runeWidthMap[r]; ok {
67 return n
68 }
69 n := vx.RenderedWidth(string([]rune{r}))
70 runeWidthMap[r] = n
71 return n
72}
73
74func stringWidth(vx *Vaxis, s string) int {
75 if vx == nil { // For tests only
76 return len(s)
77 }
78 if len(s) == 1 { // Single-character ASCII fast path
79 if s[0] == '\n' { // Replaced with ↲
80 return 1
81 }
82 if s[0] <= 0x1F {
83 if _, ok := formattingRunes[rune(s[0])]; ok {
84 return 1
85 }
86 return 0
87 }
88 if s[0] <= 0x7F {
89 return 1
90 }
91 }
92 r := []rune(s)
93 if len(r) == 1 { // Single-character fast path
94 return runeWidth(vx, r[0])
95 }
96 return vx.RenderedWidth(s)
97}
98
99func truncate(vx *Vaxis, s string, w int, tail string) string {
100 if stringWidth(vx, s) <= w {
101 return s
102 }
103 w -= stringWidth(vx, tail)
104
105 width := 0
106 var sb strings.Builder
107 for _, c := range vaxis.Characters(s) {
108 chWidth := stringWidth(vx, c.Grapheme)
109 if width+chWidth > w {
110 break
111 }
112 width += chWidth
113 sb.WriteString(c.Grapheme)
114 }
115 sb.WriteString(tail)
116 return sb.String()
117}
118
119var clusterWidthMap = make(map[string]int)
120
121// width in cells
122func firstCluster(vx *Vaxis, r []rune) (c string, width int) {
123 if len(r) == 0 { // Empty fast-path
124 return "", 0
125 }
126 if r[0] == '\t' {
127 return " ", 1
128 }
129 if r[0] <= 0x7F { // ASCII fast-path
130 return asciiStringCache[int(r[0])], runeWidth(vx, r[0])
131 }
132 c, _, _, _ = uniseg.FirstGraphemeClusterInString(string(r), -1)
133
134 var cw int
135 if n, ok := clusterWidthMap[c]; ok {
136 cw = n
137 } else {
138 cw = stringWidth(vx, c)
139 clusterWidthMap[c] = cw
140 }
141 return c, cw
142}
143
144func setCell(vx *Vaxis, x int, y int, r rune, st vaxis.Style) {
145 vx.window.SetCell(x, y, vaxis.Cell{
146 Character: vaxis.Character{
147 Grapheme: string([]rune{r}),
148 },
149 Style: st,
150 })
151}
152
153// limit = -1 means no limit
154// di is an offset in runes
155func printCluster(vx *Vaxis, x int, y int, limit int, r []rune, st vaxis.Style) (dx int, di int) {
156 if limit >= 0 && x >= limit {
157 return 0, 0
158 }
159 var c string
160 var w int
161 if len(r) > 0 && r[0] <= 0x7F { // ASCII fast-path
162 c = asciiStringCache[int(r[0])]
163 w = runeWidth(vx, r[0])
164 di = 1
165 } else {
166 c, w = firstCluster(vx, r)
167 di = len([]rune(c))
168 }
169 if limit >= 0 && w > limit-x {
170 return 0, 0
171 }
172 vx.window.SetCell(x, y, vaxis.Cell{
173 Character: vaxis.Character{
174 Grapheme: c,
175 },
176 Style: st,
177 })
178 return w, di
179}
180
181func printString(vx *Vaxis, x *int, y int, s StyledString) {
182 var st vaxis.Style
183 nextStyles := s.styles
184
185 i := 0
186 sr := []rune(s.string)
187 for len(sr) > 0 {
188 if 0 < len(nextStyles) && nextStyles[0].Start == i {
189 st = nextStyles[0].Style
190 nextStyles = nextStyles[1:]
191 }
192 dx, di := printCluster(vx, *x, y, -1, sr, st)
193 *x += dx
194 i += len(string(sr[:di]))
195 sr = sr[di:]
196 }
197}
198
199func printIdent(vx *Vaxis, x, y, width int, s StyledString) (xb int, xe int) {
200 s.string = truncate(vx, s.string, width, "\u2026")
201 x += width - stringWidth(vx, s.string)
202 var st vaxis.Style
203 if len(s.styles) != 0 && s.styles[0].Start == 0 {
204 st = s.styles[0].Style
205 }
206 setCell(vx, x-1, y, ' ', st)
207 xb = x
208 printString(vx, &x, y, s)
209 if len(s.styles) != 0 {
210 // TODO check if it's not a style that is from the truncated
211 // part of s.
212 st = s.styles[len(s.styles)-1].Style
213 }
214 setCell(vx, x, y, ' ', st)
215 xe = x
216 return
217}
218
219func printNumber(vx *Vaxis, x *int, y int, st vaxis.Style, n int) {
220 s := Styled(fmt.Sprintf("%d", n), st)
221 printString(vx, x, y, s)
222}
223
224var dateConfig sync.Once
225var dateMonthFirst bool
226
227func loadDateInfo() {
228 // Very overkill, but I like it :)
229 // Try to extract from the user locale whether they'd rather have the date
230 // printed as dd/mm or mm/dd.
231 // If we're not sure, print dd/mm.
232 l, err := localeinfo.NewLocale("")
233 if err != nil {
234 return
235 }
236 format := l.DateFormat()
237 dayIndex := -1
238 for _, s := range []string{"%d", "%e"} {
239 dayIndex = strings.Index(format, s)
240 if dayIndex >= 0 {
241 break
242 }
243 }
244 if dayIndex == -1 {
245 return
246 }
247 monthIndex := -1
248 for _, s := range []string{"%m", "%b", "%B"} {
249 monthIndex = strings.Index(format, s)
250 if monthIndex >= 0 {
251 break
252 }
253 }
254 if monthIndex == -1 {
255 return
256 }
257 if monthIndex < dayIndex {
258 dateMonthFirst = true
259 }
260}
261
262func printDate(vx *Vaxis, x int, y int, st vaxis.Style, t time.Time) {
263 dateConfig.Do(loadDateInfo)
264 _, m, d := t.Date()
265 var left, right int
266 if dateMonthFirst {
267 left, right = int(m), d
268 } else {
269 left, right = d, int(m)
270 }
271 l0 := rune(left/10) + '0'
272 l1 := rune(left%10) + '0'
273 r0 := rune(right/10) + '0'
274 r1 := rune(right%10) + '0'
275
276 setCell(vx, x+0, y, l0, st)
277 setCell(vx, x+1, y, l1, st)
278 setCell(vx, x+2, y, '/', st)
279 setCell(vx, x+3, y, r0, st)
280 setCell(vx, x+4, y, r1, st)
281}
282
283func printTime(vx *Vaxis, x int, y int, st vaxis.Style, t time.Time) {
284 hr0 := rune(t.Hour()/10) + '0'
285 hr1 := rune(t.Hour()%10) + '0'
286 mn0 := rune(t.Minute()/10) + '0'
287 mn1 := rune(t.Minute()%10) + '0'
288 setCell(vx, x+0, y, hr0, st)
289 setCell(vx, x+1, y, hr1, st)
290 setCell(vx, x+2, y, ':', st)
291 setCell(vx, x+3, y, mn0, st)
292 setCell(vx, x+4, y, mn1, st)
293}
294
295func clearArea(vx *Vaxis, x0, y0, width, height int) {
296 vx.window.New(x0, y0, width, height).Clear()
297}