diff options
author | Hubert Hirtz <hubert@hirtz.pm> | 2020-10-21 16:22:28 +0200 |
---|---|---|
committer | Hubert Hirtz <hubert@hirtz.pm> | 2020-10-26 08:45:07 +0100 |
commit | 7fae7f74c5315877e0d341ec60ceef2e87c0c1cc (patch) | |
tree | afdea6e39840dfbd3db4e864bf914fe137009298 | |
parent | Collapse bufferlist in one block (diff) |
Vertical channel list
-rw-r--r-- | app.go | 59 | ||||
-rw-r--r-- | irc/states.go | 2 | ||||
-rw-r--r-- | ui/buffers.go | 264 | ||||
-rw-r--r-- | ui/draw_utils.go | 40 | ||||
-rw-r--r-- | ui/editor.go | 10 | ||||
-rw-r--r-- | ui/style.go | 19 | ||||
-rw-r--r-- | ui/ui.go | 50 |
7 files changed, 231 insertions, 213 deletions
@@ -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 +} @@ -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) +} |