commit f7331d1

Hubert Hirtz  ·  2020-06-04 20:55:15 +0000 UTC
parent bf3c7dc
Buffers are now shown (mostly) correctly

senpai must now split on whitespace when drawing
6 files changed,  +303, -71
+3, -3
 1@@ -77,13 +77,13 @@ func main() {
 2 		case ev := <-app.Events:
 3 			switch ev := ev.(type) {
 4 			case *tcell.EventResize:
 5-				app.Draw()
 6+				app.Resize()
 7 			case *tcell.EventKey:
 8 				switch ev.Key() {
 9 				case tcell.KeyCtrlC:
10 					app.Exit()
11 				case tcell.KeyCtrlL:
12-					app.Draw()
13+					app.Resize()
14 				case tcell.KeyCtrlN:
15 					app.NextBuffer()
16 				case tcell.KeyCtrlP:
17@@ -193,7 +193,7 @@ func formatIRCMessage(nick, content string) (line string) {
18 		return
19 	}
20 
21-	line = fmt.Sprintf("%s%s\x00:  %s", string(c[:]), nick, content)
22+	line = fmt.Sprintf("%s%s\x00: %s", string(c[:]), nick, content)
23 
24 	return
25 }
+107, -5
  1@@ -1,6 +1,10 @@
  2 package ui
  3 
  4-import "time"
  5+import (
  6+	"fmt"
  7+	"strings"
  8+	"time"
  9+)
 10 
 11 var homeMessages = []string{
 12 	"\x1dYou open an IRC client.",
 13@@ -11,10 +15,74 @@ var homeMessages = []string{
 14 	"Student? No, I'm an IRC \x02client\x02!",
 15 }
 16 
 17+func IsSplitRune(c rune) bool {
 18+	return c == ' ' || c == '\t'
 19+}
 20+
 21+type Point struct {
 22+	X int
 23+	I int
 24+}
 25+
 26 type Line struct {
 27 	Time     time.Time
 28 	IsStatus bool
 29 	Content  string
 30+
 31+	SplitPoints    []Point
 32+	renderedHeight int
 33+}
 34+
 35+func (line *Line) Invalidate() {
 36+	line.renderedHeight = -1
 37+}
 38+
 39+func (line *Line) RenderedHeight(screenWidth int) (height int) {
 40+	if line.renderedHeight < 0 {
 41+		line.computeRenderedHeight(screenWidth)
 42+	}
 43+	height = line.renderedHeight
 44+	return
 45+}
 46+
 47+func (line *Line) computeRenderedHeight(screenWidth int) {
 48+	line.renderedHeight = 1
 49+	residualWidth := 0
 50+
 51+	for _, sp := range line.SplitPoints {
 52+		w := sp.X + residualWidth
 53+
 54+		if line.renderedHeight*screenWidth < w {
 55+			line.renderedHeight += 1
 56+			residualWidth = w % screenWidth
 57+		}
 58+	}
 59+}
 60+
 61+func (line *Line) computeSplitPoints() {
 62+	var wb widthBuffer
 63+	lastWasSplit := true
 64+
 65+	for i, r := range line.Content {
 66+		curIsSplit := IsSplitRune(r)
 67+
 68+		if !lastWasSplit && curIsSplit {
 69+			line.SplitPoints = append(line.SplitPoints, Point{
 70+				X: wb.Width(),
 71+				I: i,
 72+			})
 73+		}
 74+
 75+		lastWasSplit = curIsSplit
 76+		wb.WriteRune(r)
 77+	}
 78+
 79+	if !lastWasSplit {
 80+		line.SplitPoints = append(line.SplitPoints, Point{
 81+			X: wb.Width(),
 82+			I: len(line.Content),
 83+		})
 84+	}
 85 }
 86 
 87 type Buffer struct {
 88@@ -100,15 +168,49 @@ func (bs *BufferList) Idx(title string) (idx int) {
 89 }
 90 
 91 func (bs *BufferList) AddLine(idx int, line string, t time.Time, isStatus bool) {
 92+	b := &bs.List[idx]
 93 	n := len(bs.List[idx].Content)
 94 
 95-	if isStatus && n != 0 && bs.List[idx].Content[n-1].IsStatus {
 96-		bs.List[idx].Content[n-1].Content += " " + line
 97+	line = strings.TrimRight(line, "\t ")
 98+
 99+	if isStatus && n != 0 && b.Content[n-1].IsStatus {
100+		l := &b.Content[n-1]
101+		l.Content += " " + line
102+
103+		lineWidth := StringWidth(line)
104+		lastSP := l.SplitPoints[len(l.SplitPoints)-1]
105+		sp := Point{
106+			X: lastSP.X + 1 + lineWidth,
107+			I: len(l.SplitPoints),
108+		}
109+
110+		l.SplitPoints = append(l.SplitPoints, sp)
111+		l.Invalidate()
112 	} else {
113-		bs.List[idx].Content = append(bs.List[idx].Content, Line{
114+		if n == 0 || b.Content[n-1].Time.Truncate(time.Minute) != t.Truncate(time.Minute) {
115+			hour := t.Hour()
116+			minute := t.Minute()
117+
118+			line = fmt.Sprintf("\x02%02d:%02d\x00 %s", hour, minute, line)
119+		}
120+
121+		l := Line{
122 			Time:     t,
123 			IsStatus: isStatus,
124 			Content:  line,
125-		})
126+		}
127+
128+		l.computeSplitPoints()
129+		l.Invalidate()
130+
131+		b.Content = append(b.Content, l)
132+	}
133+}
134+
135+func (bs *BufferList) Invalidate() {
136+	for i := range bs.List {
137+		for j := range bs.List[i].Content {
138+			bs.List[i].Content[j].Invalidate()
139+		}
140 	}
141 }
+63, -0
 1@@ -0,0 +1,63 @@
 2+package ui
 3+
 4+import "testing"
 5+
 6+func assertSplitPoints(t *testing.T, line string, expected []Point) {
 7+	l := Line{Content: line}
 8+	l.computeSplitPoints()
 9+
10+	if len(l.SplitPoints) != len(expected) {
11+		t.Errorf("%q: expected %d split points got %d", line, len(expected), len(l.SplitPoints))
12+	}
13+
14+	for i := 0; i < len(expected); i++ {
15+		e := expected[i]
16+		a := l.SplitPoints[i]
17+
18+		if e.X != a.X {
19+			t.Errorf("%q, point #%d: expected X=%d got %d", line, i, e.X, a.X)
20+		}
21+		if e.I != a.I {
22+			t.Errorf("%q, point #%d: expected I=%d got %d", line, i, e.I, a.I)
23+		}
24+	}
25+}
26+
27+func TestLineSplitPoints(t *testing.T) {
28+	assertSplitPoints(t, "hello", []Point{
29+		{X: 5, I: 5},
30+	})
31+	assertSplitPoints(t, "hello world", []Point{
32+		{X: 5, I: 5},
33+		{X: 11, I: 11},
34+	})
35+	assertSplitPoints(t, "lorem ipsum dolor shit amet", []Point{
36+		{X: 5, I: 5},
37+		{X: 11, I: 11},
38+		{X: 17, I: 17},
39+		{X: 22, I: 22},
40+		{X: 27, I: 27},
41+	})
42+}
43+
44+func assertRenderedHeight(t *testing.T, line string, width int, expected int) {
45+	l := Line{Content: line}
46+	l.computeSplitPoints()
47+	l.Invalidate()
48+
49+	actual := l.RenderedHeight(width)
50+
51+	if actual != expected {
52+		t.Errorf("%q (width=%d) expected to take %d lines, takes %d", line, width, expected, actual)
53+	}
54+}
55+
56+func TestRenderedHeight(t *testing.T) {
57+	assertRenderedHeight(t, "hello world", 100, 1)
58+	assertRenderedHeight(t, "hello world", 10, 2)
59+
60+	assertRenderedHeight(t, "have a good day!", 100, 1)
61+	assertRenderedHeight(t, "have a good day!", 10, 2)
62+	assertRenderedHeight(t, "have a good day!", 6, 3)
63+	assertRenderedHeight(t, "have a good day!", 4, 4)
64+}
+30, -63
  1@@ -58,7 +58,7 @@ func New() (ui *UI, err error) {
  2 
  3 	ui.textInput = []rune{}
  4 
  5-	ui.Draw()
  6+	ui.Resize()
  7 
  8 	return
  9 }
 10@@ -177,7 +177,12 @@ func (ui *UI) InputEnter() (content string) {
 11 	return
 12 }
 13 
 14-func (ui *UI) Draw() {
 15+func (ui *UI) Resize() {
 16+	ui.bufferList.Invalidate()
 17+	ui.draw()
 18+}
 19+
 20+func (ui *UI) draw() {
 21 	ui.drawStatus()
 22 	ui.drawEditor()
 23 	ui.drawBuffer()
 24@@ -220,7 +225,7 @@ func (ui *UI) drawEditor() {
 25 func (ui *UI) drawBuffer() {
 26 	st := tcell.StyleDefault
 27 	w, h := ui.screen.Size()
 28-	if h < 2 {
 29+	if h < 3 {
 30 		return
 31 	}
 32 
 33@@ -236,67 +241,30 @@ func (ui *UI) drawBuffer() {
 34 		return
 35 	}
 36 
 37-	y := h - 2
 38-	start := len(b.Content) - 1
 39-	for {
 40-		lw := runewidth.StringWidth(b.Content[start].Content)
 41-
 42-		if y-(lw/w+1) < 0 {
 43-			break
 44-		}
 45-
 46-		y -= lw/w + 1
 47-
 48-		if start <= 0 {
 49-			break
 50-		}
 51-		start--
 52-	}
 53-	if start > 0 {
 54-		start++
 55-	}
 56-
 57 	var bold, italic, underline bool
 58 	var colorState int
 59 	var fgColor, bgColor int
 60 
 61-	var lastHour, lastMinute int
 62-
 63-	for _, line := range b.Content[start:] {
 64-		rs := []rune(line.Content)
 65-		x := 0
 66-
 67-		hour := line.Time.Hour()
 68-		minute := line.Time.Minute()
 69+	y0 := h - 2
 70 
 71-		if hour != lastHour || minute != lastMinute {
 72-			t := []rune{rune(hour/10) + '0', rune(hour%10) + '0', ':', rune(minute/10) + '0', rune(minute%10) + '0', ' '}
 73-			tst := tcell.StyleDefault.Bold(true)
 74+	for i := len(b.Content) - 1; 0 <= i; i-- {
 75+		line := &b.Content[i]
 76 
 77-			for _, r := range t {
 78-				if w <= x {
 79-					y++
 80-					x = 0
 81-				}
 82-				if h-2 <= y {
 83-					break
 84-				}
 85-
 86-				ui.screen.SetContent(x, y, r, nil, tst)
 87-				x += runewidth.RuneWidth(r)
 88-			}
 89+		lineHeight := line.RenderedHeight(w)
 90+		y0 -= lineHeight
 91+		y := y0
 92 
 93-			lastHour = hour
 94-			lastMinute = minute
 95-		}
 96+		rs := []rune(line.Content)
 97+		x := 0
 98 
 99 		for _, r := range rs {
100 			if w <= x {
101 				y++
102 				x = 0
103 			}
104-			if h-2 <= y {
105-				break
106+
107+			if x == 0 && IsSplitRune(r) {
108+				continue
109 			}
110 
111 			if colorState == 1 {
112@@ -345,19 +313,16 @@ func (ui *UI) drawBuffer() {
113 				ui.screen.SetContent(x, y, ',', nil, st)
114 				x++
115 			} else if colorState == 5 {
116+				colorState = 0
117+				st = st.Foreground(colorFromCode(fgColor))
118+
119 				if '0' <= r && r <= '9' {
120 					bgColor = bgColor*10 + int(r-'0')
121-					colorState = 6
122+					st = st.Background(colorFromCode(bgColor))
123 					continue
124 				}
125 
126-				st = st.Foreground(colorFromCode(fgColor))
127 				st = st.Background(colorFromCode(bgColor))
128-				colorState = 0
129-			} else if colorState == 6 {
130-				st = st.Foreground(colorFromCode(fgColor))
131-				st = st.Background(colorFromCode(bgColor))
132-				colorState = 0
133 			}
134 
135 			if r == 0x00 {
136@@ -388,19 +353,21 @@ func (ui *UI) drawBuffer() {
137 				continue
138 			}
139 
140-			ui.screen.SetContent(x, y, r, nil, st)
141+			if 0 <= y {
142+				ui.screen.SetContent(x, y, r, nil, st)
143+			}
144 			x += runewidth.RuneWidth(r)
145 		}
146 
147-		y++
148+		if y0 < 0 {
149+			break
150+		}
151+
152 		st = tcell.StyleDefault
153 		bold = false
154 		italic = false
155 		underline = false
156 		colorState = 0
157-		if h-2 <= y {
158-			break
159-		}
160 	}
161 
162 	ui.screen.Show()
+75, -0
 1@@ -0,0 +1,75 @@
 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+}
+25, -0
 1@@ -0,0 +1,25 @@
 2+package ui
 3+
 4+import "testing"
 5+
 6+func assertStringWidth(t *testing.T, input string, expected int) {
 7+	actual := StringWidth(input)
 8+	if actual != expected {
 9+		t.Errorf("%q: expected width of %d got %d", input, expected, actual)
10+	}
11+}
12+
13+func TestStringWidth(t *testing.T) {
14+	assertStringWidth(t, "", 0)
15+
16+	assertStringWidth(t, "hello", 5)
17+	assertStringWidth(t, "\x02hello", 5)
18+	assertStringWidth(t, "\x035hello", 5)
19+	assertStringWidth(t, "\x0305hello", 5)
20+	assertStringWidth(t, "\x0305,0hello", 5)
21+	assertStringWidth(t, "\x0305,09hello", 5)
22+
23+	assertStringWidth(t, "\x0305,hello", 6)
24+	assertStringWidth(t, "\x03050hello", 6)
25+	assertStringWidth(t, "\x0305,090hello", 6)
26+}