summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHubert Hirtz <hubert@hirtz.pm>2021-05-26 12:44:35 +0200
committerHubert Hirtz <hubert@hirtz.pm>2021-05-26 17:43:29 +0200
commit287e40855e3bceb258af247317a288d179e55c57 (patch)
tree063ac47fada92281fc82a3e4b36a4d6a402c461d
parentDo not go into infinite loops on TLS mismatch (diff)
Pick nick colors in terminal color scheme
So that the colors go well with the terminal background.
-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)
+}