commit ffb806d
delthas
·
2024-07-02 21:49:11 +0000 UTC
parent e64baed
Move to Vaxis (from vaxis-tcell) This brings, at long last, proper multi-codepoint grapheme cluster support. Meaning, editing ZWJ emoji now works flawlessly. Fixes: https://todo.sr.ht/~delthas/senpai/57
16 files changed,
+1088,
-810
M
app.go
+267,
-223
1@@ -14,7 +14,6 @@ import (
2 "unicode/utf8"
3
4 "git.sr.ht/~rockorager/vaxis"
5- "github.com/gdamore/tcell/v2"
6 "golang.org/x/net/context"
7 "golang.org/x/net/proxy"
8
9@@ -172,10 +171,9 @@ func NewApp(cfg Config) (app *App, err error) {
10 content: ev,
11 }
12 })
13- app.win.SetPrompt(ui.Styled(">",
14- tcell.
15- StyleDefault.
16- Foreground(app.cfg.Colors.Prompt)),
17+ app.win.SetPrompt(ui.Styled(">", vaxis.Style{
18+ Foreground: app.cfg.Colors.Prompt,
19+ }),
20 )
21
22 app.initWindow()
23@@ -375,7 +373,7 @@ func (app *App) ircLoop(netID string) {
24 }
25 app.queueStatusLine(netID, ui.Line{
26 Head: "!!",
27- HeadColor: tcell.ColorRed,
28+ HeadColor: ui.ColorRed,
29 Body: ui.PlainString("Connection lost"),
30 })
31 }
32@@ -392,7 +390,7 @@ func (app *App) connect(netID string) net.Conn {
33 }
34 app.queueStatusLine(netID, ui.Line{
35 Head: "!!",
36- HeadColor: tcell.ColorRed,
37+ HeadColor: ui.ColorRed,
38 Body: ui.PlainSprintf("Connection failed: %v", err),
39 })
40 return nil
41@@ -467,93 +465,89 @@ func (app *App) uiLoop() {
42 }
43
44 func (app *App) handleUIEvent(ev interface{}) bool {
45+ // TODO: eat QuitEvent here?
46 switch ev := ev.(type) {
47- case *tcell.EventResize:
48+ case vaxis.Resize:
49 app.win.Resize()
50- case *tcell.EventPaste:
51- app.pasting = ev.Start()
52- case *tcell.EventMouse:
53+ case vaxis.PasteStartEvent:
54+ app.pasting = true
55+ case vaxis.PasteEndEvent:
56+ app.pasting = false
57+ case vaxis.Mouse:
58 app.handleMouseEvent(ev)
59- case *tcell.EventKey:
60+ case vaxis.Key:
61 app.handleKeyEvent(ev)
62- case tcell.EventInterrupt:
63- // unknown key event, ignore
64- return true
65- case *tcell.EventError:
66- // happens when the terminal is closing: in which case, exit
67- return false
68- case tcell.EventVaxis:
69- // TODO use vaxis events
70- return true
71 case *ui.NotifyEvent:
72 app.win.JumpBufferNetwork(ev.NetID, ev.Buffer)
73 case statusLine:
74 app.addStatusLine(ev.netID, ev.line)
75 default:
76- panic("unreachable")
77+ // TODO: missing event types
78 }
79 return true
80 }
81
82-func (app *App) handleMouseEvent(ev *tcell.EventMouse) {
83- x, y := ev.Position()
84+func (app *App) handleMouseEvent(ev vaxis.Mouse) {
85+ x, y := ev.Col, ev.Row
86 w, h := app.win.Size()
87- if ev.Buttons()&tcell.WheelUp != 0 {
88- if x < app.win.ChannelWidth() || (app.win.ChannelWidth() == 0 && y == h-1) {
89- app.win.ScrollChannelUpBy(4)
90- } else if x > w-app.win.MemberWidth() {
91- app.win.ScrollMemberUpBy(4)
92- } else {
93- app.win.ScrollUpBy(4)
94- app.requestHistory()
95- }
96- }
97- if ev.Buttons()&tcell.WheelDown != 0 {
98- if x < app.win.ChannelWidth() || (app.win.ChannelWidth() == 0 && y == h-1) {
99- app.win.ScrollChannelDownBy(4)
100- } else if x > w-app.win.MemberWidth() {
101- app.win.ScrollMemberDownBy(4)
102- } else {
103- app.win.ScrollDownBy(4)
104+ if ev.EventType == vaxis.EventPress {
105+ if ev.Button == vaxis.MouseWheelUp {
106+ if x < app.win.ChannelWidth() || (app.win.ChannelWidth() == 0 && y == h-1) {
107+ app.win.ScrollChannelUpBy(4)
108+ } else if x > w-app.win.MemberWidth() {
109+ app.win.ScrollMemberUpBy(4)
110+ } else {
111+ app.win.ScrollUpBy(4)
112+ app.requestHistory()
113+ }
114 }
115- }
116- if ev.Buttons()&tcell.ButtonPrimary != 0 {
117- if app.win.ChannelColClicked() {
118- app.win.ResizeChannelCol(x + 1)
119- } else if app.win.MemberColClicked() {
120- app.win.ResizeMemberCol(w - x)
121- } else if x == app.win.ChannelWidth()-1 {
122- app.win.ClickChannelCol(true)
123- app.win.SetMouseShape(vaxis.MouseShapeResizeHorizontal)
124- } else if x < app.win.ChannelWidth() {
125- app.win.ClickBuffer(y + app.win.ChannelOffset())
126- } else if app.win.ChannelWidth() == 0 && y == h-1 {
127- app.win.ClickBuffer(app.win.HorizontalBufferOffset(x))
128- } else if x == w-app.win.MemberWidth() {
129- app.win.ClickMemberCol(true)
130- app.win.SetMouseShape(vaxis.MouseShapeResizeHorizontal)
131- } else if x > w-app.win.MemberWidth() && y >= 2 {
132- app.win.ClickMember(y - 2 + app.win.MemberOffset())
133+ if ev.Button == vaxis.MouseWheelDown {
134+ if x < app.win.ChannelWidth() || (app.win.ChannelWidth() == 0 && y == h-1) {
135+ app.win.ScrollChannelDownBy(4)
136+ } else if x > w-app.win.MemberWidth() {
137+ app.win.ScrollMemberDownBy(4)
138+ } else {
139+ app.win.ScrollDownBy(4)
140+ }
141 }
142- }
143- if ev.Buttons()&tcell.ButtonMiddle != 0 {
144- i := -1
145- if x < app.win.ChannelWidth() {
146- i = y + app.win.ChannelOffset()
147- } else if app.win.ChannelWidth() == 0 && y == h-1 {
148- i = app.win.HorizontalBufferOffset(x)
149+ if ev.Button == vaxis.MouseLeftButton {
150+ if app.win.ChannelColClicked() {
151+ app.win.ResizeChannelCol(x + 1)
152+ } else if app.win.MemberColClicked() {
153+ app.win.ResizeMemberCol(w - x)
154+ } else if x == app.win.ChannelWidth()-1 {
155+ app.win.ClickChannelCol(true)
156+ app.win.SetMouseShape(vaxis.MouseShapeResizeHorizontal)
157+ } else if x < app.win.ChannelWidth() {
158+ app.win.ClickBuffer(y + app.win.ChannelOffset())
159+ } else if app.win.ChannelWidth() == 0 && y == h-1 {
160+ app.win.ClickBuffer(app.win.HorizontalBufferOffset(x))
161+ } else if x == w-app.win.MemberWidth() {
162+ app.win.ClickMemberCol(true)
163+ app.win.SetMouseShape(vaxis.MouseShapeResizeHorizontal)
164+ } else if x > w-app.win.MemberWidth() && y >= 2 {
165+ app.win.ClickMember(y - 2 + app.win.MemberOffset())
166+ }
167 }
168- netID, channel, ok := app.win.Buffer(i)
169- if ok && channel != "" {
170- s := app.sessions[netID]
171- if s != nil && s.IsChannel(channel) {
172- s.Part(channel, "")
173- } else {
174- app.win.RemoveBuffer(netID, channel)
175+ if ev.Button == vaxis.MouseMiddleButton {
176+ i := -1
177+ if x < app.win.ChannelWidth() {
178+ i = y + app.win.ChannelOffset()
179+ } else if app.win.ChannelWidth() == 0 && y == h-1 {
180+ i = app.win.HorizontalBufferOffset(x)
181+ }
182+ netID, channel, ok := app.win.Buffer(i)
183+ if ok && channel != "" {
184+ s := app.sessions[netID]
185+ if s != nil && s.IsChannel(channel) {
186+ s.Part(channel, "")
187+ } else {
188+ app.win.RemoveBuffer(netID, channel)
189+ }
190 }
191 }
192 }
193- if ev.Buttons() == 0 {
194+ if ev.EventType == vaxis.EventRelease {
195 if x < app.win.ChannelWidth()-1 {
196 if i := y + app.win.ChannelOffset(); i == app.win.ClickedBuffer() {
197 app.win.GoToBufferNo(i)
198@@ -572,7 +566,7 @@ func (app *App) handleMouseEvent(ev *tcell.EventMouse) {
199 app.win.AddLine(netID, target, ui.Line{
200 At: time.Now(),
201 Head: "--",
202- HeadColor: tcell.ColorRed,
203+ HeadColor: ui.ColorRed,
204 Body: ui.PlainSprintf("Adding networks is not available: %v", err),
205 })
206 } else {
207@@ -633,156 +627,157 @@ func (app *App) handleMouseEvent(ev *tcell.EventMouse) {
208 }
209 }
210
211-func (app *App) handleKeyEvent(ev *tcell.EventKey) {
212- switch ev.Key() {
213- case tcell.KeyCtrlC:
214+func (app *App) handleKeyEvent(ev vaxis.Key) {
215+ switch ev.EventType {
216+ case vaxis.EventPress, vaxis.EventRepeat, vaxis.EventPaste:
217+ default:
218+ return
219+ }
220+ if ev.Text != "" {
221+ for _, r := range ev.Text {
222+ app.win.InputRune(r)
223+ }
224+ app.typing()
225+ return
226+ }
227+
228+ if keyMatches(ev, 'c', vaxis.ModCtrl) {
229 if app.win.InputClear() {
230 app.typing()
231 } else {
232 app.win.InputSet("/quit")
233 }
234- case tcell.KeyCtrlF:
235+ } else if keyMatches(ev, 'f', vaxis.ModCtrl) {
236 if len(app.win.InputContent()) == 0 {
237 app.win.InputSet("/search ")
238 }
239- case tcell.KeyCtrlA:
240+ } else if keyMatches(ev, 'a', vaxis.ModCtrl) {
241 app.win.InputHome()
242- case tcell.KeyCtrlE:
243+ } else if keyMatches(ev, 'e', vaxis.ModCtrl) {
244 app.win.InputEnd()
245- case tcell.KeyCtrlL:
246+ } else if keyMatches(ev, 'l', vaxis.ModCtrl) {
247 app.win.Resize()
248- case tcell.KeyCtrlU, tcell.KeyPgUp:
249+ } else if keyMatches(ev, 'u', vaxis.ModCtrl) || keyMatches(ev, vaxis.KeyPgUp, 0) {
250 app.win.ScrollUp()
251 app.requestHistory()
252- case tcell.KeyCtrlD, tcell.KeyPgDn:
253+ } else if keyMatches(ev, 'd', vaxis.ModCtrl) || keyMatches(ev, vaxis.KeyPgDown, 0) {
254 app.win.ScrollDown()
255- case tcell.KeyCtrlN:
256+ } else if keyMatches(ev, 'n', vaxis.ModCtrl) {
257 app.win.NextBuffer()
258 app.win.ScrollToBuffer()
259- case tcell.KeyCtrlP:
260+ } else if keyMatches(ev, 'p', vaxis.ModCtrl) {
261 app.win.PreviousBuffer()
262 app.win.ScrollToBuffer()
263- case tcell.KeyRight:
264- if ev.Modifiers() == tcell.ModAlt {
265- app.win.NextBuffer()
266- app.win.ScrollToBuffer()
267- } else if ev.Modifiers() == tcell.ModShift {
268- app.win.NextUnreadBuffer()
269- app.win.ScrollToBuffer()
270- } else if ev.Modifiers() == tcell.ModCtrl {
271- app.win.InputRightWord()
272- } else {
273- app.win.InputRight()
274- }
275- case tcell.KeyLeft:
276- if ev.Modifiers() == tcell.ModAlt {
277- app.win.PreviousBuffer()
278- app.win.ScrollToBuffer()
279- } else if ev.Modifiers() == tcell.ModShift {
280- app.win.PreviousUnreadBuffer()
281- app.win.ScrollToBuffer()
282- } else if ev.Modifiers() == tcell.ModCtrl {
283- app.win.InputLeftWord()
284- } else {
285- app.win.InputLeft()
286- }
287- case tcell.KeyUp:
288- if ev.Modifiers() == tcell.ModAlt {
289- app.win.PreviousBuffer()
290- } else {
291- app.win.InputUp()
292- }
293- case tcell.KeyDown:
294- if ev.Modifiers() == tcell.ModAlt {
295- app.win.NextBuffer()
296- } else {
297- app.win.InputDown()
298- }
299- case tcell.KeyHome:
300- if ev.Modifiers() == tcell.ModAlt {
301- app.win.GoToBufferNo(0)
302- } else {
303- app.win.InputHome()
304- }
305- case tcell.KeyEnd:
306- if ev.Modifiers() == tcell.ModAlt {
307- maxInt := int(^uint(0) >> 1)
308- app.win.GoToBufferNo(maxInt)
309- } else {
310- app.win.InputEnd()
311- }
312- case tcell.KeyBackspace, tcell.KeyBackspace2:
313- var ok bool
314- if ev.Modifiers() == tcell.ModAlt {
315- ok = app.win.InputDeleteWord()
316- } else {
317- ok = app.win.InputBackspace()
318+ } else if keyMatches(ev, vaxis.KeyRight, vaxis.ModAlt) {
319+ app.win.NextBuffer()
320+ app.win.ScrollToBuffer()
321+ } else if keyMatches(ev, vaxis.KeyRight, vaxis.ModShift) {
322+ app.win.NextUnreadBuffer()
323+ app.win.ScrollToBuffer()
324+ } else if keyMatches(ev, vaxis.KeyRight, vaxis.ModCtrl) {
325+ app.win.InputRightWord()
326+ } else if keyMatches(ev, vaxis.KeyRight, 0) {
327+ app.win.InputRight()
328+ } else if keyMatches(ev, vaxis.KeyLeft, vaxis.ModAlt) {
329+ app.win.PreviousBuffer()
330+ app.win.ScrollToBuffer()
331+ } else if keyMatches(ev, vaxis.KeyLeft, vaxis.ModShift) {
332+ app.win.PreviousUnreadBuffer()
333+ app.win.ScrollToBuffer()
334+ } else if keyMatches(ev, vaxis.KeyLeft, vaxis.ModCtrl) {
335+ app.win.InputLeftWord()
336+ } else if keyMatches(ev, vaxis.KeyLeft, 0) {
337+ app.win.InputLeft()
338+ } else if keyMatches(ev, vaxis.KeyUp, vaxis.ModAlt) {
339+ app.win.PreviousBuffer()
340+ } else if keyMatches(ev, vaxis.KeyUp, 0) {
341+ app.win.InputUp()
342+ } else if keyMatches(ev, vaxis.KeyDown, vaxis.ModAlt) {
343+ app.win.NextBuffer()
344+ } else if keyMatches(ev, vaxis.KeyDown, 0) {
345+ app.win.InputDown()
346+ } else if keyMatches(ev, vaxis.KeyHome, vaxis.ModAlt) {
347+ app.win.GoToBufferNo(0)
348+ } else if keyMatches(ev, vaxis.KeyHome, 0) {
349+ app.win.InputHome()
350+ } else if keyMatches(ev, vaxis.KeyEnd, vaxis.ModAlt) {
351+ maxInt := int(^uint(0) >> 1)
352+ app.win.GoToBufferNo(maxInt)
353+ } else if keyMatches(ev, vaxis.KeyEnd, 0) {
354+ app.win.InputEnd()
355+ } else if keyMatches(ev, vaxis.KeyBackspace, vaxis.ModAlt) {
356+ if app.win.InputDeleteWord() {
357+ app.typing()
358 }
359- if ok {
360+ } else if keyMatches(ev, vaxis.KeyBackspace, 0) {
361+ if app.win.InputBackspace() {
362 app.typing()
363 }
364- case tcell.KeyDelete:
365- ok := app.win.InputDelete()
366- if ok {
367+ } else if keyMatches(ev, vaxis.KeyDelete, 0) {
368+ if app.win.InputDelete() {
369 app.typing()
370 }
371- case tcell.KeyCtrlW:
372- ok := app.win.InputDeleteWord()
373- if ok {
374+ } else if keyMatches(ev, 'w', vaxis.ModCtrl) {
375+ if app.win.InputDeleteWord() {
376 app.typing()
377 }
378- case tcell.KeyCtrlR:
379+ } else if keyMatches(ev, 'r', vaxis.ModCtrl) {
380 app.win.InputBackSearch()
381- case tcell.KeyTab:
382- ok := app.win.InputAutoComplete()
383- if ok {
384+ } else if keyMatches(ev, vaxis.KeyTab, 0) {
385+ if app.win.InputAutoComplete() {
386 app.typing()
387 }
388- case tcell.KeyEscape:
389+ } else if keyMatches(ev, vaxis.KeyEsc, 0) {
390 app.win.CloseOverlay()
391- case tcell.KeyF7:
392+ } else if keyMatches(ev, vaxis.KeyF07, 0) {
393 app.win.ToggleChannelList()
394- case tcell.KeyF8:
395+ } else if keyMatches(ev, vaxis.KeyF08, 0) {
396 app.win.ToggleMemberList()
397- case tcell.KeyCR, tcell.KeyLF:
398- if app.pasting {
399+ } else if keyMatches(ev, '\n', 0) || keyMatches(ev, '\r', 0) || keyMatches(ev, 'j', vaxis.ModCtrl) || keyMatches(ev, vaxis.KeyKeyPadEnter, 0) {
400+ if ev.EventType == vaxis.EventPaste {
401 app.win.InputRune('\n')
402- break
403- }
404- netID, buffer := app.win.CurrentBuffer()
405- input := string(app.win.InputContent())
406- var err error
407- for _, part := range strings.Split(input, "\n") {
408- if err = app.handleInput(buffer, part); err != nil {
409- app.win.AddLine(netID, buffer, ui.Line{
410- At: time.Now(),
411- Head: "!!",
412- HeadColor: tcell.ColorRed,
413- Notify: ui.NotifyUnread,
414- Body: ui.PlainSprintf("%q: %s", input, err),
415- })
416- break
417+ } else {
418+ netID, buffer := app.win.CurrentBuffer()
419+ input := string(app.win.InputContent())
420+ var err error
421+ for _, part := range strings.Split(input, "\n") {
422+ if err = app.handleInput(buffer, part); err != nil {
423+ app.win.AddLine(netID, buffer, ui.Line{
424+ At: time.Now(),
425+ Head: "!!",
426+ HeadColor: ui.ColorRed,
427+ Notify: ui.NotifyUnread,
428+ Body: ui.PlainSprintf("%q: %s", input, err),
429+ })
430+ break
431+ }
432 }
433- }
434- if err == nil {
435- app.win.InputFlush()
436- }
437- case tcell.KeyRune:
438- if ev.Modifiers() == tcell.ModAlt {
439- switch ev.Rune() {
440- case 'n':
441- app.win.ScrollDownHighlight()
442- case 'p':
443- app.win.ScrollUpHighlight()
444- case '1', '2', '3', '4', '5', '6', '7', '8', '9':
445- app.win.GoToBufferNo(int(ev.Rune()-'0') - 1)
446+ if err == nil {
447+ app.win.InputFlush()
448 }
449- } else {
450- app.win.InputRune(ev.Rune())
451- app.typing()
452 }
453- default:
454- return
455+ } else if keyMatches(ev, 'n', vaxis.ModAlt) {
456+ app.win.ScrollDownHighlight()
457+ } else if keyMatches(ev, 'p', vaxis.ModAlt) {
458+ app.win.ScrollUpHighlight()
459+ } else if keyMatches(ev, '1', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad1, vaxis.ModAlt) {
460+ app.win.GoToBufferNo(0)
461+ } else if keyMatches(ev, '2', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad2, vaxis.ModAlt) {
462+ app.win.GoToBufferNo(1)
463+ } else if keyMatches(ev, '3', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad3, vaxis.ModAlt) {
464+ app.win.GoToBufferNo(2)
465+ } else if keyMatches(ev, '4', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad4, vaxis.ModAlt) {
466+ app.win.GoToBufferNo(3)
467+ } else if keyMatches(ev, '5', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad5, vaxis.ModAlt) {
468+ app.win.GoToBufferNo(4)
469+ } else if keyMatches(ev, '6', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad6, vaxis.ModAlt) {
470+ app.win.GoToBufferNo(5)
471+ } else if keyMatches(ev, '7', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad7, vaxis.ModAlt) {
472+ app.win.GoToBufferNo(6)
473+ } else if keyMatches(ev, '8', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad8, vaxis.ModAlt) {
474+ app.win.GoToBufferNo(7)
475+ } else if keyMatches(ev, '9', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad9, vaxis.ModAlt) {
476+ app.win.GoToBufferNo(8)
477 }
478 }
479
480@@ -853,7 +848,7 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
481 if err != nil {
482 app.win.AddLine(netID, "", ui.Line{
483 Head: "!!",
484- HeadColor: tcell.ColorRed,
485+ HeadColor: ui.ColorRed,
486 Notify: ui.NotifyUnread,
487 Body: ui.PlainSprintf("Received corrupt message %q: %s", msg.String(), err),
488 })
489@@ -894,8 +889,10 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
490 }
491 var body ui.StyledStringBuilder
492 body.WriteString(fmt.Sprintf("%s\u2192%s", ev.FormerNick, s.Nick()))
493- textStyle := tcell.StyleDefault.Foreground(app.cfg.Colors.Status)
494- arrowStyle := tcell.StyleDefault
495+ textStyle := vaxis.Style{
496+ Foreground: app.cfg.Colors.Status,
497+ }
498+ var arrowStyle vaxis.Style
499 body.AddStyle(0, textStyle)
500 body.AddStyle(len(ev.FormerNick), arrowStyle)
501 body.AddStyle(body.Len()-len(s.Nick()), textStyle)
502@@ -1007,7 +1004,9 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
503 Head: "--",
504 HeadColor: app.cfg.Colors.Status,
505 Notify: notify,
506- Body: ui.Styled(body, tcell.StyleDefault.Foreground(app.cfg.Colors.Status)),
507+ Body: ui.Styled(body, vaxis.Style{
508+ Foreground: app.cfg.Colors.Status,
509+ }),
510 Highlight: notify == ui.NotifyHighlight,
511 Readable: true,
512 })
513@@ -1156,7 +1155,9 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
514 At: msg.TimeOrNow(),
515 Head: head,
516 HeadColor: app.cfg.Colors.Status,
517- Body: ui.Styled(ev.Message, tcell.StyleDefault.Foreground(app.cfg.Colors.Status)),
518+ Body: ui.Styled(ev.Message, vaxis.Style{
519+ Foreground: app.cfg.Colors.Status,
520+ }),
521 })
522 return
523 case irc.ErrorEvent:
524@@ -1168,7 +1169,9 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
525 At: msg.TimeOrNow(),
526 Head: fmt.Sprintf("(%s) --", ev.Code),
527 HeadColor: app.cfg.Colors.Status,
528- Body: ui.Styled(ev.Message, tcell.StyleDefault.Foreground(app.cfg.Colors.Status)),
529+ Body: ui.Styled(ev.Message, vaxis.Style{
530+ Foreground: app.cfg.Colors.Status,
531+ }),
532 })
533 return
534 case irc.SeverityFail:
535@@ -1257,7 +1260,7 @@ func (app *App) notifyHighlight(buffer, nick, content string, current bool) {
536 app.addStatusLine(netID, ui.Line{
537 At: time.Now(),
538 Head: "!!",
539- HeadColor: tcell.ColorRed,
540+ HeadColor: ui.ColorRed,
541 Body: ui.PlainString(body),
542 })
543 }
544@@ -1280,7 +1283,7 @@ func (app *App) notifyHighlight(buffer, nick, content string, current bool) {
545 app.addStatusLine(netID, ui.Line{
546 At: time.Now(),
547 Head: "!!",
548- HeadColor: tcell.ColorRed,
549+ HeadColor: ui.ColorRed,
550 Body: ui.PlainString(body),
551 })
552 }
553@@ -1335,8 +1338,10 @@ func (app *App) formatEvent(ev irc.Event) ui.Line {
554 case irc.UserNickEvent:
555 var body ui.StyledStringBuilder
556 body.WriteString(fmt.Sprintf("%s\u2192%s", ev.FormerNick, ev.User))
557- textStyle := tcell.StyleDefault.Foreground(app.cfg.Colors.Status)
558- arrowStyle := tcell.StyleDefault
559+ textStyle := vaxis.Style{
560+ Foreground: app.cfg.Colors.Status,
561+ }
562+ var arrowStyle vaxis.Style
563 body.AddStyle(0, textStyle)
564 body.AddStyle(len(ev.FormerNick), arrowStyle)
565 body.AddStyle(body.Len()-len(ev.User), textStyle)
566@@ -1352,9 +1357,13 @@ func (app *App) formatEvent(ev irc.Event) ui.Line {
567 case irc.UserJoinEvent:
568 var body ui.StyledStringBuilder
569 body.Grow(len(ev.User) + 1)
570- body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGreen))
571+ body.SetStyle(vaxis.Style{
572+ Foreground: vaxis.IndexColor(2),
573+ })
574 body.WriteByte('+')
575- body.SetStyle(tcell.StyleDefault.Foreground(app.cfg.Colors.Status))
576+ body.SetStyle(vaxis.Style{
577+ Foreground: app.cfg.Colors.Status,
578+ })
579 body.WriteString(ev.User)
580 return ui.Line{
581 At: ev.Time,
582@@ -1368,9 +1377,13 @@ func (app *App) formatEvent(ev irc.Event) ui.Line {
583 case irc.UserPartEvent:
584 var body ui.StyledStringBuilder
585 body.Grow(len(ev.User) + 1)
586- body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorRed))
587+ body.SetStyle(vaxis.Style{
588+ Foreground: ui.ColorRed,
589+ })
590 body.WriteByte('-')
591- body.SetStyle(tcell.StyleDefault.Foreground(app.cfg.Colors.Status))
592+ body.SetStyle(vaxis.Style{
593+ Foreground: app.cfg.Colors.Status,
594+ })
595 body.WriteString(ev.User)
596 return ui.Line{
597 At: ev.Time,
598@@ -1384,9 +1397,13 @@ func (app *App) formatEvent(ev irc.Event) ui.Line {
599 case irc.UserQuitEvent:
600 var body ui.StyledStringBuilder
601 body.Grow(len(ev.User) + 1)
602- body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorRed))
603+ body.SetStyle(vaxis.Style{
604+ Foreground: ui.ColorRed,
605+ })
606 body.WriteByte('-')
607- body.SetStyle(tcell.StyleDefault.Foreground(app.cfg.Colors.Status))
608+ body.SetStyle(vaxis.Style{
609+ Foreground: app.cfg.Colors.Status,
610+ })
611 body.WriteString(ev.User)
612 return ui.Line{
613 At: ev.Time,
614@@ -1406,8 +1423,10 @@ func (app *App) formatEvent(ev irc.Event) ui.Line {
615 Head: "--",
616 HeadColor: app.cfg.Colors.Status,
617 Notify: ui.NotifyUnread,
618- Body: ui.Styled(body, tcell.StyleDefault.Foreground(app.cfg.Colors.Status)),
619- Readable: true,
620+ Body: ui.Styled(body, vaxis.Style{
621+ Foreground: app.cfg.Colors.Status,
622+ }),
623+ Readable: true,
624 }
625 case irc.ModeChangeEvent:
626 body := fmt.Sprintf("[%s]", ev.Mode)
627@@ -1417,7 +1436,9 @@ func (app *App) formatEvent(ev irc.Event) ui.Line {
628 At: ev.Time,
629 Head: "--",
630 HeadColor: app.cfg.Colors.Status,
631- Body: ui.Styled(body, tcell.StyleDefault.Foreground(app.cfg.Colors.Status)),
632+ Body: ui.Styled(body, vaxis.Style{
633+ Foreground: app.cfg.Colors.Status,
634+ }),
635 Mergeable: mergeable,
636 Data: []irc.Event{ev},
637 Readable: true,
638@@ -1479,7 +1500,7 @@ func (app *App) formatMessage(s *irc.Session, ev irc.MessageEvent) (buffer strin
639 }
640
641 head := ev.User
642- headColor := tcell.ColorWhite
643+ headColor := vaxis.IndexColor(15)
644 if isAction || isNotice {
645 head = "*"
646 } else {
647@@ -1489,16 +1510,20 @@ func (app *App) formatMessage(s *irc.Session, ev irc.MessageEvent) (buffer strin
648 var body ui.StyledStringBuilder
649 if isNotice {
650 color := ui.IdentColor(app.cfg.Colors.Nicks, ev.User, isFromSelf)
651- body.SetStyle(tcell.StyleDefault.Foreground(color))
652+ body.SetStyle(vaxis.Style{
653+ Foreground: color,
654+ })
655 body.WriteString(ev.User)
656- body.SetStyle(tcell.StyleDefault)
657+ body.SetStyle(vaxis.Style{})
658 body.WriteString(": ")
659 body.WriteStyledString(ui.IRCString(content))
660 } else if isAction {
661 color := ui.IdentColor(app.cfg.Colors.Nicks, ev.User, isFromSelf)
662- body.SetStyle(tcell.StyleDefault.Foreground(color))
663+ body.SetStyle(vaxis.Style{
664+ Foreground: color,
665+ })
666 body.WriteString(ev.User)
667- body.SetStyle(tcell.StyleDefault)
668+ body.SetStyle(vaxis.Style{})
669 body.WriteString(" ")
670 body.WriteStyledString(ui.IRCString(content))
671 } else {
672@@ -1625,16 +1650,14 @@ func (app *App) updatePrompt() {
673 command := isCommand(app.win.InputContent())
674 var prompt ui.StyledString
675 if buffer == "" || command {
676- prompt = ui.Styled(">",
677- tcell.
678- StyleDefault.
679- Foreground(app.cfg.Colors.Prompt),
680+ prompt = ui.Styled(">", vaxis.Style{
681+ Foreground: app.cfg.Colors.Prompt,
682+ },
683 )
684 } else if s == nil {
685- prompt = ui.Styled("<offline>",
686- tcell.
687- StyleDefault.
688- Foreground(tcell.ColorRed),
689+ prompt = ui.Styled("<offline>", vaxis.Style{
690+ Foreground: ui.ColorRed,
691+ },
692 )
693 } else {
694 prompt = ui.IdentString(app.cfg.Colors.Nicks, s.Nick(), true)
695@@ -1659,7 +1682,28 @@ func (app *App) printTopic(netID, buffer string) (ok bool) {
696 At: time.Now(),
697 Head: "--",
698 HeadColor: app.cfg.Colors.Status,
699- Body: ui.Styled(body, tcell.StyleDefault.Foreground(app.cfg.Colors.Status)),
700+ Body: ui.Styled(body, vaxis.Style{
701+ Foreground: app.cfg.Colors.Status,
702+ }),
703 })
704 return true
705 }
706+
707+func keyMatches(k vaxis.Key, r rune, mods vaxis.ModifierMask) bool {
708+ m := k.Modifiers
709+ m &^= vaxis.ModCapsLock
710+ m &^= vaxis.ModNumLock
711+ if k.Keycode == r && mods == m {
712+ // ctrl+a and user pressed ctrl+a
713+ // ctrl+. and user pressed ctrl+. on a US keyboard
714+ return true
715+ }
716+ if m&vaxis.ModShift != 0 {
717+ m &^= vaxis.ModShift
718+ if k.ShiftedCode == r && mods == m {
719+ // ctrl+. and user pressed ctrl+shift+; on a French keyboard
720+ return true
721+ }
722+ }
723+ return false
724+}
+0,
-4
1@@ -12,14 +12,10 @@ import (
2 "syscall"
3 "time"
4
5- "github.com/gdamore/tcell/v2"
6-
7 "git.sr.ht/~delthas/senpai"
8 )
9
10 func main() {
11- tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
12-
13 var configPath string
14 var nickname string
15 var debug bool
+14,
-6
1@@ -9,8 +9,8 @@ import (
2 "strings"
3 "time"
4
5+ "git.sr.ht/~rockorager/vaxis"
6 "github.com/delthas/go-libnp"
7- "github.com/gdamore/tcell/v2"
8 "golang.org/x/net/context"
9
10 "git.sr.ht/~delthas/senpai/irc"
11@@ -369,9 +369,11 @@ func commandDoHelp(app *App, args []string) (err error) {
12 addLineCommand := func(sb *ui.StyledStringBuilder, name string, cmd *command) {
13 sb.Reset()
14 sb.Grow(len(name) + 1 + len(cmd.Usage))
15- sb.SetStyle(tcell.StyleDefault.Bold(true))
16+ sb.SetStyle(vaxis.Style{
17+ Attribute: vaxis.AttrBold,
18+ })
19 sb.WriteString(name)
20- sb.SetStyle(tcell.StyleDefault)
21+ sb.SetStyle(vaxis.Style{})
22 sb.WriteByte(' ')
23 sb.WriteString(cmd.Usage)
24 app.win.AddLine(netID, buffer, ui.Line{
25@@ -498,13 +500,19 @@ func commandDoNames(app *App, args []string) (err error) {
26 return fmt.Errorf("this is not a channel")
27 }
28 var sb ui.StyledStringBuilder
29- sb.SetStyle(tcell.StyleDefault.Foreground(app.cfg.Colors.Status))
30+ sb.SetStyle(vaxis.Style{
31+ Foreground: app.cfg.Colors.Status,
32+ })
33 sb.WriteString("Names: ")
34 for _, name := range s.Names(buffer) {
35 if name.PowerLevel != "" {
36- sb.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGreen))
37+ sb.SetStyle(vaxis.Style{
38+ Foreground: vaxis.IndexColor(2),
39+ })
40 sb.WriteString(name.PowerLevel)
41- sb.SetStyle(tcell.StyleDefault.Foreground(app.cfg.Colors.Status))
42+ sb.SetStyle(vaxis.Style{
43+ Foreground: app.cfg.Colors.Status,
44+ })
45 }
46 sb.WriteString(name.Name.Name)
47 sb.WriteByte(' ')
+28,
-28
1@@ -9,57 +9,57 @@ import (
2 "strconv"
3 "strings"
4
5- "git.sr.ht/~delthas/senpai/ui"
6+ "git.sr.ht/~rockorager/vaxis"
7
8- "github.com/gdamore/tcell/v2"
9+ "git.sr.ht/~delthas/senpai/ui"
10
11 "git.sr.ht/~emersion/go-scfg"
12 )
13
14-func parseColor(s string, c *tcell.Color) error {
15+func parseColor(s string, c *vaxis.Color) error {
16 if strings.HasPrefix(s, "#") {
17 hex, err := strconv.ParseInt(s[1:], 16, 32)
18 if err != nil {
19 return err
20 }
21
22- *c = tcell.NewHexColor(int32(hex))
23+ *c = vaxis.HexColor(uint32(hex))
24 return nil
25 }
26 ok := true
27 switch s {
28 case "black":
29- *c = tcell.ColorBlack
30+ *c = vaxis.IndexColor(0)
31 case "maroon":
32- *c = tcell.ColorBrown
33+ *c = vaxis.IndexColor(1)
34 case "green":
35- *c = tcell.ColorGreen
36+ *c = vaxis.IndexColor(2)
37 case "olive":
38- *c = tcell.ColorOlive
39+ *c = vaxis.IndexColor(3)
40 case "navy":
41- *c = tcell.ColorNavy
42+ *c = vaxis.IndexColor(4)
43 case "purple":
44- *c = tcell.ColorPurple
45+ *c = vaxis.IndexColor(5)
46 case "teal":
47- *c = tcell.ColorTeal
48+ *c = vaxis.IndexColor(6)
49 case "silver":
50- *c = tcell.ColorSilver
51+ *c = vaxis.IndexColor(7)
52 case "gray", "grey":
53- *c = tcell.ColorGray
54+ *c = vaxis.IndexColor(8)
55 case "red":
56- *c = tcell.ColorRed
57+ *c = vaxis.IndexColor(9)
58 case "lime":
59- *c = tcell.ColorLime
60+ *c = vaxis.IndexColor(10)
61 case "yellow":
62- *c = tcell.ColorYellow
63+ *c = vaxis.IndexColor(11)
64 case "blue":
65- *c = tcell.ColorBlue
66+ *c = vaxis.IndexColor(12)
67 case "fuschia":
68- *c = tcell.ColorFuchsia
69+ *c = vaxis.IndexColor(13)
70 case "aqua":
71- *c = tcell.ColorAqua
72+ *c = vaxis.IndexColor(14)
73 case "white":
74- *c = tcell.ColorWhite
75+ *c = vaxis.IndexColor(15)
76 default:
77 ok = false
78 }
79@@ -73,7 +73,7 @@ func parseColor(s string, c *tcell.Color) error {
80 }
81
82 if code == -1 {
83- *c = tcell.ColorDefault
84+ *c = vaxis.Color(0)
85 return nil
86 }
87
88@@ -81,7 +81,7 @@ func parseColor(s string, c *tcell.Color) error {
89 return fmt.Errorf("color code must be between 0-255. If you meant to use true colors, use #aabbcc notation")
90 }
91
92- *c = tcell.PaletteColor(code)
93+ *c = vaxis.IndexColor(uint8(code))
94
95 return nil
96 }
97@@ -149,13 +149,13 @@ func Defaults() Config {
98 TextMaxWidth: 0,
99 StatusEnabled: true,
100 Colors: ui.ConfigColors{
101- Status: tcell.ColorGray,
102- Prompt: tcell.ColorDefault,
103- Unread: tcell.ColorDefault,
104+ Status: ui.ColorGray,
105+ Prompt: vaxis.Color(0),
106+ Unread: vaxis.Color(0),
107 Nicks: ui.ColorScheme{
108 Type: ui.ColorSchemeBase,
109- Others: tcell.ColorDefault,
110- Self: tcell.ColorRed,
111+ Others: vaxis.Color(0),
112+ Self: vaxis.Color(9),
113 },
114 },
115 Debug: false,
116@@ -410,7 +410,7 @@ func unmarshal(filename string, cfg *Config) (err error) {
117 }
118 }
119
120- var color tcell.Color
121+ var color vaxis.Color
122 if err = parseColor(colorStr, &color); err != nil {
123 return err
124 }
M
go.mod
+10,
-15
1@@ -3,27 +3,22 @@ module git.sr.ht/~delthas/senpai
2 go 1.18
3
4 require (
5- git.sr.ht/~emersion/go-scfg v0.0.0-20231004133111-9dce55c8d63b
6- git.sr.ht/~rockorager/vaxis v0.8.5
7+ git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082
8+ git.sr.ht/~rockorager/vaxis v0.9.2
9 github.com/delthas/go-libnp v0.0.0-20221222161248-0e45ece1f878
10- github.com/delthas/go-localeinfo v0.0.0-20221116001557-686a1e185118
11- github.com/gdamore/tcell/v2 v2.6.1-0.20230327043120-47ec3a77754f
12+ github.com/delthas/go-localeinfo v0.0.0-20240607105203-b2e834fc307d
13 github.com/godbus/dbus/v5 v5.1.0
14- github.com/mattn/go-runewidth v0.0.15
15- golang.org/x/net v0.18.0
16- golang.org/x/time v0.4.0
17+ github.com/rivo/uniseg v0.4.7
18+ golang.org/x/net v0.26.0
19+ golang.org/x/time v0.5.0
20 mvdan.cc/xurls/v2 v2.5.0
21 )
22
23 require (
24- github.com/containerd/console v1.0.3 // indirect
25+ github.com/containerd/console v1.0.4 // indirect
26+ github.com/mattn/go-runewidth v0.0.15 // indirect
27 github.com/mattn/go-sixel v0.0.5 // indirect
28- github.com/rivo/uniseg v0.4.4 // indirect
29 github.com/soniakeys/quant v1.0.0 // indirect
30- golang.org/x/image v0.13.0 // indirect
31- golang.org/x/sys v0.14.0 // indirect
32+ golang.org/x/image v0.18.0 // indirect
33+ golang.org/x/sys v0.21.0 // indirect
34 )
35-
36-replace github.com/gdamore/tcell/v2 => git.sr.ht/~delthas/vaxis-tcell v0.4.8-0.20240531132546-8a798f9059aa
37-
38-replace git.sr.ht/~rockorager/vaxis => git.sr.ht/~delthas/vaxis v0.8.6-0.20240617102656-146503a7e4f2
M
go.sum
+19,
-21
1@@ -1,17 +1,15 @@
2-git.sr.ht/~delthas/vaxis v0.8.6-0.20240617102656-146503a7e4f2 h1:/uP/UbVBm53m5NN0elWEM0XIx+JA6XhqDAex4qiZuTc=
3-git.sr.ht/~delthas/vaxis v0.8.6-0.20240617102656-146503a7e4f2/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
4-git.sr.ht/~delthas/vaxis-tcell v0.4.8-0.20240531132546-8a798f9059aa h1:BJ+FlhBKLzXI3DXqGPBIh8J6IOuu+VriDacOcYfV0p8=
5-git.sr.ht/~delthas/vaxis-tcell v0.4.8-0.20240531132546-8a798f9059aa/go.mod h1:H3Xxz36WTDXi7zJPOhHkAHVbrwreMn/4awEGxOpEHfU=
6-git.sr.ht/~emersion/go-scfg v0.0.0-20231004133111-9dce55c8d63b h1:Lf4oYBOJVmbYzrfqWfXUvSpXQPNMgnbN0efn5A7bH3M=
7-git.sr.ht/~emersion/go-scfg v0.0.0-20231004133111-9dce55c8d63b/go.mod h1:ybgvEJTIx5XbaspSviB3KNa6OdPmAZqDoSud7z8fFlw=
8-github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
9-github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
10+git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082 h1:9Udx5fm4vRtmgDIBjy2ef5QioHbzpw5oHabbhpAUyEw=
11+git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082/go.mod h1:ybgvEJTIx5XbaspSviB3KNa6OdPmAZqDoSud7z8fFlw=
12+git.sr.ht/~rockorager/vaxis v0.9.2 h1:OKPaCG3S4b6jWnwUPC0zX+Zw16hhLcSgC2mLw/nwsV0=
13+git.sr.ht/~rockorager/vaxis v0.9.2/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
14+github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
15+github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
16 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
17 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
18 github.com/delthas/go-libnp v0.0.0-20221222161248-0e45ece1f878 h1:v8W8eW7eb2bHFXBA80UKcoe0TvEu46NlTHSDRvgAbMU=
19 github.com/delthas/go-libnp v0.0.0-20221222161248-0e45ece1f878/go.mod h1:aGVXnhWpDlt5U4SphG97o1gszctZKvBTXy320E8Buw4=
20-github.com/delthas/go-localeinfo v0.0.0-20221116001557-686a1e185118 h1:Xzf9ra1QRJXD62gwudjI2iBq7x9CusvHd83Dg2OnUmE=
21-github.com/delthas/go-localeinfo v0.0.0-20221116001557-686a1e185118/go.mod h1:sG54BxlyQgIskYURLrg7mvhoGBe0Qq12DNtYRALwNa4=
22+github.com/delthas/go-localeinfo v0.0.0-20240607105203-b2e834fc307d h1:rH7dc+QFN4+Y8q5MxWUvt5PXsVpm1UKCEyuwf9BNO1s=
23+github.com/delthas/go-localeinfo v0.0.0-20240607105203-b2e834fc307d/go.mod h1:sG54BxlyQgIskYURLrg7mvhoGBe0Qq12DNtYRALwNa4=
24 github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
25 github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
26 github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
27@@ -20,20 +18,20 @@ github.com/mattn/go-sixel v0.0.5 h1:55w2FR5ncuhKhXrM5ly1eiqMQfZsnAHIpYNGZX03Cv8=
28 github.com/mattn/go-sixel v0.0.5/go.mod h1:h2Sss+DiUEHy0pUqcIB6PFXo5Cy8sTQEFr3a9/5ZLNw=
29 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
30 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
31-github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
32-github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
33+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
34+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
35 github.com/soniakeys/quant v1.0.0 h1:N1um9ktjbkZVcywBVAAYpZYSHxEfJGzshHCxx/DaI0Y=
36 github.com/soniakeys/quant v1.0.0/go.mod h1:HI1k023QuVbD4H8i9YdfZP2munIHU4QpjsImz6Y6zds=
37 github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
38-golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
39-golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
40-golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
41-golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
42-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
43-golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
44-golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
45-golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY=
46-golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
47+golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
48+golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
49+golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
50+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
51+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
52+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
53+golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
54+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
55+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
56 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
57 mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
58 mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
+126,
-93
1@@ -6,7 +6,7 @@ import (
2 "strings"
3 "time"
4
5- "github.com/gdamore/tcell/v2"
6+ "git.sr.ht/~rockorager/vaxis"
7 )
8
9 const Overlay = "/overlay"
10@@ -16,7 +16,8 @@ func IsSplitRune(r rune) bool {
11 }
12
13 type point struct {
14- X, I int
15+ X int // in cells
16+ I int // in bytes
17 Split bool
18 }
19
20@@ -40,7 +41,7 @@ type Line struct {
21 At time.Time
22 Head string
23 Body StyledString
24- HeadColor tcell.Color
25+ HeadColor vaxis.Color
26 Notify NotifyType
27 Highlight bool
28 Readable bool
29@@ -56,7 +57,7 @@ func (l *Line) IsZero() bool {
30 return l.Body.string == ""
31 }
32
33-func (l *Line) computeSplitPoints() {
34+func (l *Line) computeSplitPoints(vx *Vaxis) {
35 if l.splitPoints == nil {
36 l.splitPoints = []point{}
37 }
38@@ -77,7 +78,7 @@ func (l *Line) computeSplitPoints() {
39 }
40
41 lastWasSplit = curIsSplit
42- width += runeWidth(r)
43+ width += runeWidth(vx, r)
44 }
45
46 if !lastWasSplit {
47@@ -89,7 +90,8 @@ func (l *Line) computeSplitPoints() {
48 }
49 }
50
51-func (l *Line) NewLines(width int) []int {
52+// NewLines returns the offsets, in bytes, where the line should be split.
53+func (l *Line) NewLines(vx *Vaxis, width int) []int {
54 // Beware! This function was made by your local Test Driven Developper™ who
55 // doesn't understand one bit of this function and how it works (though it
56 // might not work that well if you're here...). The code below is thus very
57@@ -149,20 +151,22 @@ func (l *Line) NewLines(width int) []int {
58 // terminal. In this case, no newline is placed before (like in the
59 // 2nd if-else branch). The for loop is used to place newlines in
60 // the word.
61- // TODO handle multi-codepoint graphemes?? :(
62- wordWidth := 0
63- h := 1
64- for j, r := range l.Body.string[sp1.I:sp2.I] {
65- wordWidth += runeWidth(r)
66- if h*width < x+wordWidth {
67+ s := l.Body.string[sp1.I:sp2.I]
68+ j := 0
69+ for s != "" {
70+ c, wordWidth := firstCluster(vx, []rune(s))
71+ if width < x+wordWidth {
72+ x = 0
73 l.newLines = append(l.newLines, sp1.I+j)
74- h++
75 }
76+ x += wordWidth
77+ j += len(c)
78+ s = s[len(c):]
79 }
80- x = (x + wordWidth) % width
81- if x == 0 {
82+ if x == width {
83 // The placement of the word is such that it ends right at the
84 // end of the row.
85+ x = 0
86 l.newLines = append(l.newLines, sp2.I)
87 }
88 } else {
89@@ -212,12 +216,12 @@ type buffer struct {
90 lines []Line
91 topic string
92
93- scrollAmt int
94+ scrollAmt int // offset in lines from the bottom
95 isAtTop bool
96 }
97
98 type BufferList struct {
99- colors ConfigColors
100+ ui *UI
101
102 list []buffer
103 overlay *buffer
104@@ -229,18 +233,15 @@ type BufferList struct {
105 textWidth int
106
107 showBufferNumbers bool
108-
109- doMergeLine func(former *Line, addition Line)
110 }
111
112 // NewBufferList returns a new BufferList.
113 // Call Resize() once before using it.
114-func NewBufferList(colors ConfigColors, mergeLine func(*Line, Line)) BufferList {
115+func NewBufferList(ui *UI) BufferList {
116 return BufferList{
117- colors: colors,
118- list: []buffer{},
119- clicked: -1,
120- doMergeLine: mergeLine,
121+ ui: ui,
122+ list: []buffer{},
123+ clicked: -1,
124 }
125 }
126
127@@ -276,7 +277,7 @@ func (bs *BufferList) To(i int) bool {
128 if len(bs.list) <= bs.current {
129 bs.current = len(bs.list) - 1
130 }
131- bs.clearRead(i)
132+ bs.clearRead(bs.current)
133 b := bs.list[bs.current]
134 b.unreadRuler = b.read
135 if len(b.lines) > 0 {
136@@ -423,12 +424,12 @@ func (bs *BufferList) RemoveNetwork(netID string) {
137 }
138
139 func (bs *BufferList) mergeLine(former *Line, addition Line) (keepLine bool) {
140- bs.doMergeLine(former, addition)
141+ bs.ui.config.MergeLine(former, addition)
142 if former.Body.string == "" {
143 return false
144 }
145 former.width = 0
146- former.computeSplitPoints()
147+ former.computeSplitPoints(bs.ui.vx)
148 return true
149 }
150
151@@ -453,10 +454,10 @@ func (bs *BufferList) AddLine(netID, title string, line Line) {
152 }
153 // TODO change b.scrollAmt if it's not 0 and bs.current is idx.
154 } else {
155- line.computeSplitPoints()
156+ line.computeSplitPoints(bs.ui.vx)
157 b.lines = append(b.lines, line)
158 if b == current && 0 < b.scrollAmt {
159- b.scrollAmt += len(line.NewLines(bs.textWidth)) + 1
160+ b.scrollAmt += len(line.NewLines(bs.ui.vx, bs.textWidth)) + 1
161 }
162 }
163
164@@ -495,7 +496,7 @@ func (bs *BufferList) AddLines(netID, title string, before, after []Line) {
165 if b.openedOnce {
166 line.Body = line.Body.ParseURLs()
167 }
168- line.computeSplitPoints()
169+ line.computeSplitPoints(bs.ui.vx)
170 }
171 lines = append(lines, line)
172 }
173@@ -577,7 +578,7 @@ func (bs *BufferList) UpdateRead() (netID, title string, timestamp time.Time) {
174 if y >= b.scrollAmt && line.Readable {
175 break
176 }
177- y += len(line.NewLines(bs.textWidth)) + 1
178+ y += len(line.NewLines(bs.ui.vx, bs.textWidth)) + 1
179 }
180 if line != nil && line.At.After(b.read) {
181 b.read = line.At
182@@ -626,7 +627,7 @@ func (bs *BufferList) ScrollUpHighlight() bool {
183 b.scrollAmt = y - bs.tlHeight + 1
184 return true
185 }
186- y += len(line.NewLines(bs.textWidth)) + 1
187+ y += len(line.NewLines(bs.ui.vx, bs.textWidth)) + 1
188 }
189 return false
190 }
191@@ -640,7 +641,7 @@ func (bs *BufferList) ScrollDownHighlight() bool {
192 if line.Highlight {
193 yLastHighlight = y
194 }
195- y += len(line.NewLines(bs.textWidth)) + 1
196+ y += len(line.NewLines(bs.ui.vx, bs.textWidth)) + 1
197 }
198 b.scrollAmt = yLastHighlight
199 return b.scrollAmt != 0
200@@ -671,7 +672,7 @@ func (bs *BufferList) cur() *buffer {
201 return &bs.list[bs.current]
202 }
203
204-func (bs *BufferList) DrawVerticalBufferList(screen tcell.Screen, x0, y0, width, height int, offset *int) {
205+func (bs *BufferList) DrawVerticalBufferList(vx *Vaxis, x0, y0, width, height int, offset *int) {
206 if y0+len(bs.list)-*offset < height {
207 *offset = y0 + len(bs.list) - height
208 if *offset < 0 {
209@@ -680,25 +681,27 @@ func (bs *BufferList) DrawVerticalBufferList(screen tcell.Screen, x0, y0, width,
210 }
211
212 width--
213- drawVerticalLine(screen, x0+width, y0, height)
214- clearArea(screen, x0, y0, width, height)
215+ drawVerticalLine(vx, x0+width, y0, height)
216+ clearArea(vx, x0, y0, width, height)
217
218 indexPadding := 1 + int(math.Ceil(math.Log10(float64(len(bs.list)))))
219 for i, b := range bs.list[*offset:] {
220 bi := *offset + i
221 x := x0
222 y := y0 + i
223- st := tcell.StyleDefault
224+ var st vaxis.Style
225 if b.unread {
226- st = st.Bold(true).Foreground(bs.colors.Unread)
227+ st.Attribute |= vaxis.AttrBold
228+ st.Foreground = bs.ui.config.Colors.Unread
229 }
230 if bi == bs.current || bi == bs.clicked {
231- st = st.Reverse(true)
232+ st.Attribute |= vaxis.AttrReverse
233 }
234 if bs.showBufferNumbers {
235- indexSt := st.Foreground(tcell.ColorGray)
236+ indexSt := st
237+ indexSt.Foreground = ColorGray
238 indexText := fmt.Sprintf("%d:", bi+1)
239- printString(screen, &x, y, Styled(indexText, indexSt))
240+ printString(vx, &x, y, Styled(indexText, indexSt))
241 x = x0 + indexPadding
242 }
243
244@@ -707,28 +710,36 @@ func (bs *BufferList) DrawVerticalBufferList(screen tcell.Screen, x0, y0, width,
245 title = b.netName
246 } else {
247 if bi == bs.current || bi == bs.clicked {
248- screen.SetContent(x, y, ' ', nil, tcell.StyleDefault.Reverse(true))
249- screen.SetContent(x+1, y, ' ', nil, tcell.StyleDefault.Reverse(true))
250+ st := vaxis.Style{
251+ Attribute: vaxis.AttrReverse,
252+ }
253+ setCell(vx, x, y, ' ', st)
254+ setCell(vx, x+1, y, ' ', st)
255 }
256 x += 2
257 title = b.title
258 }
259- title = truncate(title, width-(x-x0), "\u2026")
260- printString(screen, &x, y, Styled(title, st))
261+ title = truncate(vx, title, width-(x-x0), "\u2026")
262+ printString(vx, &x, y, Styled(title, st))
263
264 if bi == bs.current || bi == bs.clicked {
265- st := tcell.StyleDefault.Reverse(true)
266+ st := vaxis.Style{
267+ Attribute: vaxis.AttrReverse,
268+ }
269 for ; x < x0+width; x++ {
270- screen.SetContent(x, y, ' ', nil, st)
271+ setCell(vx, x, y, ' ', st)
272 }
273- screen.SetContent(x, y, 0x2590, nil, st)
274+ setCell(vx, x, y, ' ', st)
275+ setCell(vx, x, y, '▐', st)
276 }
277
278 if b.highlights != 0 {
279- highlightSt := st.Foreground(tcell.ColorRed).Reverse(true)
280+ highlightSt := st
281+ highlightSt.Foreground = ColorRed
282+ highlightSt.Attribute |= vaxis.AttrReverse
283 highlightText := fmt.Sprintf(" %d ", b.highlights)
284 x = x0 + width - len(highlightText)
285- printString(screen, &x, y, Styled(highlightText, highlightSt))
286+ printString(vx, &x, y, Styled(highlightText, highlightSt))
287 }
288 }
289 }
290@@ -741,7 +752,7 @@ func (bs *BufferList) HorizontalBufferOffset(x int, offset int) int {
291 return -1
292 }
293 }
294- x -= bufferWidth(&b)
295+ x -= bs.bufferWidth(&b)
296 if x < 0 {
297 return offset + i
298 }
299@@ -761,7 +772,7 @@ func (bs *BufferList) GetLeftMost(screenWidth int) int {
300 if leftMost < bs.current {
301 width++
302 }
303- width += bufferWidth(&bs.list[leftMost])
304+ width += bs.bufferWidth(&bs.list[leftMost])
305 if width > screenWidth {
306 return leftMost + 1 // Went offscreen, need to go one step back
307 }
308@@ -770,12 +781,12 @@ func (bs *BufferList) GetLeftMost(screenWidth int) int {
309 return 0
310 }
311
312-func bufferWidth(b *buffer) int {
313+func (bs *BufferList) bufferWidth(b *buffer) int {
314 width := 0
315 if b.title == "" {
316- width += stringWidth(b.netName)
317+ width += stringWidth(bs.ui.vx, b.netName)
318 } else {
319- width += stringWidth(b.title)
320+ width += stringWidth(bs.ui.vx, b.title)
321 }
322 if 0 < b.highlights {
323 width += 2 + len(fmt.Sprintf("%d", b.highlights))
324@@ -783,12 +794,12 @@ func bufferWidth(b *buffer) int {
325 return width
326 }
327
328-func (bs *BufferList) DrawHorizontalBufferList(screen tcell.Screen, x0, y0, width int, offset *int) {
329+func (bs *BufferList) DrawHorizontalBufferList(vx *Vaxis, x0, y0, width int, offset *int) {
330 x := width
331 for i := len(bs.list) - 1; i >= 0; i-- {
332 b := &bs.list[i]
333 x--
334- x -= bufferWidth(b)
335+ x -= bs.bufferWidth(b)
336 if x <= 10 {
337 break
338 }
339@@ -803,45 +814,47 @@ func (bs *BufferList) DrawHorizontalBufferList(screen tcell.Screen, x0, y0, widt
340 if width <= x-x0 {
341 break
342 }
343- st := tcell.StyleDefault
344+ var st vaxis.Style
345 if b.unread {
346- st = st.Bold(true).Foreground(bs.colors.Unread)
347+ st.Attribute |= vaxis.AttrBold
348+ st.Foreground = bs.ui.config.Colors.Unread
349 } else if i == bs.current {
350- st = st.Underline(true)
351+ st.UnderlineStyle = vaxis.UnderlineSingle
352 }
353 if i == bs.clicked {
354- st = st.Reverse(true)
355+ st.Attribute |= vaxis.AttrReverse
356 }
357
358 var title string
359 if b.title == "" {
360- st = st.Dim(true)
361+ st.Attribute |= vaxis.AttrDim
362 title = b.netName
363 } else {
364 title = b.title
365 }
366- title = truncate(title, width-x, "\u2026")
367- printString(screen, &x, y0, Styled(title, st))
368+ title = truncate(vx, title, width-x, "\u2026")
369+ printString(vx, &x, y0, Styled(title, st))
370
371 if 0 < b.highlights {
372- st = st.Foreground(tcell.ColorRed).Reverse(true)
373- screen.SetContent(x, y0, ' ', nil, st)
374+ st.Foreground = ColorRed
375+ st.Attribute |= vaxis.AttrReverse
376+ setCell(vx, x, y0, ' ', st)
377 x++
378- printNumber(screen, &x, y0, st, b.highlights)
379- screen.SetContent(x, y0, ' ', nil, st)
380+ printNumber(vx, &x, y0, st, b.highlights)
381+ setCell(vx, x, y0, ' ', st)
382 x++
383 }
384- screen.SetContent(x, y0, ' ', nil, tcell.StyleDefault)
385+ setCell(vx, x, y0, ' ', vaxis.Style{})
386 x++
387 }
388 for x < width {
389- screen.SetContent(x, y0, ' ', nil, tcell.StyleDefault)
390+ setCell(vx, x, y0, ' ', vaxis.Style{})
391 x++
392 }
393 }
394
395-func (bs *BufferList) DrawTimeline(screen tcell.Screen, x0, y0, nickColWidth int) {
396- clearArea(screen, x0, y0, bs.tlInnerWidth+nickColWidth+9, bs.tlHeight+2)
397+func (bs *BufferList) DrawTimeline(vx *Vaxis, x0, y0, nickColWidth int) {
398+ clearArea(vx, x0, y0, bs.tlInnerWidth+nickColWidth+9, bs.tlHeight+2)
399
400 b := bs.cur()
401 if !b.openedOnce {
402@@ -852,9 +865,9 @@ func (bs *BufferList) DrawTimeline(screen tcell.Screen, x0, y0, nickColWidth int
403 }
404
405 xTopic := x0
406- printString(screen, &xTopic, y0, Styled(b.topic, tcell.StyleDefault))
407+ printString(vx, &xTopic, y0, Styled(b.topic, vaxis.Style{}))
408 y0++
409- drawHorizontalLine(screen, x0, y0, bs.tlInnerWidth+nickColWidth+9)
410+ drawHorizontalLine(vx, x0, y0, bs.tlInnerWidth+nickColWidth+9)
411 y0++
412
413 if bs.textWidth < bs.tlInnerWidth {
414@@ -871,15 +884,17 @@ func (bs *BufferList) DrawTimeline(screen tcell.Screen, x0, y0, nickColWidth int
415 x1 := x0 + 9 + nickColWidth
416
417 line := &b.lines[i]
418- nls := line.NewLines(bs.textWidth)
419+ nls := line.NewLines(bs.ui.vx, bs.textWidth)
420
421 if !rulerDrawn {
422 isRead := !line.At.After(b.unreadRuler)
423 if isRead && yi > y0 {
424 yi--
425- st := tcell.StyleDefault.Foreground(tcell.ColorGray)
426- printIdent(screen, x0+7, yi, nickColWidth, Styled("--", st))
427- drawHorizontalLine(screen, x0, yi, 9+nickColWidth+bs.tlInnerWidth)
428+ st := vaxis.Style{
429+ Foreground: ColorGray,
430+ }
431+ printIdent(vx, x0+7, yi, nickColWidth, Styled("--", st))
432+ drawHorizontalLine(vx, x0, yi, 9+nickColWidth+bs.tlInnerWidth)
433 rulerDrawn = true
434 }
435 }
436@@ -898,36 +913,45 @@ func (bs *BufferList) DrawTimeline(screen tcell.Screen, x0, y0, nickColWidth int
437 showDate = yb != ya || mb != ma || dd != da
438 }
439 if showDate {
440- st := tcell.StyleDefault.Bold(true)
441+ st := vaxis.Style{
442+ Attribute: vaxis.AttrBold,
443+ }
444 // as a special case, always draw the first visible message date, even if it is a continuation line
445 yd := yi
446 if yd < y0 {
447 yd = y0
448 }
449- printDate(screen, x0, yd, st, line.At.Local())
450+ printDate(vx, x0, yd, st, line.At.Local())
451 } else if b.lines[i-1].At.Truncate(time.Minute) != line.At.Truncate(time.Minute) && yi >= y0 {
452- st := tcell.StyleDefault.Foreground(tcell.ColorGray)
453- printTime(screen, x0, yi, st, line.At.Local())
454+ st := vaxis.Style{
455+ Foreground: ColorGray,
456+ }
457+ printTime(vx, x0, yi, st, line.At.Local())
458 }
459
460 if yi >= y0 {
461- identSt := tcell.StyleDefault.
462- Foreground(line.HeadColor).
463- Reverse(line.Highlight)
464- printIdent(screen, x0+7, yi, nickColWidth, Styled(line.Head, identSt))
465+ identSt := vaxis.Style{
466+ Foreground: line.HeadColor,
467+ }
468+ if line.Highlight {
469+ identSt.Attribute |= vaxis.AttrReverse
470+ }
471+ printIdent(vx, x0+7, yi, nickColWidth, Styled(line.Head, identSt))
472 }
473
474 x := x1
475 y := yi
476- style := tcell.StyleDefault
477+ var style vaxis.Style
478 nextStyles := line.Body.styles
479
480- for i, r := range line.Body.string {
481- if 0 < len(nextStyles) && nextStyles[0].Start == i {
482+ lbi := 0
483+ l := []rune(line.Body.string)
484+ for len(l) > 0 {
485+ if 0 < len(nextStyles) && nextStyles[0].Start == lbi {
486 style = nextStyles[0].Style
487 nextStyles = nextStyles[1:]
488 }
489- if 0 < len(nls) && i == nls[0] {
490+ if 0 < len(nls) && lbi == nls[0] {
491 x = x1
492 y++
493 nls = nls[1:]
494@@ -936,14 +960,23 @@ func (bs *BufferList) DrawTimeline(screen tcell.Screen, x0, y0, nickColWidth int
495 }
496 }
497
498- if y != yi && x == x1 && IsSplitRune(r) {
499+ if y != yi && x == x1 && IsSplitRune(l[0]) {
500+ lbi += len(string(l[0]))
501+ l = l[1:]
502 continue
503 }
504
505 if y >= y0 {
506- screen.SetContent(x, y, r, nil, style)
507+ dx, di := printCluster(vx, x, y, -1, l, style)
508+ x += dx
509+ lbi += len(string(l[:di]))
510+ l = l[di:]
511+ } else {
512+ c, cw := firstCluster(vx, l)
513+ x += cw
514+ lbi += len(c)
515+ l = l[len([]rune(c)):]
516 }
517- x += runeWidth(r)
518 }
519 }
520
+3,
-3
1@@ -7,7 +7,7 @@ import (
2
3 func assertSplitPoints(t *testing.T, body string, expected []point) {
4 l := Line{Body: PlainString(body)}
5- l.computeSplitPoints()
6+ l.computeSplitPoints(nil)
7
8 if len(l.splitPoints) != len(expected) {
9 t.Errorf("%q: expected %d split points got %d", body, len(expected), len(l.splitPoints))
10@@ -72,9 +72,9 @@ func showSplit(s string, nls []int) string {
11
12 func assertNewLines(t *testing.T, body string, width int, expected int) {
13 l := Line{Body: PlainString(body)}
14- l.computeSplitPoints()
15+ l.computeSplitPoints(nil)
16
17- actual := l.NewLines(width)
18+ actual := l.NewLines(nil, width)
19
20 if len(actual)+1 != expected {
21 s := showSplit(body, actual)
+54,
-49
1@@ -3,15 +3,18 @@ package ui
2 import (
3 "hash/fnv"
4
5- "github.com/gdamore/tcell/v2"
6+ "git.sr.ht/~rockorager/vaxis"
7 )
8
9+var ColorRed = vaxis.IndexColor(9)
10+var ColorGray = vaxis.IndexColor(8)
11+
12 type ColorSchemeType int
13
14 type ColorScheme struct {
15 Type ColorSchemeType
16- Others tcell.Color
17- Self tcell.Color
18+ Others vaxis.Color
19+ Self vaxis.Color
20 }
21
22 const (
23@@ -20,59 +23,59 @@ const (
24 ColorSchemeFixed
25 )
26
27-var colors = map[ColorSchemeType][]tcell.Color{
28+var colors = map[ColorSchemeType][]vaxis.Color{
29 // base 16 colors, excluding grayscale colors.
30 ColorSchemeBase: {
31- tcell.ColorMaroon,
32- tcell.ColorGreen,
33- tcell.ColorOlive,
34- tcell.ColorNavy,
35- tcell.ColorPurple,
36- tcell.ColorTeal,
37- tcell.ColorSilver,
38- tcell.ColorRed,
39- tcell.ColorLime,
40- tcell.ColorYellow,
41- tcell.ColorBlue,
42- tcell.ColorFuchsia,
43- tcell.ColorAqua,
44+ vaxis.IndexColor(1),
45+ vaxis.IndexColor(2),
46+ vaxis.IndexColor(3),
47+ vaxis.IndexColor(4),
48+ vaxis.IndexColor(5),
49+ vaxis.IndexColor(6),
50+ vaxis.IndexColor(7),
51+ vaxis.IndexColor(9),
52+ vaxis.IndexColor(10),
53+ vaxis.IndexColor(11),
54+ vaxis.IndexColor(12),
55+ vaxis.IndexColor(13),
56+ vaxis.IndexColor(14),
57 },
58 // all XTerm extended colors with HSL saturation=1, light=0.5
59 ColorSchemeExtended: {
60- tcell.Color196, // HSL hue: 0°
61- tcell.Color202, // HSL hue: 22°
62- tcell.Color208, // HSL hue: 32°
63- tcell.Color214, // HSL hue: 41°
64- tcell.Color220, // HSL hue: 51°
65- tcell.Color226, // HSL hue: 60°
66- tcell.Color190, // HSL hue: 69°
67- tcell.Color154, // HSL hue: 79°
68- tcell.Color118, // HSL hue: 88°
69- tcell.Color82, // HSL hue: 98°
70- tcell.Color46, // HSL hue: 120°
71- tcell.Color47, // HSL hue: 142°
72- tcell.Color48, // HSL hue: 152°
73- tcell.Color49, // HSL hue: 161°
74- tcell.Color50, // HSL hue: 171°
75- tcell.Color51, // HSL hue: 180°
76- tcell.Color45, // HSL hue: 189°
77- tcell.Color39, // HSL hue: 199°
78- tcell.Color33, // HSL hue: 208°
79- tcell.Color27, // HSL hue: 218°
80- tcell.Color21, // HSL hue: 240°
81- tcell.Color57, // HSL hue: 262°
82- tcell.Color93, // HSL hue: 272°
83- tcell.Color129, // HSL hue: 281°
84- tcell.Color165, // HSL hue: 291°
85- tcell.Color201, // HSL hue: 300°
86- tcell.Color200, // HSL hue: 309°
87- tcell.Color199, // HSL hue: 319°
88- tcell.Color198, // HSL hue: 328°
89- tcell.Color197, // HSL hue: 338°
90+ vaxis.IndexColor(196), // HSL hue: 0°
91+ vaxis.IndexColor(202), // HSL hue: 22°
92+ vaxis.IndexColor(208), // HSL hue: 32°
93+ vaxis.IndexColor(214), // HSL hue: 41°
94+ vaxis.IndexColor(220), // HSL hue: 51°
95+ vaxis.IndexColor(226), // HSL hue: 60°
96+ vaxis.IndexColor(190), // HSL hue: 69°
97+ vaxis.IndexColor(154), // HSL hue: 79°
98+ vaxis.IndexColor(118), // HSL hue: 88°
99+ vaxis.IndexColor(82), // HSL hue: 98°
100+ vaxis.IndexColor(46), // HSL hue: 120°
101+ vaxis.IndexColor(47), // HSL hue: 142°
102+ vaxis.IndexColor(48), // HSL hue: 152°
103+ vaxis.IndexColor(49), // HSL hue: 161°
104+ vaxis.IndexColor(50), // HSL hue: 171°
105+ vaxis.IndexColor(51), // HSL hue: 180°
106+ vaxis.IndexColor(45), // HSL hue: 189°
107+ vaxis.IndexColor(39), // HSL hue: 199°
108+ vaxis.IndexColor(33), // HSL hue: 208°
109+ vaxis.IndexColor(27), // HSL hue: 218°
110+ vaxis.IndexColor(21), // HSL hue: 240°
111+ vaxis.IndexColor(57), // HSL hue: 262°
112+ vaxis.IndexColor(93), // HSL hue: 272°
113+ vaxis.IndexColor(129), // HSL hue: 281°
114+ vaxis.IndexColor(165), // HSL hue: 291°
115+ vaxis.IndexColor(201), // HSL hue: 300°
116+ vaxis.IndexColor(200), // HSL hue: 309°
117+ vaxis.IndexColor(199), // HSL hue: 319°
118+ vaxis.IndexColor(198), // HSL hue: 328°
119+ vaxis.IndexColor(197), // HSL hue: 338°
120 },
121 }
122
123-func IdentColor(scheme ColorScheme, ident string, self bool) tcell.Color {
124+func IdentColor(scheme ColorScheme, ident string, self bool) vaxis.Color {
125 h := fnv.New32()
126 _, _ = h.Write([]byte(ident))
127 if scheme.Type == ColorSchemeFixed {
128@@ -88,6 +91,8 @@ func IdentColor(scheme ColorScheme, ident string, self bool) tcell.Color {
129
130 func IdentString(scheme ColorScheme, ident string, self bool) StyledString {
131 color := IdentColor(scheme, ident, self)
132- style := tcell.StyleDefault.Foreground(color)
133+ style := vaxis.Style{
134+ Foreground: color,
135+ }
136 return Styled(ident, style)
137 }
+175,
-40
1@@ -6,44 +6,181 @@ import (
2 "sync"
3 "time"
4
5+ "git.sr.ht/~rockorager/vaxis"
6 "github.com/delthas/go-localeinfo"
7-
8- "github.com/gdamore/tcell/v2"
9+ "github.com/rivo/uniseg"
10 )
11
12-func printString(screen tcell.Screen, x *int, y int, s StyledString) {
13- style := tcell.StyleDefault
14+var asciiStringCache []string
15+
16+func init() {
17+ asciiStringCache = make([]string, 0x80)
18+ for i := range asciiStringCache {
19+ asciiStringCache[i] = string(rune(i))
20+ }
21+}
22+
23+var runeWidthMap = make(map[rune]int)
24+
25+func runeWidth(vx *Vaxis, r rune) int {
26+ if vx == nil { // For tests only
27+ return 1
28+ }
29+ if r == '\n' {
30+ r = '↲'
31+ }
32+ if r <= 0x1F {
33+ return 0
34+ }
35+ if r <= 0x7F {
36+ return 1
37+ }
38+ if n, ok := runeWidthMap[r]; ok {
39+ return n
40+ }
41+ n := vx.RenderedWidth(string([]rune{r}))
42+ runeWidthMap[r] = n
43+ return n
44+}
45+
46+func stringWidth(vx *Vaxis, s string) int {
47+ if vx == nil { // For tests only
48+ return len(s)
49+ }
50+ if len(s) == 1 { // Single-character ASCII fast path
51+ if s[0] <= 0x1F {
52+ return 0
53+ }
54+ if s[0] <= 0x7F {
55+ return 1
56+ }
57+ }
58+ r := []rune(s)
59+ if len(r) == 1 { // Single-character fast path
60+ return runeWidth(vx, r[0])
61+ }
62+ return vx.RenderedWidth(s)
63+}
64+
65+func truncate(vx *Vaxis, s string, w int, tail string) string {
66+ if stringWidth(vx, s) <= w {
67+ return s
68+ }
69+ w -= stringWidth(vx, tail)
70+
71+ width := 0
72+ var sb strings.Builder
73+ for _, c := range vaxis.Characters(s) {
74+ chWidth := stringWidth(vx, c.Grapheme)
75+ if width+chWidth > w {
76+ break
77+ }
78+ width += chWidth
79+ sb.WriteString(c.Grapheme)
80+ }
81+ sb.WriteString(tail)
82+ return sb.String()
83+}
84+
85+var clusterWidthMap = make(map[string]int)
86+
87+// width in cells
88+func firstCluster(vx *Vaxis, r []rune) (c string, width int) {
89+ if len(r) == 0 { // Empty fast-path
90+ return "", 0
91+ }
92+ if r[0] == '\t' {
93+ return " ", 1
94+ }
95+ if r[0] <= 0x7F { // ASCII fast-path
96+ return asciiStringCache[int(r[0])], runeWidth(vx, r[0])
97+ }
98+ c, _, _, _ = uniseg.FirstGraphemeClusterInString(string(r), -1)
99+
100+ var cw int
101+ if n, ok := clusterWidthMap[c]; ok {
102+ cw = n
103+ } else {
104+ cw = stringWidth(vx, c)
105+ clusterWidthMap[c] = cw
106+ }
107+ return c, cw
108+}
109+
110+func setCell(vx *Vaxis, x int, y int, r rune, st vaxis.Style) {
111+ vx.window.SetCell(x, y, vaxis.Cell{
112+ Character: vaxis.Character{
113+ Grapheme: string([]rune{r}),
114+ },
115+ Style: st,
116+ })
117+}
118+
119+// limit = -1 means no limit
120+// di is an offset in runes
121+func printCluster(vx *Vaxis, x int, y int, limit int, r []rune, st vaxis.Style) (dx int, di int) {
122+ if limit >= 0 && x >= limit {
123+ return 0, 0
124+ }
125+ var c string
126+ var w int
127+ if len(r) > 0 && r[0] <= 0x7F { // ASCII fast-path
128+ c = asciiStringCache[int(r[0])]
129+ w = runeWidth(vx, r[0])
130+ di = 1
131+ } else {
132+ c, w = firstCluster(vx, r)
133+ di = len([]rune(c))
134+ }
135+ if limit >= 0 && w > limit-x {
136+ return 0, 0
137+ }
138+ vx.window.SetCell(x, y, vaxis.Cell{
139+ Character: vaxis.Character{
140+ Grapheme: c,
141+ },
142+ Style: st,
143+ })
144+ return w, di
145+}
146+
147+func printString(vx *Vaxis, x *int, y int, s StyledString) {
148+ var st vaxis.Style
149 nextStyles := s.styles
150- for i, r := range s.string {
151+
152+ i := 0
153+ sr := []rune(s.string)
154+ for len(sr) > 0 {
155 if 0 < len(nextStyles) && nextStyles[0].Start == i {
156- style = nextStyles[0].Style
157+ st = nextStyles[0].Style
158 nextStyles = nextStyles[1:]
159 }
160- screen.SetContent(*x, y, r, nil, style)
161- *x += runeWidth(r)
162+ dx, di := printCluster(vx, *x, y, -1, sr, st)
163+ *x += dx
164+ sr = sr[di:]
165 }
166 }
167
168-func printIdent(screen tcell.Screen, x, y, width int, s StyledString) {
169- s.string = truncate(s.string, width, "\u2026")
170- x += width - stringWidth(s.string)
171- st := tcell.StyleDefault
172+func printIdent(vx *Vaxis, x, y, width int, s StyledString) {
173+ s.string = truncate(vx, s.string, width, "\u2026")
174+ x += width - stringWidth(vx, s.string)
175+ var st vaxis.Style
176 if len(s.styles) != 0 && s.styles[0].Start == 0 {
177 st = s.styles[0].Style
178 }
179- screen.SetContent(x-1, y, ' ', nil, st)
180- printString(screen, &x, y, s)
181+ setCell(vx, x-1, y, ' ', st)
182+ printString(vx, &x, y, s)
183 if len(s.styles) != 0 {
184 // TODO check if it's not a style that is from the truncated
185 // part of s.
186 st = s.styles[len(s.styles)-1].Style
187 }
188- screen.SetContent(x, y, ' ', nil, st)
189+ setCell(vx, x, y, ' ', st)
190 }
191
192-func printNumber(screen tcell.Screen, x *int, y int, st tcell.Style, n int) {
193+func printNumber(vx *Vaxis, x *int, y int, st vaxis.Style, n int) {
194 s := Styled(fmt.Sprintf("%d", n), st)
195- printString(screen, x, y, s)
196+ printString(vx, x, y, s)
197 }
198
199 var dateConfig sync.Once
200@@ -84,7 +221,7 @@ func loadDateInfo() {
201 }
202 }
203
204-func printDate(screen tcell.Screen, x int, y int, st tcell.Style, t time.Time) {
205+func printDate(vx *Vaxis, x int, y int, st vaxis.Style, t time.Time) {
206 dateConfig.Do(loadDateInfo)
207 _, m, d := t.Date()
208 var left, right int
209@@ -97,42 +234,40 @@ func printDate(screen tcell.Screen, x int, y int, st tcell.Style, t time.Time) {
210 l1 := rune(left%10) + '0'
211 r0 := rune(right/10) + '0'
212 r1 := rune(right%10) + '0'
213- screen.SetContent(x+0, y, l0, nil, st)
214- screen.SetContent(x+1, y, l1, nil, st)
215- screen.SetContent(x+2, y, '/', nil, st)
216- screen.SetContent(x+3, y, r0, nil, st)
217- screen.SetContent(x+4, y, r1, nil, st)
218+
219+ setCell(vx, x+0, y, l0, st)
220+ setCell(vx, x+1, y, l1, st)
221+ setCell(vx, x+2, y, '/', st)
222+ setCell(vx, x+3, y, r0, st)
223+ setCell(vx, x+4, y, r1, st)
224 }
225
226-func printTime(screen tcell.Screen, x int, y int, st tcell.Style, t time.Time) {
227+func printTime(vx *Vaxis, x int, y int, st vaxis.Style, t time.Time) {
228 hr0 := rune(t.Hour()/10) + '0'
229 hr1 := rune(t.Hour()%10) + '0'
230 mn0 := rune(t.Minute()/10) + '0'
231 mn1 := rune(t.Minute()%10) + '0'
232- screen.SetContent(x+0, y, hr0, nil, st)
233- screen.SetContent(x+1, y, hr1, nil, st)
234- screen.SetContent(x+2, y, ':', nil, st)
235- screen.SetContent(x+3, y, mn0, nil, st)
236- screen.SetContent(x+4, y, mn1, nil, st)
237+ setCell(vx, x+0, y, hr0, st)
238+ setCell(vx, x+1, y, hr1, st)
239+ setCell(vx, x+2, y, ':', st)
240+ setCell(vx, x+3, y, mn0, st)
241+ setCell(vx, x+4, y, mn1, st)
242 }
243
244-func clearArea(screen tcell.Screen, x0, y0, width, height int) {
245- for x := x0; x < x0+width; x++ {
246- for y := y0; y < y0+height; y++ {
247- screen.SetContent(x, y, ' ', nil, tcell.StyleDefault)
248- }
249- }
250+func clearArea(vx *Vaxis, x0, y0, width, height int) {
251+ vx.window.New(x0, y0, width, height).Clear()
252 }
253
254-func drawHorizontalLine(screen tcell.Screen, x0, y, width int) {
255- st := tcell.StyleDefault.Foreground(tcell.ColorGray)
256+func drawHorizontalLine(vx *Vaxis, x0, y, width int) {
257 for x := x0; x < x0+width; x++ {
258- screen.SetContent(x, y, 0x2500, nil, st)
259+ setCell(vx, x, y, '─', vaxis.Style{
260+ Foreground: ColorGray,
261+ })
262 }
263 }
264
265-func drawVerticalLine(screen tcell.Screen, x, y0, height int) {
266+func drawVerticalLine(vx *Vaxis, x, y0, height int) {
267 for y := y0; y < y0+height; y++ {
268- screen.SetContent(x, y, 0x2502, nil, tcell.StyleDefault)
269+ setCell(vx, x, y, '│', vaxis.Style{})
270 }
271 }
+175,
-124
1@@ -3,7 +3,7 @@ package ui
2 import (
3 "strings"
4
5- "github.com/gdamore/tcell/v2"
6+ "git.sr.ht/~rockorager/vaxis"
7 )
8
9 type Completion struct {
10@@ -11,40 +11,56 @@ type Completion struct {
11 EndIdx int
12 Text []rune
13 Display []rune
14- CursorIdx int
15+ CursorIdx int // in runes
16+}
17+
18+type editorLine struct {
19+ runes []rune
20+ clusters []int
21+}
22+
23+func newEditorLine() editorLine {
24+ return editorLine{
25+ runes: []rune{},
26+ clusters: []int{0},
27+ }
28+}
29+
30+func (l *editorLine) copy() editorLine {
31+ return editorLine{
32+ runes: append([]rune{}, l.runes...),
33+ clusters: append([]int{}, l.clusters...),
34+ }
35 }
36
37 // Editor is the text field where the user writes messages and commands.
38 type Editor struct {
39- colors ConfigColors
40+ ui *UI
41
42 // text contains the written runes. An empty slice means no text is written.
43- text [][]rune
44+ text []editorLine
45
46 // history contains the original content of each previously sent message.
47 // It gets out of sync with text when history is modified à la readline,
48 // then overwrites text when a new message is added.
49- history [][]rune
50+ history []editorLine
51
52 lineIdx int
53
54- // textWidth[i] contains the width of string(text[:i]). Therefore
55+ // textWidth[i] contains the width of text.runes[:text.clusters[i]]. Therefore
56 // len(textWidth) is always strictly greater than 0 and textWidth[0] is
57 // always 0.
58 textWidth []int
59
60- // cursorIdx is the index in text of the placement of the cursor, or is
61- // equal to len(text) if the cursor is at the end.
62+ // cursorIdx is the index of clusters in text of the placement of the cursor.
63 cursorIdx int
64
65- // offsetIdx is the number of elements of text that are skipped when
66- // rendering.
67+ // offsetIdx is the number of clusters in text that are skipped when rendering.
68 offsetIdx int
69
70 // width is the width of the screen.
71 width int
72
73- autoComplete func(cursorIdx int, text []rune) []Completion
74 autoCache []Completion
75 autoCacheIdx int
76
77@@ -58,13 +74,12 @@ type Editor struct {
78
79 // NewEditor returns a new Editor.
80 // Call Resize() once before using it.
81-func NewEditor(colors ConfigColors, autoComplete func(cursorIdx int, text []rune) []Completion) Editor {
82+func NewEditor(ui *UI) Editor {
83 return Editor{
84- colors: colors,
85- text: [][]rune{{}},
86- history: [][]rune{},
87- textWidth: []int{0},
88- autoComplete: autoComplete,
89+ ui: ui,
90+ text: []editorLine{newEditorLine()},
91+ history: []editorLine{},
92+ textWidth: []int{0},
93 }
94 }
95
96@@ -80,18 +95,57 @@ func (e *Editor) Resize(width int) {
97
98 // Content result must not be modified.
99 func (e *Editor) Content() []rune {
100- return e.text[e.lineIdx]
101-}
102-
103-func (e *Editor) TextLen() int {
104- return len(e.text[e.lineIdx])
105+ return e.text[e.lineIdx].runes
106+}
107+
108+func (e *Editor) Empty() bool {
109+ return len(e.text[e.lineIdx].runes) == 0
110+}
111+
112+// recompute must be called when runes is changed, to update
113+// clusters, textWidth.
114+func (e *Editor) recompute() {
115+ c := make([]int, 0, len(e.text[e.lineIdx].runes)+1)
116+ w := make([]int, 0, len(e.text[e.lineIdx].runes)+1)
117+ nc := 0
118+ nw := 0
119+ for _, g := range vaxis.Characters(string(e.text[e.lineIdx].runes)) {
120+ c = append(c, nc)
121+ w = append(w, nw)
122+ nc += len([]rune(g.Grapheme))
123+ nw += stringWidth(e.ui.vx, g.Grapheme)
124+ }
125+ c = append(c, nc)
126+ w = append(w, nw)
127+ e.text[e.lineIdx].clusters = c
128+ e.textWidth = w
129+}
130+
131+// setCursor sets cursorIdx to the (grapheme cluster) offset
132+// corresponding to the passed rune offset runeIdx, rounding up
133+// to the next grapheme cluster as needed.
134+func (e *Editor) setCursor(runeIdx int) {
135+ for i, o := range e.text[e.lineIdx].clusters {
136+ if o >= runeIdx {
137+ e.cursorIdx = i
138+ break
139+ }
140+ }
141+ if e.width <= e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx] {
142+ e.offsetIdx += 16
143+ // If we went too far right, go back enough to show one cluster.
144+ max := len(e.text[e.lineIdx].clusters) - 2
145+ if max < e.offsetIdx {
146+ e.offsetIdx = max
147+ }
148+ }
149 }
150
151 func (e *Editor) PutRune(r rune) {
152 e.autoCache = nil
153 lowerRune := runeToLower(r)
154- if e.backsearch && e.cursorIdx < e.TextLen() {
155- lowerNext := runeToLower(e.text[e.lineIdx][e.cursorIdx])
156+ if e.backsearch && e.cursorIdx < len(e.text[e.lineIdx].clusters)-1 {
157+ lowerNext := runeToLower(e.text[e.lineIdx].runes[e.text[e.lineIdx].clusters[e.cursorIdx]])
158 if lowerRune == lowerNext {
159 e.right()
160 e.backsearchPattern = append(e.backsearchPattern, lowerRune)
161@@ -99,7 +153,6 @@ func (e *Editor) PutRune(r rune) {
162 }
163 }
164 e.putRune(r)
165- e.right()
166 if e.backsearch {
167 wasEmpty := len(e.backsearchPattern) == 0
168 e.backsearchPattern = append(e.backsearchPattern, lowerRune)
169@@ -111,30 +164,32 @@ func (e *Editor) PutRune(r rune) {
170 }
171 }
172
173+// putRune inserts a rune at the current cursor position,
174+// then updates moves the cursor position after that rune.
175+// (If inserting the rune merged a grapheme cluster, we
176+// move the cursor after that cluster.)
177 func (e *Editor) putRune(r rune) {
178- e.text[e.lineIdx] = append(e.text[e.lineIdx], ' ')
179- copy(e.text[e.lineIdx][e.cursorIdx+1:], e.text[e.lineIdx][e.cursorIdx:])
180- e.text[e.lineIdx][e.cursorIdx] = r
181- e.bumpOldestChange()
182+ ci := e.text[e.lineIdx].clusters[e.cursorIdx]
183+ e.text[e.lineIdx].runes = append(e.text[e.lineIdx].runes, ' ')
184+ copy(e.text[e.lineIdx].runes[ci+1:], e.text[e.lineIdx].runes[ci:])
185+ e.text[e.lineIdx].runes[ci] = r
186
187- rw := runeWidth(r)
188- tw := e.textWidth[len(e.textWidth)-1]
189- e.textWidth = append(e.textWidth, tw+rw)
190- for i := e.cursorIdx + 1; i < len(e.textWidth); i++ {
191- e.textWidth[i] = rw + e.textWidth[i-1]
192- }
193+ e.recompute()
194+ e.setCursor(ci + 1)
195+
196+ e.bumpOldestChange()
197 }
198
199-func (e *Editor) RemRune() (ok bool) {
200+func (e *Editor) RemCluster() (ok bool) {
201 ok = 0 < e.cursorIdx
202 if !ok {
203 return
204 }
205- e.remRuneAt(e.cursorIdx - 1)
206+ e.remClusterAt(e.cursorIdx - 1)
207 e.left()
208 e.autoCache = nil
209 if e.backsearch {
210- if e.TextLen() == 0 || len(e.backsearchPattern) == 0 {
211+ if e.Empty() || len(e.backsearchPattern) == 0 {
212 e.backsearchEnd()
213 } else {
214 e.backsearchPattern = e.backsearchPattern[:len(e.backsearchPattern)-1]
215@@ -144,29 +199,24 @@ func (e *Editor) RemRune() (ok bool) {
216 return
217 }
218
219-func (e *Editor) RemRuneForward() (ok bool) {
220- ok = e.cursorIdx < len(e.text[e.lineIdx])
221+func (e *Editor) RemClusterForward() (ok bool) {
222+ ok = e.cursorIdx < len(e.text[e.lineIdx].clusters)-1
223 if !ok {
224 return
225 }
226- e.remRuneAt(e.cursorIdx)
227+ e.remClusterAt(e.cursorIdx)
228 e.autoCache = nil
229 e.backsearchEnd()
230 return
231 }
232
233-func (e *Editor) remRuneAt(idx int) {
234- // TODO avoid looping twice
235- rw := e.textWidth[idx+1] - e.textWidth[idx]
236- for i := idx + 1; i < len(e.textWidth); i++ {
237- e.textWidth[i] -= rw
238- }
239- copy(e.textWidth[idx+1:], e.textWidth[idx+2:])
240- e.textWidth = e.textWidth[:len(e.textWidth)-1]
241-
242- copy(e.text[e.lineIdx][idx:], e.text[e.lineIdx][idx+1:])
243- e.text[e.lineIdx] = e.text[e.lineIdx][:len(e.text[e.lineIdx])-1]
244+func (e *Editor) remClusterAt(idx int) {
245+ rs := e.text[e.lineIdx].clusters[idx]
246+ re := e.text[e.lineIdx].clusters[idx+1]
247+ copy(e.text[e.lineIdx].runes[rs:], e.text[e.lineIdx].runes[re:])
248+ e.text[e.lineIdx].runes = e.text[e.lineIdx].runes[:len(e.text[e.lineIdx].runes)-(re-rs)]
249
250+ e.recompute()
251 e.bumpOldestChange()
252 }
253
254@@ -182,16 +232,16 @@ func (e *Editor) RemWord() (ok bool) {
255 // Hello world|
256 // Hello |
257 // |
258- for e.cursorIdx > 0 && line[e.cursorIdx-1] == ' ' {
259- e.remRuneAt(e.cursorIdx - 1)
260+ for e.cursorIdx > 0 && line.runes[line.clusters[e.cursorIdx-1]] == ' ' {
261+ e.remClusterAt(e.cursorIdx - 1)
262 e.left()
263 }
264
265 for i := e.cursorIdx - 1; i >= 0; i -= 1 {
266- if line[i] == ' ' {
267+ if line.runes[line.clusters[i]] == ' ' {
268 break
269 }
270- e.remRuneAt(i)
271+ e.remClusterAt(i)
272 e.left()
273 }
274
275@@ -201,19 +251,19 @@ func (e *Editor) RemWord() (ok bool) {
276 }
277
278 func (e *Editor) Flush() string {
279- content := string(e.text[e.lineIdx])
280+ l := e.text[e.lineIdx]
281+ content := string(l.runes)
282 if len(content) > 0 {
283- // intended []rune -> string -> []rune conversion to make copies
284- e.history = append(e.history, []rune(content))
285+ e.history = append(e.history, l.copy())
286 }
287 for i, line := range e.history[e.oldestTextChange:] {
288 i := i + e.oldestTextChange
289- e.text[i] = append(e.text[i][:0], line...)
290+ e.text[i] = line.copy()
291 }
292 if len(content) > 0 {
293- e.text = append(e.text, []rune{})
294+ e.text = append(e.text, newEditorLine())
295 } else {
296- e.text[len(e.text)-1] = []rune{}
297+ e.text[len(e.text)-1] = newEditorLine()
298 }
299 e.lineIdx = len(e.text) - 1
300 e.textWidth = e.textWidth[:1]
301@@ -226,10 +276,10 @@ func (e *Editor) Flush() string {
302 }
303
304 func (e *Editor) Clear() bool {
305- if e.TextLen() == 0 {
306+ if e.Empty() {
307 return false
308 }
309- e.text[e.lineIdx] = []rune{}
310+ e.text[e.lineIdx] = newEditorLine()
311 e.bumpOldestChange()
312 e.textWidth = e.textWidth[:1]
313 e.cursorIdx = 0
314@@ -240,10 +290,10 @@ func (e *Editor) Clear() bool {
315
316 func (e *Editor) Set(text string) {
317 r := []rune(text)
318- e.text[e.lineIdx] = r
319+ e.text[e.lineIdx].runes = r
320+ e.recompute()
321 e.bumpOldestChange()
322- e.cursorIdx = len(r)
323- e.computeTextWidth()
324+ e.cursorIdx = len(e.text[e.lineIdx].clusters) - 1
325 e.offsetIdx = 0
326 for e.width < e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx]+16 {
327 e.offsetIdx++
328@@ -259,13 +309,14 @@ func (e *Editor) Right() {
329 }
330
331 func (e *Editor) right() {
332- if e.cursorIdx == len(e.text[e.lineIdx]) {
333+ if e.cursorIdx == len(e.text[e.lineIdx].clusters)-1 {
334 return
335 }
336 e.cursorIdx++
337 if e.width <= e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx] {
338 e.offsetIdx += 16
339- max := len(e.text[e.lineIdx]) - 1
340+ // If we went too far right, go back enough to show one cluster.
341+ max := len(e.text[e.lineIdx].clusters) - 2
342 if max < e.offsetIdx {
343 e.offsetIdx = max
344 }
345@@ -275,14 +326,14 @@ func (e *Editor) right() {
346 func (e *Editor) RightWord() {
347 line := e.text[e.lineIdx]
348
349- if e.cursorIdx == len(line) {
350+ if e.cursorIdx == len(line.clusters)-1 {
351 return
352 }
353
354- for e.cursorIdx < len(line) && line[e.cursorIdx] == ' ' {
355+ for e.cursorIdx < len(line.clusters)-1 && line.runes[line.clusters[e.cursorIdx]] == ' ' {
356 e.Right()
357 }
358- for i := e.cursorIdx; i < len(line) && line[i] != ' '; i += 1 {
359+ for i := e.cursorIdx; i < len(line.clusters)-1 && line.runes[line.clusters[i]] != ' '; i++ {
360 e.Right()
361 }
362 }
363@@ -312,10 +363,10 @@ func (e *Editor) LeftWord() {
364
365 line := e.text[e.lineIdx]
366
367- for e.cursorIdx > 0 && line[e.cursorIdx-1] == ' ' {
368+ for e.cursorIdx > 0 && line.runes[line.clusters[e.cursorIdx-1]] == ' ' {
369 e.left()
370 }
371- for i := e.cursorIdx - 1; i >= 0 && line[i] != ' '; i -= 1 {
372+ for i := e.cursorIdx - 1; i >= 0 && line.runes[line.clusters[i]] != ' '; i-- {
373 e.left()
374 }
375
376@@ -334,10 +385,10 @@ func (e *Editor) Home() {
377 }
378
379 func (e *Editor) End() {
380- if e.cursorIdx == len(e.text[e.lineIdx]) {
381+ if e.cursorIdx == len(e.text[e.lineIdx].clusters)-1 {
382 return
383 }
384- e.cursorIdx = len(e.text[e.lineIdx])
385+ e.cursorIdx = len(e.text[e.lineIdx].clusters) - 1
386 for e.width < e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx]+16 {
387 e.offsetIdx++
388 }
389@@ -354,7 +405,7 @@ func (e *Editor) Up() {
390 return
391 }
392 e.lineIdx--
393- e.computeTextWidth()
394+ e.recompute()
395 e.cursorIdx = 0
396 e.offsetIdx = 0
397 e.autoCache = nil
398@@ -372,7 +423,7 @@ func (e *Editor) Down() {
399 return
400 }
401 e.lineIdx++
402- e.computeTextWidth()
403+ e.recompute()
404 e.cursorIdx = 0
405 e.offsetIdx = 0
406 e.autoCache = nil
407@@ -382,7 +433,7 @@ func (e *Editor) Down() {
408
409 func (e *Editor) AutoComplete() (ok bool) {
410 if e.autoCache == nil {
411- e.autoCache = e.autoComplete(e.cursorIdx, e.text[e.lineIdx])
412+ e.autoCache = e.ui.config.AutoComplete(e.text[e.lineIdx].clusters[e.cursorIdx], e.text[e.lineIdx].runes)
413 if len(e.autoCache) == 0 {
414 e.autoCache = nil
415 return false
416@@ -391,10 +442,10 @@ func (e *Editor) AutoComplete() (ok bool) {
417 return
418 }
419
420- e.text[e.lineIdx] = e.autoCache[e.autoCacheIdx].Text
421+ e.text[e.lineIdx].runes = e.autoCache[e.autoCacheIdx].Text
422+ e.recompute()
423 e.bumpOldestChange()
424- e.cursorIdx = e.autoCache[e.autoCacheIdx].CursorIdx
425- e.computeTextWidth()
426+ e.setCursor(e.autoCache[e.autoCacheIdx].CursorIdx)
427 if len(e.textWidth) <= e.offsetIdx {
428 e.offsetIdx = 0
429 }
430@@ -410,7 +461,7 @@ func (e *Editor) AutoComplete() (ok bool) {
431 func (e *Editor) BackSearch() {
432 if !e.backsearch {
433 e.backsearch = true
434- e.backsearchPattern = []rune(strings.ToLower(string(e.text[e.lineIdx])))
435+ e.backsearchPattern = []rune(strings.ToLower(string(e.text[e.lineIdx].runes)))
436 }
437 e.backsearchUpdate(e.lineIdx - 1)
438 }
439@@ -421,10 +472,10 @@ func (e *Editor) backsearchUpdate(start int) {
440 }
441 pattern := string(e.backsearchPattern)
442 for i := start; i >= 0; i-- {
443- if match := strings.Index(strings.ToLower(string(e.text[i])), pattern); match >= 0 {
444+ if match := strings.Index(strings.ToLower(string(e.text[i].runes)), pattern); match >= 0 {
445 e.lineIdx = i
446- e.computeTextWidth()
447- e.cursorIdx = runeOffset(string(e.text[i]), match) + len(e.backsearchPattern)
448+ e.recompute()
449+ e.setCursor(runeOffset(string(e.text[i].runes), match) + len(e.backsearchPattern))
450 e.offsetIdx = 0
451 for e.width < e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx]+16 {
452 e.offsetIdx++
453@@ -439,15 +490,6 @@ func (e *Editor) backsearchEnd() {
454 e.backsearch = false
455 }
456
457-func (e *Editor) computeTextWidth() {
458- e.textWidth = e.textWidth[:1]
459- rw := 0
460- for _, r := range e.text[e.lineIdx] {
461- rw += runeWidth(r)
462- e.textWidth = append(e.textWidth, rw)
463- }
464-}
465-
466 // call this everytime e.text is modified
467 func (e *Editor) bumpOldestChange() {
468 if e.oldestTextChange > e.lineIdx {
469@@ -455,18 +497,18 @@ func (e *Editor) bumpOldestChange() {
470 }
471 }
472
473-func (e *Editor) Draw(screen tcell.Screen, x0, y int, hint string) {
474- st := tcell.StyleDefault
475+func (e *Editor) Draw(vx *Vaxis, x0, y int, hint string) {
476+ var st vaxis.Style
477
478 x := x0
479- i := e.offsetIdx
480- text := e.text[e.lineIdx]
481+ i := e.text[e.lineIdx].clusters[e.offsetIdx]
482+ text := e.text[e.lineIdx].runes
483 showCursor := true
484
485 if len(text) == 0 && len(hint) > 0 && !e.backsearch {
486 i = 0
487 text = []rune(hint)
488- st = st.Foreground(e.colors.Status)
489+ st.Foreground = e.ui.config.Colors.Status
490 showCursor = false
491 }
492
493@@ -478,32 +520,37 @@ func (e *Editor) Draw(screen tcell.Screen, x0, y int, hint string) {
494 autoEnd = e.autoCache[e.autoCacheIdx].EndIdx
495 }
496
497- for i < len(text) && x < x0+e.width {
498- r := text[i]
499+ ci := e.text[e.lineIdx].clusters[e.cursorIdx]
500+ for i < len(text) {
501+ r := text[i:]
502 s := st
503- if e.backsearch && i < e.cursorIdx && i >= e.cursorIdx-len(e.backsearchPattern) {
504- s = s.Underline(true)
505+ if e.backsearch && i < ci && i >= ci-len(e.backsearchPattern) {
506+ s.UnderlineStyle = vaxis.UnderlineSingle
507 }
508 if i >= autoStart && i < autoEnd {
509- s = s.Underline(true)
510+ s.UnderlineStyle = vaxis.UnderlineSingle
511 }
512 if i == autoStart {
513 autoX = x
514 }
515- if r == '\n' {
516- s = s.Bold(true).Foreground(tcell.ColorRed)
517- r = '↲'
518+ if r[0] == '\n' {
519+ s.Attribute |= vaxis.AttrBold
520+ s.Foreground = ColorRed
521+ r = []rune{'↲'}
522+ }
523+ dx, di := printCluster(vx, x, y, x0+e.width, r, s)
524+ if di == 0 {
525+ break
526 }
527- screen.SetContent(x, y, r, nil, s)
528- x += runeWidth(r)
529- i++
530+ x += dx
531+ i += di
532 }
533 if i == autoStart {
534 autoX = x
535 }
536
537 for x < x0+e.width {
538- screen.SetContent(x, y, ' ', nil, st)
539+ setCell(vx, x, y, ' ', st)
540 x++
541 }
542
543@@ -533,27 +580,31 @@ func (e *Editor) Draw(screen tcell.Screen, x0, y int, hint string) {
544
545 x := autoX
546 y := y - i - 1
547- for _, r := range display {
548- if x >= x0+e.width {
549- break
550+ i := 0
551+ for i < len(display) {
552+ s := vaxis.Style{
553+ Background: vaxis.IndexColor(0),
554+ Attribute: vaxis.AttrReverse,
555 }
556- s := st.Background(tcell.ColorBlack)
557- s = s.Reverse(true)
558 if i+autoOff == e.autoCacheIdx {
559- s = s.Bold(true)
560+ s.Attribute |= vaxis.AttrBold
561 } else {
562- s = s.Dim(true)
563+ s.Attribute |= vaxis.AttrDim
564+ }
565+ dx, di := printCluster(vx, x, y, x0+e.width, display[i:], s)
566+ if di == 0 {
567+ break
568 }
569- screen.SetContent(x, y, r, nil, s)
570- x += runeWidth(r)
571+ x += dx
572+ i += di
573 }
574 }
575
576 if showCursor {
577 cursorX := x0 + e.textWidth[e.cursorIdx] - e.textWidth[e.offsetIdx]
578- screen.ShowCursor(cursorX, y)
579+ vx.ShowCursor(cursorX, y, vaxis.CursorBeam)
580 } else {
581- screen.HideCursor()
582+ vx.HideCursor()
583 }
584 }
585
586@@ -563,7 +614,7 @@ func runeToLower(r rune) rune {
587 return []rune(strings.ToLower(string(r)))[0]
588 }
589
590-// runeOffset returns the rune index of the rune starting at byte i in string s
591+// runeOffset returns the rune index of the rune starting at byte pos in string s
592 func runeOffset(s string, pos int) int {
593 n := 0
594 for i := range s {
+26,
-12
1@@ -3,7 +3,10 @@ package ui
2 import "testing"
3
4 var hell = Editor{
5- text: [][]rune{{'h', 'e', 'l', 'l'}},
6+ text: []editorLine{{
7+ runes: []rune{'h', 'e', 'l', 'l'},
8+ clusters: []int{0, 1, 2, 3, 4},
9+ }},
10 textWidth: []int{0, 1, 2, 3, 4},
11 cursorIdx: 4,
12 offsetIdx: 0,
13@@ -14,9 +17,17 @@ func assertEditorEq(t *testing.T, actual, expected Editor) {
14 if len(actual.text) != len(expected.text) {
15 t.Errorf("expected text len to be %d, got %d\n", len(expected.text), len(actual.text))
16 } else {
17- for i := 0; i < len(actual.text); i++ {
18- a := actual.text[0][i]
19- e := expected.text[0][i]
20+ for i := 0; i < len(actual.text[0].clusters); i++ {
21+ a := actual.text[0].clusters[i]
22+ e := expected.text[0].clusters[i]
23+
24+ if a != e {
25+ t.Errorf("expected cluster #%d to be '%c', got '%c'\n", i, e, a)
26+ }
27+ }
28+ for i := 0; i < len(actual.text[0].runes); i++ {
29+ a := actual.text[0].runes[i]
30+ e := expected.text[0].runes[i]
31
32 if a != e {
33 t.Errorf("expected rune #%d to be '%c', got '%c'\n", i, e, a)
34@@ -51,11 +62,14 @@ func assertEditorEq(t *testing.T, actual, expected Editor) {
35 }
36
37 func TestOneLetter(t *testing.T) {
38- e := NewEditor(ConfigColors{}, nil)
39+ e := NewEditor(&UI{})
40 e.Resize(5)
41 e.PutRune('h')
42 assertEditorEq(t, e, Editor{
43- text: [][]rune{{'h'}},
44+ text: []editorLine{{
45+ runes: []rune{'h'},
46+ clusters: []int{0, 1},
47+ }},
48 textWidth: []int{0, 1},
49 cursorIdx: 1,
50 offsetIdx: 0,
51@@ -64,7 +78,7 @@ func TestOneLetter(t *testing.T) {
52 }
53
54 func TestFourLetters(t *testing.T) {
55- e := NewEditor(ConfigColors{}, nil)
56+ e := NewEditor(&UI{})
57 e.Resize(5)
58 e.PutRune('h')
59 e.PutRune('e')
60@@ -74,7 +88,7 @@ func TestFourLetters(t *testing.T) {
61 }
62
63 func TestOneLeft(t *testing.T) {
64- e := NewEditor(ConfigColors{}, nil)
65+ e := NewEditor(&UI{})
66 e.Resize(5)
67 e.PutRune('h')
68 e.PutRune('l')
69@@ -86,11 +100,11 @@ func TestOneLeft(t *testing.T) {
70 }
71
72 func TestOneRem(t *testing.T) {
73- e := NewEditor(ConfigColors{}, nil)
74+ e := NewEditor(&UI{})
75 e.Resize(5)
76 e.PutRune('h')
77 e.PutRune('l')
78- e.RemRune()
79+ e.RemCluster()
80 e.PutRune('e')
81 e.PutRune('l')
82 e.PutRune('l')
83@@ -98,13 +112,13 @@ func TestOneRem(t *testing.T) {
84 }
85
86 func TestLeftAndRem(t *testing.T) {
87- e := NewEditor(ConfigColors{}, nil)
88+ e := NewEditor(&UI{})
89 e.Resize(5)
90 e.PutRune('h')
91 e.PutRune('l')
92 e.PutRune('e')
93 e.Left()
94- e.RemRune()
95+ e.RemCluster()
96 e.Right()
97 e.PutRune('l')
98 e.PutRune('l')
+1,
-1
1@@ -44,7 +44,7 @@ func (ui *UI) notify(target NotifyEvent, title, content string) int {
2 }
3 }
4
5- ui.screen.Notify(title, content)
6+ ui.vx.Notify(title, content)
7 return -1
8 }
9
+69,
-99
1@@ -8,50 +8,32 @@ import (
2 "strings"
3 "unicode/utf8"
4
5+ "git.sr.ht/~rockorager/vaxis"
6 "mvdan.cc/xurls/v2"
7-
8- "github.com/gdamore/tcell/v2"
9- "github.com/mattn/go-runewidth"
10 )
11
12-var condition = runewidth.Condition{}
13-
14-func runeWidth(r rune) int {
15- if r == '\n' {
16- r = '↲'
17- }
18- return condition.RuneWidth(r)
19-}
20-
21-func stringWidth(s string) int {
22- return condition.StringWidth(s)
23-}
24-
25-func truncate(s string, w int, tail string) string {
26- return condition.Truncate(s, w, tail)
27-}
28-
29 // Taken from <https://modern.ircdocs.horse/formatting.html>
30
31-var baseCodes = []tcell.Color{
32- tcell.ColorWhite, tcell.ColorBlack, tcell.ColorBlue, tcell.ColorGreen,
33- tcell.ColorRed, tcell.ColorBrown, tcell.ColorPurple, tcell.ColorOrange,
34- tcell.ColorYellow, tcell.ColorLightGreen, tcell.ColorTeal, tcell.ColorLightCyan,
35- tcell.ColorLightBlue, tcell.ColorPink, tcell.ColorGray, tcell.ColorLightGray,
36-}
37-
38-// unused
39-var ansiCodes = []uint64{
40- /* 16-27 */ 52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89,
41- /* 28-39 */ 88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125,
42- /* 40-51 */ 124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161,
43- /* 52-63 */ 196, 208, 226, 154, 46, 86, 51, 75, 21, 171, 201, 198,
44- /* 64-75 */ 203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205,
45- /* 76-87 */ 217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212,
46- /* 88-98 */ 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231,
47+var baseCodes = []vaxis.Color{
48+ vaxis.IndexColor(15),
49+ vaxis.IndexColor(0),
50+ vaxis.IndexColor(4),
51+ vaxis.IndexColor(2),
52+ vaxis.IndexColor(9),
53+ vaxis.IndexColor(1),
54+ vaxis.IndexColor(5),
55+ vaxis.IndexColor(3),
56+ vaxis.IndexColor(11),
57+ vaxis.IndexColor(10),
58+ vaxis.IndexColor(6),
59+ vaxis.IndexColor(14),
60+ vaxis.IndexColor(12),
61+ vaxis.IndexColor(13),
62+ vaxis.IndexColor(8),
63+ vaxis.IndexColor(7),
64 }
65
66-var hexCodes = []int32{
67+var hexCodes = []uint32{
68 0x470000, 0x472100, 0x474700, 0x324700, 0x004700, 0x00472c, 0x004747, 0x002747, 0x000047, 0x2e0047, 0x470047, 0x47002a,
69 0x740000, 0x743a00, 0x747400, 0x517400, 0x007400, 0x007449, 0x007474, 0x004074, 0x000074, 0x4b0074, 0x740074, 0x740045,
70 0xb50000, 0xb56300, 0xb5b500, 0x7db500, 0x00b500, 0x00b571, 0x00b5b5, 0x0063b5, 0x0000b5, 0x7500b5, 0xb500b5, 0xb5006b,
71@@ -61,20 +43,20 @@ var hexCodes = []int32{
72 0x000000, 0x131313, 0x282828, 0x363636, 0x4d4d4d, 0x656565, 0x818181, 0x9f9f9f, 0xbcbcbc, 0xe2e2e2, 0xffffff,
73 }
74
75-func colorFromCode(code int) (color tcell.Color) {
76+func colorFromCode(code int) (color vaxis.Color) {
77 if code < 0 || 99 <= code {
78- color = tcell.ColorDefault
79+ color = vaxis.Color(0)
80 } else if code < 16 {
81 color = baseCodes[code]
82 } else {
83- color = tcell.NewHexColor(hexCodes[code-16])
84+ color = vaxis.HexColor(hexCodes[code-16])
85 }
86 return
87 }
88
89 type rangedStyle struct {
90 Start int // byte index at which Style is effective
91- Style tcell.Style
92+ Style vaxis.Style
93 }
94
95 type StyledString struct {
96@@ -90,7 +72,7 @@ func PlainSprintf(format string, a ...interface{}) StyledString {
97 return PlainString(fmt.Sprintf(format, a...))
98 }
99
100-func Styled(s string, style tcell.Style) StyledString {
101+func Styled(s string, style vaxis.Style) StyledString {
102 rStyle := rangedStyle{
103 Start: 0,
104 Style: style,
105@@ -105,25 +87,7 @@ func (s StyledString) String() string {
106 return s.string
107 }
108
109-func (s StyledString) Truncate(w int, tail StyledString) StyledString {
110- if stringWidth(s.string) < w {
111- return s
112- }
113- var sb StyledStringBuilder
114- truncated := truncate(s.string, w-stringWidth(tail.string), "")
115- sb.WriteString(truncated)
116- for _, style := range s.styles {
117- if len(truncated) <= style.Start {
118- break
119- }
120- sb.styles = append(sb.styles, style)
121- }
122- sb.WriteStyledString(tail)
123- return sb.StyledString()
124-}
125-
126-// https://github.com/mvdan/xurls/pull/75
127-var urlRegex, _ = xurls.StrictMatchingScheme(`([a-zA-Z][a-zA-Z.\-+]*://|(bitcoin|cid|file|geo|magnet|mailto|mid|sms|tel|xmpp):)`)
128+var urlRegex, _ = xurls.StrictMatchingScheme(xurls.AnyScheme)
129
130 func (s StyledString) ParseURLs() StyledString {
131 if !strings.ContainsRune(s.string, '.') {
132@@ -137,7 +101,6 @@ func (s StyledString) ParseURLs() StyledString {
133 j := 0
134 lastStyle := rangedStyle{
135 Start: -1,
136- Style: tcell.StyleDefault,
137 }
138 for i := 0; i < len(urls); i++ {
139 u := urls[i]
140@@ -155,16 +118,20 @@ func (s StyledString) ParseURLs() StyledString {
141 }
142 if st.Start == ub {
143 // a style already starts at this position, edit it
144- lastStyle.Style = lastStyle.Style.Url(link).UrlId(id)
145+ lastStyle.Style.Hyperlink = link
146+ lastStyle.Style.HyperlinkParams = fmt.Sprintf("id=%v", id)
147 }
148 lastStyle = st
149 styles = append(styles, st)
150 }
151 if lastStyle.Start != ub {
152 // no style existed at this position, add one from the last style
153+ st := lastStyle.Style
154+ st.Hyperlink = link
155+ st.HyperlinkParams = fmt.Sprintf("id=%v", id)
156 styles = append(styles, rangedStyle{
157 Start: ub,
158- Style: lastStyle.Style.Url(link).UrlId(id),
159+ Style: st,
160 })
161 }
162 // find last style starting before or at url end
163@@ -174,16 +141,20 @@ func (s StyledString) ParseURLs() StyledString {
164 break
165 }
166 if st.Start < ue {
167- st.Style = st.Style.Url(link).UrlId(id)
168+ st.Style.Hyperlink = link
169+ st.Style.HyperlinkParams = fmt.Sprintf("id=%v", id)
170 }
171 lastStyle = st
172 styles = append(styles, st)
173 }
174 if lastStyle.Start != ue {
175 // no style existed at this position, add one from the last style without the hyperlink
176+ st := lastStyle.Style
177+ st.Hyperlink = ""
178+ st.HyperlinkParams = ""
179 styles = append(styles, rangedStyle{
180 Start: ue,
181- Style: lastStyle.Style.Url("").UrlId(""),
182+ Style: st,
183 })
184 }
185 }
186@@ -199,7 +170,7 @@ func isDigit(c byte) bool {
187 return '0' <= c && c <= '9'
188 }
189
190-func parseColorNumber(raw string) (color tcell.Color, n int) {
191+func parseColorNumber(raw string) (color vaxis.Color, n int) {
192 if len(raw) == 0 || !isDigit(raw[0]) {
193 return
194 }
195@@ -217,27 +188,27 @@ func parseColorNumber(raw string) (color tcell.Color, n int) {
196 return colorFromCode(code), 2
197 }
198
199-func parseColor(raw string) (fg, bg tcell.Color, n int) {
200+func parseColor(raw string) (fg, bg vaxis.Color, n int) {
201 fg, n = parseColorNumber(raw)
202 raw = raw[n:]
203
204 if len(raw) == 0 || raw[0] != ',' {
205- return fg, tcell.ColorDefault, n
206+ return fg, vaxis.Color(0), n
207 }
208
209 n++
210 bg, p := parseColorNumber(raw[1:])
211 n += p
212
213- if bg == tcell.ColorDefault {
214+ if bg == vaxis.Color(0) {
215 // Lone comma, do not parse as part of a color code.
216- return fg, tcell.ColorDefault, n - 1
217+ return fg, vaxis.Color(0), n - 1
218 }
219
220 return fg, bg, n
221 }
222
223-func parseHexColorNumber(raw string) (color tcell.Color, n int) {
224+func parseHexColorNumber(raw string) (color vaxis.Color, n int) {
225 if len(raw) < 6 {
226 return
227 }
228@@ -248,24 +219,24 @@ func parseHexColorNumber(raw string) (color tcell.Color, n int) {
229 if err != nil {
230 return
231 }
232- return tcell.NewHexColor(int32(value)), 6
233+ return vaxis.HexColor(uint32(value)), 6
234 }
235
236-func parseHexColor(raw string) (fg, bg tcell.Color, n int) {
237+func parseHexColor(raw string) (fg, bg vaxis.Color, n int) {
238 fg, n = parseHexColorNumber(raw)
239 raw = raw[n:]
240
241 if len(raw) == 0 || raw[0] != ',' {
242- return fg, tcell.ColorDefault, n
243+ return fg, vaxis.Color(0), n
244 }
245
246 n++
247 bg, p := parseHexColorNumber(raw[1:])
248 n += p
249
250- if bg == tcell.ColorDefault {
251+ if bg == vaxis.Color(0) {
252 // Lone comma, do not parse as part of a color code.
253- return fg, tcell.ColorDefault, n - 1
254+ return fg, vaxis.Color(0), n - 1
255 }
256
257 return fg, bg, n
258@@ -274,20 +245,18 @@ func parseHexColor(raw string) (fg, bg tcell.Color, n int) {
259 func IRCString(raw string) StyledString {
260 var formatted strings.Builder
261 var styles []rangedStyle
262- var last tcell.Style
263+ var last vaxis.Style
264
265 for len(raw) != 0 {
266 r, runeSize := utf8.DecodeRuneInString(raw)
267- _, _, lastAttrs := last.Decompose()
268 current := last
269 if r == 0x0F {
270- current = tcell.StyleDefault
271+ current = vaxis.Style{}
272 } else if r == 0x02 {
273- lastWasBold := lastAttrs&tcell.AttrBold != 0
274- current = last.Bold(!lastWasBold)
275+ current.Attribute ^= vaxis.AttrBold
276 } else if r == 0x03 || r == 0x04 {
277- var fg tcell.Color
278- var bg tcell.Color
279+ var fg vaxis.Color
280+ var bg vaxis.Color
281 var n int
282 if r == 0x03 {
283 fg, bg, n = parseColor(raw[1:])
284@@ -298,25 +267,26 @@ func IRCString(raw string) StyledString {
285 if n == 0 {
286 // Both `fg` and `bg` are equal to
287 // tcell.ColorDefault.
288- current = last.Foreground(tcell.ColorDefault).
289- Background(tcell.ColorDefault)
290- } else if bg == tcell.ColorDefault {
291- current = last.Foreground(fg)
292+ current.Foreground = vaxis.Color(0)
293+ current.Background = vaxis.Color(0)
294+ } else if bg == vaxis.Color(0) {
295+ current.Foreground = fg
296 } else {
297- current = last.Foreground(fg).Background(bg)
298+ current.Foreground = fg
299+ current.Background = bg
300 }
301 } else if r == 0x16 {
302- lastWasReverse := lastAttrs&tcell.AttrReverse != 0
303- current = last.Reverse(!lastWasReverse)
304+ current.Attribute ^= vaxis.AttrReverse
305 } else if r == 0x1D {
306- lastWasItalic := lastAttrs&tcell.AttrItalic != 0
307- current = last.Italic(!lastWasItalic)
308+ current.Attribute ^= vaxis.AttrItalic
309 } else if r == 0x1E {
310- lastWasStrikeThrough := lastAttrs&tcell.AttrStrikeThrough != 0
311- current = last.StrikeThrough(!lastWasStrikeThrough)
312+ current.Attribute ^= vaxis.AttrStrikethrough
313 } else if r == 0x1F {
314- lastWasUnderline := lastAttrs&tcell.AttrUnderline != 0
315- current = last.Underline(!lastWasUnderline)
316+ if last.UnderlineStyle == vaxis.UnderlineOff {
317+ current.UnderlineStyle = vaxis.UnderlineSingle
318+ } else {
319+ current.UnderlineStyle = vaxis.UnderlineOff
320+ }
321 } else {
322 formatted.WriteRune(r)
323 }
324@@ -362,7 +332,7 @@ func (sb *StyledStringBuilder) WriteStyledString(s StyledString) {
325 sb.WriteString(s.string)
326 }
327
328-func (sb *StyledStringBuilder) AddStyle(start int, style tcell.Style) {
329+func (sb *StyledStringBuilder) AddStyle(start int, style vaxis.Style) {
330 for i := 0; i < len(sb.styles); i++ {
331 if sb.styles[i].Start == i {
332 sb.styles[i].Style = style
333@@ -382,7 +352,7 @@ func (sb *StyledStringBuilder) AddStyle(start int, style tcell.Style) {
334 })
335 }
336
337-func (sb *StyledStringBuilder) SetStyle(style tcell.Style) {
338+func (sb *StyledStringBuilder) SetStyle(style vaxis.Style) {
339 sb.styles = append(sb.styles, rangedStyle{
340 Start: sb.Len(),
341 Style: style,
+11,
-11
1@@ -3,7 +3,7 @@ package ui
2 import (
3 "testing"
4
5- "github.com/gdamore/tcell/v2"
6+ "git.sr.ht/~rockorager/vaxis"
7 )
8
9 func assertIRCString(t *testing.T, input string, expected StyledString) {
10@@ -35,62 +35,62 @@ func TestIRCString(t *testing.T) {
11 assertIRCString(t, "\x02hello", StyledString{
12 string: "hello",
13 styles: []rangedStyle{
14- {Start: 0, Style: tcell.StyleDefault.Bold(true)},
15+ {Start: 0, Style: vaxis.Style{Attribute: vaxis.AttrBold}},
16 },
17 })
18 assertIRCString(t, "\x035hello", StyledString{
19 string: "hello",
20 styles: []rangedStyle{
21- {Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
22+ {Start: 0, Style: vaxis.Style{Foreground: vaxis.IndexColor(1)}},
23 },
24 })
25 assertIRCString(t, "\x0305hello", StyledString{
26 string: "hello",
27 styles: []rangedStyle{
28- {Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
29+ {Start: 0, Style: vaxis.Style{Foreground: vaxis.IndexColor(1)}},
30 },
31 })
32 assertIRCString(t, "\x0305,0hello", StyledString{
33 string: "hello",
34 styles: []rangedStyle{
35- {Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown).Background(tcell.ColorWhite)},
36+ {Start: 0, Style: vaxis.Style{Foreground: vaxis.IndexColor(1), Background: vaxis.IndexColor(15)}},
37 },
38 })
39 assertIRCString(t, "\x035,00hello", StyledString{
40 string: "hello",
41 styles: []rangedStyle{
42- {Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown).Background(tcell.ColorWhite)},
43+ {Start: 0, Style: vaxis.Style{Foreground: vaxis.IndexColor(1), Background: vaxis.IndexColor(15)}},
44 },
45 })
46 assertIRCString(t, "\x0305,00hello", StyledString{
47 string: "hello",
48 styles: []rangedStyle{
49- {Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown).Background(tcell.ColorWhite)},
50+ {Start: 0, Style: vaxis.Style{Foreground: vaxis.IndexColor(1), Background: vaxis.IndexColor(15)}},
51 },
52 })
53
54 assertIRCString(t, "\x035,hello", StyledString{
55 string: ",hello",
56 styles: []rangedStyle{
57- {Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
58+ {Start: 0, Style: vaxis.Style{Foreground: vaxis.IndexColor(1)}},
59 },
60 })
61 assertIRCString(t, "\x0305,hello", StyledString{
62 string: ",hello",
63 styles: []rangedStyle{
64- {Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
65+ {Start: 0, Style: vaxis.Style{Foreground: vaxis.IndexColor(1)}},
66 },
67 })
68 assertIRCString(t, "\x03050hello", StyledString{
69 string: "0hello",
70 styles: []rangedStyle{
71- {Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
72+ {Start: 0, Style: vaxis.Style{Foreground: vaxis.IndexColor(1)}},
73 },
74 })
75 assertIRCString(t, "\x0305,000hello", StyledString{
76 string: "0hello",
77 styles: []rangedStyle{
78- {Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown).Background(tcell.ColorWhite)},
79+ {Start: 0, Style: vaxis.Style{Foreground: vaxis.IndexColor(1), Background: vaxis.IndexColor(15)}},
80 },
81 })
82 }
M
ui/ui.go
+110,
-81
1@@ -7,7 +7,6 @@ import (
2 "time"
3
4 "git.sr.ht/~rockorager/vaxis"
5- "github.com/gdamore/tcell/v2"
6
7 "git.sr.ht/~delthas/senpai/irc"
8 )
9@@ -27,15 +26,20 @@ type Config struct {
10 }
11
12 type ConfigColors struct {
13- Status tcell.Color
14- Prompt tcell.Color
15- Unread tcell.Color
16+ Status vaxis.Color
17+ Prompt vaxis.Color
18+ Unread vaxis.Color
19 Nicks ColorScheme
20 }
21
22+type Vaxis struct {
23+ *vaxis.Vaxis
24+ window vaxis.Window
25+}
26+
27 type UI struct {
28- screen tcell.Screen
29- Events chan tcell.Event
30+ vx *Vaxis
31+ Events chan vaxis.Event
32 exit atomic.Value // bool
33 config Config
34
35@@ -68,34 +72,33 @@ func New(config Config) (ui *UI, err error) {
36 ui.memberWidth = config.MemberColWidth
37 }
38
39- ui.screen, err = tcell.NewScreen()
40- if err != nil {
41- return
42- }
43-
44- err = ui.screen.Init()
45+ var vx *vaxis.Vaxis
46+ vx, err = vaxis.New(vaxis.Options{
47+ DisableMouse: !config.Mouse,
48+ CSIuBitMask: vaxis.CSIuDisambiguate | vaxis.CSIuReportEvents | vaxis.CSIuAlternateKeys | vaxis.CSIuAllKeys | vaxis.CSIuAssociatedText,
49+ })
50 if err != nil {
51 return
52 }
53- if ui.screen.HasMouse() && config.Mouse {
54- ui.screen.EnableMouse()
55+ ui.vx = &Vaxis{
56+ Vaxis: vx,
57+ window: vx.Window(),
58 }
59- ui.screen.EnablePaste()
60- ui.screen.SetCursorStyle(tcell.CursorStyleSteadyBar)
61- ui.screen.SetTitle("senpai")
62- ui.screen.SetAppID("senpai")
63
64- _, h := ui.screen.Size()
65- ui.screen.Clear()
66- ui.screen.ShowCursor(0, h-2)
67+ ui.vx.SetTitle("senpai")
68+ ui.vx.SetAppID("senpai")
69+
70+ _, h := ui.vx.window.Size()
71+ ui.vx.window.Clear()
72+ ui.vx.ShowCursor(0, h-2, vaxis.CursorBeam)
73
74 ui.exit.Store(false)
75
76- ui.Events = make(chan tcell.Event, 128)
77+ ui.Events = make(chan vaxis.Event, 128)
78 go func() {
79 for !ui.ShouldExit() {
80- ev := ui.screen.PollEvent()
81- if ev == nil {
82+ ev := ui.vx.PollEvent()
83+ if _, ok := ev.(vaxis.QuitEvent); ok {
84 ui.Exit()
85 break
86 }
87@@ -104,8 +107,8 @@ func New(config Config) (ui *UI, err error) {
88 close(ui.Events)
89 }()
90
91- ui.bs = NewBufferList(config.Colors, ui.config.MergeLine)
92- ui.e = NewEditor(config.Colors, ui.config.AutoComplete)
93+ ui.bs = NewBufferList(ui)
94+ ui.e = NewEditor(ui)
95 ui.Resize()
96
97 return
98@@ -120,11 +123,8 @@ func (ui *UI) Exit() {
99 }
100
101 func (ui *UI) Close() {
102- // See: https://github.com/gdamore/tcell/issues/623
103- ui.screen.SetCursorStyle(tcell.CursorStyleDefault)
104- ui.screen.Sync()
105-
106- ui.screen.Fini()
107+ ui.vx.Refresh() // TODO is this needed?
108+ ui.vx.Close()
109 }
110
111 func (ui *UI) Buffer(i int) (netID, title string, ok bool) {
112@@ -425,11 +425,11 @@ func (ui *UI) SetTitle(title string) {
113 return
114 }
115 ui.title = title
116- ui.screen.SetTitle(title)
117+ ui.vx.SetTitle(title)
118 }
119
120 func (ui *UI) SetMouseShape(shape vaxis.MouseShape) {
121- ui.screen.Vaxis().SetMouseShape(shape)
122+ ui.vx.SetMouseShape(shape)
123 }
124
125 // InputContent result must not be modified.
126@@ -474,11 +474,11 @@ func (ui *UI) InputDown() {
127 }
128
129 func (ui *UI) InputBackspace() (ok bool) {
130- return ui.e.RemRune()
131+ return ui.e.RemCluster()
132 }
133
134 func (ui *UI) InputDelete() (ok bool) {
135- return ui.e.RemRuneForward()
136+ return ui.e.RemClusterForward()
137 }
138
139 func (ui *UI) InputDeleteWord() (ok bool) {
140@@ -506,7 +506,8 @@ func (ui *UI) InputBackSearch() {
141 }
142
143 func (ui *UI) Resize() {
144- w, h := ui.screen.Size()
145+ ui.vx.window = ui.vx.Window() // Refresh window size
146+ w, h := ui.vx.window.Size()
147 innerWidth := w - 9 - ui.channelWidth - ui.config.NickColWidth - ui.memberWidth
148 if innerWidth <= 0 {
149 innerWidth = 1 // will break display somewhat, but this is an edge case
150@@ -522,32 +523,32 @@ func (ui *UI) Resize() {
151 ui.bs.ResizeTimeline(innerWidth, h-2, textWidth)
152 }
153 ui.ScrollToBuffer()
154- ui.screen.Sync()
155+ ui.vx.Refresh()
156 }
157
158 func (ui *UI) Size() (int, int) {
159- return ui.screen.Size()
160+ return ui.vx.window.Size()
161 }
162
163 func (ui *UI) Beep() {
164- ui.screen.Beep()
165+ ui.vx.Bell()
166 }
167
168 func (ui *UI) Notify(title string, body string) {
169- ui.screen.Notify(title, body)
170+ ui.vx.Notify(title, body)
171 }
172
173 func (ui *UI) Draw(members []irc.Member) {
174- w, h := ui.screen.Size()
175+ w, h := ui.vx.window.Size()
176
177- ui.bs.DrawTimeline(ui.screen, ui.channelWidth, 0, ui.config.NickColWidth)
178+ ui.bs.DrawTimeline(ui.vx, ui.channelWidth, 0, ui.config.NickColWidth)
179 if ui.channelWidth == 0 {
180- ui.bs.DrawHorizontalBufferList(ui.screen, 0, h-1, w-ui.memberWidth, &ui.channelOffset)
181+ ui.bs.DrawHorizontalBufferList(ui.vx, 0, h-1, w-ui.memberWidth, &ui.channelOffset)
182 } else {
183- ui.bs.DrawVerticalBufferList(ui.screen, 0, 0, ui.channelWidth, h, &ui.channelOffset)
184+ ui.bs.DrawVerticalBufferList(ui.vx, 0, 0, ui.channelWidth, h, &ui.channelOffset)
185 }
186 if ui.memberWidth != 0 {
187- ui.drawVerticalMemberList(ui.screen, w-ui.memberWidth, 0, ui.memberWidth, h, members, &ui.memberOffset)
188+ ui.drawVerticalMemberList(ui.vx, w-ui.memberWidth, 0, ui.memberWidth, h, members, &ui.memberOffset)
189 }
190 if ui.channelWidth == 0 {
191 ui.drawStatusBar(ui.channelWidth, h-3, w-ui.memberWidth)
192@@ -556,19 +557,21 @@ func (ui *UI) Draw(members []irc.Member) {
193 }
194
195 prompt := ui.prompt
196- if ui.bs.HasOverlay() && ui.e.TextLen() == 0 {
197- prompt = Styled(">", tcell.StyleDefault.Foreground(ui.config.Colors.Prompt))
198+ if ui.bs.HasOverlay() && ui.e.Empty() {
199+ prompt = Styled(">", vaxis.Style{
200+ Foreground: ui.config.Colors.Prompt,
201+ })
202 }
203 if ui.channelWidth == 0 {
204 for x := 0; x < 9+ui.config.NickColWidth; x++ {
205- ui.screen.SetContent(x, h-2, ' ', nil, tcell.StyleDefault)
206+ setCell(ui.vx, x, h-2, ' ', vaxis.Style{})
207 }
208- printIdent(ui.screen, 7, h-2, ui.config.NickColWidth, prompt)
209+ printIdent(ui.vx, 7, h-2, ui.config.NickColWidth, prompt)
210 } else {
211 for x := ui.channelWidth; x < 9+ui.channelWidth+ui.config.NickColWidth; x++ {
212- ui.screen.SetContent(x, h-1, ' ', nil, tcell.StyleDefault)
213+ setCell(ui.vx, x, h-1, ' ', vaxis.Style{})
214 }
215- printIdent(ui.screen, ui.channelWidth+7, h-1, ui.config.NickColWidth, prompt)
216+ printIdent(ui.vx, ui.channelWidth+7, h-1, ui.config.NickColWidth, prompt)
217 }
218
219 var hint string
220@@ -576,12 +579,12 @@ func (ui *UI) Draw(members []irc.Member) {
221 hint = ui.overlayHint
222 }
223 if ui.channelWidth == 0 {
224- ui.e.Draw(ui.screen, 9+ui.config.NickColWidth, h-2, hint)
225+ ui.e.Draw(ui.vx, 9+ui.config.NickColWidth, h-2, hint)
226 } else {
227- ui.e.Draw(ui.screen, 9+ui.channelWidth+ui.config.NickColWidth, h-1, hint)
228+ ui.e.Draw(ui.vx, 9+ui.channelWidth+ui.config.NickColWidth, h-1, hint)
229 }
230
231- ui.screen.Show()
232+ ui.vx.Render()
233 }
234
235 func (ui *UI) ScrollToBuffer() {
236@@ -590,7 +593,7 @@ func (ui *UI) ScrollToBuffer() {
237 return
238 }
239
240- w, h := ui.screen.Size()
241+ w, h := ui.vx.window.Size()
242 var first int
243 if ui.channelWidth > 0 {
244 first = ui.bs.current - h + 1
245@@ -603,28 +606,32 @@ func (ui *UI) ScrollToBuffer() {
246 }
247
248 func (ui *UI) drawStatusBar(x0, y, width int) {
249- clearArea(ui.screen, x0, y, width, 1)
250+ clearArea(ui.vx, x0, y, width, 1)
251
252 if ui.status == "" {
253 return
254 }
255
256 var s StyledStringBuilder
257- s.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
258+ s.SetStyle(vaxis.Style{
259+ Foreground: ColorGray,
260+ })
261 s.WriteString("--")
262
263 x := x0 + 5 + ui.config.NickColWidth
264- printString(ui.screen, &x, y, s.StyledString())
265+ printString(ui.vx, &x, y, s.StyledString())
266 x += 2
267
268 s.Reset()
269- s.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
270+ s.SetStyle(vaxis.Style{
271+ Foreground: ColorGray,
272+ })
273 s.WriteString(ui.status)
274
275- printString(ui.screen, &x, y, s.StyledString())
276+ printString(ui.vx, &x, y, s.StyledString())
277 }
278
279-func (ui *UI) drawVerticalMemberList(screen tcell.Screen, x0, y0, width, height int, members []irc.Member, offset *int) {
280+func (ui *UI) drawVerticalMemberList(vx *Vaxis, x0, y0, width, height int, members []irc.Member, offset *int) {
281 if y0+len(members)-*offset < height {
282 *offset = y0 + len(members) - height
283 if *offset < 0 {
284@@ -632,15 +639,17 @@ func (ui *UI) drawVerticalMemberList(screen tcell.Screen, x0, y0, width, height
285 }
286 }
287
288- drawVerticalLine(screen, x0, y0, height)
289+ drawVerticalLine(vx, x0, y0, height)
290 x0++
291 width--
292- clearArea(screen, x0, y0, width, height)
293+ clearArea(vx, x0, y0, width, height)
294
295 if _, channel := ui.bs.Current(); channel == "" {
296 x := x0 + 1
297- printString(screen, &x, y0, Styled("Help", tcell.StyleDefault.Foreground(ui.config.Colors.Status)))
298- drawHorizontalLine(screen, x0, y0+1, width)
299+ printString(vx, &x, y0, Styled("Help", vaxis.Style{
300+ Foreground: ui.config.Colors.Status,
301+ }))
302+ drawHorizontalLine(vx, x0, y0+1, width)
303 y0 += 2
304
305 lines := []string{
306@@ -649,10 +658,13 @@ func (ui *UI) drawVerticalMemberList(screen tcell.Screen, x0, y0, width, height
307 "→Message user",
308 }
309 for i, line := range lines {
310- reverse := i*2 == ui.memberClicked
311+ var st vaxis.Style
312+ if i*2 == ui.memberClicked {
313+ st.Attribute |= vaxis.AttrReverse
314+ }
315 x := x0
316- printString(screen, &x, y0, Styled(line, tcell.StyleDefault.Reverse(reverse)))
317- drawHorizontalLine(screen, x0, y0+1, width)
318+ printString(vx, &x, y0, Styled(line, st))
319+ drawHorizontalLine(vx, x0, y0+1, width)
320 y0 += 2
321 }
322 return
323@@ -669,49 +681,66 @@ func (ui *UI) drawVerticalMemberList(screen tcell.Screen, x0, y0, width, height
324 } else {
325 memberString = fmt.Sprintf("%d member", len(members))
326 }
327- memberString = truncate(memberString, width-1, "\u2026")
328+ memberString = truncate(vx, memberString, width-1, "\u2026")
329 xMembers := x0 + 1
330- printString(screen, &xMembers, y0, Styled(memberString, tcell.StyleDefault.Foreground(ui.config.Colors.Status)))
331+ printString(vx, &xMembers, y0, Styled(memberString, vaxis.Style{
332+ Foreground: ui.config.Colors.Status,
333+ }))
334 }
335 y0++
336 height--
337- drawHorizontalLine(screen, x0, y0, width)
338+ drawHorizontalLine(vx, x0, y0, width)
339 y0++
340 height--
341
342 padding := 1
343 for _, m := range members {
344 if m.Disconnected {
345- padding = runeWidth(0x274C)
346+ padding = runeWidth(vx, 0x274C)
347 break
348 }
349 }
350
351 for i, m := range members[*offset:] {
352- reverse := i+*offset == ui.memberClicked
353+ var attr vaxis.AttributeMask
354+ if i+*offset == ui.memberClicked {
355+ attr |= vaxis.AttrReverse
356+ }
357 x := x0
358 y := y0 + i
359 if m.Disconnected {
360- disconnectedSt := tcell.StyleDefault.Foreground(tcell.ColorRed).Reverse(reverse)
361- printString(screen, &x, y, Styled("\u274C", disconnectedSt))
362+ disconnectedSt := vaxis.Style{
363+ Foreground: ColorRed,
364+ Attribute: attr,
365+ }
366+ printString(vx, &x, y, Styled("\u274C", disconnectedSt))
367 } else if m.PowerLevel != "" {
368 x += padding - 1
369 powerLevelText := m.PowerLevel[:1]
370- powerLevelSt := tcell.StyleDefault.Foreground(tcell.ColorGreen).Reverse(reverse)
371- printString(screen, &x, y, Styled(powerLevelText, powerLevelSt))
372+ powerLevelSt := vaxis.Style{
373+ Foreground: vaxis.IndexColor(2),
374+ Attribute: attr,
375+ }
376+ printString(vx, &x, y, Styled(powerLevelText, powerLevelSt))
377 } else {
378 x += padding
379 }
380
381 var name StyledString
382- nameText := truncate(m.Name.Name, width-1, "\u2026")
383+ nameText := truncate(vx, m.Name.Name, width-1, "\u2026")
384 if m.Away {
385- name = Styled(nameText, tcell.StyleDefault.Foreground(tcell.ColorGray).Reverse(reverse))
386+ name = Styled(nameText, vaxis.Style{
387+ Foreground: ColorGray,
388+ Attribute: attr,
389+ })
390 } else {
391 color := IdentColor(ui.config.Colors.Nicks, m.Name.Name, m.Self)
392- name = Styled(nameText, tcell.StyleDefault.Foreground(color).Reverse(reverse))
393+ name = Styled(nameText, vaxis.Style{
394+ Foreground: color,
395+ Attribute: attr,
396+ })
397 }
398
399- printString(screen, &x, y, name)
400+ printString(vx, &x, y, name)
401 }
402 }