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+}
M
ui/ui.go
+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+}