summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
authorHubert Hirtz <hubert.hirtz@laposte.net>2020-06-04 22:55:15 +0200
committerHubert Hirtz <hubert.hirtz@laposte.net>2020-06-04 22:55:15 +0200
commitf7331d1c665bd997fe5bc41c6d375d30ea9f4457 (patch)
treefe3ba9f932ace2b8582fa0f9b903349aa44017cb /ui
parentFix color codes (diff)
Buffers are now shown (mostly) correctly
senpai must now split on whitespace when drawing
Diffstat (limited to 'ui')
-rw-r--r--ui/buffers.go112
-rw-r--r--ui/buffers_test.go63
-rw-r--r--ui/ui.go93
-rw-r--r--ui/width.go75
-rw-r--r--ui/width_test.go25
5 files changed, 300 insertions, 68 deletions
diff --git a/ui/buffers.go b/ui/buffers.go
index 8ab5b44..dfef365 100644
--- a/ui/buffers.go
+++ b/ui/buffers.go
@@ -1,6 +1,10 @@
package ui
-import "time"
+import (
+ "fmt"
+ "strings"
+ "time"
+)
var homeMessages = []string{
"\x1dYou open an IRC client.",
@@ -11,10 +15,74 @@ var homeMessages = []string{
"Student? No, I'm an IRC \x02client\x02!",
}
+func IsSplitRune(c rune) bool {
+ return c == ' ' || c == '\t'
+}
+
+type Point struct {
+ X int
+ I int
+}
+
type Line struct {
Time time.Time
IsStatus bool
Content string
+
+ SplitPoints []Point
+ renderedHeight int
+}
+
+func (line *Line) Invalidate() {
+ line.renderedHeight = -1
+}
+
+func (line *Line) RenderedHeight(screenWidth int) (height int) {
+ if line.renderedHeight < 0 {
+ line.computeRenderedHeight(screenWidth)
+ }
+ height = line.renderedHeight
+ return
+}
+
+func (line *Line) computeRenderedHeight(screenWidth int) {
+ line.renderedHeight = 1
+ residualWidth := 0
+
+ for _, sp := range line.SplitPoints {
+ w := sp.X + residualWidth
+
+ if line.renderedHeight*screenWidth < w {
+ line.renderedHeight += 1
+ residualWidth = w % screenWidth
+ }
+ }
+}
+
+func (line *Line) computeSplitPoints() {
+ var wb widthBuffer
+ lastWasSplit := true
+
+ for i, r := range line.Content {
+ curIsSplit := IsSplitRune(r)
+
+ if !lastWasSplit && curIsSplit {
+ line.SplitPoints = append(line.SplitPoints, Point{
+ X: wb.Width(),
+ I: i,
+ })
+ }
+
+ lastWasSplit = curIsSplit
+ wb.WriteRune(r)
+ }
+
+ if !lastWasSplit {
+ line.SplitPoints = append(line.SplitPoints, Point{
+ X: wb.Width(),
+ I: len(line.Content),
+ })
+ }
}
type Buffer struct {
@@ -100,15 +168,49 @@ func (bs *BufferList) Idx(title string) (idx int) {
}
func (bs *BufferList) AddLine(idx int, line string, t time.Time, isStatus bool) {
+ b := &bs.List[idx]
n := len(bs.List[idx].Content)
- if isStatus && n != 0 && bs.List[idx].Content[n-1].IsStatus {
- bs.List[idx].Content[n-1].Content += " " + line
+ line = strings.TrimRight(line, "\t ")
+
+ if isStatus && n != 0 && b.Content[n-1].IsStatus {
+ l := &b.Content[n-1]
+ l.Content += " " + line
+
+ lineWidth := StringWidth(line)
+ lastSP := l.SplitPoints[len(l.SplitPoints)-1]
+ sp := Point{
+ X: lastSP.X + 1 + lineWidth,
+ I: len(l.SplitPoints),
+ }
+
+ l.SplitPoints = append(l.SplitPoints, sp)
+ l.Invalidate()
} else {
- bs.List[idx].Content = append(bs.List[idx].Content, Line{
+ if n == 0 || b.Content[n-1].Time.Truncate(time.Minute) != t.Truncate(time.Minute) {
+ hour := t.Hour()
+ minute := t.Minute()
+
+ line = fmt.Sprintf("\x02%02d:%02d\x00 %s", hour, minute, line)
+ }
+
+ l := Line{
Time: t,
IsStatus: isStatus,
Content: line,
- })
+ }
+
+ l.computeSplitPoints()
+ l.Invalidate()
+
+ b.Content = append(b.Content, l)
+ }
+}
+
+func (bs *BufferList) Invalidate() {
+ for i := range bs.List {
+ for j := range bs.List[i].Content {
+ bs.List[i].Content[j].Invalidate()
+ }
}
}
diff --git a/ui/buffers_test.go b/ui/buffers_test.go
new file mode 100644
index 0000000..fdcbece
--- /dev/null
+++ b/ui/buffers_test.go
@@ -0,0 +1,63 @@
+package ui
+
+import "testing"
+
+func assertSplitPoints(t *testing.T, line string, expected []Point) {
+ l := Line{Content: line}
+ l.computeSplitPoints()
+
+ if len(l.SplitPoints) != len(expected) {
+ t.Errorf("%q: expected %d split points got %d", line, len(expected), len(l.SplitPoints))
+ }
+
+ for i := 0; i < len(expected); i++ {
+ e := expected[i]
+ a := l.SplitPoints[i]
+
+ if e.X != a.X {
+ t.Errorf("%q, point #%d: expected X=%d got %d", line, i, e.X, a.X)
+ }
+ if e.I != a.I {
+ t.Errorf("%q, point #%d: expected I=%d got %d", line, i, e.I, a.I)
+ }
+ }
+}
+
+func TestLineSplitPoints(t *testing.T) {
+ assertSplitPoints(t, "hello", []Point{
+ {X: 5, I: 5},
+ })
+ assertSplitPoints(t, "hello world", []Point{
+ {X: 5, I: 5},
+ {X: 11, I: 11},
+ })
+ assertSplitPoints(t, "lorem ipsum dolor shit amet", []Point{
+ {X: 5, I: 5},
+ {X: 11, I: 11},
+ {X: 17, I: 17},
+ {X: 22, I: 22},
+ {X: 27, I: 27},
+ })
+}
+
+func assertRenderedHeight(t *testing.T, line string, width int, expected int) {
+ l := Line{Content: line}
+ l.computeSplitPoints()
+ l.Invalidate()
+
+ actual := l.RenderedHeight(width)
+
+ if actual != expected {
+ t.Errorf("%q (width=%d) expected to take %d lines, takes %d", line, width, expected, actual)
+ }
+}
+
+func TestRenderedHeight(t *testing.T) {
+ assertRenderedHeight(t, "hello world", 100, 1)
+ assertRenderedHeight(t, "hello world", 10, 2)
+
+ assertRenderedHeight(t, "have a good day!", 100, 1)
+ assertRenderedHeight(t, "have a good day!", 10, 2)
+ assertRenderedHeight(t, "have a good day!", 6, 3)
+ assertRenderedHeight(t, "have a good day!", 4, 4)
+}
diff --git a/ui/ui.go b/ui/ui.go
index 9ad5597..6b25064 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -58,7 +58,7 @@ func New() (ui *UI, err error) {
ui.textInput = []rune{}
- ui.Draw()
+ ui.Resize()
return
}
@@ -177,7 +177,12 @@ func (ui *UI) InputEnter() (content string) {
return
}
-func (ui *UI) Draw() {
+func (ui *UI) Resize() {
+ ui.bufferList.Invalidate()
+ ui.draw()
+}
+
+func (ui *UI) draw() {
ui.drawStatus()
ui.drawEditor()
ui.drawBuffer()
@@ -220,7 +225,7 @@ func (ui *UI) drawEditor() {
func (ui *UI) drawBuffer() {
st := tcell.StyleDefault
w, h := ui.screen.Size()
- if h < 2 {
+ if h < 3 {
return
}
@@ -236,67 +241,30 @@ func (ui *UI) drawBuffer() {
return
}
- y := h - 2
- start := len(b.Content) - 1
- for {
- lw := runewidth.StringWidth(b.Content[start].Content)
-
- if y-(lw/w+1) < 0 {
- break
- }
-
- y -= lw/w + 1
-
- if start <= 0 {
- break
- }
- start--
- }
- if start > 0 {
- start++
- }
-
var bold, italic, underline bool
var colorState int
var fgColor, bgColor int
- var lastHour, lastMinute int
-
- for _, line := range b.Content[start:] {
- rs := []rune(line.Content)
- x := 0
-
- hour := line.Time.Hour()
- minute := line.Time.Minute()
+ y0 := h - 2
- if hour != lastHour || minute != lastMinute {
- t := []rune{rune(hour/10) + '0', rune(hour%10) + '0', ':', rune(minute/10) + '0', rune(minute%10) + '0', ' '}
- tst := tcell.StyleDefault.Bold(true)
+ for i := len(b.Content) - 1; 0 <= i; i-- {
+ line := &b.Content[i]
- for _, r := range t {
- if w <= x {
- y++
- x = 0
- }
- if h-2 <= y {
- break
- }
-
- ui.screen.SetContent(x, y, r, nil, tst)
- x += runewidth.RuneWidth(r)
- }
+ lineHeight := line.RenderedHeight(w)
+ y0 -= lineHeight
+ y := y0
- lastHour = hour
- lastMinute = minute
- }
+ rs := []rune(line.Content)
+ x := 0
for _, r := range rs {
if w <= x {
y++
x = 0
}
- if h-2 <= y {
- break
+
+ if x == 0 && IsSplitRune(r) {
+ continue
}
if colorState == 1 {
@@ -345,19 +313,16 @@ func (ui *UI) drawBuffer() {
ui.screen.SetContent(x, y, ',', nil, st)
x++
} else if colorState == 5 {
+ colorState = 0
+ st = st.Foreground(colorFromCode(fgColor))
+
if '0' <= r && r <= '9' {
bgColor = bgColor*10 + int(r-'0')
- colorState = 6
+ st = st.Background(colorFromCode(bgColor))
continue
}
- st = st.Foreground(colorFromCode(fgColor))
st = st.Background(colorFromCode(bgColor))
- colorState = 0
- } else if colorState == 6 {
- st = st.Foreground(colorFromCode(fgColor))
- st = st.Background(colorFromCode(bgColor))
- colorState = 0
}
if r == 0x00 {
@@ -388,19 +353,21 @@ func (ui *UI) drawBuffer() {
continue
}
- ui.screen.SetContent(x, y, r, nil, st)
+ if 0 <= y {
+ ui.screen.SetContent(x, y, r, nil, st)
+ }
x += runewidth.RuneWidth(r)
}
- y++
+ if y0 < 0 {
+ break
+ }
+
st = tcell.StyleDefault
bold = false
italic = false
underline = false
colorState = 0
- if h-2 <= y {
- break
- }
}
ui.screen.Show()
diff --git a/ui/width.go b/ui/width.go
new file mode 100644
index 0000000..6634993
--- /dev/null
+++ b/ui/width.go
@@ -0,0 +1,75 @@
+package ui
+
+import (
+ "github.com/mattn/go-runewidth"
+)
+
+type widthBuffer struct {
+ width int
+ color int
+ comma bool
+}
+
+func (wb *widthBuffer) Width() int {
+ return wb.width
+}
+
+func (wb *widthBuffer) WriteString(s string) {
+ for _, r := range s {
+ wb.WriteRune(r)
+ }
+}
+
+func (wb *widthBuffer) WriteRune(r rune) {
+ if wb.color == 1 {
+ if '0' <= r && r <= '9' {
+ wb.color = 2
+ return
+ }
+ wb.color = 0
+ } else if wb.color == 2 {
+ if '0' <= r && r <= '9' {
+ wb.color = 3
+ return
+ }
+ if r == ',' {
+ wb.color = 4
+ return
+ }
+ wb.color = 0
+ } else if wb.color == 3 {
+ if r == ',' {
+ wb.color = 4
+ return
+ }
+ wb.color = 0
+ } else if wb.color == 4 {
+ if '0' <= r && r <= '9' {
+ wb.color = 5
+ return
+ }
+
+ wb.width++
+ wb.color = 0
+ } else if wb.color == 5 {
+ wb.color = 0
+ if '0' <= r && r <= '9' {
+ return
+ }
+ }
+
+ if r == 0x03 {
+ wb.color = 1
+ return
+ }
+
+ wb.width += runewidth.RuneWidth(r)
+}
+
+func StringWidth(s string) int {
+ var wb widthBuffer
+
+ wb.WriteString(s)
+
+ return wb.Width()
+}
diff --git a/ui/width_test.go b/ui/width_test.go
new file mode 100644
index 0000000..ae478ab
--- /dev/null
+++ b/ui/width_test.go
@@ -0,0 +1,25 @@
+package ui
+
+import "testing"
+
+func assertStringWidth(t *testing.T, input string, expected int) {
+ actual := StringWidth(input)
+ if actual != expected {
+ t.Errorf("%q: expected width of %d got %d", input, expected, actual)
+ }
+}
+
+func TestStringWidth(t *testing.T) {
+ assertStringWidth(t, "", 0)
+
+ assertStringWidth(t, "hello", 5)
+ assertStringWidth(t, "\x02hello", 5)
+ assertStringWidth(t, "\x035hello", 5)
+ assertStringWidth(t, "\x0305hello", 5)
+ assertStringWidth(t, "\x0305,0hello", 5)
+ assertStringWidth(t, "\x0305,09hello", 5)
+
+ assertStringWidth(t, "\x0305,hello", 6)
+ assertStringWidth(t, "\x03050hello", 6)
+ assertStringWidth(t, "\x0305,090hello", 6)
+}