summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app.go174
-rw-r--r--commands.go44
-rw-r--r--ui/buffers.go84
-rw-r--r--ui/buffers_test.go20
-rw-r--r--ui/draw_utils.go39
-rw-r--r--ui/style.go375
-rw-r--r--ui/style_test.go103
-rw-r--r--ui/ui.go21
-rw-r--r--window.go27
9 files changed, 515 insertions, 372 deletions
diff --git a/app.go b/app.go
index c49c2aa..76f26f1 100644
--- a/app.go
+++ b/app.go
@@ -8,6 +8,7 @@ import (
"os/exec"
"strings"
"time"
+ "unicode"
"git.sr.ht/~taiite/senpai/irc"
"git.sr.ht/~taiite/senpai/ui"
@@ -69,7 +70,7 @@ func NewApp(cfg Config) (app *App, err error) {
if err != nil {
return
}
- app.win.SetPrompt(">")
+ app.win.SetPrompt(ui.PlainString(">"))
app.initWindow()
@@ -147,7 +148,7 @@ func (app *App) ircLoop() {
app.queueStatusLine(ui.Line{
At: time.Now(),
Head: "IN --",
- Body: msg.String(),
+ Body: ui.PlainString(msg.String()),
})
}
app.events <- event{
@@ -161,8 +162,8 @@ func (app *App) ircLoop() {
}
app.queueStatusLine(ui.Line{
Head: "!!",
- HeadColor: ui.ColorRed,
- Body: "Connection lost",
+ HeadColor: tcell.ColorRed,
+ Body: ui.PlainString("Connection lost"),
})
time.Sleep(10 * time.Second)
}
@@ -172,7 +173,7 @@ func (app *App) connect() net.Conn {
for {
app.queueStatusLine(ui.Line{
Head: "--",
- Body: fmt.Sprintf("Connecting to %s...", app.cfg.Addr),
+ Body: ui.PlainSprintf("Connecting to %s...", app.cfg.Addr),
})
conn, err := app.tryConnect()
if err == nil {
@@ -180,8 +181,8 @@ func (app *App) connect() net.Conn {
}
app.queueStatusLine(ui.Line{
Head: "!!",
- HeadColor: ui.ColorRed,
- Body: fmt.Sprintf("Connection failed: %v", err),
+ HeadColor: tcell.ColorRed,
+ Body: ui.PlainSprintf("Connection failed: %v", err),
})
time.Sleep(1 * time.Minute)
}
@@ -229,7 +230,7 @@ func (app *App) debugOutputMessages(out chan<- irc.Message) chan<- irc.Message {
app.queueStatusLine(ui.Line{
At: time.Now(),
Head: "OUT --",
- Body: msg.String(),
+ Body: ui.PlainString(msg.String()),
})
out <- msg
}
@@ -408,8 +409,8 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) {
app.win.AddLine(app.win.CurrentBuffer(), false, ui.Line{
At: time.Now(),
Head: "!!",
- HeadColor: ui.ColorRed,
- Body: fmt.Sprintf("%q: %s", input, err),
+ HeadColor: tcell.ColorRed,
+ Body: ui.PlainSprintf("%q: %s", input, err),
})
}
app.updatePrompt()
@@ -459,28 +460,48 @@ func (app *App) handleIRCEvent(ev interface{}) {
// Mutate UI state
switch ev := ev.(type) {
case irc.RegisteredEvent:
- body := "Connected to the server"
+ body := new(ui.StyledStringBuilder)
+ body.WriteString("Connected to the server")
if app.s.Nick() != app.cfg.Nick {
- body += " as " + app.s.Nick()
+ body.WriteString(" as ")
+ body.WriteString(app.s.Nick())
}
app.win.AddLine(Home, false, ui.Line{
At: msg.TimeOrNow(),
Head: "--",
- Body: body,
+ Body: body.StyledString(),
})
case irc.SelfNickEvent:
- app.win.AddLine(app.win.CurrentBuffer(), true, ui.Line{
+ body := new(ui.StyledStringBuilder)
+ body.Grow(len(ev.FormerNick) + 4 + len(app.s.Nick()))
+ body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
+ body.WriteString(ev.FormerNick)
+ body.SetStyle(tcell.StyleDefault)
+ body.WriteRune('\u2192') // right arrow
+ body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
+ body.WriteString(app.s.Nick())
+ app.addStatusLine(ui.Line{
At: msg.TimeOrNow(),
Head: "--",
- Body: fmt.Sprintf("\x0314%s\x03\u2192\x0314%s\x03", ev.FormerNick, app.s.Nick()),
+ HeadColor: tcell.ColorGray,
+ Body: body.StyledString(),
Highlight: true,
})
case irc.UserNickEvent:
+ body := new(ui.StyledStringBuilder)
+ body.Grow(len(ev.FormerNick) + 4 + len(ev.User))
+ body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
+ body.WriteString(ev.FormerNick)
+ body.SetStyle(tcell.StyleDefault)
+ body.WriteRune('\u2192') // right arrow
+ body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
+ body.WriteString(ev.User)
for _, c := range app.s.ChannelsSharedWith(ev.User) {
app.win.AddLine(c, false, ui.Line{
At: msg.TimeOrNow(),
Head: "--",
- Body: fmt.Sprintf("\x0314%s\x03\u2192\x0314%s\x03", ev.FormerNick, ev.User),
+ HeadColor: tcell.ColorGray,
+ Body: body.StyledString(),
Mergeable: true,
})
}
@@ -490,41 +511,68 @@ func (app *App) handleIRCEvent(ev interface{}) {
WithLimit(200).
Before(msg.TimeOrNow())
case irc.UserJoinEvent:
+ body := new(ui.StyledStringBuilder)
+ body.Grow(len(ev.User) + 1)
+ body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGreen))
+ body.WriteByte('+')
+ body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
+ body.WriteString(ev.User)
app.win.AddLine(ev.Channel, false, ui.Line{
At: msg.TimeOrNow(),
Head: "--",
- Body: fmt.Sprintf("\x033+\x0314%s\x03", ev.User),
+ HeadColor: tcell.ColorGray,
+ Body: body.StyledString(),
Mergeable: true,
})
case irc.SelfPartEvent:
app.win.RemoveBuffer(ev.Channel)
case irc.UserPartEvent:
+ body := new(ui.StyledStringBuilder)
+ body.Grow(len(ev.User) + 1)
+ body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorRed))
+ body.WriteByte('-')
+ body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
+ body.WriteString(ev.User)
app.win.AddLine(ev.Channel, false, ui.Line{
At: msg.TimeOrNow(),
Head: "--",
- Body: fmt.Sprintf("\x034-\x0314%s\x03", ev.User),
+ HeadColor: tcell.ColorGray,
+ Body: body.StyledString(),
Mergeable: true,
})
case irc.UserQuitEvent:
+ body := new(ui.StyledStringBuilder)
+ body.Grow(len(ev.User) + 1)
+ body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorRed))
+ body.WriteByte('-')
+ body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
+ body.WriteString(ev.User)
for _, c := range ev.Channels {
app.win.AddLine(c, false, ui.Line{
At: msg.TimeOrNow(),
Head: "--",
- Body: fmt.Sprintf("\x034-\x0314%s\x03", ev.User),
+ HeadColor: tcell.ColorGray,
+ Body: body.StyledString(),
Mergeable: true,
})
}
case irc.TopicChangeEvent:
+ body := new(ui.StyledStringBuilder)
+ body.Grow(len(ev.Topic) + 18)
+ body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
+ body.WriteString("Topic changed to: ")
+ body.WriteString(ev.Topic)
app.win.AddLine(ev.Channel, false, ui.Line{
- At: msg.TimeOrNow(),
- Head: "--",
- Body: fmt.Sprintf("\x0314Topic changed to: %s\x03", ev.Topic),
+ At: msg.TimeOrNow(),
+ Head: "--",
+ HeadColor: tcell.ColorGray,
+ Body: body.StyledString(),
})
case irc.MessageEvent:
buffer, line, hlNotification := app.formatMessage(ev)
app.win.AddLine(buffer, hlNotification, line)
if hlNotification {
- app.notifyHighlight(buffer, ev.User, ev.Content)
+ app.notifyHighlight(buffer, ev.User, line.Body.String())
}
if !app.s.IsChannel(msg.Params[0]) && !app.s.IsMe(ev.User) {
app.lastQuery = msg.Prefix.Name
@@ -561,7 +609,7 @@ func (app *App) handleIRCEvent(ev interface{}) {
app.addStatusLine(ui.Line{
At: msg.TimeOrNow(),
Head: head,
- Body: body,
+ Body: ui.PlainString(body),
})
}
}
@@ -608,7 +656,7 @@ func (app *App) notifyHighlight(buffer, nick, content string) {
fmt.Sprintf("BUFFER=%s", buffer),
fmt.Sprintf("HERE=%s", here),
fmt.Sprintf("SENDER=%s", nick),
- fmt.Sprintf("MESSAGE=%s", cleanMessage(content)),
+ fmt.Sprintf("MESSAGE=%s", content),
)
output, err := cmd.CombinedOutput()
if err != nil {
@@ -616,8 +664,8 @@ func (app *App) notifyHighlight(buffer, nick, content string) {
app.addStatusLine(ui.Line{
At: time.Now(),
Head: "!!",
- HeadColor: ui.ColorRed,
- Body: body,
+ HeadColor: tcell.ColorRed,
+ Body: ui.PlainString(body),
})
}
}
@@ -690,33 +738,44 @@ func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line,
hlNotification = (isHighlight || isQuery) && !isFromSelf
head := ev.User
- headColor := ui.ColorWhite
+ headColor := tcell.ColorWhite
if isFromSelf && isQuery {
head = "\u2192 " + ev.Target
- headColor = ui.IdentColor(ev.Target)
+ headColor = identColor(ev.Target)
} else if isAction || isNotice {
head = "*"
} else {
- headColor = ui.IdentColor(head)
- }
-
- body := strings.TrimSuffix(ev.Content, "\x01")
- if isNotice && isAction {
- c := ircColorSequence(ui.IdentColor(ev.User))
- body = fmt.Sprintf("%s%s\x0F:%s", c, ev.User, body[7:])
+ headColor = identColor(head)
+ }
+
+ content := strings.TrimSuffix(ev.Content, "\x01")
+ content = strings.TrimRightFunc(ev.Content, unicode.IsSpace)
+ if isAction {
+ content = content[7:]
+ }
+ body := new(ui.StyledStringBuilder)
+ if isNotice {
+ color := identColor(ev.User)
+ body.SetStyle(tcell.StyleDefault.Foreground(color))
+ body.WriteString(ev.User)
+ body.SetStyle(tcell.StyleDefault)
+ body.WriteString(": ")
+ body.WriteStyledString(ui.IRCString(content))
} else if isAction {
- c := ircColorSequence(ui.IdentColor(ev.User))
- body = fmt.Sprintf("%s%s\x0F%s", c, ev.User, body[7:])
- } else if isNotice {
- c := ircColorSequence(ui.IdentColor(ev.User))
- body = fmt.Sprintf("%s%s\x0F: %s", c, ev.User, body)
+ color := identColor(ev.User)
+ body.SetStyle(tcell.StyleDefault.Foreground(color))
+ body.WriteString(ev.User)
+ body.SetStyle(tcell.StyleDefault)
+ body.WriteStyledString(ui.IRCString(content))
+ } else {
+ body.WriteStyledString(ui.IRCString(content))
}
line = ui.Line{
At: ev.Time,
Head: head,
- Body: body,
HeadColor: headColor,
+ Body: body.StyledString(),
Highlight: hlLine,
}
return
@@ -726,34 +785,11 @@ func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line,
func (app *App) updatePrompt() {
buffer := app.win.CurrentBuffer()
command := app.win.InputIsCommand()
+ var prompt ui.StyledString
if buffer == Home || command {
- app.win.SetPrompt(">")
+ prompt = ui.PlainString(">")
} else {
- app.win.SetPrompt(app.s.Nick())
- }
-}
-
-// ircColorSequence returns the color formatting sequence of a color code.
-func ircColorSequence(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[:])
-}
-
-// cleanMessage removes IRC formatting from a string.
-func cleanMessage(s string) string {
- var res strings.Builder
- var sb ui.StyleBuffer
- res.Grow(len(s))
- for _, r := range s {
- if _, ok := sb.WriteRune(r); ok != 0 {
- if 1 < ok {
- res.WriteRune(',')
- }
- res.WriteRune(r)
- }
+ prompt = identString(app.s.Nick())
}
- return res.String()
+ app.win.SetPrompt(prompt)
}
diff --git a/commands.go b/commands.go
index 009bc81..a704acf 100644
--- a/commands.go
+++ b/commands.go
@@ -7,6 +7,7 @@ import (
"git.sr.ht/~taiite/senpai/irc"
"git.sr.ht/~taiite/senpai/ui"
+ "github.com/gdamore/tcell/v2"
)
type command struct {
@@ -151,7 +152,7 @@ func commandDoHelp(app *App, buffer string, args []string) (err error) {
app.win.AddLine(app.win.CurrentBuffer(), false, ui.Line{
At: t,
Head: "--",
- Body: "Available commands:",
+ Body: ui.PlainString("Available commands:"),
})
for cmdName, cmd := range commands {
if cmd.Desc == "" {
@@ -159,11 +160,11 @@ func commandDoHelp(app *App, buffer string, args []string) (err error) {
}
app.win.AddLine(app.win.CurrentBuffer(), false, ui.Line{
At: t,
- Body: fmt.Sprintf(" \x02%s\x02 %s", cmdName, cmd.Usage),
+ Body: ui.PlainSprintf(" \x02%s\x02 %s", cmdName, cmd.Usage),
})
app.win.AddLine(app.win.CurrentBuffer(), false, ui.Line{
At: t,
- Body: fmt.Sprintf(" %s", cmd.Desc),
+ Body: ui.PlainSprintf(" %s", cmd.Desc),
})
app.win.AddLine(app.win.CurrentBuffer(), false, ui.Line{
At: t,
@@ -175,19 +176,26 @@ func commandDoHelp(app *App, buffer string, args []string) (err error) {
app.win.AddLine(app.win.CurrentBuffer(), false, ui.Line{
At: t,
Head: "--",
- Body: fmt.Sprintf("Commands that match \"%s\":", search),
+ Body: ui.PlainSprintf("Commands that match \"%s\":", search),
})
for cmdName, cmd := range commands {
if !strings.Contains(cmdName, search) {
continue
}
+ usage := new(ui.StyledStringBuilder)
+ usage.Grow(len(cmdName) + 1 + len(cmd.Usage))
+ usage.SetStyle(tcell.StyleDefault.Bold(true))
+ usage.WriteString(cmdName)
+ usage.SetStyle(tcell.StyleDefault)
+ usage.WriteByte(' ')
+ usage.WriteString(cmd.Usage)
app.win.AddLine(app.win.CurrentBuffer(), false, ui.Line{
At: t,
- Body: fmt.Sprintf("\x02%s\x02 %s", cmdName, cmd.Usage),
+ Body: usage.StyledString(),
})
app.win.AddLine(app.win.CurrentBuffer(), false, ui.Line{
At: t,
- Body: fmt.Sprintf(" %s", cmd.Desc),
+ Body: ui.PlainSprintf(" %s", cmd.Desc),
})
app.win.AddLine(app.win.CurrentBuffer(), false, ui.Line{
At: t,
@@ -197,7 +205,7 @@ func commandDoHelp(app *App, buffer string, args []string) (err error) {
if !found {
app.win.AddLine(app.win.CurrentBuffer(), false, ui.Line{
At: t,
- Body: fmt.Sprintf(" no command matches %q", args[0]),
+ Body: ui.PlainSprintf(" no command matches %q", args[0]),
})
}
}
@@ -252,22 +260,24 @@ func commandDoMsg(app *App, buffer string, args []string) (err error) {
}
func commandDoNames(app *App, buffer string, args []string) (err error) {
- var sb strings.Builder
- sb.WriteString("\x0314Names: ")
+ sb := new(ui.StyledStringBuilder)
+ sb.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGrey))
+ sb.WriteString("Names: ")
for _, name := range app.s.Names(buffer) {
if name.PowerLevel != "" {
- sb.WriteString("\x033")
+ sb.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGreen))
sb.WriteString(name.PowerLevel)
- sb.WriteString("\x0314")
+ sb.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGrey))
}
sb.WriteString(name.Name.Name)
- sb.WriteRune(' ')
+ sb.WriteByte(' ')
}
- body := sb.String()
+ body := sb.StyledString()
+ // TODO remove last space
app.win.AddLine(buffer, false, ui.Line{
At: time.Now(),
Head: "--",
- Body: body[:len(body)-1],
+ Body: body,
})
return
}
@@ -351,14 +361,14 @@ func commandDoTopic(app *App, buffer string, args []string) (err error) {
topic, who, at := app.s.Topic(buffer)
if who == nil {
- body = fmt.Sprintf("\x0314Topic: %s", topic)
+ body = fmt.Sprintf("Topic: %s", topic)
} else {
- body = fmt.Sprintf("\x0314Topic (by %s, %s): %s", who, at.Local().Format("Mon Jan 2 15:04:05"), topic)
+ body = fmt.Sprintf("Topic (by %s, %s): %s", who, at.Local().Format("Mon Jan 2 15:04:05"), topic)
}
app.win.AddLine(buffer, false, ui.Line{
At: time.Now(),
Head: "--",
- Body: body,
+ Body: ui.Styled(body, tcell.StyleDefault.Foreground(tcell.ColorGray)),
})
} else {
app.s.ChangeTopic(buffer, args[0])
diff --git a/ui/buffers.go b/ui/buffers.go
index b6020a8..0ebaadf 100644
--- a/ui/buffers.go
+++ b/ui/buffers.go
@@ -19,8 +19,8 @@ type point struct {
type Line struct {
At time.Time
Head string
- Body string
- HeadColor int
+ Body StyledString
+ HeadColor tcell.Color
Highlight bool
Mergeable bool
@@ -34,29 +34,29 @@ func (l *Line) computeSplitPoints() {
l.splitPoints = []point{}
}
- var wb widthBuffer
+ width := 0
lastWasSplit := false
l.splitPoints = l.splitPoints[:0]
- for i, r := range l.Body {
+ for i, r := range l.Body.string {
curIsSplit := IsSplitRune(r)
if i == 0 || lastWasSplit != curIsSplit {
l.splitPoints = append(l.splitPoints, point{
- X: wb.Width(),
+ X: width,
I: i,
Split: curIsSplit,
})
}
lastWasSplit = curIsSplit
- wb.WriteRune(r)
+ width += runeWidth(r)
}
if !lastWasSplit {
l.splitPoints = append(l.splitPoints, point{
- X: wb.Width(),
- I: len(l.Body),
+ X: width,
+ I: len(l.Body.string),
Split: true,
})
}
@@ -118,16 +118,17 @@ func (l *Line) NewLines(width int) []int {
// 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
+ // TODO handle multi-codepoint graphemes?? :(
+ wordWidth := 0
h := 1
- for j, r := range l.Body[sp1.I:sp2.I] {
- wb.WriteRune(r)
- if h*width < x+wb.Width() {
+ for j, r := range l.Body.string[sp1.I:sp2.I] {
+ wordWidth += runeWidth(r)
+ if h*width < x+wordWidth {
l.newLines = append(l.newLines, sp1.I+j)
h++
}
}
- x = (x + wb.Width()) % width
+ x = (x + wordWidth) % width
if x == 0 {
// The placement of the word is such that it ends right at the
// end of the row.
@@ -147,7 +148,7 @@ func (l *Line) NewLines(width int) []int {
}
}
- if 0 < len(l.newLines) && l.newLines[len(l.newLines)-1] == len(l.Body) {
+ if 0 < len(l.newLines) && l.newLines[len(l.newLines)-1] == len(l.Body.string) {
// 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]
@@ -255,14 +256,19 @@ func (bs *BufferList) AddLine(title string, highlight bool, line Line) {
b := &bs.list[idx]
n := len(b.lines)
- line.Body = strings.TrimRight(line.Body, "\t ")
line.At = line.At.UTC()
if line.Mergeable && n != 0 && b.lines[n-1].Mergeable {
l := &b.lines[n-1]
- l.Body += " " + line.Body
+ newBody := new(StyledStringBuilder)
+ newBody.Grow(len(l.Body.string) + 2 + len(line.Body.string))
+ newBody.WriteStyledString(l.Body)
+ newBody.WriteString(" ")
+ newBody.WriteStyledString(line.Body)
+ l.Body = newBody.StyledString()
l.computeSplitPoints()
l.width = 0
+ // TODO change b.scrollAmt if it's not 0 and bs.current is idx.
} else {
line.computeSplitPoints()
b.lines = append(b.lines, line)
@@ -307,7 +313,7 @@ func (bs *BufferList) AddLines(title string, lines []Line) {
limit = i
break
}
- if l.Body == firstLineBody {
+ if l.Body.string == firstLineBody.string {
// This line happened at the same millisecond
// as the first line of the buffer, and has the
// same contents. Heuristic: it's the same
@@ -396,7 +402,7 @@ func (bs *BufferList) DrawVerticalBufferList(screen tcell.Screen, x0, y0, width,
st = st.Reverse(true)
}
title := truncate(b.title, width, "\u2026")
- printString(screen, &x, y, st, title)
+ printString(screen, &x, y, Styled(title, st))
if 0 < b.highlights {
st = st.Foreground(tcell.ColorRed).Reverse(true)
screen.SetContent(x, y, ' ', nil, st)
@@ -417,10 +423,9 @@ func (bs *BufferList) DrawVerticalBufferList(screen tcell.Screen, x0, y0, width,
}
func (bs *BufferList) DrawTimeline(screen tcell.Screen, x0, y0, nickColWidth int) {
- st := tcell.StyleDefault
for x := x0; x < x0+bs.tlWidth; x++ {
for y := y0; y < y0+bs.tlHeight; y++ {
- screen.SetContent(x, y, ' ', nil, st)
+ screen.SetContent(x, y, ' ', nil, tcell.StyleDefault)
}
}
@@ -441,18 +446,25 @@ func (bs *BufferList) DrawTimeline(screen tcell.Screen, x0, y0, nickColWidth int
}
if i == 0 || b.lines[i-1].At.Truncate(time.Minute) != line.At.Truncate(time.Minute) {
- printTime(screen, x0, yi, st.Bold(true), line.At.Local())
+ st := tcell.StyleDefault.Bold(true)
+ printTime(screen, x0, yi, st, line.At.Local())
}
- identSt := st.Foreground(colorFromCode(line.HeadColor)).Reverse(line.Highlight)
- printIdent(screen, x0+7, yi, nickColWidth, identSt, line.Head)
+ identSt := tcell.StyleDefault.
+ Foreground(line.HeadColor).
+ Reverse(line.Highlight)
+ printIdent(screen, x0+7, yi, nickColWidth, Styled(line.Head, identSt))
x := x1
y := yi
+ style := tcell.StyleDefault
+ nextStyles := line.Body.styles
- var sb StyleBuffer
- sb.Reset()
- for i, r := range line.Body {
+ for i, r := range line.Body.string {
+ if 0 < len(nextStyles) && nextStyles[0].Start == i {
+ style = nextStyles[0].Style
+ nextStyles = nextStyles[1:]
+ }
if 0 < len(nls) && i == nls[0] {
x = x1
y++
@@ -466,26 +478,10 @@ func (bs *BufferList) DrawTimeline(screen tcell.Screen, x0, y0, nickColWidth int
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(r)
- }
+ screen.SetContent(x, y, r, nil, style)
+ x += runeWidth(r)
}
-
- sb.Reset()
}
b.isAtTop = y0 <= yi
}
-
-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 b78f8e9..13a32de 100644
--- a/ui/buffers_test.go
+++ b/ui/buffers_test.go
@@ -6,7 +6,7 @@ import (
)
func assertSplitPoints(t *testing.T, body string, expected []point) {
- l := Line{Body: body}
+ l := Line{Body: PlainString(body)}
l.computeSplitPoints()
if len(l.splitPoints) != len(expected) {
@@ -71,7 +71,7 @@ func showSplit(s string, nls []int) string {
}
func assertNewLines(t *testing.T, body string, width int, expected int) {
- l := Line{Body: body}
+ l := Line{Body: PlainString(body)}
l.computeSplitPoints()
actual := l.NewLines(width)
@@ -141,6 +141,8 @@ func TestRenderedHeight(t *testing.T) {
assertNewLines(t, "have a good day!", 17, 1) // |have a good day! |
// LEN=15, WIDTH=11
+ // TODO find other zero- or two-width chars, since IRC color codes are
+ // now handled by app.go
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 |
@@ -160,17 +162,3 @@ func TestRenderedHeight(t *testing.T) {
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/draw_utils.go b/ui/draw_utils.go
index 893a77f..947e71b 100644
--- a/ui/draw_utils.go
+++ b/ui/draw_utils.go
@@ -7,24 +7,39 @@ import (
"github.com/gdamore/tcell/v2"
)
-func printIdent(screen tcell.Screen, x, y, width int, st tcell.Style, s string) {
- s = truncate(s, width, "\u2026")
- x += width - StringWidth(s)
- screen.SetContent(x-1, y, ' ', nil, st)
- printString(screen, &x, y, st, s)
- screen.SetContent(x, y, ' ', nil, st)
+func printString(screen tcell.Screen, x *int, y int, s StyledString) {
+ style := tcell.StyleDefault
+ nextStyles := s.styles
+ for i, r := range s.string {
+ if 0 < len(nextStyles) && nextStyles[0].Start == i {
+ style = nextStyles[0].Style
+ nextStyles = nextStyles[1:]
+ }
+ screen.SetContent(*x, y, r, nil, style)
+ *x += runeWidth(r)
+ }
}
-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(r)
+func printIdent(screen tcell.Screen, x, y, width int, s StyledString) {
+ s.string = truncate(s.string, width, "\u2026")
+ x += width - stringWidth(s.string)
+ st := tcell.StyleDefault
+ if len(s.styles) != 0 && s.styles[0].Start == 0 {
+ st = s.styles[0].Style
+ }
+ screen.SetContent(x-1, y, ' ', nil, st)
+ printString(screen, &x, y, s)
+ if len(s.styles) != 0 {
+ // TODO check if it's not a style that is from the truncated
+ // part of s.
+ st = s.styles[len(s.styles)-1].Style
}
+ screen.SetContent(x, y, ' ', nil, st)
}
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)
+ s := Styled(fmt.Sprintf("%d", n), st)
+ printString(screen, x, y, s)
}
func printTime(screen tcell.Screen, x int, y int, st tcell.Style, t time.Time) {
diff --git a/ui/style.go b/ui/style.go
index d86e9f4..f596ac0 100644
--- a/ui/style.go
+++ b/ui/style.go
@@ -1,7 +1,10 @@
package ui
import (
- "hash/fnv"
+ "fmt"
+ "strconv"
+ "strings"
+ "unicode/utf8"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
@@ -13,234 +16,246 @@ func runeWidth(r rune) int {
return condition.RuneWidth(r)
}
+func stringWidth(s string) int {
+ return condition.StringWidth(s)
+}
+
func truncate(s string, w int, tail string) string {
return condition.Truncate(s, w, tail)
}
-type widthBuffer struct {
- width int
- color colorBuffer
+// Taken from <https://modern.ircdocs.horse/formatting.html>
+
+var baseCodes = []tcell.Color{
+ tcell.ColorWhite, tcell.ColorBlack, tcell.ColorBlue, tcell.ColorGreen,
+ tcell.ColorRed, tcell.ColorBrown, tcell.ColorPurple, tcell.ColorOrange,
+ tcell.ColorYellow, tcell.ColorLightGreen, tcell.ColorTeal, tcell.ColorLightCyan,
+ tcell.ColorLightBlue, tcell.ColorPink, tcell.ColorGrey, tcell.ColorLightGrey,
}
-func (wb *widthBuffer) Width() int {
- return wb.width
+// unused
+var ansiCodes = []uint64{
+ /* 16-27 */ 52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89,
+ /* 28-39 */ 88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125,
+ /* 40-51 */ 124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161,
+ /* 52-63 */ 196, 208, 226, 154, 46, 86, 51, 75, 21, 171, 201, 198,
+ /* 64-75 */ 203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205,
+ /* 76-87 */ 217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212,
+ /* 88-98 */ 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231,
}
-func (wb *widthBuffer) WriteString(s string) {
- for _, r := range s {
- wb.WriteRune(r)
- }
+var hexCodes = []int32{
+ 0x470000, 0x472100, 0x474700, 0x324700, 0x004700, 0x00472c, 0x004747, 0x002747, 0x000047, 0x2e0047, 0x470047, 0x47002a,
+ 0x740000, 0x743a00, 0x747400, 0x517400, 0x007400, 0x007449, 0x007474, 0x004074, 0x000074, 0x4b0074, 0x740074, 0x740045,
+ 0xb50000, 0xb56300, 0xb5b500, 0x7db500, 0x00b500, 0x00b571, 0x00b5b5, 0x0063b5, 0x0000b5, 0x7500b5, 0xb500b5, 0xb5006b,
+ 0xff0000, 0xff8c00, 0xffff00, 0xb2ff00, 0x00ff00, 0x00ffa0, 0x00ffff, 0x008cff, 0x0000ff, 0xa500ff, 0xff00ff, 0xff0098,
+ 0xff5959, 0xffb459, 0xffff71, 0xcfff60, 0x6fff6f, 0x65ffc9, 0x6dffff, 0x59b4ff, 0x5959ff, 0xc459ff, 0xff66ff, 0xff59bc,
+ 0xff9c9c, 0xffd39c, 0xffff9c, 0xe2ff9c, 0x9cff9c, 0x9cffdb, 0x9cffff, 0x9cd3ff, 0x9c9cff, 0xdc9cff, 0xff9cff, 0xff94d3,
+ 0x000000, 0x131313, 0x282828, 0x363636, 0x4d4d4d, 0x656565, 0x818181, 0x9f9f9f, 0xbcbcbc, 0xe2e2e2, 0xffffff,
}
-func (wb *widthBuffer) WriteRune(r rune) {
- if ok := wb.color.WriteRune(r); ok != 0 {
- if 1 < ok {
- wb.width++
- }
- wb.width += runeWidth(r)
+func colorFromCode(code int) (color tcell.Color) {
+ if code < 0 || 99 <= code {
+ color = tcell.ColorDefault
+ } else if code < 16 {
+ color = baseCodes[code]
+ } else {
+ color = tcell.NewHexColor(hexCodes[code-16])
}
+ return
}
-func StringWidth(s string) int {
- var wb widthBuffer
- wb.WriteString(s)
- return wb.Width()
+type rangedStyle struct {
+ Start int // byte index at which Style is effective
+ Style tcell.Style
}
-type StyleBuffer struct {
- st tcell.Style
- color colorBuffer
- bold bool
- reverse bool
- italic bool
- strikethrough bool
- underline bool
+type StyledString struct {
+ string
+ styles []rangedStyle // sorted, elements cannot have the same Start value
}
-func (sb *StyleBuffer) Reset() {
- sb.color.Reset()
- sb.st = tcell.StyleDefault
- sb.bold = false
- sb.reverse = false
- sb.italic = false
- sb.strikethrough = false
- sb.underline = false
+func PlainString(s string) StyledString {
+ return StyledString{string: s}
}
-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 == 0x16 {
- sb.reverse = !sb.reverse
- sb.st = st.Reverse(sb.reverse)
- return sb.st, 0
- }
- if r == 0x1D {
- sb.italic = !sb.italic
- sb.st = st.Italic(sb.italic)
- return sb.st, 0
- }
- if r == 0x1E {
- sb.strikethrough = !sb.strikethrough
- sb.st = st.StrikeThrough(sb.strikethrough)
- return sb.st, 0
- }
- if r == 0x1F {
- sb.underline = !sb.underline
- sb.st = st.Underline(sb.underline)
- return sb.st, 0
+func PlainSprintf(format string, a ...interface{}) StyledString {
+ return PlainString(fmt.Sprintf(format, a...))
+}
+
+func Styled(s string, style tcell.Style) StyledString {
+ rStyle := rangedStyle{
+ Start: 0,
+ Style: style,
}
- if ok = sb.color.WriteRune(r); ok != 0 {
- sb.st = sb.color.Style(sb.st)
+ return StyledString{
+ string: s,
+ styles: []rangedStyle{rStyle},
}
-
- return sb.st, ok
}
-type colorBuffer struct {
- state int
- fg, bg int
+func (s StyledString) String() string {
+ return s.string
}
-func (cb *colorBuffer) Reset() {
- cb.state = 0
- cb.fg = -1
- cb.bg = -1
+func isDigit(c byte) bool {
+ return '0' <= c && c <= '9'
}
-func (cb *colorBuffer) Style(st tcell.Style) tcell.Style {
- if 0 <= cb.fg {
- st = st.Foreground(colorFromCode(cb.fg))
- } else {
- st = st.Foreground(tcell.ColorDefault)
+func parseColorNumber(raw string) (color tcell.Color, n int) {
+ if len(raw) == 0 || !isDigit(raw[0]) {
+ return
}
- if 0 <= cb.bg {
- st = st.Background(colorFromCode(cb.bg))
- } else {
- st = st.Background(tcell.ColorDefault)
+
+ // len(raw) >= 1 and its first character is a digit.
+
+ if len(raw) == 1 || !isDigit(raw[1]) {
+ code, _ := strconv.Atoi(raw[:1])
+ return colorFromCode(code), 1
}
- return st
+
+ // len(raw) >= 2 and the two first characters are digits.
+
+ code, _ := strconv.Atoi(raw[:2])
+ return colorFromCode(code), 2
}
-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
- }
+func parseColor(raw string) (fg, bg tcell.Color, n int) {
+ fg, n = parseColorNumber(raw)
+ raw = raw[n:]
+
+ if len(raw) == 0 || raw[0] != ',' {
+ return fg, tcell.ColorDefault, n
}
- if r == 0x03 {
- cb.state = 1
- cb.fg = -1
- cb.bg = -1
- return
+ n++
+ bg, p := parseColorNumber(raw[1:])
+ n += p
+
+ if bg == tcell.ColorDefault {
+ // Lone comma, do not parse as part of a color code.
+ return fg, tcell.ColorDefault, n - 1
}
- cb.state = 0
- ok++
- return
+ return fg, bg, n
}
-const (
- ColorWhite = iota
- ColorBlack
- ColorBlue
- ColorGreen
- ColorRed
-)
+func IRCString(raw string) StyledString {
+ var formatted strings.Builder
+ var styles []rangedStyle
+ var last tcell.Style
-// Taken from <https://modern.ircdocs.horse/formatting.html>
+ for len(raw) != 0 {
+ r, runeSize := utf8.DecodeRuneInString(raw)
+ if r == utf8.RuneError {
+ break
+ }
+ _, _, lastAttrs := last.Decompose()
+ current := last
+ if r == 0x0F {
+ current = tcell.StyleDefault
+ } else if r == 0x02 {
+ lastWasBold := lastAttrs&tcell.AttrBold != 0
+ current = last.Bold(!lastWasBold)
+ } else if r == 0x03 {
+ fg, bg, n := parseColor(raw[1:])
+ raw = raw[n:]
+ if n == 0 {
+ // Both `fg` and `bg` are equal to
+ // tcell.ColorDefault.
+ current = last.Foreground(tcell.ColorDefault).
+ Background(tcell.ColorDefault)
+ } else if bg == tcell.ColorDefault {
+ current = last.Foreground(fg)
+ } else {
+ current = last.Foreground(fg).Background(bg)
+ }
+ } else if r == 0x16 {
+ lastWasReverse := lastAttrs&tcell.AttrReverse != 0
+ current = last.Reverse(!lastWasReverse)
+ } else if r == 0x1D {
+ lastWasItalic := lastAttrs&tcell.AttrItalic != 0
+ current = last.Italic(!lastWasItalic)
+ } else if r == 0x1E {
+ lastWasStrikeThrough := lastAttrs&tcell.AttrStrikeThrough != 0
+ current = last.StrikeThrough(!lastWasStrikeThrough)
+ } else if r == 0x1F {
+ lastWasUnderline := lastAttrs&tcell.AttrUnderline != 0
+ current = last.Underline(!lastWasUnderline)
+ } else {
+ formatted.WriteRune(r)
+ }
+ if last != current {
+ if len(styles) != 0 && styles[len(styles)-1].Start == formatted.Len() {
+ styles[len(styles)-1] = rangedStyle{
+ Start: formatted.Len(),
+ Style: current,
+ }
+ } else {
+ styles = append(styles, rangedStyle{
+ Start: formatted.Len(),
+ Style: current,
+ })
+ }
+ }
+ last = current
+ raw = raw[runeSize:]
+ }
-var baseCodes = []tcell.Color{
- tcell.ColorWhite, tcell.ColorBlack, tcell.ColorBlue, tcell.ColorGreen,
- tcell.ColorRed, tcell.ColorBrown, tcell.ColorPurple, tcell.ColorOrange,
- tcell.ColorYellow, tcell.ColorLightGreen, tcell.ColorTeal, tcell.ColorLightCyan,
- tcell.ColorLightBlue, tcell.ColorPink, tcell.ColorGrey, tcell.ColorLightGrey,
+ return StyledString{
+ string: formatted.String(),
+ styles: styles,
+ }
}
-var ansiCodes = []uint64{
- /* 16-27 */ 52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89,
- /* 28-39 */ 88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125,
- /* 40-51 */ 124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161,
- /* 52-63 */ 196, 208, 226, 154, 46, 86, 51, 75, 21, 171, 201, 198,
- /* 64-75 */ 203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205,
- /* 76-87 */ 217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212,
- /* 88-98 */ 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231,
+type StyledStringBuilder struct {
+ strings.Builder
+ styles []rangedStyle
}
-var hexCodes = []int32{
- 0x470000, 0x472100, 0x474700, 0x324700, 0x004700, 0x00472c, 0x004747, 0x002747, 0x000047, 0x2e0047, 0x470047, 0x47002a,
- 0x740000, 0x743a00, 0x747400, 0x517400, 0x007400, 0x007449, 0x007474, 0x004074, 0x000074, 0x4b0074, 0x740074, 0x740045,
- 0xb50000, 0xb56300, 0xb5b500, 0x7db500, 0x00b500, 0x00b571, 0x00b5b5, 0x0063b5, 0x0000b5, 0x7500b5, 0xb500b5, 0xb5006b,
- 0xff0000, 0xff8c00, 0xffff00, 0xb2ff00, 0x00ff00, 0x00ffa0, 0x00ffff, 0x008cff, 0x0000ff, 0xa500ff, 0xff00ff, 0xff0098,
- 0xff5959, 0xffb459, 0xffff71, 0xcfff60, 0x6fff6f, 0x65ffc9, 0x6dffff, 0x59b4ff, 0x5959ff, 0xc459ff, 0xff66ff, 0xff59bc,
- 0xff9c9c, 0xffd39c, 0xffff9c, 0xe2ff9c, 0x9cff9c, 0x9cffdb, 0x9cffff, 0x9cd3ff, 0x9c9cff, 0xdc9cff, 0xff9cff, 0xff94d3,
- 0x000000, 0x131313, 0x282828, 0x363636, 0x4d4d4d, 0x656565, 0x818181, 0x9f9f9f, 0xbcbcbc, 0xe2e2e2, 0xffffff,
+func (sb *StyledStringBuilder) WriteStyledString(s StyledString) {
+ start := len(sb.styles)
+ sb.styles = append(sb.styles, s.styles...)
+ for i := start; i < len(sb.styles); i++ {
+ sb.styles[i].Start += sb.Len()
+ }
+ sb.WriteString(s.string)
}
-func colorFromCode(code int) (color tcell.Color) {
- if code < 0 || 99 <= code {
- color = tcell.ColorDefault
- } else if code < 16 {
- color = baseCodes[code]
- } else {
- color = tcell.NewHexColor(hexCodes[code-16])
+func (sb *StyledStringBuilder) AddStyle(start int, style tcell.Style) {
+ for i := 0; i < len(sb.styles); i++ {
+ if sb.styles[i].Start == i {
+ sb.styles[i].Style = style
+ break
+ } else if sb.styles[i].Start < i {
+ sb.styles = append(sb.styles[:i+1], sb.styles[i:]...)
+ sb.styles[i+1] = rangedStyle{
+ Start: start,
+ Style: style,
+ }
+ break
+ }
}
- return
+ sb.styles = append(sb.styles, rangedStyle{
+ Start: start,
+ Style: style,
+ })
}
-// see <https://modern.ircdocs.horse/formatting.html>
-var identColorBlacklist = []int{
- 1, 8, 16, 17, 24, 27, 28, 36, 48, 60, 88, 89, 90, 91,
+func (sb *StyledStringBuilder) SetStyle(style tcell.Style) {
+ sb.styles = append(sb.styles, rangedStyle{
+ Start: sb.Len(),
+ Style: style,
+ })
}
-func IdentColor(s string) (code int) {
- h := fnv.New32()
- _, _ = h.Write([]byte(s))
-
- code = int(h.Sum32()) % (99 - len(identColorBlacklist))
- for _, c := range identColorBlacklist {
- if c <= code {
- code++
- }
+func (sb *StyledStringBuilder) StyledString() StyledString {
+ styles := sb.styles
+ if len(sb.styles) != 0 && sb.styles[len(sb.styles)-1].Start == sb.Len() {
+ styles = sb.styles[:len(sb.styles)-1]
+ }
+ return StyledString{
+ string: sb.String(),
+ styles: styles,
}
-
- return
}
diff --git a/ui/style_test.go b/ui/style_test.go
index ae478ab..4ba2e9d 100644
--- a/ui/style_test.go
+++ b/ui/style_test.go
@@ -1,25 +1,96 @@
package ui
-import "testing"
+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)
+ "github.com/gdamore/tcell/v2"
+)
+
+func assertIRCString(t *testing.T, input string, expected StyledString) {
+ actual := IRCString(input)
+ if actual.string != expected.string {
+ t.Errorf("%q: expected string %q, got %q", input, expected.string, actual.string)
+ }
+ if len(actual.styles) != len(expected.styles) {
+ t.Errorf("%q: expected %d styles, got %d", input, len(expected.styles), len(actual.styles))
+ return
+ }
+ for i := range actual.styles {
+ if actual.styles[i] != expected.styles[i] {
+ t.Errorf("%q: style #%d expected to be %+v, got %+v", input, i, expected.styles[i], actual.styles[i])
+ }
}
}
-func TestStringWidth(t *testing.T) {
- assertStringWidth(t, "", 0)
+func TestIRCString(t *testing.T) {
+ assertIRCString(t, "", StyledString{
+ string: "",
+ styles: nil,
+ })
- 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)
+ assertIRCString(t, "hello", StyledString{
+ string: "hello",
+ styles: nil,
+ })
+ assertIRCString(t, "\x02hello", StyledString{
+ string: "hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Bold(true)},
+ },
+ })
+ assertIRCString(t, "\x035hello", StyledString{
+ string: "hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
+ },
+ })
+ assertIRCString(t, "\x0305hello", StyledString{
+ string: "hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
+ },
+ })
+ assertIRCString(t, "\x0305,0hello", StyledString{
+ string: "hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown).Background(tcell.ColorWhite)},
+ },
+ })
+ assertIRCString(t, "\x035,00hello", StyledString{
+ string: "hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown).Background(tcell.ColorWhite)},
+ },
+ })
+ assertIRCString(t, "\x0305,00hello", StyledString{
+ string: "hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown).Background(tcell.ColorWhite)},
+ },
+ })
- assertStringWidth(t, "\x0305,hello", 6)
- assertStringWidth(t, "\x03050hello", 6)
- assertStringWidth(t, "\x0305,090hello", 6)
+ assertIRCString(t, "\x035,hello", StyledString{
+ string: ",hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
+ },
+ })
+ assertIRCString(t, "\x0305,hello", StyledString{
+ string: ",hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
+ },
+ })
+ assertIRCString(t, "\x03050hello", StyledString{
+ string: "0hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
+ },
+ })
+ assertIRCString(t, "\x0305,000hello", StyledString{
+ string: "0hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown).Background(tcell.ColorWhite)},
+ },
+ })
}
diff --git a/ui/ui.go b/ui/ui.go
index 9f730a3..be43272 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -12,7 +12,7 @@ type Config struct {
NickColWidth int
ChanColWidth int
AutoComplete func(cursorIdx int, text []rune) []Completion
- Mouse bool
+ Mouse bool
}
type UI struct {
@@ -23,7 +23,7 @@ type UI struct {
bs BufferList
e Editor
- prompt string
+ prompt StyledString
status string
}
@@ -159,7 +159,7 @@ func (ui *UI) SetStatus(status string) {
ui.status = status
}
-func (ui *UI) SetPrompt(prompt string) {
+func (ui *UI) SetPrompt(prompt StyledString) {
ui.prompt = prompt
}
@@ -245,8 +245,7 @@ func (ui *UI) Draw() {
for x := ui.config.ChanColWidth; x < 9+ui.config.ChanColWidth+ui.config.NickColWidth; x++ {
ui.screen.SetContent(x, h-1, ' ', nil, tcell.StyleDefault)
}
- st := tcell.StyleDefault.Foreground(colorFromCode(IdentColor(ui.prompt)))
- printIdent(ui.screen, ui.config.ChanColWidth+7, h-1, ui.config.NickColWidth, st, ui.prompt)
+ printIdent(ui.screen, ui.config.ChanColWidth+7, h-1, ui.config.NickColWidth, ui.prompt)
ui.screen.Show()
}
@@ -262,8 +261,16 @@ func (ui *UI) drawStatusBar(x0, y, width int) {
return
}
+ s := new(StyledStringBuilder)
+ s.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
+ s.WriteString("--")
+
x := x0 + 5 + ui.config.NickColWidth
- printString(ui.screen, &x, y, st, "--")
+ printString(ui.screen, &x, y, s.StyledString())
x += 2
- printString(ui.screen, &x, y, st, ui.status)
+
+ s.Reset()
+ s.WriteString(ui.status)
+
+ printString(ui.screen, &x, y, s.StyledString())
}
diff --git a/window.go b/window.go
index 3633c18..3d80776 100644
--- a/window.go
+++ b/window.go
@@ -1,30 +1,23 @@
package senpai
import (
- "math/rand"
+ "hash/fnv"
"strings"
"time"
"git.sr.ht/~taiite/senpai/ui"
+ "github.com/gdamore/tcell/v2"
)
var Home = "home"
-var homeMessages = []string{
- "\x1dYou open an IRC client.",
- "Welcome to the Internet Relay Network!",
- "DMs & cie go here.",
- "May the IRC be with you.",
- "Hey! I'm senpai, you everyday IRC student!",
- "Student? No, I'm an IRC \x02client\x02!",
-}
+const welcomeMessage = "senpai dev build. See senpai(1) for a list of keybindings and commands. Private messages and status notices go here."
func (app *App) initWindow() {
- hmIdx := rand.Intn(len(homeMessages))
app.win.AddBuffer(Home)
app.win.AddLine(Home, false, ui.Line{
Head: "--",
- Body: homeMessages[hmIdx],
+ Body: ui.PlainString(welcomeMessage),
At: time.Now(),
})
}
@@ -67,3 +60,15 @@ func (app *App) setStatus() {
}
app.win.SetStatus(status)
}
+
+func identColor(ident string) tcell.Color {
+ h := fnv.New32()
+ _, _ = h.Write([]byte(ident))
+ return tcell.Color((h.Sum32()%15)+1) + tcell.ColorValid
+}
+
+func identString(ident string) ui.StyledString {
+ color := identColor(ident)
+ style := tcell.StyleDefault.Foreground(color)
+ return ui.Styled(ident, style)
+}