commit 3c8dcea
Hubert Hirtz
·
2020-08-03 21:04:51 +0000 UTC
parent 22f5b82
Rework display - Put timestamps, nicks and messages into separate columns - Print a bar in the "typing" row - Fix word wrapping - Improve channel list display
7 files changed,
+900,
-931
+66,
-126
1@@ -3,16 +3,16 @@ package main
2 import (
3 "crypto/tls"
4 "fmt"
5- "git.sr.ht/~taiite/senpai"
6- "git.sr.ht/~taiite/senpai/irc"
7- "git.sr.ht/~taiite/senpai/ui"
8- "github.com/gdamore/tcell"
9- "hash/fnv"
10 "log"
11 "math/rand"
12 "os"
13 "strings"
14 "time"
15+
16+ "git.sr.ht/~taiite/senpai"
17+ "git.sr.ht/~taiite/senpai/irc"
18+ "git.sr.ht/~taiite/senpai/ui"
19+ "github.com/gdamore/tcell"
20 )
21
22 func init() {
23@@ -39,7 +39,7 @@ func main() {
24 defer app.Close()
25
26 addr := cfg.Addr
27- app.AddLine("home", fmt.Sprintf("Connecting to %s...", addr), time.Now(), false)
28+ app.AddLine(ui.Home, ui.NewLineNow("--", fmt.Sprintf("Connecting to %s...", addr)))
29
30 conn, err := tls.Dial("tcp", addr, nil)
31 if err != nil {
32@@ -60,48 +60,48 @@ func main() {
33 for !app.ShouldExit() {
34 select {
35 case ev := <-s.Poll():
36- handleIRCEvent(app, ev)
37+ handleIRCEvent(app, &s, ev)
38 case ev := <-app.Events:
39 handleUIEvent(app, &s, ev)
40 }
41 }
42 }
43
44-func handleIRCEvent(app *ui.UI, ev irc.Event) {
45+func handleIRCEvent(app *ui.UI, s *irc.Session, ev irc.Event) {
46 switch ev := ev.(type) {
47 case irc.RegisteredEvent:
48- app.AddLine("home", "Connected to the server", time.Now(), false)
49+ app.AddLine("", ui.NewLineNow("--", "Connected to the server"))
50 case irc.SelfJoinEvent:
51 app.AddBuffer(ev.Channel)
52 case irc.UserJoinEvent:
53 line := fmt.Sprintf("\x033+\x0314%s", ev.Nick)
54- app.AddLine(ev.Channel, line, ev.Time, true)
55+ app.AddLine(ev.Channel, ui.NewLine(ev.Time, "", line, true))
56 case irc.SelfPartEvent:
57 app.RemoveBuffer(ev.Channel)
58 case irc.UserPartEvent:
59 line := fmt.Sprintf("\x034-\x0314%s", ev.Nick)
60- app.AddLine(ev.Channel, line, ev.Time, true)
61+ app.AddLine(ev.Channel, ui.NewLine(ev.Time, "", line, true))
62 case irc.QueryMessageEvent:
63 if ev.Command == "PRIVMSG" {
64- line := formatIRCMessage(ev.Nick, ev.Content)
65- app.AddLine("home", line, ev.Time, false)
66- app.TypingStop("home", ev.Nick)
67+ l := ui.LineFromIRCMessage(ev.Time, ev.Nick, ev.Content, false)
68+ app.AddLine(ui.Home, l)
69+ app.TypingStop(ui.Home, ev.Nick)
70 } else if ev.Command == "NOTICE" {
71- line := formatIRCNotice(ev.Nick, ev.Content)
72- app.AddLine(app.CurrentBuffer(), line, ev.Time, false)
73- app.TypingStop(app.CurrentBuffer(), ev.Nick)
74+ l := ui.LineFromIRCMessage(ev.Time, ev.Nick, ev.Content, true)
75+ app.AddLine("", l)
76+ app.TypingStop("", ev.Nick)
77 } else {
78 panic("unknown command")
79 }
80 case irc.ChannelMessageEvent:
81- line := formatIRCMessage(ev.Nick, ev.Content)
82- app.AddLine(ev.Channel, line, ev.Time, false)
83+ l := ui.LineFromIRCMessage(ev.Time, ev.Nick, ev.Content, ev.Command == "NOTICE")
84+ app.AddLine(ev.Channel, l)
85 app.TypingStop(ev.Channel, ev.Nick)
86 case irc.QueryTypingEvent:
87 if ev.State == 1 || ev.State == 2 {
88- app.TypingStart("home", ev.Nick)
89+ app.TypingStart(ui.Home, ev.Nick)
90 } else {
91- app.TypingStop("home", ev.Nick)
92+ app.TypingStop(ui.Home, ev.Nick)
93 }
94 case irc.ChannelTypingEvent:
95 if ev.State == 1 || ev.State == 2 {
96@@ -111,27 +111,16 @@ func handleIRCEvent(app *ui.UI, ev irc.Event) {
97 }
98 case irc.HistoryEvent:
99 var lines []ui.Line
100- var lastT time.Time
101- isChannel := ev.Target[0] == '#'
102 for _, m := range ev.Messages {
103 switch m := m.(type) {
104 case irc.ChannelMessageEvent:
105- if isChannel {
106- line := formatIRCMessage(m.Nick, m.Content)
107- line = strings.TrimRight(line, "\t ")
108- if lastT.Truncate(time.Minute) != m.Time.Truncate(time.Minute) {
109- lastT = m.Time
110- hour := lastT.Hour()
111- minute := lastT.Minute()
112- line = fmt.Sprintf("\x02%02d:%02d\x00 %s", hour, minute, line)
113- }
114- lines = append(lines, ui.NewLine(m.Time, false, line))
115- } else {
116- panic("TODO")
117- }
118+ l := ui.LineFromIRCMessage(m.Time, m.Nick, m.Content, m.Command == "NOTICE")
119+ lines = append(lines, l)
120+ default:
121+ panic("TODO")
122 }
123 }
124- app.AddHistoryLines(ev.Target, lines)
125+ app.AddLines(ev.Target, lines)
126 case error:
127 log.Panicln(ev)
128 }
129@@ -151,39 +140,58 @@ func handleUIEvent(app *ui.UI, s *irc.Session, ev tcell.Event) {
130 app.ScrollUp()
131 if app.IsAtTop() {
132 buffer := app.CurrentBuffer()
133- t := app.CurrentBufferOldestTime()
134- s.RequestHistory(buffer, t)
135+ at := time.Now()
136+ if t := app.CurrentBufferOldestTime(); t != nil {
137+ at = *t
138+ }
139+ s.RequestHistory(buffer, at)
140 }
141 case tcell.KeyCtrlD, tcell.KeyPgDn:
142 app.ScrollDown()
143 case tcell.KeyCtrlN:
144- if app.NextBuffer() && app.IsAtTop() {
145+ app.NextBuffer()
146+ if app.IsAtTop() {
147 buffer := app.CurrentBuffer()
148- t := app.CurrentBufferOldestTime()
149- s.RequestHistory(buffer, t)
150+ at := time.Now()
151+ if t := app.CurrentBufferOldestTime(); t != nil {
152+ at = *t
153+ }
154+ s.RequestHistory(buffer, at)
155 }
156 case tcell.KeyCtrlP:
157- if app.PreviousBuffer() && app.IsAtTop() {
158+ app.PreviousBuffer()
159+ if app.IsAtTop() {
160 buffer := app.CurrentBuffer()
161- t := app.CurrentBufferOldestTime()
162- s.RequestHistory(buffer, t)
163+ at := time.Now()
164+ if t := app.CurrentBufferOldestTime(); t != nil {
165+ at = *t
166+ }
167+ s.RequestHistory(buffer, at)
168 }
169 case tcell.KeyRight:
170 if ev.Modifiers() == tcell.ModAlt {
171- if app.NextBuffer() && app.IsAtTop() {
172+ app.NextBuffer()
173+ if app.IsAtTop() {
174 buffer := app.CurrentBuffer()
175- t := app.CurrentBufferOldestTime()
176- s.RequestHistory(buffer, t)
177+ at := time.Now()
178+ if t := app.CurrentBufferOldestTime(); t != nil {
179+ at = *t
180+ }
181+ s.RequestHistory(buffer, at)
182 }
183 } else {
184 app.InputRight()
185 }
186 case tcell.KeyLeft:
187 if ev.Modifiers() == tcell.ModAlt {
188- if app.PreviousBuffer() && app.IsAtTop() {
189+ app.PreviousBuffer()
190+ if app.IsAtTop() {
191 buffer := app.CurrentBuffer()
192- t := app.CurrentBufferOldestTime()
193- s.RequestHistory(buffer, t)
194+ at := time.Now()
195+ if t := app.CurrentBufferOldestTime(); t != nil {
196+ at = *t
197+ }
198+ s.RequestHistory(buffer, at)
199 }
200 } else {
201 app.InputLeft()
202@@ -199,7 +207,7 @@ func handleUIEvent(app *ui.UI, s *irc.Session, ev tcell.Event) {
203 handleInput(app, s, buffer, input)
204 case tcell.KeyRune:
205 app.InputRune(ev.Rune())
206- if app.CurrentBuffer() != "home" && !app.InputIsCommand() {
207+ if app.CurrentBuffer() != ui.Home && !app.InputIsCommand() {
208 s.Typing(app.CurrentBuffer())
209 }
210 }
211@@ -232,21 +240,20 @@ func handleInput(app *ui.UI, s *irc.Session, buffer, content string) {
212
213 switch cmd {
214 case "":
215- if buffer == "home" {
216+ if buffer == ui.Home || len(strings.TrimSpace(args)) == 0 {
217 return
218 }
219
220 s.PrivMsg(buffer, args)
221 if !s.HasCapability("echo-message") {
222- line := formatIRCMessage(s.Nick(), args)
223- app.AddLine(buffer, line, time.Now(), false)
224+ app.AddLine(buffer, ui.NewLineNow(s.Nick(), args))
225 }
226 case "QUOTE":
227 s.SendRaw(args)
228 case "J", "JOIN":
229 s.Join(args)
230 case "PART":
231- if buffer == "home" {
232+ if buffer == ui.Home {
233 return
234 }
235
236@@ -256,12 +263,13 @@ func handleInput(app *ui.UI, s *irc.Session, buffer, content string) {
237
238 s.Part(args)
239 case "ME":
240- if buffer == "home" {
241+ if buffer == ui.Home {
242 return
243 }
244
245 line := fmt.Sprintf("\x01ACTION %s\x01", args)
246 s.PrivMsg(buffer, line)
247+ // TODO echo message
248 case "MSG":
249 split := strings.SplitN(args, " ", 2)
250 if len(split) < 2 {
251@@ -271,74 +279,6 @@ func handleInput(app *ui.UI, s *irc.Session, buffer, content string) {
252 target := split[0]
253 content := split[1]
254 s.PrivMsg(target, content)
255+ // TODO echo mssage
256 }
257 }
258-
259-func formatIRCMessage(nick, content string) (line string) {
260- c := color(nick)
261-
262- if content == "" {
263- line = fmt.Sprintf("%s%s\x00:", c, nick)
264- return
265- }
266-
267- if content[0] == 1 {
268- content = strings.TrimSuffix(content[1:], "\x01")
269-
270- if strings.HasPrefix(content, "ACTION") {
271- line = fmt.Sprintf("%s%s\x00%s", c, nick, content[6:])
272- } else {
273- line = fmt.Sprintf("\x1dCTCP request from\x1d %s%s\x00: %s", c, nick, content)
274- }
275-
276- return
277- }
278-
279- line = fmt.Sprintf("%s%s\x00: %s", c, nick, content)
280-
281- return
282-}
283-
284-func formatIRCNotice(nick, content string) (line string) {
285- c := color(nick)
286-
287- if content == "" {
288- line = fmt.Sprintf("(%s%s\x00: )", c, nick)
289- return
290- }
291-
292- if content[0] == 1 {
293- content = strings.TrimSuffix(content[1:], "\x01")
294-
295- if strings.HasPrefix(content, "ACTION") {
296- line = fmt.Sprintf("(%s%s\x00%s)", c, nick, content[6:])
297- } else {
298- line = fmt.Sprintf("(\x1dCTCP request from\x1d %s%s\x00: %s)", c, nick, content)
299- }
300- } else {
301- line = fmt.Sprintf("(%s%s\x00: %s)", c, nick, content)
302- }
303-
304- return
305-}
306-
307-func color(nick string) string {
308- h := fnv.New32()
309- _, _ = h.Write([]byte(nick))
310-
311- sum := h.Sum32() % 96
312-
313- if 1 <= sum {
314- sum++
315- }
316- if 8 <= sum {
317- sum++
318- }
319-
320- var c [3]rune
321- c[0] = '\x03'
322- c[1] = rune(sum/10) + '0'
323- c[2] = rune(sum%10) + '0'
324-
325- return string(c[:])
326-}
+475,
-184
1@@ -2,10 +2,17 @@ package ui
2
3 import (
4 "fmt"
5+ "hash/fnv"
6+ "math"
7 "strings"
8 "time"
9+
10+ "github.com/gdamore/tcell"
11+ "github.com/mattn/go-runewidth"
12 )
13
14+var Home = "home"
15+
16 var homeMessages = []string{
17 "\x1dYou open an IRC client.",
18 "Welcome to the Internet Relay Network!",
19@@ -15,294 +22,578 @@ var homeMessages = []string{
20 "Student? No, I'm an IRC \x02client\x02!",
21 }
22
23-func IsSplitRune(c rune) bool {
24- return c == ' ' || c == '\t'
25+func IsSplitRune(r rune) bool {
26+ return r == ' ' || r == '\t'
27 }
28
29-type Point struct {
30- X int
31- I int
32-
33+type point struct {
34+ X, I int
35 Split bool
36 }
37
38 type Line struct {
39- Time time.Time
40- IsStatus bool
41- Content string
42-
43- SplitPoints []Point
44- renderedHeight int
45+ at time.Time
46+ head string
47+ body string
48+ isStatus bool
49+
50+ splitPoints []point
51+ width int
52+ newLines []int
53 }
54
55-func NewLine(t time.Time, isStatus bool, content string) (line Line) {
56- line.Time = t
57- line.IsStatus = isStatus
58- line.Content = content
59-
60- line.Invalidate()
61- line.computeSplitPoints()
62-
63- return
64+func NewLine(at time.Time, head string, body string, isStatus bool) Line {
65+ l := Line{
66+ at: at,
67+ head: head,
68+ body: body,
69+ isStatus: isStatus,
70+ splitPoints: []point{},
71+ newLines: []int{},
72+ }
73+ l.computeSplitPoints()
74+ return l
75 }
76
77-func NewLineNow(content string) (line Line) {
78- line = NewLine(time.Now(), false, content)
79- return
80+func NewLineNow(head, body string) Line {
81+ return NewLine(time.Now(), head, body, false)
82 }
83
84-func (line *Line) Invalidate() {
85- line.renderedHeight = 0
86+func LineFromIRCMessage(at time.Time, nick string, content string, isNotice bool) Line {
87+ if strings.HasPrefix(content, "\x01ACTION") {
88+ c := ircColorCode(identColor(nick))
89+ content = fmt.Sprintf("%s%s\x0F%s", c, nick, content[7:])
90+ nick = "*"
91+ } else if isNotice {
92+ c := ircColorCode(identColor(nick))
93+ content = fmt.Sprintf("(%s%s\x0F: %s)", c, nick, content)
94+ nick = "*"
95+ }
96+ return NewLine(at, nick, content, false)
97 }
98
99-func (line *Line) RenderedHeight(screenWidth int) (height int) {
100- if line.renderedHeight <= 0 {
101- line.computeRenderedHeight(screenWidth)
102+func (l *Line) computeSplitPoints() {
103+ var wb widthBuffer
104+ lastWasSplit := false
105+ l.splitPoints = l.splitPoints[:0]
106+
107+ for i, r := range l.body {
108+ curIsSplit := IsSplitRune(r)
109+
110+ if i == 0 || lastWasSplit != curIsSplit {
111+ l.splitPoints = append(l.splitPoints, point{
112+ X: wb.Width(),
113+ I: i,
114+ Split: curIsSplit,
115+ })
116+ }
117+
118+ lastWasSplit = curIsSplit
119+ wb.WriteRune(r)
120+ }
121+
122+ if !lastWasSplit {
123+ l.splitPoints = append(l.splitPoints, point{
124+ X: wb.Width(),
125+ I: len(l.body),
126+ Split: true,
127+ })
128 }
129- height = line.renderedHeight
130- return
131 }
132
133-// TODO clean and understand the fucking function
134-func (line *Line) computeRenderedHeight(screenWidth int) {
135- var lastSP Point
136- line.renderedHeight = 1
137- x := 0
138+func (l *Line) NewLines(width int) []int {
139+ // Beware! This function was made by your local Test Driven Developperâ„¢ who
140+ // doesn't understand one bit of this function and how it works (though it
141+ // might not work that well if you're here...). The code below is thus very
142+ // cryptic and not well structured. However, I'm going to try to explain
143+ // some of those lines!
144
145- //fmt.Printf("\n%d %q\n", screenWidth, line.Content)
146- for _, sp := range line.SplitPoints {
147- l := sp.X - lastSP.X
148+ if l.width == width {
149+ return l.newLines
150+ }
151+ l.newLines = l.newLines[:0]
152+ l.width = width
153
154- if !sp.Split && x == 0 {
155- // Don't add space at the beginning of a row
156- } else if screenWidth < l {
157- line.renderedHeight += (x + l) / screenWidth
158- x = (x + l) % screenWidth
159- } else if screenWidth == l {
160+ x := 0
161+ for i := 1; i < len(l.splitPoints); i++ {
162+ // Iterate through the split points 2 by 2. Split points are placed at
163+ // the begining of whitespace (see IsSplitRune) and at the begining of
164+ // non-whitespace. Iterating on 2 points each time, sp1 and sp2, allow
165+ // consideration of a "word" of (non-)whitespace.
166+ // Split points have the index I in the string and the width X of the
167+ // screen. Finally, the Split field is set to true if the split point
168+ // is at the begining of a whitespace.
169+
170+ // Below, "row" means a line in the terminal, while "line" means (l *Line).
171+
172+ sp1 := l.splitPoints[i-1]
173+ sp2 := l.splitPoints[i]
174+
175+ if 0 < len(l.newLines) && x == 0 && sp1.Split {
176+ // Except for the first row, let's skip the whitespace at the start
177+ // of the row.
178+ } else if !sp1.Split && sp2.X-sp1.X == width {
179+ // Some word occupies the width of the terminal, lets place a
180+ // newline at the PREVIOUS split point (i-2, which is whitespace)
181+ // ONLY if there isn't already one.
182+ if 1 < i && l.newLines[len(l.newLines)-1] != l.splitPoints[i-2].I {
183+ l.newLines = append(l.newLines, l.splitPoints[i-2].I)
184+ }
185+ // and also place a newline after the word.
186+ x = 0
187+ l.newLines = append(l.newLines, sp2.I)
188+ } else if sp2.X-sp1.X+x < width {
189+ // It fits. Advance the X coordinate with the width of the word.
190+ x += sp2.X - sp1.X
191+ } else if sp2.X-sp1.X+x == width {
192+ // It fits, but there is no more space in the row.
193+ x = 0
194+ l.newLines = append(l.newLines, sp2.I)
195+ } else if width < sp2.X-sp1.X {
196+ // It doesn't fit at all. The word is longer than the width of the
197+ // terminal. In this case, no newline is placed before (like in the
198+ // 2nd if-else branch). The for loop is used to place newlines in
199+ // the word.
200+ var wb widthBuffer
201+ h := 1
202+ for j, r := range l.body[sp1.I:sp2.I] {
203+ wb.WriteRune(r)
204+ if h*width < x+wb.Width() {
205+ l.newLines = append(l.newLines, sp1.I+j)
206+ h++
207+ }
208+ }
209+ x = (x + wb.Width()) % width
210 if x == 0 {
211- line.renderedHeight++
212- } else {
213- line.renderedHeight += 2
214- x = 0
215+ // The placement of the word is such that it ends right at the
216+ // end of the row.
217+ l.newLines = append(l.newLines, sp2.I)
218 }
219- } else if screenWidth < x+l {
220- line.renderedHeight++
221- if sp.Split {
222- x = l % screenWidth
223- } else {
224+ } else {
225+ // So... IIUC this branch would be the same as
226+ // else if width < sp2.X-sp1.X+x
227+ // IE. It doesn't fit, but the word can still be placed on the next
228+ // row.
229+ l.newLines = append(l.newLines, sp1.I)
230+ if sp1.Split {
231 x = 0
232+ } else {
233+ x = sp2.X - sp1.X
234 }
235- } else if screenWidth == x+l {
236- line.renderedHeight++
237- x = 0
238- } else {
239- x = x + l
240 }
241-
242- //fmt.Printf("%d %d %t occupied by %q\n", line.renderedHeight, x, sp.Split, line.Content[:sp.I])
243- lastSP = sp
244 }
245
246- if x == 0 && 1 < line.renderedHeight {
247- line.renderedHeight--
248+ if 0 < len(l.newLines) && l.newLines[len(l.newLines)-1] == len(l.body) {
249+ // DROP any newline that is placed at the end of the string because we
250+ // don't care about those.
251+ l.newLines = l.newLines[:len(l.newLines)-1]
252 }
253+
254+ return l.newLines
255 }
256
257-func (line *Line) computeSplitPoints() {
258- var wb widthBuffer
259- lastWasSplit := false
260+type buffer struct {
261+ title string
262+ highlights int
263+ unread bool
264
265- for i, r := range line.Content {
266- curIsSplit := IsSplitRune(r)
267+ lines []Line
268+ typings []string
269
270- if lastWasSplit != curIsSplit {
271- line.SplitPoints = append(line.SplitPoints, Point{
272- X: wb.Width(),
273- I: i,
274- Split: curIsSplit,
275- })
276+ scrollAmt int
277+ isAtTop bool
278+}
279+
280+func (b *buffer) DrawLines(screen tcell.Screen, width int, height int) {
281+ st := tcell.StyleDefault
282+ for x := 0; x < width; x++ {
283+ for y := 0; y < height; y++ {
284+ screen.SetContent(x, y, ' ', nil, st)
285 }
286+ }
287
288- lastWasSplit = curIsSplit
289- wb.WriteRune(r)
290+ nickColWidth := 16
291+
292+ var sb styleBuffer
293+ sb.Reset()
294+ y0 := b.scrollAmt + height
295+ for i := len(b.lines) - 1; 0 <= i; i-- {
296+ if y0 < 0 {
297+ break
298+ }
299+
300+ x0 := 5 + 1 + nickColWidth + 2
301+
302+ line := &b.lines[i]
303+ nls := line.NewLines(width - x0)
304+ y0 -= len(nls) + 1
305+ if height <= y0 {
306+ continue
307+ }
308+
309+ if i == 0 || b.lines[i-1].at.Truncate(time.Minute) != line.at.Truncate(time.Minute) {
310+ printTime(screen, 0, y0, st.Bold(true), line.at)
311+ }
312+
313+ head := truncate(line.head, nickColWidth)
314+ x := 6 + nickColWidth - StringWidth(head)
315+ c := identColor(line.head)
316+ printString(screen, &x, y0, st.Foreground(colorFromCode(c)), head)
317+
318+ x = x0
319+ y := y0
320+
321+ for i, r := range line.body {
322+ if 0 < len(nls) && i == nls[0] {
323+ x = x0
324+ y++
325+ nls = nls[1:]
326+ if height < y {
327+ break
328+ }
329+ }
330+
331+ if y != y0 && x == x0 && IsSplitRune(r) {
332+ continue
333+ }
334+
335+ if st, ok := sb.WriteRune(r); ok != 0 {
336+ if 1 < ok {
337+ screen.SetContent(x, y, ',', nil, st)
338+ x++
339+ }
340+ screen.SetContent(x, y, r, nil, st)
341+ x += runewidth.RuneWidth(r)
342+ }
343+ }
344+
345+ sb.Reset()
346 }
347
348- if !lastWasSplit {
349- line.SplitPoints = append(line.SplitPoints, Point{
350- X: wb.Width(),
351- I: len(line.Content),
352- Split: true,
353- })
354+ b.isAtTop = 0 <= y0
355+}
356+
357+type bufferList struct {
358+ list []buffer
359+ current int
360+
361+ width int
362+ height int
363+}
364+
365+func newBufferList(width, height int) bufferList {
366+ return bufferList{
367+ list: []buffer{},
368+ width: width,
369+ height: height,
370 }
371 }
372
373-type Buffer struct {
374- Title string
375- Highlights int
376- Content []Line
377- Typings []string
378+func (bs *bufferList) Resize(width, height int) {
379+ bs.width = width
380+ bs.height = height
381 }
382
383-type BufferList struct {
384- List []Buffer
385- Current int
386+func (bs *bufferList) Next() {
387+ bs.current = (bs.current + 1) % len(bs.list)
388 }
389
390-func (bs *BufferList) Add(title string) (pos int, ok bool) {
391- for i, b := range bs.List {
392- if b.Title == title {
393- pos = i
394+func (bs *bufferList) Previous() {
395+ bs.current = (bs.current - 1 + len(bs.list)) % len(bs.list)
396+}
397+
398+func (bs *bufferList) Add(title string) (ok bool) {
399+ lTitle := strings.ToLower(title)
400+ for _, b := range bs.list {
401+ if strings.ToLower(b.title) == lTitle {
402 return
403 }
404 }
405
406- pos = len(bs.List)
407 ok = true
408- bs.List = append(bs.List, Buffer{Title: title})
409-
410+ bs.list = append(bs.list, buffer{title: title})
411 return
412 }
413
414-func (bs *BufferList) Remove(title string) (ok bool) {
415- for i, b := range bs.List {
416- if b.Title == title {
417+func (bs *bufferList) Remove(title string) (ok bool) {
418+ lTitle := strings.ToLower(title)
419+ for i, b := range bs.list {
420+ if strings.ToLower(b.title) == lTitle {
421 ok = true
422- bs.List = append(bs.List[:i], bs.List[i+1:]...)
423-
424- if i == bs.Current {
425- bs.Current = 0
426+ bs.list = append(bs.list[:i], bs.list[i+1:]...)
427+ if len(bs.list) <= bs.current {
428+ bs.current--
429 }
430-
431 return
432 }
433 }
434-
435 return
436 }
437
438-func (bs *BufferList) Previous() (ok bool) {
439- if bs.Current <= 0 {
440- ok = false
441+func (bs *bufferList) AddLine(title string, line Line) {
442+ idx := bs.idx(title)
443+ if idx < 0 {
444+ return
445+ }
446+
447+ b := &bs.list[idx]
448+ n := len(b.lines)
449+ line.body = strings.TrimRight(line.body, "\t ")
450+
451+ if line.isStatus && n != 0 && b.lines[n-1].isStatus {
452+ l := &b.lines[n-1]
453+ l.body += " " + line.body
454+ l.computeSplitPoints()
455+ l.width = 0
456 } else {
457- bs.Current--
458- ok = true
459+ b.lines = append(b.lines, line)
460+ if idx == bs.current && 0 < b.scrollAmt {
461+ b.scrollAmt++
462+ }
463 }
464+}
465
466- return
467+func (bs *bufferList) AddLines(title string, lines []Line) {
468+ idx := bs.idx(title)
469+ if idx < 0 {
470+ return
471+ }
472+
473+ b := &bs.list[idx]
474+ limit := len(lines)
475+
476+ if 0 < len(b.lines) {
477+ firstLineTime := b.lines[0].at.Round(time.Millisecond)
478+ for i := len(lines) - 1; 0 <= i; i-- {
479+ if firstLineTime == lines[i].at.Round(time.Millisecond) {
480+ limit = i
481+ break
482+ }
483+ }
484+ }
485+
486+ b.lines = append(lines[:limit], b.lines...)
487 }
488
489-func (bs *BufferList) Next() (ok bool) {
490- if bs.Current+1 < len(bs.List) {
491- bs.Current++
492- ok = true
493- } else {
494- ok = false
495+func (bs *bufferList) TypingStart(title, nick string) {
496+ idx := bs.idx(title)
497+ if idx < 0 {
498+ return
499 }
500+ b := &bs.list[idx]
501
502- return
503+ lNick := strings.ToLower(nick)
504+ for _, n := range b.typings {
505+ if strings.ToLower(n) == lNick {
506+ return
507+ }
508+ }
509+ b.typings = append(b.typings, nick)
510 }
511
512-func (bs *BufferList) Idx(title string) (idx int) {
513- if title == "" {
514- idx = 0
515+func (bs *bufferList) TypingStop(title, nick string) {
516+ idx := bs.idx(title)
517+ if idx < 0 {
518 return
519 }
520+ b := &bs.list[idx]
521
522- for pos, b := range bs.List {
523- if b.Title == title {
524- idx = pos
525+ lNick := strings.ToLower(nick)
526+ for i, n := range b.typings {
527+ if strings.ToLower(n) == lNick {
528+ b.typings = append(b.typings[:i], b.typings[i+1:]...)
529 return
530 }
531 }
532+}
533+
534+func (bs *bufferList) Current() (title string) {
535+ return bs.list[bs.current].title
536+}
537
538- idx = -1
539+func (bs *bufferList) CurrentOldestTime() (t *time.Time) {
540+ ls := bs.list[bs.current].lines
541+ if 0 < len(ls) {
542+ t = &ls[0].at
543+ }
544 return
545 }
546
547-func (bs *BufferList) AddLine(idx int, line string, t time.Time, isStatus bool) {
548- b := &bs.List[idx]
549- n := len(bs.List[idx].Content)
550+func (bs *bufferList) ScrollUp() {
551+ b := &bs.list[bs.current]
552+ if b.isAtTop {
553+ return
554+ }
555+ b.scrollAmt += bs.height / 2
556+}
557+
558+func (bs *bufferList) ScrollDown() {
559+ b := &bs.list[bs.current]
560+ b.scrollAmt -= bs.height / 2
561
562- line = strings.TrimRight(line, "\t ")
563+ if b.scrollAmt < 0 {
564+ b.scrollAmt = 0
565+ }
566+}
567
568- if isStatus && n != 0 && b.Content[n-1].IsStatus {
569- l := &b.Content[n-1]
570- l.Content += " " + line
571+func (bs *bufferList) IsAtTop() bool {
572+ b := &bs.list[bs.current]
573+ return b.isAtTop
574+}
575+
576+func (bs *bufferList) idx(title string) int {
577+ if title == "" {
578+ return bs.current
579+ }
580
581- lineWidth := StringWidth(line)
582- lastSP := l.SplitPoints[len(l.SplitPoints)-1]
583- sp := Point{
584- X: lastSP.X + 1 + lineWidth,
585- I: len(l.SplitPoints),
586+ lTitle := strings.ToLower(title)
587+ for i, b := range bs.list {
588+ if strings.ToLower(b.title) == lTitle {
589+ return i
590 }
591+ }
592+ return -1
593+}
594
595- l.SplitPoints = append(l.SplitPoints, sp)
596- l.Invalidate()
597- } else {
598- if n == 0 || b.Content[n-1].Time.Truncate(time.Minute) != t.Truncate(time.Minute) {
599- hour := t.Hour()
600- minute := t.Minute()
601+func (bs *bufferList) Draw(screen tcell.Screen) {
602+ bs.list[bs.current].DrawLines(screen, bs.width, bs.height-3)
603+ bs.drawStatusBar(screen, bs.height-3)
604+ bs.drawTitleList(screen, bs.height-1)
605+}
606
607- line = fmt.Sprintf("\x02%02d:%02d\x00 %s", hour, minute, line)
608+func (bs *bufferList) drawStatusBar(screen tcell.Screen, y int) {
609+ st := tcell.StyleDefault.Dim(true)
610+ nicks := bs.list[bs.current].typings
611+ verb := " is typing..."
612+ x := 0
613+
614+ if 1 < len(nicks) {
615+ verb = " are typing..."
616+ for _, nick := range nicks[:len(nicks)-2] {
617+ printString(screen, &x, y, st, nick)
618+ printString(screen, &x, y, st, ", ")
619 }
620+ printString(screen, &x, y, st, nicks[len(nicks)-2])
621+ printString(screen, &x, y, st, " and ")
622+ }
623+ if 0 < len(nicks) {
624+ printString(screen, &x, y, st, nicks[len(nicks)-1])
625+ printString(screen, &x, y, st, verb)
626+ }
627
628- l := NewLine(t, isStatus, line)
629- b.Content = append(b.Content, l)
630+ if 0 < x {
631+ screen.SetContent(x, y, 0x251c, nil, st)
632+ x++
633+ }
634+ for x < bs.width {
635+ screen.SetContent(x, y, 0x2500, nil, st)
636+ x++
637 }
638 }
639
640-func (bs *BufferList) AddHistoryLines(idx int, lines []Line) {
641- if len(lines) == 0 {
642- return
643+func (bs *bufferList) drawTitleList(screen tcell.Screen, y int) {
644+ var widths []int
645+ for _, b := range bs.list {
646+ width := StringWidth(b.title)
647+ if 0 < b.highlights {
648+ width += int(math.Log10(float64(b.highlights))) + 1
649+ }
650+ widths = append(widths, width)
651 }
652
653- b := &bs.List[idx]
654- limit := -1
655+ st := tcell.StyleDefault
656
657- if len(b.Content) != 0 {
658- firstTime := b.Content[0].Time.Round(time.Millisecond)
659- for i := len(lines) - 1; i >= 0; i-- {
660- if firstTime == lines[i].Time.Round(time.Millisecond) {
661- limit = i
662- break
663- }
664- }
665+ for x := 0; x < bs.width; x++ {
666+ screen.SetContent(x, y, ' ', nil, st)
667 }
668
669- if limit == -1 {
670- limit = len(lines)
671+ x := (bs.width - widths[bs.current]) / 2
672+ printString(screen, &x, y, st.Underline(true), bs.list[bs.current].title)
673+ x += 2
674+
675+ i := (bs.current + 1) % len(bs.list)
676+ for x < bs.width && i != bs.current {
677+ b := &bs.list[i]
678+ st = tcell.StyleDefault
679+ if b.unread {
680+ st = st.Bold(true)
681+ }
682+ printString(screen, &x, y, st, b.title)
683+ if 0 < b.highlights {
684+ st = st.Foreground(tcell.ColorRed).Reverse(true)
685+ printNumber(screen, &x, y, st, b.highlights)
686+ }
687+ x += 2
688+ i = (i + 1) % len(bs.list)
689 }
690
691- bs.List[idx].Content = append(lines[:limit], b.Content...)
692+ i = (bs.current - 1 + len(bs.list)) % len(bs.list)
693+ x = (bs.width - widths[bs.current]) / 2
694+ for 0 < x && i != bs.current {
695+ x -= widths[i] + 2
696+ b := &bs.list[i]
697+ st = tcell.StyleDefault
698+ if b.unread {
699+ st = st.Bold(true)
700+ }
701+ printString(screen, &x, y, st, b.title)
702+ if 0 < b.highlights {
703+ st = st.Foreground(tcell.ColorRed).Reverse(true)
704+ printNumber(screen, &x, y, st, b.highlights)
705+ }
706+ x -= widths[i]
707+ i = (i - 1 + len(bs.list)) % len(bs.list)
708+ }
709 }
710
711-func (bs *BufferList) Invalidate() {
712- for i := range bs.List {
713- for j := range bs.List[i].Content {
714- bs.List[i].Content[j].Invalidate()
715- }
716+func printString(screen tcell.Screen, x *int, y int, st tcell.Style, s string) {
717+ for _, r := range s {
718+ screen.SetContent(*x, y, r, nil, st)
719+ *x += runewidth.RuneWidth(r)
720 }
721 }
722
723-func (bs *BufferList) TypingStart(idx int, nick string) {
724- b := &bs.List[idx]
725+func printNumber(screen tcell.Screen, x *int, y int, st tcell.Style, n int) {
726+ s := fmt.Sprintf("%d", n)
727+ printString(screen, x, y, st, s)
728+}
729
730- for _, n := range b.Typings {
731- if n == nick {
732- return
733- }
734- }
735+func printTime(screen tcell.Screen, x int, y int, st tcell.Style, t time.Time) {
736+ hr0 := rune(t.Hour()/10) + '0'
737+ hr1 := rune(t.Hour()%10) + '0'
738+ mn0 := rune(t.Minute()/10) + '0'
739+ mn1 := rune(t.Minute()%10) + '0'
740+ screen.SetContent(x+0, y, hr0, nil, st)
741+ screen.SetContent(x+1, y, hr1, nil, st)
742+ screen.SetContent(x+2, y, ':', nil, st)
743+ screen.SetContent(x+3, y, mn0, nil, st)
744+ screen.SetContent(x+4, y, mn1, nil, st)
745+}
746
747- b.Typings = append(b.Typings, nick)
748+func truncate(s string, w int) string {
749+ c := runewidth.Condition{ZeroWidthJoiner: true}
750+ return c.Truncate(s, w, "\u2026")
751 }
752
753-func (bs *BufferList) TypingStop(idx int, nick string) {
754- b := &bs.List[idx]
755+func identColor(s string) (code int) {
756+ h := fnv.New32()
757+ _, _ = h.Write([]byte(s))
758
759- for i, n := range b.Typings {
760- if n == nick {
761- b.Typings = append(b.Typings[:i], b.Typings[i+1:]...)
762- return
763- }
764+ code = int(h.Sum32()) % 96
765+ if 1 <= code {
766+ code++
767 }
768+ if 8 <= code {
769+ code++
770+ }
771+
772+ return
773+}
774+
775+func ircColorCode(code int) string {
776+ var c [3]rune
777+ c[0] = 0x03
778+ c[1] = rune(code/10) + '0'
779+ c[2] = rune(code%10) + '0'
780+ return string(c[:])
781 }
+121,
-83
1@@ -1,42 +1,48 @@
2 package ui
3
4-import "testing"
5+import (
6+ "strings"
7+ "testing"
8+)
9
10-func assertSplitPoints(t *testing.T, line string, expected []Point) {
11- l := Line{Content: line}
12+func assertSplitPoints(t *testing.T, body string, expected []point) {
13+ l := Line{body: body}
14 l.computeSplitPoints()
15
16- if len(l.SplitPoints) != len(expected) {
17- t.Errorf("%q: expected %d split points got %d", line, len(expected), len(l.SplitPoints))
18+ if len(l.splitPoints) != len(expected) {
19+ t.Errorf("%q: expected %d split points got %d", body, len(expected), len(l.splitPoints))
20 return
21 }
22
23 for i := 0; i < len(expected); i++ {
24 e := expected[i]
25- a := l.SplitPoints[i]
26+ a := l.splitPoints[i]
27
28 if e.X != a.X {
29- t.Errorf("%q, point #%d: expected X=%d got %d", line, i, e.X, a.X)
30+ t.Errorf("%q, point #%d: expected X=%d got %d", body, i, e.X, a.X)
31 }
32 if e.I != a.I {
33- t.Errorf("%q, point #%d: expected I=%d got %d", line, i, e.I, a.I)
34+ t.Errorf("%q, point #%d: expected I=%d got %d", body, i, e.I, a.I)
35 }
36 if e.Split != a.Split {
37- t.Errorf("%q, point #%d: expected Split=%t got %t", line, i, e.Split, a.Split)
38+ t.Errorf("%q, point #%d: expected Split=%t got %t", body, i, e.Split, a.Split)
39 }
40 }
41 }
42
43 func TestLineSplitPoints(t *testing.T) {
44- assertSplitPoints(t, "hello", []Point{
45+ assertSplitPoints(t, "hello", []point{
46+ {X: 0, I: 0, Split: false},
47 {X: 5, I: 5, Split: true},
48 })
49- assertSplitPoints(t, "hello world", []Point{
50+ assertSplitPoints(t, "hello world", []point{
51+ {X: 0, I: 0, Split: false},
52 {X: 5, I: 5, Split: true},
53 {X: 6, I: 6, Split: false},
54 {X: 11, I: 11, Split: true},
55 })
56- assertSplitPoints(t, "lorem ipsum dolor shit amet", []Point{
57+ assertSplitPoints(t, "lorem ipsum dolor shit amet", []point{
58+ {X: 0, I: 0, Split: false},
59 {X: 5, I: 5, Split: true},
60 {X: 6, I: 6, Split: false},
61 {X: 11, I: 11, Split: true},
62@@ -49,90 +55,122 @@ func TestLineSplitPoints(t *testing.T) {
63 })
64 }
65
66-func assertRenderedHeight(t *testing.T, line string, width int, expected int) {
67- l := Line{Content: line}
68+func showSplit(s string, nls []int) string {
69+ var sb strings.Builder
70+ sb.Grow(len(s) + len(nls))
71+
72+ for i, r := range s {
73+ if 0 < len(nls) && i == nls[0] {
74+ sb.WriteRune('|')
75+ nls = nls[1:]
76+ }
77+ sb.WriteRune(r)
78+ }
79+
80+ return sb.String()
81+}
82+
83+func assertNewLines(t *testing.T, body string, width int, expected int) {
84+ l := Line{body: body}
85 l.computeSplitPoints()
86- l.Invalidate()
87
88- actual := l.RenderedHeight(width)
89+ actual := l.NewLines(width)
90
91- if actual != expected {
92- t.Errorf("%q with width=%d expected to take %d lines, takes %d", line, width, expected, actual)
93+ if len(actual)+1 != expected {
94+ s := showSplit(body, actual)
95+ t.Errorf("%q with width=%d expected to take %d lines, takes %d: '%s' (%v)", body, width, expected, len(actual)+1, s, actual)
96+ return
97 }
98 }
99
100 func TestRenderedHeight(t *testing.T) {
101- assertRenderedHeight(t, "0123456789", 1, 10)
102- assertRenderedHeight(t, "0123456789", 2, 5)
103- assertRenderedHeight(t, "0123456789", 3, 4)
104- assertRenderedHeight(t, "0123456789", 4, 3)
105- assertRenderedHeight(t, "0123456789", 5, 2)
106- assertRenderedHeight(t, "0123456789", 6, 2)
107- assertRenderedHeight(t, "0123456789", 7, 2)
108- assertRenderedHeight(t, "0123456789", 8, 2)
109- assertRenderedHeight(t, "0123456789", 9, 2)
110- assertRenderedHeight(t, "0123456789", 10, 1)
111- assertRenderedHeight(t, "0123456789", 11, 1)
112+ assertNewLines(t, "0123456789", 1, 10)
113+ assertNewLines(t, "0123456789", 2, 5)
114+ assertNewLines(t, "0123456789", 3, 4)
115+ assertNewLines(t, "0123456789", 4, 3)
116+ assertNewLines(t, "0123456789", 5, 2)
117+ assertNewLines(t, "0123456789", 6, 2)
118+ assertNewLines(t, "0123456789", 7, 2)
119+ assertNewLines(t, "0123456789", 8, 2)
120+ assertNewLines(t, "0123456789", 9, 2)
121+ assertNewLines(t, "0123456789", 10, 1)
122+ assertNewLines(t, "0123456789", 11, 1)
123
124 // LEN=9, WIDTH=9
125- assertRenderedHeight(t, "take care", 1, 8) // |t|a|k|e|c|a|r|e|
126- assertRenderedHeight(t, "take care", 2, 4) // |ta|ke|ca|re|
127- assertRenderedHeight(t, "take care", 3, 3) // |tak|e c|are|
128- assertRenderedHeight(t, "take care", 4, 2) // |take|care|
129- assertRenderedHeight(t, "take care", 5, 2) // |take |care |
130- assertRenderedHeight(t, "take care", 6, 2) // |take |care |
131- assertRenderedHeight(t, "take care", 7, 2) // |take |care |
132- assertRenderedHeight(t, "take care", 8, 2) // |take |care |
133- assertRenderedHeight(t, "take care", 9, 1) // |take care|
134- assertRenderedHeight(t, "take care", 10, 1) // |take care |
135+ assertNewLines(t, "take care", 1, 8) // |t|a|k|e|c|a|r|e|
136+ assertNewLines(t, "take care", 2, 4) // |ta|ke|ca|re|
137+ assertNewLines(t, "take care", 3, 3) // |tak|e c|are|
138+ assertNewLines(t, "take care", 4, 2) // |take|care|
139+ assertNewLines(t, "take care", 5, 2) // |take |care |
140+ assertNewLines(t, "take care", 6, 2) // |take |care |
141+ assertNewLines(t, "take care", 7, 2) // |take |care |
142+ assertNewLines(t, "take care", 8, 2) // |take |care |
143+ assertNewLines(t, "take care", 9, 1) // |take care|
144+ assertNewLines(t, "take care", 10, 1) // |take care |
145
146 // LEN=10, WIDTH=10
147- assertRenderedHeight(t, "take care", 1, 8) // |t|a|k|e|c|a|r|e|
148- assertRenderedHeight(t, "take care", 2, 4) // |ta|ke|ca|re|
149- assertRenderedHeight(t, "take care", 3, 4) // |tak|e |car|e |
150- assertRenderedHeight(t, "take care", 4, 2) // |take|care|
151- assertRenderedHeight(t, "take care", 5, 2) // |take |care |
152- assertRenderedHeight(t, "take care", 6, 2) // |take |care |
153- assertRenderedHeight(t, "take care", 7, 2) // |take |care |
154- assertRenderedHeight(t, "take care", 8, 2) // |take |care |
155- assertRenderedHeight(t, "take care", 9, 2) // |take |care |
156- assertRenderedHeight(t, "take care", 10, 1) // |take care|
157- assertRenderedHeight(t, "take care", 11, 1) // |take care |
158+ assertNewLines(t, "take care", 1, 8) // |t|a|k|e|c|a|r|e|
159+ assertNewLines(t, "take care", 2, 4) // |ta|ke|ca|re|
160+ assertNewLines(t, "take care", 3, 4) // |tak|e |car|e |
161+ assertNewLines(t, "take care", 4, 2) // |take|care|
162+ assertNewLines(t, "take care", 5, 2) // |take |care |
163+ assertNewLines(t, "take care", 6, 2) // |take |care |
164+ assertNewLines(t, "take care", 7, 2) // |take |care |
165+ assertNewLines(t, "take care", 8, 2) // |take |care |
166+ assertNewLines(t, "take care", 9, 2) // |take |care |
167+ assertNewLines(t, "take care", 10, 1) // |take care|
168+ assertNewLines(t, "take care", 11, 1) // |take care |
169
170 // LEN=16, WIDTH=16
171- assertRenderedHeight(t, "have a good day!", 1, 13) // |h|a|v|e|a|g|o|o|d|d|a|y|!|
172- assertRenderedHeight(t, "have a good day!", 2, 7) // |ha|ve|a |go|od|da|y!|
173- assertRenderedHeight(t, "have a good day!", 3, 5) // |hav|e a|goo|d d|ay!|
174- assertRenderedHeight(t, "have a good day!", 4, 4) // |have|a |good|day!|
175- assertRenderedHeight(t, "have a good day!", 5, 4) // |have |a |good |day! |
176- assertRenderedHeight(t, "have a good day!", 6, 3) // |have a|good |day! |
177- assertRenderedHeight(t, "have a good day!", 7, 3) // |have a |good |day! |
178- assertRenderedHeight(t, "have a good day!", 8, 3) // |have a |good |day! |
179- assertRenderedHeight(t, "have a good day!", 9, 2) // |have a |good day!|
180- assertRenderedHeight(t, "have a good day!", 10, 2) // |have a |good day! |
181- assertRenderedHeight(t, "have a good day!", 11, 2) // |have a good|day! |
182- assertRenderedHeight(t, "have a good day!", 12, 2) // |have a good |day! |
183- assertRenderedHeight(t, "have a good day!", 13, 2) // |have a good |day! |
184- assertRenderedHeight(t, "have a good day!", 14, 2) // |have a good |day! |
185- assertRenderedHeight(t, "have a good day!", 15, 2) // |have a good |day! |
186- assertRenderedHeight(t, "have a good day!", 16, 1) // |have a good day!|
187- assertRenderedHeight(t, "have a good day!", 17, 1) // |have a good day! |
188+ assertNewLines(t, "have a good day!", 1, 13) // |h|a|v|e|a|g|o|o|d|d|a|y|!|
189+ assertNewLines(t, "have a good day!", 2, 7) // |ha|ve|a |go|od|da|y!|
190+ assertNewLines(t, "have a good day!", 3, 5) // |hav|e a|goo|d d|ay!|
191+ assertNewLines(t, "have a good day!", 4, 4) // |have|a |good|day!|
192+ assertNewLines(t, "have a good day!", 5, 4) // |have |a |good |day! |
193+ assertNewLines(t, "have a good day!", 6, 3) // |have a|good |day! |
194+ assertNewLines(t, "have a good day!", 7, 3) // |have a |good |day! |
195+ assertNewLines(t, "have a good day!", 8, 3) // |have a |good |day! |
196+ assertNewLines(t, "have a good day!", 9, 2) // |have a |good day!|
197+ assertNewLines(t, "have a good day!", 10, 2) // |have a |good day! |
198+ assertNewLines(t, "have a good day!", 11, 2) // |have a good|day! |
199+ assertNewLines(t, "have a good day!", 12, 2) // |have a good |day! |
200+ assertNewLines(t, "have a good day!", 13, 2) // |have a good |day! |
201+ assertNewLines(t, "have a good day!", 14, 2) // |have a good |day! |
202+ assertNewLines(t, "have a good day!", 15, 2) // |have a good |day! |
203+ assertNewLines(t, "have a good day!", 16, 1) // |have a good day!|
204+ assertNewLines(t, "have a good day!", 17, 1) // |have a good day! |
205
206 // LEN=15, WIDTH=11
207- assertRenderedHeight(t, "\x0342barmand\x03: cc", 1, 10) // |b|a|r|m|a|n|d|:|c|c|
208- assertRenderedHeight(t, "\x0342barmand\x03: cc", 2, 5) // |ba|rm|an|d:|cc|
209- assertRenderedHeight(t, "\x0342barmand\x03: cc", 3, 4) // |bar|man|d: |cc |
210- assertRenderedHeight(t, "\x0342barmand\x03: cc", 4, 3) // |barm|and:|cc |
211- assertRenderedHeight(t, "\x0342barmand\x03: cc", 5, 3) // |barma|nd: |cc |
212- assertRenderedHeight(t, "\x0342barmand\x03: cc", 6, 2) // |barman|d: cc |
213- assertRenderedHeight(t, "\x0342barmand\x03: cc", 7, 2) // |barmand|: cc |
214- assertRenderedHeight(t, "\x0342barmand\x03: cc", 8, 2) // |barmand:|cc |
215- assertRenderedHeight(t, "\x0342barmand\x03: cc", 9, 2) // |barmand: |cc |
216- assertRenderedHeight(t, "\x0342barmand\x03: cc", 10, 2) // |barmand: |cc |
217- assertRenderedHeight(t, "\x0342barmand\x03: cc", 11, 1) // |barmand: cc|
218- assertRenderedHeight(t, "\x0342barmand\x03: cc", 12, 1) // |barmand: cc |
219- assertRenderedHeight(t, "\x0342barmand\x03: cc", 13, 1) // |barmand: cc |
220- assertRenderedHeight(t, "\x0342barmand\x03: cc", 14, 1) // |barmand: cc |
221- assertRenderedHeight(t, "\x0342barmand\x03: cc", 15, 1) // |barmand: cc |
222- assertRenderedHeight(t, "\x0342barmand\x03: cc", 16, 1) // |barmand: cc |
223+ assertNewLines(t, "\x0342barmand\x03: cc", 1, 10) // |b|a|r|m|a|n|d|:|c|c|
224+ assertNewLines(t, "\x0342barmand\x03: cc", 2, 5) // |ba|rm|an|d:|cc|
225+ assertNewLines(t, "\x0342barmand\x03: cc", 3, 4) // |bar|man|d: |cc |
226+ assertNewLines(t, "\x0342barmand\x03: cc", 4, 3) // |barm|and:|cc |
227+ assertNewLines(t, "\x0342barmand\x03: cc", 5, 3) // |barma|nd: |cc |
228+ assertNewLines(t, "\x0342barmand\x03: cc", 6, 2) // |barman|d: cc |
229+ assertNewLines(t, "\x0342barmand\x03: cc", 7, 2) // |barmand|: cc |
230+ assertNewLines(t, "\x0342barmand\x03: cc", 8, 2) // |barmand:|cc |
231+ assertNewLines(t, "\x0342barmand\x03: cc", 9, 2) // |barmand: |cc |
232+ assertNewLines(t, "\x0342barmand\x03: cc", 10, 2) // |barmand: |cc |
233+ assertNewLines(t, "\x0342barmand\x03: cc", 11, 1) // |barmand: cc|
234+ assertNewLines(t, "\x0342barmand\x03: cc", 12, 1) // |barmand: cc |
235+ assertNewLines(t, "\x0342barmand\x03: cc", 13, 1) // |barmand: cc |
236+ assertNewLines(t, "\x0342barmand\x03: cc", 14, 1) // |barmand: cc |
237+ assertNewLines(t, "\x0342barmand\x03: cc", 15, 1) // |barmand: cc |
238+ assertNewLines(t, "\x0342barmand\x03: cc", 16, 1) // |barmand: cc |
239+
240+ assertNewLines(t, "cc en direct du word wrapping des familles le tests ça v a va va v a va", 46, 2)
241+}
242+
243+/*
244+func assertTrimWidth(t *testing.T, s string, w int, expected string) {
245+ actual := trimWidth(s, w)
246+ if actual != expected {
247+ t.Errorf("%q (width=%d): expected to be trimmed as %q, got %q\n", s, w, expected, actual)
248+ }
249+}
250+
251+func TestTrimWidth(t *testing.T) {
252+ assertTrimWidth(t, "ludovicchabant/fn", 16, "ludovicchabant/…")
253+ assertTrimWidth(t, "zzzzzzzzzzzzzz黒猫/sr", 16, "zzzzzzzzzzzzzz黒…")
254 }
255+// */
+191,
-0
1@@ -0,0 +1,191 @@
2+package ui
3+
4+import (
5+ "github.com/gdamore/tcell"
6+ "github.com/mattn/go-runewidth"
7+)
8+
9+type widthBuffer struct {
10+ width int
11+ color colorBuffer
12+}
13+
14+func (wb *widthBuffer) Width() int {
15+ return wb.width
16+}
17+
18+func (wb *widthBuffer) WriteString(s string) {
19+ for _, r := range s {
20+ wb.WriteRune(r)
21+ }
22+}
23+
24+func (wb *widthBuffer) WriteRune(r rune) {
25+ if ok := wb.color.WriteRune(r); ok != 0 {
26+ if 1 < ok {
27+ wb.width++
28+ }
29+ wb.width += runewidth.RuneWidth(r)
30+ }
31+}
32+
33+func StringWidth(s string) int {
34+ var wb widthBuffer
35+ wb.WriteString(s)
36+ return wb.Width()
37+}
38+
39+type styleBuffer struct {
40+ st tcell.Style
41+ color colorBuffer
42+ bold bool
43+ italic bool
44+ underline bool
45+}
46+
47+func (sb *styleBuffer) Reset() {
48+ sb.color.Reset()
49+ sb.st = tcell.StyleDefault
50+ sb.bold = false
51+ sb.italic = false
52+ sb.underline = false
53+}
54+
55+func (sb *styleBuffer) WriteRune(r rune) (st tcell.Style, ok int) {
56+ if r == 0x00 || r == 0x0F {
57+ sb.Reset()
58+ return sb.st, 0
59+ }
60+ if r == 0x02 {
61+ sb.bold = !sb.bold
62+ sb.st = sb.st.Bold(sb.bold)
63+ return sb.st, 0
64+ }
65+ if r == 0x1D {
66+ sb.italic = !sb.italic
67+ //sb.st = st.Italic(sb.italic)
68+ return sb.st, 0
69+ }
70+ if r == 0x1F {
71+ sb.underline = !sb.underline
72+ sb.st = st.Underline(sb.underline)
73+ return sb.st, 0
74+ }
75+ if ok = sb.color.WriteRune(r); ok != 0 {
76+ sb.st = sb.color.Style(sb.st)
77+ }
78+
79+ return sb.st, ok
80+}
81+
82+type colorBuffer struct {
83+ state int
84+ fg, bg int
85+}
86+
87+func (cb *colorBuffer) Reset() {
88+ cb.state = 0
89+ cb.fg = -1
90+ cb.bg = -1
91+}
92+
93+func (cb *colorBuffer) Style(st tcell.Style) tcell.Style {
94+ if 0 <= cb.fg {
95+ st = st.Foreground(colorFromCode(cb.fg))
96+ }
97+ if 0 <= cb.bg {
98+ st = st.Background(colorFromCode(cb.bg))
99+ }
100+ return st
101+}
102+
103+func (cb *colorBuffer) WriteRune(r rune) (ok int) {
104+ if cb.state == 1 {
105+ if '0' <= r && r <= '9' {
106+ cb.fg = int(r - '0')
107+ cb.state = 2
108+ return
109+ }
110+ } else if cb.state == 2 {
111+ if '0' <= r && r <= '9' {
112+ cb.fg = 10*cb.fg + int(r-'0')
113+ cb.state = 3
114+ return
115+ }
116+ if r == ',' {
117+ cb.state = 4
118+ return
119+ }
120+ } else if cb.state == 3 {
121+ if r == ',' {
122+ cb.state = 4
123+ return
124+ }
125+ } else if cb.state == 4 {
126+ if '0' <= r && r <= '9' {
127+ cb.bg = int(r - '0')
128+ cb.state = 5
129+ return
130+ }
131+ ok++
132+ } else if cb.state == 5 {
133+ cb.state = 0
134+ if '0' <= r && r <= '9' {
135+ cb.bg = 10*cb.bg + int(r-'0')
136+ return
137+ }
138+ }
139+
140+ if r == 0x03 {
141+ cb.state = 1
142+ cb.fg = -1
143+ cb.bg = -1
144+ return
145+ }
146+
147+ cb.state = 0
148+ ok++
149+ return
150+}
151+
152+func colorFromCode(code int) (color tcell.Color) {
153+ switch code {
154+ case 0:
155+ color = tcell.ColorWhite
156+ case 1:
157+ color = tcell.ColorBlack
158+ case 2:
159+ color = tcell.ColorBlue
160+ case 3:
161+ color = tcell.ColorGreen
162+ case 4:
163+ color = tcell.ColorRed
164+ case 5:
165+ color = tcell.ColorBrown
166+ case 6:
167+ color = tcell.ColorPurple
168+ case 7:
169+ color = tcell.ColorOrange
170+ case 8:
171+ color = tcell.ColorYellow
172+ case 9:
173+ color = tcell.ColorLightGreen
174+ case 10:
175+ color = tcell.ColorTeal
176+ case 11:
177+ color = tcell.ColorFuchsia
178+ case 12:
179+ color = tcell.ColorLightBlue
180+ case 13:
181+ color = tcell.ColorPink
182+ case 14:
183+ color = tcell.ColorGrey
184+ case 15:
185+ color = tcell.ColorLightGrey
186+ case 99:
187+ color = tcell.ColorDefault
188+ default:
189+ color = tcell.Color(code)
190+ }
191+ return
192+}
R ui/width_test.go =>
ui/style_test.go
+0,
-0
M
ui/ui.go
+47,
-463
1@@ -1,11 +1,11 @@
2 package ui
3
4 import (
5- "github.com/gdamore/tcell"
6- "github.com/mattn/go-runewidth"
7 "math/rand"
8 "sync/atomic"
9 "time"
10+
11+ "github.com/gdamore/tcell"
12 )
13
14 type UI struct {
15@@ -13,11 +13,8 @@ type UI struct {
16 Events chan tcell.Event
17 exit atomic.Value // bool
18
19- bufferList BufferList
20- scrollAmt int
21- scrollAtTop bool
22-
23- e editor
24+ bs bufferList
25+ e editor
26 }
27
28 func New() (ui *UI, err error) {
29@@ -47,15 +44,9 @@ func New() (ui *UI, err error) {
30 ui.exit.Store(false)
31
32 hmIdx := rand.Intn(len(homeMessages))
33- ui.bufferList = BufferList{
34- List: []Buffer{
35- {
36- Title: "home",
37- Highlights: 0,
38- Content: []Line{NewLineNow(homeMessages[hmIdx])},
39- },
40- },
41- }
42+ ui.bs = newBufferList(w, h)
43+ ui.bs.Add(Home)
44+ ui.bs.AddLine("", NewLineNow("--", homeMessages[hmIdx]))
45
46 ui.e = newEditor(w)
47
48@@ -76,147 +67,70 @@ func (ui *UI) Close() {
49 ui.screen.Fini()
50 }
51
52-func (ui *UI) CurrentBuffer() (title string) {
53- title = ui.bufferList.List[ui.bufferList.Current].Title
54- return
55+func (ui *UI) CurrentBuffer() string {
56+ return ui.bs.Current()
57 }
58
59-func (ui *UI) CurrentBufferOldestTime() (t time.Time) {
60- b := ui.bufferList.List[ui.bufferList.Current].Content
61- if len(b) == 0 {
62- t = time.Now()
63- } else {
64- t = b[0].Time
65- }
66- return
67+func (ui *UI) CurrentBufferOldestTime() (t *time.Time) {
68+ return ui.bs.CurrentOldestTime()
69 }
70
71-func (ui *UI) NextBuffer() (ok bool) {
72- ok = ui.bufferList.Next()
73- if ok {
74- ui.scrollAmt = 0
75- ui.scrollAtTop = false
76- ui.drawTyping()
77- ui.drawBuffer()
78- ui.drawStatus()
79- }
80- return
81+func (ui *UI) NextBuffer() {
82+ ui.bs.Next()
83+ ui.draw()
84 }
85
86-func (ui *UI) PreviousBuffer() (ok bool) {
87- ok = ui.bufferList.Previous()
88- if ok {
89- ui.scrollAmt = 0
90- ui.scrollAtTop = false
91- ui.drawTyping()
92- ui.drawBuffer()
93- ui.drawStatus()
94- }
95- return
96+func (ui *UI) PreviousBuffer() {
97+ ui.bs.Previous()
98+ ui.draw()
99 }
100
101 func (ui *UI) ScrollUp() {
102- if ui.scrollAtTop {
103- return
104- }
105-
106- _, h := ui.screen.Size()
107- ui.scrollAmt += h / 2
108- ui.drawBuffer()
109+ ui.bs.ScrollUp()
110+ ui.draw()
111 }
112
113 func (ui *UI) ScrollDown() {
114- if ui.scrollAmt == 0 {
115- return
116- }
117-
118- _, h := ui.screen.Size()
119- ui.scrollAmt -= h / 2
120- if ui.scrollAmt < 0 {
121- ui.scrollAmt = 0
122- }
123- ui.scrollAtTop = false
124-
125- ui.drawBuffer()
126+ ui.bs.ScrollDown()
127+ ui.draw()
128 }
129
130 func (ui *UI) IsAtTop() bool {
131- return ui.scrollAtTop
132+ return ui.bs.IsAtTop()
133 }
134
135 func (ui *UI) AddBuffer(title string) {
136- _, ok := ui.bufferList.Add(title)
137+ ok := ui.bs.Add(title)
138 if ok {
139- ui.drawStatus()
140- ui.drawTyping()
141- ui.drawBuffer() // TODO only invalidate buffer list
142+ ui.draw()
143 }
144 }
145
146 func (ui *UI) RemoveBuffer(title string) {
147- ok := ui.bufferList.Remove(title)
148+ ok := ui.bs.Remove(title)
149 if ok {
150- ui.drawStatus()
151- ui.drawTyping()
152- ui.drawBuffer()
153+ ui.draw()
154 }
155 }
156
157-func (ui *UI) AddLine(buffer string, line string, t time.Time, isStatus bool) {
158- idx := ui.bufferList.Idx(buffer)
159- if idx < 0 {
160- return
161- }
162-
163- ui.bufferList.AddLine(idx, line, t, isStatus)
164+func (ui *UI) AddLine(buffer string, line Line) {
165+ ui.bs.AddLine(buffer, line)
166+ ui.draw()
167+}
168
169- if idx == ui.bufferList.Current {
170- if 0 < ui.scrollAmt {
171- ui.scrollAmt++
172- } else {
173- ui.drawBuffer()
174- }
175- }
176+func (ui *UI) AddLines(buffer string, lines []Line) {
177+ ui.bs.AddLines(buffer, lines)
178+ ui.draw()
179 }
180
181 func (ui *UI) TypingStart(buffer, nick string) {
182- idx := ui.bufferList.Idx(buffer)
183- if idx < 0 {
184- return
185- }
186-
187- ui.bufferList.TypingStart(idx, nick)
188-
189- if idx == ui.bufferList.Current {
190- ui.drawTyping()
191- }
192+ ui.bs.TypingStart(buffer, nick)
193+ ui.draw()
194 }
195
196 func (ui *UI) TypingStop(buffer, nick string) {
197- idx := ui.bufferList.Idx(buffer)
198- if idx < 0 {
199- return
200- }
201-
202- ui.bufferList.TypingStop(idx, nick)
203-
204- if idx == ui.bufferList.Current {
205- ui.drawTyping()
206- }
207-}
208-
209-func (ui *UI) AddHistoryLines(buffer string, lines []Line) {
210- idx := ui.bufferList.Idx(buffer)
211- if idx < 0 {
212- return
213- }
214-
215- ui.bufferList.AddHistoryLines(idx, lines)
216-
217- if idx == ui.bufferList.Current {
218- ui.scrollAtTop = false
219- ui.drawBuffer()
220- }
221+ ui.bs.TypingStop(buffer, nick)
222+ ui.draw()
223 }
224
225 func (ui *UI) InputIsCommand() bool {
226@@ -229,337 +143,48 @@ func (ui *UI) InputLen() int {
227
228 func (ui *UI) InputRune(r rune) {
229 ui.e.PutRune(r)
230- _, h := ui.screen.Size()
231- ui.e.Draw(ui.screen, h-2)
232- ui.screen.Show()
233+ ui.draw()
234 }
235
236 func (ui *UI) InputRight() {
237 ui.e.Right()
238- _, h := ui.screen.Size()
239- ui.e.Draw(ui.screen, h-2)
240- ui.screen.Show()
241+ ui.draw()
242 }
243
244 func (ui *UI) InputLeft() {
245 ui.e.Left()
246- _, h := ui.screen.Size()
247- ui.e.Draw(ui.screen, h-2)
248- ui.screen.Show()
249+ ui.draw()
250 }
251
252 func (ui *UI) InputBackspace() (ok bool) {
253 ok = ui.e.RemRune()
254 if ok {
255- _, h := ui.screen.Size()
256- ui.e.Draw(ui.screen, h-2)
257- ui.screen.Show()
258+ ui.draw()
259 }
260 return
261 }
262
263 func (ui *UI) InputEnter() (content string) {
264 content = ui.e.Flush()
265- _, h := ui.screen.Size()
266- ui.e.Draw(ui.screen, h-2)
267- ui.screen.Show()
268+ ui.draw()
269 return
270 }
271
272 func (ui *UI) Resize() {
273- w, _ := ui.screen.Size()
274+ w, h := ui.screen.Size()
275 ui.e.Resize(w)
276- ui.bufferList.Invalidate()
277- ui.scrollAmt = 0
278+ ui.bs.Resize(w, h)
279 ui.draw()
280 }
281
282 func (ui *UI) draw() {
283 _, h := ui.screen.Size()
284- ui.drawStatus()
285 ui.e.Draw(ui.screen, h-2)
286- ui.drawTyping()
287- ui.drawBuffer()
288-}
289-
290-func (ui *UI) drawTyping() {
291- st := tcell.StyleDefault.Dim(true)
292- w, h := ui.screen.Size()
293- if w == 0 {
294- return
295- }
296-
297- nicks := ui.bufferList.List[ui.bufferList.Current].Typings
298- if len(nicks) == 0 {
299- return
300- }
301-
302- x := 0
303- y := h - 3
304-
305- if 1 < len(nicks) {
306- for _, nick := range nicks[:len(nicks)-2] {
307- for _, r := range nick {
308- if w <= x {
309- return
310- }
311- ui.screen.SetContent(x, y, r, nil, st)
312- x += runewidth.RuneWidth(r)
313- }
314-
315- if w <= x {
316- return
317- }
318- ui.screen.SetContent(x, y, ',', nil, st)
319- x++
320- if w <= x {
321- return
322- }
323- ui.screen.SetContent(x, y, ' ', nil, st)
324- x++
325- }
326-
327- for _, r := range nicks[len(nicks)-2] {
328- if w <= x {
329- return
330- }
331- ui.screen.SetContent(x, y, r, nil, st)
332- x += runewidth.RuneWidth(r)
333- }
334-
335- if w <= x {
336- return
337- }
338- ui.screen.SetContent(x, y, ' ', nil, st)
339- x++
340- if w <= x {
341- return
342- }
343- ui.screen.SetContent(x, y, 'a', nil, st)
344- x++
345- if w <= x {
346- return
347- }
348- ui.screen.SetContent(x, y, 'n', nil, st)
349- x++
350- if w <= x {
351- return
352- }
353- ui.screen.SetContent(x, y, 'd', nil, st)
354- x++
355- if w <= x {
356- return
357- }
358- ui.screen.SetContent(x, y, ' ', nil, st)
359- x++
360- }
361-
362- for _, r := range nicks[len(nicks)-1] {
363- if w <= x {
364- return
365- }
366- ui.screen.SetContent(x, y, r, nil, st)
367- x += runewidth.RuneWidth(r)
368- }
369-
370- verb := " are typing..."
371- if len(nicks) == 1 {
372- verb = " is typing..."
373- }
374-
375- for _, r := range verb {
376- if w <= x {
377- return
378- }
379- ui.screen.SetContent(x, y, r, nil, st)
380- x += runewidth.RuneWidth(r)
381- }
382-
383- for ; x < w; x++ {
384- ui.screen.SetContent(x, y, ' ', nil, st)
385- }
386-
387- ui.screen.Show()
388-}
389-
390-func (ui *UI) drawBuffer() {
391- st := tcell.StyleDefault
392- w, h := ui.screen.Size()
393- if h < 3 {
394- return
395- }
396-
397- for x := 0; x < w; x++ {
398- for y := 0; y < h-3; y++ {
399- ui.screen.SetContent(x, y, ' ', nil, st)
400- }
401- }
402-
403- b := ui.bufferList.List[ui.bufferList.Current]
404-
405- if len(b.Content) == 0 {
406- ui.scrollAtTop = true
407- return
408- }
409-
410- var bold, italic, underline bool
411- var colorState int
412- var fgColor, bgColor int
413-
414- yEnd := h - 3
415- y0 := ui.scrollAmt + h - 3
416-
417- for i := len(b.Content) - 1; 0 <= i; i-- {
418- line := &b.Content[i]
419-
420- if y0 < 0 {
421- break
422- }
423-
424- lineHeight := line.RenderedHeight(w)
425- y0 -= lineHeight
426- if yEnd <= y0 {
427- continue
428- }
429-
430- rs := []rune(line.Content)
431- x := 0
432- y := y0
433- var lastSP Point
434- spIdx := 0
435-
436- for i, r := range rs {
437- if i == line.SplitPoints[spIdx].I {
438- lastSP = line.SplitPoints[spIdx]
439- spIdx++
440-
441- l := line.SplitPoints[spIdx].X - lastSP.X
442-
443- if w < l {
444- } else if w == l {
445- if x == 0 {
446- y++
447- }
448- } else if w < x+l {
449- y++
450- x = 0
451- }
452- }
453- if !line.SplitPoints[spIdx].Split && x == 0 {
454- continue
455- }
456- if w <= x {
457- y++
458- x = 0
459- }
460-
461- if colorState == 1 {
462- fgColor = 0
463- bgColor = 0
464- if '0' <= r && r <= '9' {
465- fgColor = fgColor*10 + int(r-'0')
466- colorState = 2
467- continue
468- }
469- st = st.Foreground(tcell.ColorDefault)
470- st = st.Background(tcell.ColorDefault)
471- colorState = 0
472- } else if colorState == 2 {
473- if '0' <= r && r <= '9' {
474- fgColor = fgColor*10 + int(r-'0')
475- colorState = 3
476- continue
477- }
478- if r == ',' {
479- colorState = 4
480- continue
481- }
482- c := colorFromCode(fgColor)
483- st = st.Foreground(c)
484- colorState = 0
485- } else if colorState == 3 {
486- if r == ',' {
487- colorState = 4
488- continue
489- }
490- c := colorFromCode(fgColor)
491- st = st.Foreground(c)
492- colorState = 0
493- } else if colorState == 4 {
494- if '0' <= r && r <= '9' {
495- bgColor = bgColor*10 + int(r-'0')
496- colorState = 5
497- continue
498- }
499-
500- c := colorFromCode(fgColor)
501- st = st.Foreground(c)
502- colorState = 0
503-
504- ui.screen.SetContent(x, y, ',', nil, st)
505- x++
506- if w <= x {
507- y++
508- x = 0
509- }
510- } else if colorState == 5 {
511- colorState = 0
512- st = st.Foreground(colorFromCode(fgColor))
513-
514- if '0' <= r && r <= '9' {
515- bgColor = bgColor*10 + int(r-'0')
516- st = st.Background(colorFromCode(bgColor))
517- continue
518- }
519-
520- st = st.Background(colorFromCode(bgColor))
521- }
522-
523- if r == 0x00 || r == 0x0F {
524- bold = false
525- italic = false
526- underline = false
527- colorState = 0
528- st = tcell.StyleDefault
529- continue
530- }
531- if r == 0x02 {
532- bold = !bold
533- st = st.Bold(bold)
534- continue
535- }
536- if r == 0x03 {
537- colorState = 1
538- continue
539- }
540- if r == 0x1D {
541- italic = !italic
542- //st = st.Italic(italic)
543- continue
544- }
545- if r == 0x1F {
546- underline = !underline
547- st = st.Underline(underline)
548- continue
549- }
550-
551- if 0 <= y {
552- ui.screen.SetContent(x, y, r, nil, st)
553- }
554- x += runewidth.RuneWidth(r)
555- }
556-
557- st = tcell.StyleDefault
558- bold = false
559- italic = false
560- underline = false
561- colorState = 0
562- }
563-
564- ui.scrollAtTop = 0 <= y0
565+ ui.bs.Draw(ui.screen)
566 ui.screen.Show()
567 }
568
569+/*
570 func (ui *UI) drawStatus() {
571 st := tcell.StyleDefault
572 w, h := ui.screen.Size()
573@@ -618,45 +243,4 @@ func (ui *UI) drawStatus() {
574
575 ui.screen.Show()
576 }
577-
578-func colorFromCode(code int) (color tcell.Color) {
579- switch code {
580- case 0:
581- color = tcell.ColorWhite
582- case 1:
583- color = tcell.ColorBlack
584- case 2:
585- color = tcell.ColorBlue
586- case 3:
587- color = tcell.ColorGreen
588- case 4:
589- color = tcell.ColorRed
590- case 5:
591- color = tcell.ColorBrown
592- case 6:
593- color = tcell.ColorPurple
594- case 7:
595- color = tcell.ColorOrange
596- case 8:
597- color = tcell.ColorYellow
598- case 9:
599- color = tcell.ColorLightGreen
600- case 10:
601- color = tcell.ColorTeal
602- case 11:
603- color = tcell.ColorFuchsia
604- case 12:
605- color = tcell.ColorLightBlue
606- case 13:
607- color = tcell.ColorPink
608- case 14:
609- color = tcell.ColorGrey
610- case 15:
611- color = tcell.ColorLightGrey
612- case 99:
613- color = tcell.ColorDefault
614- default:
615- color = tcell.Color(code)
616- }
617- return
618-}
619+// */
+0,
-75
1@@ -1,75 +0,0 @@
2-package ui
3-
4-import (
5- "github.com/mattn/go-runewidth"
6-)
7-
8-type widthBuffer struct {
9- width int
10- color int
11- comma bool
12-}
13-
14-func (wb *widthBuffer) Width() int {
15- return wb.width
16-}
17-
18-func (wb *widthBuffer) WriteString(s string) {
19- for _, r := range s {
20- wb.WriteRune(r)
21- }
22-}
23-
24-func (wb *widthBuffer) WriteRune(r rune) {
25- if wb.color == 1 {
26- if '0' <= r && r <= '9' {
27- wb.color = 2
28- return
29- }
30- wb.color = 0
31- } else if wb.color == 2 {
32- if '0' <= r && r <= '9' {
33- wb.color = 3
34- return
35- }
36- if r == ',' {
37- wb.color = 4
38- return
39- }
40- wb.color = 0
41- } else if wb.color == 3 {
42- if r == ',' {
43- wb.color = 4
44- return
45- }
46- wb.color = 0
47- } else if wb.color == 4 {
48- if '0' <= r && r <= '9' {
49- wb.color = 5
50- return
51- }
52-
53- wb.width++
54- wb.color = 0
55- } else if wb.color == 5 {
56- wb.color = 0
57- if '0' <= r && r <= '9' {
58- return
59- }
60- }
61-
62- if r == 0x03 {
63- wb.color = 1
64- return
65- }
66-
67- wb.width += runewidth.RuneWidth(r)
68-}
69-
70-func StringWidth(s string) int {
71- var wb widthBuffer
72-
73- wb.WriteString(s)
74-
75- return wb.Width()
76-}