summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHubert Hirtz <hubert@hirtzfr.eu>2020-08-03 23:04:51 +0200
committerHubert Hirtz <hubert@hirtzfr.eu>2020-08-04 11:12:32 +0200
commit3c8dceaf96964eae12a06abb04035cf184bdbf61 (patch)
treee3de1c86b31a488ccf687a27d57d7d7e3e463f6e
parentImprove line editing (diff)
Rework display
- Put timestamps, nicks and messages into separate columns - Print a bar in the "typing" row - Fix word wrapping - Improve channel list display
Diffstat (limited to '')
-rw-r--r--cmd/irc/main.go192
-rw-r--r--ui/buffers.go659
-rw-r--r--ui/buffers_test.go204
-rw-r--r--ui/style.go191
-rw-r--r--ui/style_test.go (renamed from ui/width_test.go)0
-rw-r--r--ui/ui.go510
-rw-r--r--ui/width.go75
7 files changed, 900 insertions, 931 deletions
diff --git a/cmd/irc/main.go b/cmd/irc/main.go
index 151d99d..e2cd5f3 100644
--- a/cmd/irc/main.go
+++ b/cmd/irc/main.go
@@ -3,16 +3,16 @@ package main
import (
"crypto/tls"
"fmt"
- "git.sr.ht/~taiite/senpai"
- "git.sr.ht/~taiite/senpai/irc"
- "git.sr.ht/~taiite/senpai/ui"
- "github.com/gdamore/tcell"
- "hash/fnv"
"log"
"math/rand"
"os"
"strings"
"time"
+
+ "git.sr.ht/~taiite/senpai"
+ "git.sr.ht/~taiite/senpai/irc"
+ "git.sr.ht/~taiite/senpai/ui"
+ "github.com/gdamore/tcell"
)
func init() {
@@ -39,7 +39,7 @@ func main() {
defer app.Close()
addr := cfg.Addr
- app.AddLine("home", fmt.Sprintf("Connecting to %s...", addr), time.Now(), false)
+ app.AddLine(ui.Home, ui.NewLineNow("--", fmt.Sprintf("Connecting to %s...", addr)))
conn, err := tls.Dial("tcp", addr, nil)
if err != nil {
@@ -60,48 +60,48 @@ func main() {
for !app.ShouldExit() {
select {
case ev := <-s.Poll():
- handleIRCEvent(app, ev)
+ handleIRCEvent(app, &s, ev)
case ev := <-app.Events:
handleUIEvent(app, &s, ev)
}
}
}
-func handleIRCEvent(app *ui.UI, ev irc.Event) {
+func handleIRCEvent(app *ui.UI, s *irc.Session, ev irc.Event) {
switch ev := ev.(type) {
case irc.RegisteredEvent:
- app.AddLine("home", "Connected to the server", time.Now(), false)
+ app.AddLine("", ui.NewLineNow("--", "Connected to the server"))
case irc.SelfJoinEvent:
app.AddBuffer(ev.Channel)
case irc.UserJoinEvent:
line := fmt.Sprintf("\x033+\x0314%s", ev.Nick)
- app.AddLine(ev.Channel, line, ev.Time, true)
+ app.AddLine(ev.Channel, ui.NewLine(ev.Time, "", line, true))
case irc.SelfPartEvent:
app.RemoveBuffer(ev.Channel)
case irc.UserPartEvent:
line := fmt.Sprintf("\x034-\x0314%s", ev.Nick)
- app.AddLine(ev.Channel, line, ev.Time, true)
+ app.AddLine(ev.Channel, ui.NewLine(ev.Time, "", line, true))
case irc.QueryMessageEvent:
if ev.Command == "PRIVMSG" {
- line := formatIRCMessage(ev.Nick, ev.Content)
- app.AddLine("home", line, ev.Time, false)
- app.TypingStop("home", ev.Nick)
+ l := ui.LineFromIRCMessage(ev.Time, ev.Nick, ev.Content, false)
+ app.AddLine(ui.Home, l)
+ app.TypingStop(ui.Home, ev.Nick)
} else if ev.Command == "NOTICE" {
- line := formatIRCNotice(ev.Nick, ev.Content)
- app.AddLine(app.CurrentBuffer(), line, ev.Time, false)
- app.TypingStop(app.CurrentBuffer(), ev.Nick)
+ l := ui.LineFromIRCMessage(ev.Time, ev.Nick, ev.Content, true)
+ app.AddLine("", l)
+ app.TypingStop("", ev.Nick)
} else {
panic("unknown command")
}
case irc.ChannelMessageEvent:
- line := formatIRCMessage(ev.Nick, ev.Content)
- app.AddLine(ev.Channel, line, ev.Time, false)
+ l := ui.LineFromIRCMessage(ev.Time, ev.Nick, ev.Content, ev.Command == "NOTICE")
+ app.AddLine(ev.Channel, l)
app.TypingStop(ev.Channel, ev.Nick)
case irc.QueryTypingEvent:
if ev.State == 1 || ev.State == 2 {
- app.TypingStart("home", ev.Nick)
+ app.TypingStart(ui.Home, ev.Nick)
} else {
- app.TypingStop("home", ev.Nick)
+ app.TypingStop(ui.Home, ev.Nick)
}
case irc.ChannelTypingEvent:
if ev.State == 1 || ev.State == 2 {
@@ -111,27 +111,16 @@ func handleIRCEvent(app *ui.UI, ev irc.Event) {
}
case irc.HistoryEvent:
var lines []ui.Line
- var lastT time.Time
- isChannel := ev.Target[0] == '#'
for _, m := range ev.Messages {
switch m := m.(type) {
case irc.ChannelMessageEvent:
- if isChannel {
- line := formatIRCMessage(m.Nick, m.Content)
- line = strings.TrimRight(line, "\t ")
- if lastT.Truncate(time.Minute) != m.Time.Truncate(time.Minute) {
- lastT = m.Time
- hour := lastT.Hour()
- minute := lastT.Minute()
- line = fmt.Sprintf("\x02%02d:%02d\x00 %s", hour, minute, line)
- }
- lines = append(lines, ui.NewLine(m.Time, false, line))
- } else {
- panic("TODO")
- }
+ l := ui.LineFromIRCMessage(m.Time, m.Nick, m.Content, m.Command == "NOTICE")
+ lines = append(lines, l)
+ default:
+ panic("TODO")
}
}
- app.AddHistoryLines(ev.Target, lines)
+ app.AddLines(ev.Target, lines)
case error:
log.Panicln(ev)
}
@@ -151,39 +140,58 @@ func handleUIEvent(app *ui.UI, s *irc.Session, ev tcell.Event) {
app.ScrollUp()
if app.IsAtTop() {
buffer := app.CurrentBuffer()
- t := app.CurrentBufferOldestTime()
- s.RequestHistory(buffer, t)
+ at := time.Now()
+ if t := app.CurrentBufferOldestTime(); t != nil {
+ at = *t
+ }
+ s.RequestHistory(buffer, at)
}
case tcell.KeyCtrlD, tcell.KeyPgDn:
app.ScrollDown()
case tcell.KeyCtrlN:
- if app.NextBuffer() && app.IsAtTop() {
+ app.NextBuffer()
+ if app.IsAtTop() {
buffer := app.CurrentBuffer()
- t := app.CurrentBufferOldestTime()
- s.RequestHistory(buffer, t)
+ at := time.Now()
+ if t := app.CurrentBufferOldestTime(); t != nil {
+ at = *t
+ }
+ s.RequestHistory(buffer, at)
}
case tcell.KeyCtrlP:
- if app.PreviousBuffer() && app.IsAtTop() {
+ app.PreviousBuffer()
+ if app.IsAtTop() {
buffer := app.CurrentBuffer()
- t := app.CurrentBufferOldestTime()
- s.RequestHistory(buffer, t)
+ at := time.Now()
+ if t := app.CurrentBufferOldestTime(); t != nil {
+ at = *t
+ }
+ s.RequestHistory(buffer, at)
}
case tcell.KeyRight:
if ev.Modifiers() == tcell.ModAlt {
- if app.NextBuffer() && app.IsAtTop() {
+ app.NextBuffer()
+ if app.IsAtTop() {
buffer := app.CurrentBuffer()
- t := app.CurrentBufferOldestTime()
- s.RequestHistory(buffer, t)
+ at := time.Now()
+ if t := app.CurrentBufferOldestTime(); t != nil {
+ at = *t
+ }
+ s.RequestHistory(buffer, at)
}
} else {
app.InputRight()
}
case tcell.KeyLeft:
if ev.Modifiers() == tcell.ModAlt {
- if app.PreviousBuffer() && app.IsAtTop() {
+ app.PreviousBuffer()
+ if app.IsAtTop() {
buffer := app.CurrentBuffer()
- t := app.CurrentBufferOldestTime()
- s.RequestHistory(buffer, t)
+ at := time.Now()
+ if t := app.CurrentBufferOldestTime(); t != nil {
+ at = *t
+ }
+ s.RequestHistory(buffer, at)
}
} else {
app.InputLeft()
@@ -199,7 +207,7 @@ func handleUIEvent(app *ui.UI, s *irc.Session, ev tcell.Event) {
handleInput(app, s, buffer, input)
case tcell.KeyRune:
app.InputRune(ev.Rune())
- if app.CurrentBuffer() != "home" && !app.InputIsCommand() {
+ if app.CurrentBuffer() != ui.Home && !app.InputIsCommand() {
s.Typing(app.CurrentBuffer())
}
}
@@ -232,21 +240,20 @@ func handleInput(app *ui.UI, s *irc.Session, buffer, content string) {
switch cmd {
case "":
- if buffer == "home" {
+ if buffer == ui.Home || len(strings.TrimSpace(args)) == 0 {
return
}
s.PrivMsg(buffer, args)
if !s.HasCapability("echo-message") {
- line := formatIRCMessage(s.Nick(), args)
- app.AddLine(buffer, line, time.Now(), false)
+ app.AddLine(buffer, ui.NewLineNow(s.Nick(), args))
}
case "QUOTE":
s.SendRaw(args)
case "J", "JOIN":
s.Join(args)
case "PART":
- if buffer == "home" {
+ if buffer == ui.Home {
return
}
@@ -256,12 +263,13 @@ func handleInput(app *ui.UI, s *irc.Session, buffer, content string) {
s.Part(args)
case "ME":
- if buffer == "home" {
+ if buffer == ui.Home {
return
}
line := fmt.Sprintf("\x01ACTION %s\x01", args)
s.PrivMsg(buffer, line)
+ // TODO echo message
case "MSG":
split := strings.SplitN(args, " ", 2)
if len(split) < 2 {
@@ -271,74 +279,6 @@ func handleInput(app *ui.UI, s *irc.Session, buffer, content string) {
target := split[0]
content := split[1]
s.PrivMsg(target, content)
+ // TODO echo mssage
}
}
-
-func formatIRCMessage(nick, content string) (line string) {
- c := color(nick)
-
- if content == "" {
- line = fmt.Sprintf("%s%s\x00:", c, nick)
- return
- }
-
- if content[0] == 1 {
- content = strings.TrimSuffix(content[1:], "\x01")
-
- if strings.HasPrefix(content, "ACTION") {
- line = fmt.Sprintf("%s%s\x00%s", c, nick, content[6:])
- } else {
- line = fmt.Sprintf("\x1dCTCP request from\x1d %s%s\x00: %s", c, nick, content)
- }
-
- return
- }
-
- line = fmt.Sprintf("%s%s\x00: %s", c, nick, content)
-
- return
-}
-
-func formatIRCNotice(nick, content string) (line string) {
- c := color(nick)
-
- if content == "" {
- line = fmt.Sprintf("(%s%s\x00: )", c, nick)
- return
- }
-
- if content[0] == 1 {
- content = strings.TrimSuffix(content[1:], "\x01")
-
- if strings.HasPrefix(content, "ACTION") {
- line = fmt.Sprintf("(%s%s\x00%s)", c, nick, content[6:])
- } else {
- line = fmt.Sprintf("(\x1dCTCP request from\x1d %s%s\x00: %s)", c, nick, content)
- }
- } else {
- line = fmt.Sprintf("(%s%s\x00: %s)", c, nick, content)
- }
-
- return
-}
-
-func color(nick string) string {
- h := fnv.New32()
- _, _ = h.Write([]byte(nick))
-
- sum := h.Sum32() % 96
-
- if 1 <= sum {
- sum++
- }
- if 8 <= sum {
- sum++
- }
-
- var c [3]rune
- c[0] = '\x03'
- c[1] = rune(sum/10) + '0'
- c[2] = rune(sum%10) + '0'
-
- return string(c[:])
-}
diff --git a/ui/buffers.go b/ui/buffers.go
index afd23aa..a61766c 100644
--- a/ui/buffers.go
+++ b/ui/buffers.go
@@ -2,10 +2,17 @@ package ui
import (
"fmt"
+ "hash/fnv"
+ "math"
"strings"
"time"
+
+ "github.com/gdamore/tcell"
+ "github.com/mattn/go-runewidth"
)
+var Home = "home"
+
var homeMessages = []string{
"\x1dYou open an IRC client.",
"Welcome to the Internet Relay Network!",
@@ -15,294 +22,578 @@ var homeMessages = []string{
"Student? No, I'm an IRC \x02client\x02!",
}
-func IsSplitRune(c rune) bool {
- return c == ' ' || c == '\t'
+func IsSplitRune(r rune) bool {
+ return r == ' ' || r == '\t'
}
-type Point struct {
- X int
- I int
-
+type point struct {
+ X, I int
Split bool
}
type Line struct {
- Time time.Time
- IsStatus bool
- Content string
-
- SplitPoints []Point
- renderedHeight int
+ at time.Time
+ head string
+ body string
+ isStatus bool
+
+ splitPoints []point
+ width int
+ newLines []int
}
-func NewLine(t time.Time, isStatus bool, content string) (line Line) {
- line.Time = t
- line.IsStatus = isStatus
- line.Content = content
-
- line.Invalidate()
- line.computeSplitPoints()
-
- return
+func NewLine(at time.Time, head string, body string, isStatus bool) Line {
+ l := Line{
+ at: at,
+ head: head,
+ body: body,
+ isStatus: isStatus,
+ splitPoints: []point{},
+ newLines: []int{},
+ }
+ l.computeSplitPoints()
+ return l
}
-func NewLineNow(content string) (line Line) {
- line = NewLine(time.Now(), false, content)
- return
+func NewLineNow(head, body string) Line {
+ return NewLine(time.Now(), head, body, false)
}
-func (line *Line) Invalidate() {
- line.renderedHeight = 0
+func LineFromIRCMessage(at time.Time, nick string, content string, isNotice bool) Line {
+ if strings.HasPrefix(content, "\x01ACTION") {
+ c := ircColorCode(identColor(nick))
+ content = fmt.Sprintf("%s%s\x0F%s", c, nick, content[7:])
+ nick = "*"
+ } else if isNotice {
+ c := ircColorCode(identColor(nick))
+ content = fmt.Sprintf("(%s%s\x0F: %s)", c, nick, content)
+ nick = "*"
+ }
+ return NewLine(at, nick, content, false)
}
-func (line *Line) RenderedHeight(screenWidth int) (height int) {
- if line.renderedHeight <= 0 {
- line.computeRenderedHeight(screenWidth)
+func (l *Line) computeSplitPoints() {
+ var wb widthBuffer
+ lastWasSplit := false
+ l.splitPoints = l.splitPoints[:0]
+
+ for i, r := range l.body {
+ curIsSplit := IsSplitRune(r)
+
+ if i == 0 || lastWasSplit != curIsSplit {
+ l.splitPoints = append(l.splitPoints, point{
+ X: wb.Width(),
+ I: i,
+ Split: curIsSplit,
+ })
+ }
+
+ lastWasSplit = curIsSplit
+ wb.WriteRune(r)
+ }
+
+ if !lastWasSplit {
+ l.splitPoints = append(l.splitPoints, point{
+ X: wb.Width(),
+ I: len(l.body),
+ Split: true,
+ })
}
- height = line.renderedHeight
- return
}
-// TODO clean and understand the fucking function
-func (line *Line) computeRenderedHeight(screenWidth int) {
- var lastSP Point
- line.renderedHeight = 1
- x := 0
+func (l *Line) NewLines(width int) []int {
+ // Beware! This function was made by your local Test Driven Developper™ who
+ // doesn't understand one bit of this function and how it works (though it
+ // might not work that well if you're here...). The code below is thus very
+ // cryptic and not well structured. However, I'm going to try to explain
+ // some of those lines!
- //fmt.Printf("\n%d %q\n", screenWidth, line.Content)
- for _, sp := range line.SplitPoints {
- l := sp.X - lastSP.X
+ if l.width == width {
+ return l.newLines
+ }
+ l.newLines = l.newLines[:0]
+ l.width = width
- if !sp.Split && x == 0 {
- // Don't add space at the beginning of a row
- } else if screenWidth < l {
- line.renderedHeight += (x + l) / screenWidth
- x = (x + l) % screenWidth
- } else if screenWidth == l {
+ x := 0
+ for i := 1; i < len(l.splitPoints); i++ {
+ // Iterate through the split points 2 by 2. Split points are placed at
+ // the begining of whitespace (see IsSplitRune) and at the begining of
+ // non-whitespace. Iterating on 2 points each time, sp1 and sp2, allow
+ // consideration of a "word" of (non-)whitespace.
+ // Split points have the index I in the string and the width X of the
+ // screen. Finally, the Split field is set to true if the split point
+ // is at the begining of a whitespace.
+
+ // Below, "row" means a line in the terminal, while "line" means (l *Line).
+
+ sp1 := l.splitPoints[i-1]
+ sp2 := l.splitPoints[i]
+
+ if 0 < len(l.newLines) && x == 0 && sp1.Split {
+ // Except for the first row, let's skip the whitespace at the start
+ // of the row.
+ } else if !sp1.Split && sp2.X-sp1.X == width {
+ // Some word occupies the width of the terminal, lets place a
+ // newline at the PREVIOUS split point (i-2, which is whitespace)
+ // ONLY if there isn't already one.
+ if 1 < i && l.newLines[len(l.newLines)-1] != l.splitPoints[i-2].I {
+ l.newLines = append(l.newLines, l.splitPoints[i-2].I)
+ }
+ // and also place a newline after the word.
+ x = 0
+ l.newLines = append(l.newLines, sp2.I)
+ } else if sp2.X-sp1.X+x < width {
+ // It fits. Advance the X coordinate with the width of the word.
+ x += sp2.X - sp1.X
+ } else if sp2.X-sp1.X+x == width {
+ // It fits, but there is no more space in the row.
+ x = 0
+ l.newLines = append(l.newLines, sp2.I)
+ } else if width < sp2.X-sp1.X {
+ // It doesn't fit at all. The word is longer than the width of the
+ // terminal. In this case, no newline is placed before (like in the
+ // 2nd if-else branch). The for loop is used to place newlines in
+ // the word.
+ var wb widthBuffer
+ h := 1
+ for j, r := range l.body[sp1.I:sp2.I] {
+ wb.WriteRune(r)
+ if h*width < x+wb.Width() {
+ l.newLines = append(l.newLines, sp1.I+j)
+ h++
+ }
+ }
+ x = (x + wb.Width()) % width
if x == 0 {
- line.renderedHeight++
- } else {
- line.renderedHeight += 2
- x = 0
+ // The placement of the word is such that it ends right at the
+ // end of the row.
+ l.newLines = append(l.newLines, sp2.I)
}
- } else if screenWidth < x+l {
- line.renderedHeight++
- if sp.Split {
- x = l % screenWidth
- } else {
+ } else {
+ // So... IIUC this branch would be the same as
+ // else if width < sp2.X-sp1.X+x
+ // IE. It doesn't fit, but the word can still be placed on the next
+ // row.
+ l.newLines = append(l.newLines, sp1.I)
+ if sp1.Split {
x = 0
+ } else {
+ x = sp2.X - sp1.X
}
- } else if screenWidth == x+l {
- line.renderedHeight++
- x = 0
- } else {
- x = x + l
}
-
- //fmt.Printf("%d %d %t occupied by %q\n", line.renderedHeight, x, sp.Split, line.Content[:sp.I])
- lastSP = sp
}
- if x == 0 && 1 < line.renderedHeight {
- line.renderedHeight--
+ if 0 < len(l.newLines) && l.newLines[len(l.newLines)-1] == len(l.body) {
+ // DROP any newline that is placed at the end of the string because we
+ // don't care about those.
+ l.newLines = l.newLines[:len(l.newLines)-1]
}
+
+ return l.newLines
}
-func (line *Line) computeSplitPoints() {
- var wb widthBuffer
- lastWasSplit := false
+type buffer struct {
+ title string
+ highlights int
+ unread bool
- for i, r := range line.Content {
- curIsSplit := IsSplitRune(r)
+ lines []Line
+ typings []string
- if lastWasSplit != curIsSplit {
- line.SplitPoints = append(line.SplitPoints, Point{
- X: wb.Width(),
- I: i,
- Split: curIsSplit,
- })
+ scrollAmt int
+ isAtTop bool
+}
+
+func (b *buffer) DrawLines(screen tcell.Screen, width int, height int) {
+ st := tcell.StyleDefault
+ for x := 0; x < width; x++ {
+ for y := 0; y < height; y++ {
+ screen.SetContent(x, y, ' ', nil, st)
}
+ }
- lastWasSplit = curIsSplit
- wb.WriteRune(r)
+ nickColWidth := 16
+
+ var sb styleBuffer
+ sb.Reset()
+ y0 := b.scrollAmt + height
+ for i := len(b.lines) - 1; 0 <= i; i-- {
+ if y0 < 0 {
+ break
+ }
+
+ x0 := 5 + 1 + nickColWidth + 2
+
+ line := &b.lines[i]
+ nls := line.NewLines(width - x0)
+ y0 -= len(nls) + 1
+ if height <= y0 {
+ continue
+ }
+
+ if i == 0 || b.lines[i-1].at.Truncate(time.Minute) != line.at.Truncate(time.Minute) {
+ printTime(screen, 0, y0, st.Bold(true), line.at)
+ }
+
+ head := truncate(line.head, nickColWidth)
+ x := 6 + nickColWidth - StringWidth(head)
+ c := identColor(line.head)
+ printString(screen, &x, y0, st.Foreground(colorFromCode(c)), head)
+
+ x = x0
+ y := y0
+
+ for i, r := range line.body {
+ if 0 < len(nls) && i == nls[0] {
+ x = x0
+ y++
+ nls = nls[1:]
+ if height < y {
+ break
+ }
+ }
+
+ if y != y0 && x == x0 && IsSplitRune(r) {
+ continue
+ }
+
+ if st, ok := sb.WriteRune(r); ok != 0 {
+ if 1 < ok {
+ screen.SetContent(x, y, ',', nil, st)
+ x++
+ }
+ screen.SetContent(x, y, r, nil, st)
+ x += runewidth.RuneWidth(r)
+ }
+ }
+
+ sb.Reset()
}
- if !lastWasSplit {
- line.SplitPoints = append(line.SplitPoints, Point{
- X: wb.Width(),
- I: len(line.Content),
- Split: true,
- })
+ b.isAtTop = 0 <= y0
+}
+
+type bufferList struct {
+ list []buffer
+ current int
+
+ width int
+ height int
+}
+
+func newBufferList(width, height int) bufferList {
+ return bufferList{
+ list: []buffer{},
+ width: width,
+ height: height,
}
}
-type Buffer struct {
- Title string
- Highlights int
- Content []Line
- Typings []string
+func (bs *bufferList) Resize(width, height int) {
+ bs.width = width
+ bs.height = height
}
-type BufferList struct {
- List []Buffer
- Current int
+func (bs *bufferList) Next() {
+ bs.current = (bs.current + 1) % len(bs.list)
}
-func (bs *BufferList) Add(title string) (pos int, ok bool) {
- for i, b := range bs.List {
- if b.Title == title {
- pos = i
+func (bs *bufferList) Previous() {
+ bs.current = (bs.current - 1 + len(bs.list)) % len(bs.list)
+}
+
+func (bs *bufferList) Add(title string) (ok bool) {
+ lTitle := strings.ToLower(title)
+ for _, b := range bs.list {
+ if strings.ToLower(b.title) == lTitle {
return
}
}
- pos = len(bs.List)
ok = true
- bs.List = append(bs.List, Buffer{Title: title})
-
+ bs.list = append(bs.list, buffer{title: title})
return
}
-func (bs *BufferList) Remove(title string) (ok bool) {
- for i, b := range bs.List {
- if b.Title == title {
+func (bs *bufferList) Remove(title string) (ok bool) {
+ lTitle := strings.ToLower(title)
+ for i, b := range bs.list {
+ if strings.ToLower(b.title) == lTitle {
ok = true
- bs.List = append(bs.List[:i], bs.List[i+1:]...)
-
- if i == bs.Current {
- bs.Current = 0
+ bs.list = append(bs.list[:i], bs.list[i+1:]...)
+ if len(bs.list) <= bs.current {
+ bs.current--
}
-
return
}
}
-
return
}
-func (bs *BufferList) Previous() (ok bool) {
- if bs.Current <= 0 {
- ok = false
+func (bs *bufferList) AddLine(title string, line Line) {
+ idx := bs.idx(title)
+ if idx < 0 {
+ return
+ }
+
+ b := &bs.list[idx]
+ n := len(b.lines)
+ line.body = strings.TrimRight(line.body, "\t ")
+
+ if line.isStatus && n != 0 && b.lines[n-1].isStatus {
+ l := &b.lines[n-1]
+ l.body += " " + line.body
+ l.computeSplitPoints()
+ l.width = 0
} else {
- bs.Current--
- ok = true
+ b.lines = append(b.lines, line)
+ if idx == bs.current && 0 < b.scrollAmt {
+ b.scrollAmt++
+ }
}
+}
- return
+func (bs *bufferList) AddLines(title string, lines []Line) {
+ idx := bs.idx(title)
+ if idx < 0 {
+ return
+ }
+
+ b := &bs.list[idx]
+ limit := len(lines)
+
+ if 0 < len(b.lines) {
+ firstLineTime := b.lines[0].at.Round(time.Millisecond)
+ for i := len(lines) - 1; 0 <= i; i-- {
+ if firstLineTime == lines[i].at.Round(time.Millisecond) {
+ limit = i
+ break
+ }
+ }
+ }
+
+ b.lines = append(lines[:limit], b.lines...)
}
-func (bs *BufferList) Next() (ok bool) {
- if bs.Current+1 < len(bs.List) {
- bs.Current++
- ok = true
- } else {
- ok = false
+func (bs *bufferList) TypingStart(title, nick string) {
+ idx := bs.idx(title)
+ if idx < 0 {
+ return
}
+ b := &bs.list[idx]
- return
+ lNick := strings.ToLower(nick)
+ for _, n := range b.typings {
+ if strings.ToLower(n) == lNick {
+ return
+ }
+ }
+ b.typings = append(b.typings, nick)
}
-func (bs *BufferList) Idx(title string) (idx int) {
- if title == "" {
- idx = 0
+func (bs *bufferList) TypingStop(title, nick string) {
+ idx := bs.idx(title)
+ if idx < 0 {
return
}
+ b := &bs.list[idx]
- for pos, b := range bs.List {
- if b.Title == title {
- idx = pos
+ lNick := strings.ToLower(nick)
+ for i, n := range b.typings {
+ if strings.ToLower(n) == lNick {
+ b.typings = append(b.typings[:i], b.typings[i+1:]...)
return
}
}
+}
+
+func (bs *bufferList) Current() (title string) {
+ return bs.list[bs.current].title
+}
- idx = -1
+func (bs *bufferList) CurrentOldestTime() (t *time.Time) {
+ ls := bs.list[bs.current].lines
+ if 0 < len(ls) {
+ t = &ls[0].at
+ }
return
}
-func (bs *BufferList) AddLine(idx int, line string, t time.Time, isStatus bool) {
- b := &bs.List[idx]
- n := len(bs.List[idx].Content)
+func (bs *bufferList) ScrollUp() {
+ b := &bs.list[bs.current]
+ if b.isAtTop {
+ return
+ }
+ b.scrollAmt += bs.height / 2
+}
+
+func (bs *bufferList) ScrollDown() {
+ b := &bs.list[bs.current]
+ b.scrollAmt -= bs.height / 2
- line = strings.TrimRight(line, "\t ")
+ if b.scrollAmt < 0 {
+ b.scrollAmt = 0
+ }
+}
- if isStatus && n != 0 && b.Content[n-1].IsStatus {
- l := &b.Content[n-1]
- l.Content += " " + line
+func (bs *bufferList) IsAtTop() bool {
+ b := &bs.list[bs.current]
+ return b.isAtTop
+}
+
+func (bs *bufferList) idx(title string) int {
+ if title == "" {
+ return bs.current
+ }
- lineWidth := StringWidth(line)
- lastSP := l.SplitPoints[len(l.SplitPoints)-1]
- sp := Point{
- X: lastSP.X + 1 + lineWidth,
- I: len(l.SplitPoints),
+ lTitle := strings.ToLower(title)
+ for i, b := range bs.list {
+ if strings.ToLower(b.title) == lTitle {
+ return i
}
+ }
+ return -1
+}
- l.SplitPoints = append(l.SplitPoints, sp)
- l.Invalidate()
- } else {
- if n == 0 || b.Content[n-1].Time.Truncate(time.Minute) != t.Truncate(time.Minute) {
- hour := t.Hour()
- minute := t.Minute()
+func (bs *bufferList) Draw(screen tcell.Screen) {
+ bs.list[bs.current].DrawLines(screen, bs.width, bs.height-3)
+ bs.drawStatusBar(screen, bs.height-3)
+ bs.drawTitleList(screen, bs.height-1)
+}
- line = fmt.Sprintf("\x02%02d:%02d\x00 %s", hour, minute, line)
+func (bs *bufferList) drawStatusBar(screen tcell.Screen, y int) {
+ st := tcell.StyleDefault.Dim(true)
+ nicks := bs.list[bs.current].typings
+ verb := " is typing..."
+ x := 0
+
+ if 1 < len(nicks) {
+ verb = " are typing..."
+ for _, nick := range nicks[:len(nicks)-2] {
+ printString(screen, &x, y, st, nick)
+ printString(screen, &x, y, st, ", ")
}
+ printString(screen, &x, y, st, nicks[len(nicks)-2])
+ printString(screen, &x, y, st, " and ")
+ }
+ if 0 < len(nicks) {
+ printString(screen, &x, y, st, nicks[len(nicks)-1])
+ printString(screen, &x, y, st, verb)
+ }
- l := NewLine(t, isStatus, line)
- b.Content = append(b.Content, l)
+ if 0 < x {
+ screen.SetContent(x, y, 0x251c, nil, st)
+ x++
+ }
+ for x < bs.width {
+ screen.SetContent(x, y, 0x2500, nil, st)
+ x++
}
}
-func (bs *BufferList) AddHistoryLines(idx int, lines []Line) {
- if len(lines) == 0 {
- return
+func (bs *bufferList) drawTitleList(screen tcell.Screen, y int) {
+ var widths []int
+ for _, b := range bs.list {
+ width := StringWidth(b.title)
+ if 0 < b.highlights {
+ width += int(math.Log10(float64(b.highlights))) + 1
+ }
+ widths = append(widths, width)
}
- b := &bs.List[idx]
- limit := -1
+ st := tcell.StyleDefault
- if len(b.Content) != 0 {
- firstTime := b.Content[0].Time.Round(time.Millisecond)
- for i := len(lines) - 1; i >= 0; i-- {
- if firstTime == lines[i].Time.Round(time.Millisecond) {
- limit = i
- break
- }
- }
+ for x := 0; x < bs.width; x++ {
+ screen.SetContent(x, y, ' ', nil, st)
}
- if limit == -1 {
- limit = len(lines)
+ x := (bs.width - widths[bs.current]) / 2
+ printString(screen, &x, y, st.Underline(true), bs.list[bs.current].title)
+ x += 2
+
+ i := (bs.current + 1) % len(bs.list)
+ for x < bs.width && i != bs.current {
+ b := &bs.list[i]
+ st = tcell.StyleDefault
+ if b.unread {
+ st = st.Bold(true)
+ }
+ printString(screen, &x, y, st, b.title)
+ if 0 < b.highlights {
+ st = st.Foreground(tcell.ColorRed).Reverse(true)
+ printNumber(screen, &x, y, st, b.highlights)
+ }
+ x += 2
+ i = (i + 1) % len(bs.list)
}
- bs.List[idx].Content = append(lines[:limit], b.Content...)
+ i = (bs.current - 1 + len(bs.list)) % len(bs.list)
+ x = (bs.width - widths[bs.current]) / 2
+ for 0 < x && i != bs.current {
+ x -= widths[i] + 2
+ b := &bs.list[i]
+ st = tcell.StyleDefault
+ if b.unread {
+ st = st.Bold(true)
+ }
+ printString(screen, &x, y, st, b.title)
+ if 0 < b.highlights {
+ st = st.Foreground(tcell.ColorRed).Reverse(true)
+ printNumber(screen, &x, y, st, b.highlights)
+ }
+ x -= widths[i]
+ i = (i - 1 + len(bs.list)) % len(bs.list)
+ }
}
-func (bs *BufferList) Invalidate() {
- for i := range bs.List {
- for j := range bs.List[i].Content {
- bs.List[i].Content[j].Invalidate()
- }
+func printString(screen tcell.Screen, x *int, y int, st tcell.Style, s string) {
+ for _, r := range s {
+ screen.SetContent(*x, y, r, nil, st)
+ *x += runewidth.RuneWidth(r)
}
}
-func (bs *BufferList) TypingStart(idx int, nick string) {
- b := &bs.List[idx]
+func printNumber(screen tcell.Screen, x *int, y int, st tcell.Style, n int) {
+ s := fmt.Sprintf("%d", n)
+ printString(screen, x, y, st, s)
+}
- for _, n := range b.Typings {
- if n == nick {
- return
- }
- }
+func printTime(screen tcell.Screen, x int, y int, st tcell.Style, t time.Time) {
+ hr0 := rune(t.Hour()/10) + '0'
+ hr1 := rune(t.Hour()%10) + '0'
+ mn0 := rune(t.Minute()/10) + '0'
+ mn1 := rune(t.Minute()%10) + '0'
+ screen.SetContent(x+0, y, hr0, nil, st)
+ screen.SetContent(x+1, y, hr1, nil, st)
+ screen.SetContent(x+2, y, ':', nil, st)
+ screen.SetContent(x+3, y, mn0, nil, st)
+ screen.SetContent(x+4, y, mn1, nil, st)
+}
- b.Typings = append(b.Typings, nick)
+func truncate(s string, w int) string {
+ c := runewidth.Condition{ZeroWidthJoiner: true}
+ return c.Truncate(s, w, "\u2026")
}
-func (bs *BufferList) TypingStop(idx int, nick string) {
- b := &bs.List[idx]
+func identColor(s string) (code int) {
+ h := fnv.New32()
+ _, _ = h.Write([]byte(s))
- for i, n := range b.Typings {
- if n == nick {
- b.Typings = append(b.Typings[:i], b.Typings[i+1:]...)
- return
- }
+ code = int(h.Sum32()) % 96
+ if 1 <= code {
+ code++
}
+ if 8 <= code {
+ code++
+ }
+
+ return
+}
+
+func ircColorCode(code int) string {
+ var c [3]rune
+ c[0] = 0x03
+ c[1] = rune(code/10) + '0'
+ c[2] = rune(code%10) + '0'
+ return string(c[:])
}
diff --git a/ui/buffers_test.go b/ui/buffers_test.go
index 80ac8ec..dc1cf02 100644
--- a/ui/buffers_test.go
+++ b/ui/buffers_test.go
@@ -1,42 +1,48 @@
package ui
-import "testing"
+import (
+ "strings"
+ "testing"
+)
-func assertSplitPoints(t *testing.T, line string, expected []Point) {
- l := Line{Content: line}
+func assertSplitPoints(t *testing.T, body string, expected []point) {
+ l := Line{body: body}
l.computeSplitPoints()
- if len(l.SplitPoints) != len(expected) {
- t.Errorf("%q: expected %d split points got %d", line, len(expected), len(l.SplitPoints))
+ if len(l.splitPoints) != len(expected) {
+ t.Errorf("%q: expected %d split points got %d", body, len(expected), len(l.splitPoints))
return
}
for i := 0; i < len(expected); i++ {
e := expected[i]
- a := l.SplitPoints[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)
+ t.Errorf("%q, point #%d: expected X=%d got %d", body, 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)
+ t.Errorf("%q, point #%d: expected I=%d got %d", body, i, e.I, a.I)
}
if e.Split != a.Split {
- t.Errorf("%q, point #%d: expected Split=%t got %t", line, i, e.Split, a.Split)
+ t.Errorf("%q, point #%d: expected Split=%t got %t", body, i, e.Split, a.Split)
}
}
}
func TestLineSplitPoints(t *testing.T) {
- assertSplitPoints(t, "hello", []Point{
+ assertSplitPoints(t, "hello", []point{
+ {X: 0, I: 0, Split: false},
{X: 5, I: 5, Split: true},
})
- assertSplitPoints(t, "hello world", []Point{
+ assertSplitPoints(t, "hello world", []point{
+ {X: 0, I: 0, Split: false},
{X: 5, I: 5, Split: true},
{X: 6, I: 6, Split: false},
{X: 11, I: 11, Split: true},
})
- assertSplitPoints(t, "lorem ipsum dolor shit amet", []Point{
+ assertSplitPoints(t, "lorem ipsum dolor shit amet", []point{
+ {X: 0, I: 0, Split: false},
{X: 5, I: 5, Split: true},
{X: 6, I: 6, Split: false},
{X: 11, I: 11, Split: true},
@@ -49,90 +55,122 @@ func TestLineSplitPoints(t *testing.T) {
})
}
-func assertRenderedHeight(t *testing.T, line string, width int, expected int) {
- l := Line{Content: line}
+func showSplit(s string, nls []int) string {
+ var sb strings.Builder
+ sb.Grow(len(s) + len(nls))
+
+ for i, r := range s {
+ if 0 < len(nls) && i == nls[0] {
+ sb.WriteRune('|')
+ nls = nls[1:]
+ }
+ sb.WriteRune(r)
+ }
+
+ return sb.String()
+}
+
+func assertNewLines(t *testing.T, body string, width int, expected int) {
+ l := Line{body: body}
l.computeSplitPoints()
- l.Invalidate()
- actual := l.RenderedHeight(width)
+ actual := l.NewLines(width)
- if actual != expected {
- t.Errorf("%q with width=%d expected to take %d lines, takes %d", line, width, expected, actual)
+ if len(actual)+1 != expected {
+ s := showSplit(body, actual)
+ t.Errorf("%q with width=%d expected to take %d lines, takes %d: '%s' (%v)", body, width, expected, len(actual)+1, s, actual)
+ return
}
}
func TestRenderedHeight(t *testing.T) {
- assertRenderedHeight(t, "0123456789", 1, 10)
- assertRenderedHeight(t, "0123456789", 2, 5)
- assertRenderedHeight(t, "0123456789", 3, 4)
- assertRenderedHeight(t, "0123456789", 4, 3)
- assertRenderedHeight(t, "0123456789", 5, 2)
- assertRenderedHeight(t, "0123456789", 6, 2)
- assertRenderedHeight(t, "0123456789", 7, 2)
- assertRenderedHeight(t, "0123456789", 8, 2)
- assertRenderedHeight(t, "0123456789", 9, 2)
- assertRenderedHeight(t, "0123456789", 10, 1)
- assertRenderedHeight(t, "0123456789", 11, 1)
+ assertNewLines(t, "0123456789", 1, 10)
+ assertNewLines(t, "0123456789", 2, 5)
+ assertNewLines(t, "0123456789", 3, 4)
+ assertNewLines(t, "0123456789", 4, 3)
+ assertNewLines(t, "0123456789", 5, 2)
+ assertNewLines(t, "0123456789", 6, 2)
+ assertNewLines(t, "0123456789", 7, 2)
+ assertNewLines(t, "0123456789", 8, 2)
+ assertNewLines(t, "0123456789", 9, 2)
+ assertNewLines(t, "0123456789", 10, 1)
+ assertNewLines(t, "0123456789", 11, 1)
// LEN=9, WIDTH=9
- assertRenderedHeight(t, "take care", 1, 8) // |t|a|k|e|c|a|r|e|
- assertRenderedHeight(t, "take care", 2, 4) // |ta|ke|ca|re|
- assertRenderedHeight(t, "take care", 3, 3) // |tak|e c|are|
- assertRenderedHeight(t, "take care", 4, 2) // |take|care|
- assertRenderedHeight(t, "take care", 5, 2) // |take |care |
- assertRenderedHeight(t, "take care", 6, 2) // |take |care |
- assertRenderedHeight(t, "take care", 7, 2) // |take |care |
- assertRenderedHeight(t, "take care", 8, 2) // |take |care |
- assertRenderedHeight(t, "take care", 9, 1) // |take care|
- assertRenderedHeight(t, "take care", 10, 1) // |take care |
+ assertNewLines(t, "take care", 1, 8) // |t|a|k|e|c|a|r|e|
+ assertNewLines(t, "take care", 2, 4) // |ta|ke|ca|re|
+ assertNewLines(t, "take care", 3, 3) // |tak|e c|are|
+ assertNewLines(t, "take care", 4, 2) // |take|care|
+ assertNewLines(t, "take care", 5, 2) // |take |care |
+ assertNewLines(t, "take care", 6, 2) // |take |care |
+ assertNewLines(t, "take care", 7, 2) // |take |care |
+ assertNewLines(t, "take care", 8, 2) // |take |care |
+ assertNewLines(t, "take care", 9, 1) // |take care|
+ assertNewLines(t, "take care", 10, 1) // |take care |
// LEN=10, WIDTH=10
- assertRenderedHeight(t, "take care", 1, 8) // |t|a|k|e|c|a|r|e|
- assertRenderedHeight(t, "take care", 2, 4) // |ta|ke|ca|re|
- assertRenderedHeight(t, "take care", 3, 4) // |tak|e |car|e |
- assertRenderedHeight(t, "take care", 4, 2) // |take|care|
- assertRenderedHeight(t, "take care", 5, 2) // |take |care |
- assertRenderedHeight(t, "take care", 6, 2) // |take |care |
- assertRenderedHeight(t, "take care", 7, 2) // |take |care |
- assertRenderedHeight(t, "take care", 8, 2) // |take |care |
- assertRenderedHeight(t, "take care", 9, 2) // |take |care |
- assertRenderedHeight(t, "take care", 10, 1) // |take care|
- assertRenderedHeight(t, "take care", 11, 1) // |take care |
+ assertNewLines(t, "take care", 1, 8) // |t|a|k|e|c|a|r|e|
+ assertNewLines(t, "take care", 2, 4) // |ta|ke|ca|re|
+ assertNewLines(t, "take care", 3, 4) // |tak|e |car|e |
+ assertNewLines(t, "take care", 4, 2) // |take|care|
+ assertNewLines(t, "take care", 5, 2) // |take |care |
+ assertNewLines(t, "take care", 6, 2) // |take |care |
+ assertNewLines(t, "take care", 7, 2) // |take |care |
+ assertNewLines(t, "take care", 8, 2) // |take |care |
+ assertNewLines(t, "take care", 9, 2) // |take |care |
+ assertNewLines(t, "take care", 10, 1) // |take care|
+ assertNewLines(t, "take care", 11, 1) // |take care |
// LEN=16, WIDTH=16
- assertRenderedHeight(t, "have a good day!", 1, 13) // |h|a|v|e|a|g|o|o|d|d|a|y|!|
- assertRenderedHeight(t, "have a good day!", 2, 7) // |ha|ve|a |go|od|da|y!|
- assertRenderedHeight(t, "have a good day!", 3, 5) // |hav|e a|goo|d d|ay!|
- assertRenderedHeight(t, "have a good day!", 4, 4) // |have|a |good|day!|
- assertRenderedHeight(t, "have a good day!", 5, 4) // |have |a |good |day! |
- assertRenderedHeight(t, "have a good day!", 6, 3) // |have a|good |day! |
- assertRenderedHeight(t, "have a good day!", 7, 3) // |have a |good |day! |
- assertRenderedHeight(t, "have a good day!", 8, 3) // |have a |good |day! |
- assertRenderedHeight(t, "have a good day!", 9, 2) // |have a |good day!|
- assertRenderedHeight(t, "have a good day!", 10, 2) // |have a |good day! |
- assertRenderedHeight(t, "have a good day!", 11, 2) // |have a good|day! |
- assertRenderedHeight(t, "have a good day!", 12, 2) // |have a good |day! |
- assertRenderedHeight(t, "have a good day!", 13, 2) // |have a good |day! |
- assertRenderedHeight(t, "have a good day!", 14, 2) // |have a good |day! |
- assertRenderedHeight(t, "have a good day!", 15, 2) // |have a good |day! |
- assertRenderedHeight(t, "have a good day!", 16, 1) // |have a good day!|
- assertRenderedHeight(t, "have a good day!", 17, 1) // |have a good day! |
+ assertNewLines(t, "have a good day!", 1, 13) // |h|a|v|e|a|g|o|o|d|d|a|y|!|
+ assertNewLines(t, "have a good day!", 2, 7) // |ha|ve|a |go|od|da|y!|
+ assertNewLines(t, "have a good day!", 3, 5) // |hav|e a|goo|d d|ay!|
+ assertNewLines(t, "have a good day!", 4, 4) // |have|a |good|day!|
+ assertNewLines(t, "have a good day!", 5, 4) // |have |a |good |day! |
+ assertNewLines(t, "have a good day!", 6, 3) // |have a|good |day! |
+ assertNewLines(t, "have a good day!", 7, 3) // |have a |good |day! |
+ assertNewLines(t, "have a good day!", 8, 3) // |have a |good |day! |
+ assertNewLines(t, "have a good day!", 9, 2) // |have a |good day!|
+ assertNewLines(t, "have a good day!", 10, 2) // |have a |good day! |
+ assertNewLines(t, "have a good day!", 11, 2) // |have a good|day! |
+ assertNewLines(t, "have a good day!", 12, 2) // |have a good |day! |
+ assertNewLines(t, "have a good day!", 13, 2) // |have a good |day! |
+ assertNewLines(t, "have a good day!", 14, 2) // |have a good |day! |
+ assertNewLines(t, "have a good day!", 15, 2) // |have a good |day! |
+ assertNewLines(t, "have a good day!", 16, 1) // |have a good day!|
+ assertNewLines(t, "have a good day!", 17, 1) // |have a good day! |
// LEN=15, WIDTH=11
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 1, 10) // |b|a|r|m|a|n|d|:|c|c|
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 2, 5) // |ba|rm|an|d:|cc|
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 3, 4) // |bar|man|d: |cc |
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 4, 3) // |barm|and:|cc |
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 5, 3) // |barma|nd: |cc |
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 6, 2) // |barman|d: cc |
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 7, 2) // |barmand|: cc |
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 8, 2) // |barmand:|cc |
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 9, 2) // |barmand: |cc |
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 10, 2) // |barmand: |cc |
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 11, 1) // |barmand: cc|
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 12, 1) // |barmand: cc |
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 13, 1) // |barmand: cc |
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 14, 1) // |barmand: cc |
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 15, 1) // |barmand: cc |
- assertRenderedHeight(t, "\x0342barmand\x03: cc", 16, 1) // |barmand: cc |
+ assertNewLines(t, "\x0342barmand\x03: cc", 1, 10) // |b|a|r|m|a|n|d|:|c|c|
+ assertNewLines(t, "\x0342barmand\x03: cc", 2, 5) // |ba|rm|an|d:|cc|
+ assertNewLines(t, "\x0342barmand\x03: cc", 3, 4) // |bar|man|d: |cc |
+ assertNewLines(t, "\x0342barmand\x03: cc", 4, 3) // |barm|and:|cc |
+ assertNewLines(t, "\x0342barmand\x03: cc", 5, 3) // |barma|nd: |cc |
+ assertNewLines(t, "\x0342barmand\x03: cc", 6, 2) // |barman|d: cc |
+ assertNewLines(t, "\x0342barmand\x03: cc", 7, 2) // |barmand|: cc |
+ assertNewLines(t, "\x0342barmand\x03: cc", 8, 2) // |barmand:|cc |
+ assertNewLines(t, "\x0342barmand\x03: cc", 9, 2) // |barmand: |cc |
+ assertNewLines(t, "\x0342barmand\x03: cc", 10, 2) // |barmand: |cc |
+ assertNewLines(t, "\x0342barmand\x03: cc", 11, 1) // |barmand: cc|
+ assertNewLines(t, "\x0342barmand\x03: cc", 12, 1) // |barmand: cc |
+ assertNewLines(t, "\x0342barmand\x03: cc", 13, 1) // |barmand: cc |
+ assertNewLines(t, "\x0342barmand\x03: cc", 14, 1) // |barmand: cc |
+ assertNewLines(t, "\x0342barmand\x03: cc", 15, 1) // |barmand: cc |
+ assertNewLines(t, "\x0342barmand\x03: cc", 16, 1) // |barmand: cc |
+
+ assertNewLines(t, "cc en direct du word wrapping des familles le tests ça v a va va v a va", 46, 2)
+}
+
+/*
+func assertTrimWidth(t *testing.T, s string, w int, expected string) {
+ actual := trimWidth(s, w)
+ if actual != expected {
+ t.Errorf("%q (width=%d): expected to be trimmed as %q, got %q\n", s, w, expected, actual)
+ }
+}
+
+func TestTrimWidth(t *testing.T) {
+ assertTrimWidth(t, "ludovicchabant/fn", 16, "ludovicchabant/…")
+ assertTrimWidth(t, "zzzzzzzzzzzzzz黒猫/sr", 16, "zzzzzzzzzzzzzz黒…")
}
+// */
diff --git a/ui/style.go b/ui/style.go
new file mode 100644
index 0000000..9c09b78
--- /dev/null
+++ b/ui/style.go
@@ -0,0 +1,191 @@
+package ui
+
+import (
+ "github.com/gdamore/tcell"
+ "github.com/mattn/go-runewidth"
+)
+
+type widthBuffer struct {
+ width int
+ color colorBuffer
+}
+
+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 ok := wb.color.WriteRune(r); ok != 0 {
+ if 1 < ok {
+ wb.width++
+ }
+ wb.width += runewidth.RuneWidth(r)
+ }
+}
+
+func StringWidth(s string) int {
+ var wb widthBuffer
+ wb.WriteString(s)
+ return wb.Width()
+}
+
+type styleBuffer struct {
+ st tcell.Style
+ color colorBuffer
+ bold bool
+ italic bool
+ underline bool
+}
+
+func (sb *styleBuffer) Reset() {
+ sb.color.Reset()
+ sb.st = tcell.StyleDefault
+ sb.bold = false
+ sb.italic = false
+ sb.underline = false
+}
+
+func (sb *styleBuffer) WriteRune(r rune) (st tcell.Style, ok int) {
+ if r == 0x00 || r == 0x0F {
+ sb.Reset()
+ return sb.st, 0
+ }
+ if r == 0x02 {
+ sb.bold = !sb.bold
+ sb.st = sb.st.Bold(sb.bold)
+ return sb.st, 0
+ }
+ if r == 0x1D {
+ sb.italic = !sb.italic
+ //sb.st = st.Italic(sb.italic)
+ return sb.st, 0
+ }
+ if r == 0x1F {
+ sb.underline = !sb.underline
+ sb.st = st.Underline(sb.underline)
+ return sb.st, 0
+ }
+ if ok = sb.color.WriteRune(r); ok != 0 {
+ sb.st = sb.color.Style(sb.st)
+ }
+
+ return sb.st, ok
+}
+
+type colorBuffer struct {
+ state int
+ fg, bg int
+}
+
+func (cb *colorBuffer) Reset() {
+ cb.state = 0
+ cb.fg = -1
+ cb.bg = -1
+}
+
+func (cb *colorBuffer) Style(st tcell.Style) tcell.Style {
+ if 0 <= cb.fg {
+ st = st.Foreground(colorFromCode(cb.fg))
+ }
+ if 0 <= cb.bg {
+ st = st.Background(colorFromCode(cb.bg))
+ }
+ return st
+}
+
+func (cb *colorBuffer) WriteRune(r rune) (ok int) {
+ if cb.state == 1 {
+ if '0' <= r && r <= '9' {
+ cb.fg = int(r - '0')
+ cb.state = 2
+ return
+ }
+ } else if cb.state == 2 {
+ if '0' <= r && r <= '9' {
+ cb.fg = 10*cb.fg + int(r-'0')
+ cb.state = 3
+ return
+ }
+ if r == ',' {
+ cb.state = 4
+ return
+ }
+ } else if cb.state == 3 {
+ if r == ',' {
+ cb.state = 4
+ return
+ }
+ } else if cb.state == 4 {
+ if '0' <= r && r <= '9' {
+ cb.bg = int(r - '0')
+ cb.state = 5
+ return
+ }
+ ok++
+ } else if cb.state == 5 {
+ cb.state = 0
+ if '0' <= r && r <= '9' {
+ cb.bg = 10*cb.bg + int(r-'0')
+ return
+ }
+ }
+
+ if r == 0x03 {
+ cb.state = 1
+ cb.fg = -1
+ cb.bg = -1
+ return
+ }
+
+ cb.state = 0
+ ok++
+ return
+}
+
+func colorFromCode(code int) (color tcell.Color) {
+ switch code {
+ case 0:
+ color = tcell.ColorWhite
+ case 1:
+ color = tcell.ColorBlack
+ case 2:
+ color = tcell.ColorBlue
+ case 3:
+ color = tcell.ColorGreen
+ case 4:
+ color = tcell.ColorRed
+ case 5:
+ color = tcell.ColorBrown
+ case 6:
+ color = tcell.ColorPurple
+ case 7:
+ color = tcell.ColorOrange
+ case 8:
+ color = tcell.ColorYellow
+ case 9:
+ color = tcell.ColorLightGreen
+ case 10:
+ color = tcell.ColorTeal
+ case 11:
+ color = tcell.ColorFuchsia
+ case 12:
+ color = tcell.ColorLightBlue
+ case 13:
+ color = tcell.ColorPink
+ case 14:
+ color = tcell.ColorGrey
+ case 15:
+ color = tcell.ColorLightGrey
+ case 99:
+ color = tcell.ColorDefault
+ default:
+ color = tcell.Color(code)
+ }
+ return
+}
diff --git a/ui/width_test.go b/ui/style_test.go
index ae478ab..ae478ab 100644
--- a/ui/width_test.go
+++ b/ui/style_test.go
diff --git a/ui/ui.go b/ui/ui.go
index bbf170a..2001d66 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -1,11 +1,11 @@
package ui
import (
- "github.com/gdamore/tcell"
- "github.com/mattn/go-runewidth"
"math/rand"
"sync/atomic"
"time"
+
+ "github.com/gdamore/tcell"
)
type UI struct {
@@ -13,11 +13,8 @@ type UI struct {
Events chan tcell.Event
exit atomic.Value // bool
- bufferList BufferList
- scrollAmt int
- scrollAtTop bool
-
- e editor
+ bs bufferList
+ e editor
}
func New() (ui *UI, err error) {
@@ -47,15 +44,9 @@ func New() (ui *UI, err error) {
ui.exit.Store(false)
hmIdx := rand.Intn(len(homeMessages))
- ui.bufferList = BufferList{
- List: []Buffer{
- {
- Title: "home",
- Highlights: 0,
- Content: []Line{NewLineNow(homeMessages[hmIdx])},
- },
- },
- }
+ ui.bs = newBufferList(w, h)
+ ui.bs.Add(Home)
+ ui.bs.AddLine("", NewLineNow("--", homeMessages[hmIdx]))
ui.e = newEditor(w)
@@ -76,147 +67,70 @@ func (ui *UI) Close() {
ui.screen.Fini()
}
-func (ui *UI) CurrentBuffer() (title string) {
- title = ui.bufferList.List[ui.bufferList.Current].Title
- return
+func (ui *UI) CurrentBuffer() string {
+ return ui.bs.Current()
}
-func (ui *UI) CurrentBufferOldestTime() (t time.Time) {
- b := ui.bufferList.List[ui.bufferList.Current].Content
- if len(b) == 0 {
- t = time.Now()
- } else {
- t = b[0].Time
- }
- return
+func (ui *UI) CurrentBufferOldestTime() (t *time.Time) {
+ return ui.bs.CurrentOldestTime()
}
-func (ui *UI) NextBuffer() (ok bool) {
- ok = ui.bufferList.Next()
- if ok {
- ui.scrollAmt = 0
- ui.scrollAtTop = false
- ui.drawTyping()
- ui.drawBuffer()
- ui.drawStatus()
- }
- return
+func (ui *UI) NextBuffer() {
+ ui.bs.Next()
+ ui.draw()
}
-func (ui *UI) PreviousBuffer() (ok bool) {
- ok = ui.bufferList.Previous()
- if ok {
- ui.scrollAmt = 0
- ui.scrollAtTop = false
- ui.drawTyping()
- ui.drawBuffer()
- ui.drawStatus()
- }
- return
+func (ui *UI) PreviousBuffer() {
+ ui.bs.Previous()
+ ui.draw()
}
func (ui *UI) ScrollUp() {
- if ui.scrollAtTop {
- return
- }
-
- _, h := ui.screen.Size()
- ui.scrollAmt += h / 2
- ui.drawBuffer()
+ ui.bs.ScrollUp()
+ ui.draw()
}
func (ui *UI) ScrollDown() {
- if ui.scrollAmt == 0 {
- return
- }
-
- _, h := ui.screen.Size()
- ui.scrollAmt -= h / 2
- if ui.scrollAmt < 0 {
- ui.scrollAmt = 0
- }
- ui.scrollAtTop = false
-
- ui.drawBuffer()
+ ui.bs.ScrollDown()
+ ui.draw()
}
func (ui *UI) IsAtTop() bool {
- return ui.scrollAtTop
+ return ui.bs.IsAtTop()
}
func (ui *UI) AddBuffer(title string) {
- _, ok := ui.bufferList.Add(title)
+ ok := ui.bs.Add(title)
if ok {
- ui.drawStatus()
- ui.drawTyping()
- ui.drawBuffer() // TODO only invalidate buffer list
+ ui.draw()
}
}
func (ui *UI) RemoveBuffer(title string) {
- ok := ui.bufferList.Remove(title)
+ ok := ui.bs.Remove(title)
if ok {
- ui.drawStatus()
- ui.drawTyping()
- ui.drawBuffer()
+ ui.draw()
}
}
-func (ui *UI) AddLine(buffer string, line string, t time.Time, isStatus bool) {
- idx := ui.bufferList.Idx(buffer)
- if idx < 0 {
- return
- }
-
- ui.bufferList.AddLine(idx, line, t, isStatus)
+func (ui *UI) AddLine(buffer string, line Line) {
+ ui.bs.AddLine(buffer, line)
+ ui.draw()
+}
- if idx == ui.bufferList.Current {
- if 0 < ui.scrollAmt {
- ui.scrollAmt++
- } else {
- ui.drawBuffer()
- }
- }
+func (ui *UI) AddLines(buffer string, lines []Line) {
+ ui.bs.AddLines(buffer, lines)
+ ui.draw()
}
func (ui *UI) TypingStart(buffer, nick string) {
- idx := ui.bufferList.Idx(buffer)
- if idx < 0 {
- return
- }
-
- ui.bufferList.TypingStart(idx, nick)
-
- if idx == ui.bufferList.Current {
- ui.drawTyping()
- }
+ ui.bs.TypingStart(buffer, nick)
+ ui.draw()
}
func (ui *UI) TypingStop(buffer, nick string) {
- idx := ui.bufferList.Idx(buffer)
- if idx < 0 {
- return
- }
-
- ui.bufferList.TypingStop(idx, nick)
-
- if idx == ui.bufferList.Current {
- ui.drawTyping()
- }
-}
-
-func (ui *UI) AddHistoryLines(buffer string, lines []Line) {
- idx := ui.bufferList.Idx(buffer)
- if idx < 0 {
- return
- }
-
- ui.bufferList.AddHistoryLines(idx, lines)
-
- if idx == ui.bufferList.Current {
- ui.scrollAtTop = false
- ui.drawBuffer()
- }
+ ui.bs.TypingStop(buffer, nick)
+ ui.draw()
}
func (ui *UI) InputIsCommand() bool {
@@ -229,337 +143,48 @@ func (ui *UI) InputLen() int {
func (ui *UI) InputRune(r rune) {
ui.e.PutRune(r)
- _, h := ui.screen.Size()
- ui.e.Draw(ui.screen, h-2)
- ui.screen.Show()
+ ui.draw()
}
func (ui *UI) InputRight() {
ui.e.Right()
- _, h := ui.screen.Size()
- ui.e.Draw(ui.screen, h-2)
- ui.screen.Show()
+ ui.draw()
}
func (ui *UI) InputLeft() {
ui.e.Left()
- _, h := ui.screen.Size()
- ui.e.Draw(ui.screen, h-2)
- ui.screen.Show()
+ ui.draw()
}
func (ui *UI) InputBackspace() (ok bool) {
ok = ui.e.RemRune()
if ok {
- _, h := ui.screen.Size()
- ui.e.Draw(ui.screen, h-2)
- ui.screen.Show()
+ ui.draw()
}
return
}
func (ui *UI) InputEnter() (content string) {
content = ui.e.Flush()
- _, h := ui.screen.Size()
- ui.e.Draw(ui.screen, h-2)
- ui.screen.Show()
+ ui.draw()
return
}
func (ui *UI) Resize() {
- w, _ := ui.screen.Size()
+ w, h := ui.screen.Size()
ui.e.Resize(w)
- ui.bufferList.Invalidate()
- ui.scrollAmt = 0
+ ui.bs.Resize(w, h)
ui.draw()
}
func (ui *UI) draw() {
_, h := ui.screen.Size()
- ui.drawStatus()
ui.e.Draw(ui.screen, h-2)
- ui.drawTyping()
- ui.drawBuffer()
-}
-
-func (ui *UI) drawTyping() {
- st := tcell.StyleDefault.Dim(true)
- w, h := ui.screen.Size()
- if w == 0 {
- return
- }
-
- nicks := ui.bufferList.List[ui.bufferList.Current].Typings
- if len(nicks) == 0 {
- return
- }
-
- x := 0
- y := h - 3
-
- if 1 < len(nicks) {
- for _, nick := range nicks[:len(nicks)-2] {
- for _, r := range nick {
- if w <= x {
- return
- }
- ui.screen.SetContent(x, y, r, nil, st)
- x += runewidth.RuneWidth(r)
- }
-
- if w <= x {
- return
- }
- ui.screen.SetContent(x, y, ',', nil, st)
- x++
- if w <= x {
- return
- }
- ui.screen.SetContent(x, y, ' ', nil, st)
- x++
- }
-
- for _, r := range nicks[len(nicks)-2] {
- if w <= x {
- return
- }
- ui.screen.SetContent(x, y, r, nil, st)
- x += runewidth.RuneWidth(r)
- }
-
- if w <= x {
- return
- }
- ui.screen.SetContent(x, y, ' ', nil, st)
- x++
- if w <= x {
- return
- }
- ui.screen.SetContent(x, y, 'a', nil, st)
- x++
- if w <= x {
- return
- }
- ui.screen.SetContent(x, y, 'n', nil, st)
- x++
- if w <= x {
- return
- }
- ui.screen.SetContent(x, y, 'd', nil, st)
- x++
- if w <= x {
- return
- }
- ui.screen.SetContent(x, y, ' ', nil, st)
- x++
- }
-
- for _, r := range nicks[len(nicks)-1] {
- if w <= x {
- return
- }
- ui.screen.SetContent(x, y, r, nil, st)
- x += runewidth.RuneWidth(r)
- }
-
- verb := " are typing..."
- if len(nicks) == 1 {
- verb = " is typing..."
- }
-
- for _, r := range verb {
- if w <= x {
- return
- }
- ui.screen.SetContent(x, y, r, nil, st)
- x += runewidth.RuneWidth(r)
- }
-
- for ; x < w; x++ {
- ui.screen.SetContent(x, y, ' ', nil, st)
- }
-
- ui.screen.Show()
-}
-
-func (ui *UI) drawBuffer() {
- st := tcell.StyleDefault
- w, h := ui.screen.Size()
- if h < 3 {
- return
- }
-
- for x := 0; x < w; x++ {
- for y := 0; y < h-3; y++ {
- ui.screen.SetContent(x, y, ' ', nil, st)
- }
- }
-
- b := ui.bufferList.List[ui.bufferList.Current]
-
- if len(b.Content) == 0 {
- ui.scrollAtTop = true
- return
- }
-
- var bold, italic, underline bool
- var colorState int
- var fgColor, bgColor int
-
- yEnd := h - 3
- y0 := ui.scrollAmt + h - 3
-
- for i := len(b.Content) - 1; 0 <= i; i-- {
- line := &b.Content[i]
-
- if y0 < 0 {
- break
- }
-
- lineHeight := line.RenderedHeight(w)
- y0 -= lineHeight
- if yEnd <= y0 {
- continue
- }
-
- rs := []rune(line.Content)
- x := 0
- y := y0
- var lastSP Point
- spIdx := 0
-
- for i, r := range rs {
- if i == line.SplitPoints[spIdx].I {
- lastSP = line.SplitPoints[spIdx]
- spIdx++
-
- l := line.SplitPoints[spIdx].X - lastSP.X
-
- if w < l {
- } else if w == l {
- if x == 0 {
- y++
- }
- } else if w < x+l {
- y++
- x = 0
- }
- }
- if !line.SplitPoints[spIdx].Split && x == 0 {
- continue
- }
- if w <= x {
- y++
- x = 0
- }
-
- if colorState == 1 {
- fgColor = 0
- bgColor = 0
- if '0' <= r && r <= '9' {
- fgColor = fgColor*10 + int(r-'0')
- colorState = 2
- continue
- }
- st = st.Foreground(tcell.ColorDefault)
- st = st.Background(tcell.ColorDefault)
- colorState = 0
- } else if colorState == 2 {
- if '0' <= r && r <= '9' {
- fgColor = fgColor*10 + int(r-'0')
- colorState = 3
- continue
- }
- if r == ',' {
- colorState = 4
- continue
- }
- c := colorFromCode(fgColor)
- st = st.Foreground(c)
- colorState = 0
- } else if colorState == 3 {
- if r == ',' {
- colorState = 4
- continue
- }
- c := colorFromCode(fgColor)
- st = st.Foreground(c)
- colorState = 0
- } else if colorState == 4 {
- if '0' <= r && r <= '9' {
- bgColor = bgColor*10 + int(r-'0')
- colorState = 5
- continue
- }
-
- c := colorFromCode(fgColor)
- st = st.Foreground(c)
- colorState = 0
-
- ui.screen.SetContent(x, y, ',', nil, st)
- x++
- if w <= x {
- y++
- x = 0
- }
- } else if colorState == 5 {
- colorState = 0
- st = st.Foreground(colorFromCode(fgColor))
-
- if '0' <= r && r <= '9' {
- bgColor = bgColor*10 + int(r-'0')
- st = st.Background(colorFromCode(bgColor))
- continue
- }
-
- st = st.Background(colorFromCode(bgColor))
- }
-
- if r == 0x00 || r == 0x0F {
- bold = false
- italic = false
- underline = false
- colorState = 0
- st = tcell.StyleDefault
- continue
- }
- if r == 0x02 {
- bold = !bold
- st = st.Bold(bold)
- continue
- }
- if r == 0x03 {
- colorState = 1
- continue
- }
- if r == 0x1D {
- italic = !italic
- //st = st.Italic(italic)
- continue
- }
- if r == 0x1F {
- underline = !underline
- st = st.Underline(underline)
- continue
- }
-
- if 0 <= y {
- ui.screen.SetContent(x, y, r, nil, st)
- }
- x += runewidth.RuneWidth(r)
- }
-
- st = tcell.StyleDefault
- bold = false
- italic = false
- underline = false
- colorState = 0
- }
-
- ui.scrollAtTop = 0 <= y0
+ ui.bs.Draw(ui.screen)
ui.screen.Show()
}
+/*
func (ui *UI) drawStatus() {
st := tcell.StyleDefault
w, h := ui.screen.Size()
@@ -618,45 +243,4 @@ func (ui *UI) drawStatus() {
ui.screen.Show()
}
-
-func colorFromCode(code int) (color tcell.Color) {
- switch code {
- case 0:
- color = tcell.ColorWhite
- case 1:
- color = tcell.ColorBlack
- case 2:
- color = tcell.ColorBlue
- case 3:
- color = tcell.ColorGreen
- case 4:
- color = tcell.ColorRed
- case 5:
- color = tcell.ColorBrown
- case 6:
- color = tcell.ColorPurple
- case 7:
- color = tcell.ColorOrange
- case 8:
- color = tcell.ColorYellow
- case 9:
- color = tcell.ColorLightGreen
- case 10:
- color = tcell.ColorTeal
- case 11:
- color = tcell.ColorFuchsia
- case 12:
- color = tcell.ColorLightBlue
- case 13:
- color = tcell.ColorPink
- case 14:
- color = tcell.ColorGrey
- case 15:
- color = tcell.ColorLightGrey
- case 99:
- color = tcell.ColorDefault
- default:
- color = tcell.Color(code)
- }
- return
-}
+// */
diff --git a/ui/width.go b/ui/width.go
deleted file mode 100644
index 6634993..0000000
--- a/ui/width.go
+++ /dev/null
@@ -1,75 +0,0 @@
-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()
-}