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
+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-}