summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHubert Hirtz <hubert@hirtz.pm>2020-10-21 16:22:28 +0200
committerHubert Hirtz <hubert@hirtz.pm>2020-10-26 08:45:07 +0100
commit7fae7f74c5315877e0d341ec60ceef2e87c0c1cc (patch)
treeafdea6e39840dfbd3db4e864bf914fe137009298
parentCollapse bufferlist in one block (diff)
Vertical channel list
-rw-r--r--app.go59
-rw-r--r--irc/states.go2
-rw-r--r--ui/buffers.go264
-rw-r--r--ui/draw_utils.go40
-rw-r--r--ui/editor.go10
-rw-r--r--ui/style.go19
-rw-r--r--ui/ui.go50
7 files changed, 231 insertions, 213 deletions
diff --git a/app.go b/app.go
index c63565f..2c8e7d7 100644
--- a/app.go
+++ b/app.go
@@ -3,7 +3,6 @@ package senpai
import (
"crypto/tls"
"fmt"
- "hash/fnv"
"os/exec"
"strings"
"time"
@@ -45,6 +44,7 @@ func NewApp(cfg Config) (app *App, err error) {
if err != nil {
return
}
+ app.win.SetPrompt(">")
app.initWindow()
@@ -261,19 +261,31 @@ func (app *App) handleUIEvent(ev tcell.Event) {
case tcell.KeyRight:
if ev.Modifiers() == tcell.ModAlt {
app.win.NextBuffer()
+ app.updatePrompt()
} else {
app.win.InputRight()
}
case tcell.KeyLeft:
if ev.Modifiers() == tcell.ModAlt {
app.win.PreviousBuffer()
+ app.updatePrompt()
} else {
app.win.InputLeft()
}
case tcell.KeyUp:
- app.win.InputUp()
+ if ev.Modifiers() == tcell.ModAlt {
+ app.win.PreviousBuffer()
+ } else {
+ app.win.InputUp()
+ }
+ app.updatePrompt()
case tcell.KeyDown:
- app.win.InputDown()
+ if ev.Modifiers() == tcell.ModAlt {
+ app.win.NextBuffer()
+ } else {
+ app.win.InputDown()
+ }
+ app.updatePrompt()
case tcell.KeyHome:
app.win.InputHome()
case tcell.KeyEnd:
@@ -282,11 +294,13 @@ func (app *App) handleUIEvent(ev tcell.Event) {
ok := app.win.InputBackspace()
if ok {
app.typing()
+ app.updatePrompt()
}
case tcell.KeyDelete:
ok := app.win.InputDelete()
if ok {
app.typing()
+ app.updatePrompt()
}
case tcell.KeyTab:
ok := app.win.InputAutoComplete()
@@ -305,9 +319,11 @@ func (app *App) handleUIEvent(ev tcell.Event) {
Body: fmt.Sprintf("%q: %s", input, err),
})
}
+ app.updatePrompt()
case tcell.KeyRune:
app.win.InputRune(ev.Rune())
app.typing()
+ app.updatePrompt()
default:
return
}
@@ -442,22 +458,22 @@ func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line,
headColor := ui.ColorWhite
if isFromSelf && isQuery {
head = "\u2192 " + ev.Target
- headColor = app.identColor(ev.Target)
+ headColor = ui.IdentColor(ev.Target)
} else if isAction || isNotice {
head = "*"
} else {
- headColor = app.identColor(head)
+ headColor = ui.IdentColor(head)
}
body := strings.TrimSuffix(ev.Content, "\x01")
if isNotice && isAction {
- c := ircColorSequence(app.identColor(ev.User.Name))
+ c := ircColorSequence(ui.IdentColor(ev.User.Name))
body = fmt.Sprintf("(%s%s\x0F:%s)", c, ev.User.Name, body[7:])
} else if isAction {
- c := ircColorSequence(app.identColor(ev.User.Name))
+ c := ircColorSequence(ui.IdentColor(ev.User.Name))
body = fmt.Sprintf("%s%s\x0F%s", c, ev.User.Name, body[7:])
} else if isNotice {
- c := ircColorSequence(app.identColor(ev.User.Name))
+ c := ircColorSequence(ui.IdentColor(ev.User.Name))
body = fmt.Sprintf("(%s%s\x0F: %s)", c, ev.User.Name, body)
}
@@ -471,6 +487,16 @@ func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line,
return
}
+func (app *App) updatePrompt() {
+ buffer := app.win.CurrentBuffer()
+ command := app.win.InputIsCommand()
+ if buffer == Home || command {
+ app.win.SetPrompt(">")
+ } else {
+ app.win.SetPrompt(app.s.Nick())
+ }
+}
+
func ircColorSequence(code int) string {
var c [3]rune
c[0] = 0x03
@@ -479,23 +505,6 @@ func ircColorSequence(code int) string {
return string(c[:])
}
-// see <https://modern.ircdocs.horse/formatting.html>
-var identColorBlacklist = []int{1, 8, 16, 27, 28, 88, 89, 90, 91}
-
-func (app *App) 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++
- }
- }
-
- return
-}
-
func cleanMessage(s string) string {
var res strings.Builder
var sb ui.StyleBuffer
diff --git a/irc/states.go b/irc/states.go
index 4317369..c9d344e 100644
--- a/irc/states.go
+++ b/irc/states.go
@@ -872,6 +872,8 @@ func (s *Session) handle(msg Message) (err error) {
delete(s.users, nickCf)
s.users[newNickCf] = formerUser
u = formerUser.Name.Copy()
+ } else {
+ break
}
if nickCf == s.nickCf {
diff --git a/ui/buffers.go b/ui/buffers.go
index 1347635..173264b 100644
--- a/ui/buffers.go
+++ b/ui/buffers.go
@@ -1,8 +1,6 @@
package ui
import (
- "fmt"
- "math"
"strings"
"time"
@@ -169,101 +167,31 @@ type buffer struct {
isAtTop bool
}
-func (b *buffer) DrawLines(screen tcell.Screen, x0, width, height, nickColWidth int) {
- st := tcell.StyleDefault
- for x := 0; x < width; x++ {
- for y := 0; y < height; y++ {
- screen.SetContent(x, y, ' ', nil, st)
- }
- }
-
- y0 := b.scrollAmt + height
- for i := len(b.lines) - 1; 0 <= i; i-- {
- if y0 < 0 {
- break
- }
-
- x1 := x0 + 9 + nickColWidth
-
- line := &b.lines[i]
- nls := line.NewLines(width - x1)
- 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, x0, y0, st.Bold(true), line.At.Local())
- }
-
- head := truncate(line.Head, nickColWidth, "\u2026")
- x := x0 + 7 + nickColWidth - StringWidth(head)
- st = st.Foreground(colorFromCode(line.HeadColor))
- if line.Highlight {
- st = st.Reverse(true)
- }
- screen.SetContent(x-1, y0, ' ', nil, st)
- screen.SetContent(x0+7+nickColWidth, y0, ' ', nil, st)
- printString(screen, &x, y0, st, head)
- st = st.Reverse(false).Foreground(tcell.ColorDefault)
-
- x = x1
- y := y0
-
- var sb StyleBuffer
- sb.Reset()
- for i, r := range line.Body {
- if 0 < len(nls) && i == nls[0] {
- x = x1
- y++
- nls = nls[1:]
- if height < y {
- break
- }
- }
-
- if y != y0 && x == x1 && 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(r)
- }
- }
-
- sb.Reset()
- }
-
- b.isAtTop = 0 <= y0
-}
-
type BufferList struct {
list []buffer
current int
- status string
- width int
- height int
+ tlWidth int
+ tlHeight int
nickColWidth int
}
-func NewBufferList(width, height, nickColWidth int) BufferList {
+func NewBufferList(tlWidth, tlHeight, nickColWidth int) BufferList {
return BufferList{
list: []buffer{},
- width: width,
- height: height,
+ tlWidth: tlWidth,
+ tlHeight: tlHeight,
nickColWidth: nickColWidth,
}
}
-func (bs *BufferList) Resize(width, height int) {
- bs.width = width
- bs.height = height
+func (bs *BufferList) ResizeTimeline(tlWidth, tlHeight, nickColWidth int) {
+ bs.tlWidth = tlWidth
+ bs.tlHeight = tlHeight
+}
+
+func (bs *BufferList) tlInnerWidth() int {
+ return bs.tlWidth - bs.nickColWidth - 9
}
func (bs *BufferList) Next() {
@@ -326,7 +254,7 @@ func (bs *BufferList) AddLine(title string, highlight bool, line Line) {
line.computeSplitPoints()
b.lines = append(b.lines, line)
if idx == bs.current && 0 < b.scrollAmt {
- b.scrollAmt += len(line.NewLines(bs.width-9-bs.nickColWidth)) + 1
+ b.scrollAmt += len(line.NewLines(bs.tlInnerWidth())) + 1
}
}
@@ -363,10 +291,6 @@ func (bs *BufferList) AddLines(title string, lines []Line) {
b.lines = append(lines[:limit], b.lines...)
}
-func (bs *BufferList) SetStatus(status string) {
- bs.status = status
-}
-
func (bs *BufferList) Current() (title string) {
return bs.list[bs.current].title
}
@@ -384,12 +308,12 @@ func (bs *BufferList) ScrollUp() {
if b.isAtTop {
return
}
- b.scrollAmt += bs.height / 2
+ b.scrollAmt += bs.tlHeight / 2
}
func (bs *BufferList) ScrollDown() {
b := &bs.list[bs.current]
- b.scrollAmt -= bs.height / 2
+ b.scrollAmt -= bs.tlHeight / 2
if b.scrollAmt < 0 {
b.scrollAmt = 0
@@ -415,79 +339,28 @@ func (bs *BufferList) idx(title string) int {
return -1
}
-func (bs *BufferList) Draw(screen tcell.Screen) {
- bs.list[bs.current].DrawLines(screen, 0, bs.width, bs.height-2, bs.nickColWidth)
- bs.drawStatusBar(screen, bs.height-2)
- bs.drawTitleList(screen, bs.height-1)
-}
-
-func (bs *BufferList) drawStatusBar(screen tcell.Screen, y int) {
- st := tcell.StyleDefault.Dim(true)
-
- for x := 0; x < bs.width; x++ {
- screen.SetContent(x, y, 0x2500, nil, st)
- }
-
- if bs.status == "" {
- return
- }
-
- x := 2
- screen.SetContent(1, y, 0x2524, nil, st)
- printString(screen, &x, y, st, bs.status)
- screen.SetContent(x, y, 0x251c, nil, st)
-}
-
-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))) + 3
- }
- widths = append(widths, width)
- }
-
+func (bs *BufferList) DrawVerticalBufferList(screen tcell.Screen, x0, y0, width, height int) {
+ width--
st := tcell.StyleDefault
- for x := 0; x < bs.width; x++ {
- screen.SetContent(x, y, ' ', nil, st)
- }
-
- 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)
+ for y := y0; y < y0+height; y++ {
+ for x := x0; x < x0+width; x++ {
screen.SetContent(x, y, ' ', nil, st)
- x++
- printNumber(screen, &x, y, st, b.highlights)
- screen.SetContent(x, y, ' ', nil, st)
- x++
}
- x += 2
- i = (i + 1) % len(bs.list)
+ screen.SetContent(x0+width, y, 0x2502, nil, st.Dim(true))
}
- 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]
+ for i, b := range bs.list {
st = tcell.StyleDefault
+ x := x0
+ y := y0 + i
if b.unread {
st = st.Bold(true)
+ } else if y == bs.current {
+ st = st.Underline(true)
}
- printString(screen, &x, y, st, b.title)
+ title := truncate(b.title, width, "\u2026")
+ printString(screen, &x, y, st, title)
if 0 < b.highlights {
st = st.Foreground(tcell.ColorRed).Reverse(true)
screen.SetContent(x, y, ' ', nil, st)
@@ -496,33 +369,74 @@ func (bs *BufferList) drawTitleList(screen tcell.Screen, y int) {
screen.SetContent(x, y, ' ', nil, st)
x++
}
- x -= widths[i]
- i = (i - 1 + len(bs.list)) % len(bs.list)
+ y++
}
}
-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 (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)
+ }
}
-}
-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)
-}
+ b := &bs.list[bs.current]
+ yi := b.scrollAmt + y0 + bs.tlHeight
+ for i := len(b.lines) - 1; 0 <= i; i-- {
+ if yi < 0 {
+ break
+ }
+
+ x1 := x0 + 9 + nickColWidth
+
+ line := &b.lines[i]
+ nls := line.NewLines(bs.tlInnerWidth())
+ yi -= len(nls) + 1
+ if y0+bs.tlHeight <= yi {
+ continue
+ }
+
+ 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())
+ }
+
+ identSt := st.Foreground(colorFromCode(line.HeadColor)).Reverse(line.Highlight)
+ printIdent(screen, x0+7, yi, nickColWidth, identSt, line.Head)
+
+ x := x1
+ y := yi
+
+ var sb StyleBuffer
+ sb.Reset()
+ for i, r := range line.Body {
+ if 0 < len(nls) && i == nls[0] {
+ x = x1
+ y++
+ nls = nls[1:]
+ if bs.tlHeight < y {
+ break
+ }
+ }
+
+ if y != yi && x == x1 && 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(r)
+ }
+ }
+
+ sb.Reset()
+ }
-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.isAtTop = y0 <= yi
}
func IrcColorCode(code int) string {
diff --git a/ui/draw_utils.go b/ui/draw_utils.go
new file mode 100644
index 0000000..893a77f
--- /dev/null
+++ b/ui/draw_utils.go
@@ -0,0 +1,40 @@
+package ui
+
+import (
+ "fmt"
+ "time"
+
+ "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, st tcell.Style, s string) {
+ for _, r := range s {
+ screen.SetContent(*x, y, r, nil, st)
+ *x += runeWidth(r)
+ }
+}
+
+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)
+}
+
+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)
+}
diff --git a/ui/editor.go b/ui/editor.go
index cfc26b5..8632e81 100644
--- a/ui/editor.go
+++ b/ui/editor.go
@@ -242,20 +242,20 @@ func (e *Editor) computeTextWidth() {
}
}
-func (e *Editor) Draw(screen tcell.Screen, y int) {
+func (e *Editor) Draw(screen tcell.Screen, x0, y int) {
st := tcell.StyleDefault
- x := 0
+ x := x0
i := e.offsetIdx
- for i < len(e.text[e.lineIdx]) && x < e.width {
+ for i < len(e.text[e.lineIdx]) && x < x0+e.width {
r := e.text[e.lineIdx][i]
screen.SetContent(x, y, r, nil, st)
x += runeWidth(r)
i++
}
- for x < e.width {
+ for x < x0+e.width {
screen.SetContent(x, y, ' ', nil, st)
x++
}
@@ -265,7 +265,7 @@ func (e *Editor) Draw(screen tcell.Screen, y int) {
if e.cursorIdx+1 < len(e.textWidth) {
curEnd = e.textWidth[e.cursorIdx+1] - e.textWidth[e.offsetIdx]
}
- for x := curStart; x < curEnd; x++ {
+ for x := x0 + curStart; x < x0+curEnd; x++ {
screen.ShowCursor(x, y)
}
}
diff --git a/ui/style.go b/ui/style.go
index 77dc515..acc1a41 100644
--- a/ui/style.go
+++ b/ui/style.go
@@ -1,6 +1,8 @@
package ui
import (
+ "hash/fnv"
+
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
)
@@ -223,3 +225,20 @@ func colorFromCode(code int) (color tcell.Color) {
}
return
}
+
+// see <https://modern.ircdocs.horse/formatting.html>
+var identColorBlacklist = []int{1, 8, 16, 27, 28, 88, 89, 90, 91}
+
+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++
+ }
+ }
+
+ return
+}
diff --git a/ui/ui.go b/ui/ui.go
index a85aed7..3acbe1e 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -18,8 +18,10 @@ type UI struct {
exit atomic.Value // bool
config Config
- bs BufferList
- e Editor
+ bs BufferList
+ e Editor
+ prompt string
+ status string
}
func New(config Config) (ui *UI, err error) {
@@ -115,7 +117,11 @@ func (ui *UI) AddLines(buffer string, lines []Line) {
}
func (ui *UI) SetStatus(status string) {
- ui.bs.SetStatus(status)
+ ui.status = status
+}
+
+func (ui *UI) SetPrompt(prompt string) {
+ ui.prompt = prompt
}
func (ui *UI) InputIsCommand() bool {
@@ -172,13 +178,41 @@ func (ui *UI) InputEnter() (content string) {
func (ui *UI) Resize() {
w, h := ui.screen.Size()
- ui.e.Resize(w)
- ui.bs.Resize(w, h-1)
+ ui.e.Resize(w - 25 - ui.config.NickColWidth)
+ ui.bs.ResizeTimeline(w-16, h-2, ui.config.NickColWidth)
}
func (ui *UI) Draw() {
- _, h := ui.screen.Size()
- ui.e.Draw(ui.screen, h-1)
- ui.bs.Draw(ui.screen)
+ w, h := ui.screen.Size()
+
+ ui.e.Draw(ui.screen, 25+ui.config.NickColWidth, h-1)
+
+ ui.bs.DrawTimeline(ui.screen, 16, 0, ui.config.NickColWidth)
+ ui.bs.DrawVerticalBufferList(ui.screen, 0, 0, 16, h)
+ ui.drawStatusBar(16, h-2, w-16)
+
+ for x := 16; x < 25+ui.config.NickColWidth; x++ {
+ ui.screen.SetContent(x, h-1, ' ', nil, tcell.StyleDefault)
+ }
+ st := tcell.StyleDefault.Foreground(colorFromCode(IdentColor(ui.prompt)))
+ printIdent(ui.screen, 23, h-1, ui.config.NickColWidth, st, ui.prompt)
+
ui.screen.Show()
}
+
+func (ui *UI) drawStatusBar(x0, y, width int) {
+ st := tcell.StyleDefault.Dim(true)
+
+ for x := x0; x < x0+width; x++ {
+ ui.screen.SetContent(x, y, ' ', nil, st)
+ }
+
+ if ui.status == "" {
+ return
+ }
+
+ x := x0 + 5 + ui.config.NickColWidth
+ printString(ui.screen, &x, y, st, "--")
+ x += 2
+ printString(ui.screen, &x, y, st, ui.status)
+}