diff options
Diffstat (limited to 'ui')
-rw-r--r-- | ui/buffers.go | 153 | ||||
-rw-r--r-- | ui/editor.go | 46 | ||||
-rw-r--r-- | ui/style.go | 8 | ||||
-rw-r--r-- | ui/ui.go | 69 |
4 files changed, 107 insertions, 169 deletions
diff --git a/ui/buffers.go b/ui/buffers.go index c1426f9..606a7ee 100644 --- a/ui/buffers.go +++ b/ui/buffers.go @@ -2,7 +2,6 @@ package ui import ( "fmt" - "hash/fnv" "math" "strings" "time" @@ -31,55 +30,28 @@ type point struct { } type Line struct { - at time.Time - head string - body string - - isStatus bool - isHighlight bool + At time.Time + Head string + Body string + HeadColor int + Highlight bool + Mergeable bool splitPoints []point width int newLines []int } -func NewLine(at time.Time, head string, body string, isStatus bool, isHighlight bool) Line { - l := Line{ - at: at, - head: head, - body: body, - isStatus: isStatus, - isHighlight: isHighlight, - splitPoints: []point{}, - newLines: []int{}, - } - l.computeSplitPoints() - return l -} - -func NewLineNow(head, body string) Line { - return NewLine(time.Now(), head, body, false, false) -} - -func LineFromIRCMessage(at time.Time, nick string, content string, isNotice bool, isHighlight bool) Line { - if strings.HasPrefix(content, "\x01ACTION") { - c := ircColorCode(identColor(nick)) - content = fmt.Sprintf("%s%s\x0F%s", c, nick, content[7:]) - nick = "*" - } else if isNotice { - c := ircColorCode(identColor(nick)) - content = fmt.Sprintf("(%s%s\x0F: %s)", c, nick, content) - nick = "*" +func (l *Line) computeSplitPoints() { + if l.splitPoints == nil { + l.splitPoints = []point{} } - return NewLine(at, nick, content, false, isHighlight) -} -func (l *Line) computeSplitPoints() { var wb widthBuffer lastWasSplit := false l.splitPoints = l.splitPoints[:0] - for i, r := range l.body { + for i, r := range l.Body { curIsSplit := IsSplitRune(r) if i == 0 || lastWasSplit != curIsSplit { @@ -97,7 +69,7 @@ func (l *Line) computeSplitPoints() { if !lastWasSplit { l.splitPoints = append(l.splitPoints, point{ X: wb.Width(), - I: len(l.body), + I: len(l.Body), Split: true, }) } @@ -113,6 +85,9 @@ func (l *Line) NewLines(width int) []int { if l.width == width { return l.newLines } + if l.newLines == nil { + l.newLines = []int{} + } l.newLines = l.newLines[:0] l.width = width @@ -158,7 +133,7 @@ func (l *Line) NewLines(width int) []int { // the word. var wb widthBuffer h := 1 - for j, r := range l.body[sp1.I:sp2.I] { + for j, r := range l.Body[sp1.I:sp2.I] { wb.WriteRune(r) if h*width < x+wb.Width() { l.newLines = append(l.newLines, sp1.I+j) @@ -185,7 +160,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) { // 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] @@ -229,20 +204,19 @@ func (b *buffer) DrawLines(screen tcell.Screen, width, height, nickColWidth int) continue } - if i == 0 || b.lines[i-1].at.Truncate(time.Minute) != line.at.Truncate(time.Minute) { - printTime(screen, 0, y0, st.Bold(true), line.at.Local()) + if i == 0 || b.lines[i-1].At.Truncate(time.Minute) != line.At.Truncate(time.Minute) { + printTime(screen, 0, y0, st.Bold(true), line.At.Local()) } - head := truncate(line.head, nickColWidth, "\u2026") + head := truncate(line.Head, nickColWidth, "\u2026") x := 7 + nickColWidth - StringWidth(head) - c := identColor(line.head) - st = st.Foreground(colorFromCode(c)) - if line.isHighlight { + st = st.Foreground(colorFromCode(line.HeadColor)) + if line.Highlight { st = st.Reverse(true) } screen.SetContent(x-1, y0, ' ', nil, st) screen.SetContent(7+nickColWidth, y0, ' ', nil, st) - printString(screen, &x, y0, st.Foreground(colorFromCode(c)), head) + printString(screen, &x, y0, st, head) st = st.Reverse(false).Foreground(tcell.ColorDefault) x = x0 @@ -250,7 +224,7 @@ func (b *buffer) DrawLines(screen tcell.Screen, width, height, nickColWidth int) var sb StyleBuffer sb.Reset() - for i, r := range line.body { + for i, r := range line.Body { if 0 < len(nls) && i == nls[0] { x = x0 y++ @@ -280,7 +254,7 @@ func (b *buffer) DrawLines(screen tcell.Screen, width, height, nickColWidth int) b.isAtTop = 0 <= y0 } -type bufferList struct { +type BufferList struct { list []buffer current int @@ -289,8 +263,8 @@ type bufferList struct { nickColWidth int } -func newBufferList(width, height, nickColWidth int) bufferList { - return bufferList{ +func NewBufferList(width, height, nickColWidth int) BufferList { + return BufferList{ list: []buffer{}, width: width, height: height, @@ -298,24 +272,24 @@ func newBufferList(width, height, nickColWidth int) bufferList { } } -func (bs *bufferList) Resize(width, height int) { +func (bs *BufferList) Resize(width, height int) { bs.width = width bs.height = height } -func (bs *bufferList) Next() { +func (bs *BufferList) Next() { bs.current = (bs.current + 1) % len(bs.list) bs.list[bs.current].highlights = 0 bs.list[bs.current].unread = false } -func (bs *bufferList) Previous() { +func (bs *BufferList) Previous() { bs.current = (bs.current - 1 + len(bs.list)) % len(bs.list) bs.list[bs.current].highlights = 0 bs.list[bs.current].unread = false } -func (bs *bufferList) Add(title string) (ok bool) { +func (bs *BufferList) Add(title string) (ok bool) { lTitle := strings.ToLower(title) for _, b := range bs.list { if strings.ToLower(b.title) == lTitle { @@ -328,7 +302,7 @@ func (bs *bufferList) Add(title string) (ok bool) { return } -func (bs *bufferList) Remove(title string) (ok bool) { +func (bs *BufferList) Remove(title string) (ok bool) { lTitle := strings.ToLower(title) for i, b := range bs.list { if strings.ToLower(b.title) == lTitle { @@ -343,7 +317,7 @@ func (bs *bufferList) Remove(title string) (ok bool) { return } -func (bs *bufferList) AddLine(title string, line Line) { +func (bs *BufferList) AddLine(title string, highlight bool, line Line) { idx := bs.idx(title) if idx < 0 { return @@ -351,29 +325,30 @@ func (bs *bufferList) AddLine(title string, line Line) { b := &bs.list[idx] n := len(b.lines) - line.body = strings.TrimRight(line.body, "\t ") + line.Body = strings.TrimRight(line.Body, "\t ") - if line.isStatus && n != 0 && b.lines[n-1].isStatus { + if line.Mergeable && n != 0 && b.lines[n-1].Mergeable { l := &b.lines[n-1] - l.body += " " + line.body + l.Body += " " + line.Body l.computeSplitPoints() l.width = 0 } else { + 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 } } - if !line.isStatus && idx != bs.current { + if !line.Mergeable && idx != bs.current { b.unread = true } - if line.isHighlight && idx != bs.current { + if highlight && idx != bs.current { b.highlights++ } } -func (bs *bufferList) AddLines(title string, lines []Line) { +func (bs *BufferList) AddLines(title string, lines []Line) { idx := bs.idx(title) if idx < 0 { return @@ -383,19 +358,22 @@ func (bs *bufferList) AddLines(title string, lines []Line) { limit := len(lines) if 0 < len(b.lines) { - firstLineTime := b.lines[0].at.Unix() + firstLineTime := b.lines[0].At.Unix() for i, l := range lines { - if firstLineTime < l.at.Unix() { + if firstLineTime < l.At.Unix() { limit = i break } } } + for i := 0; i < limit; i++ { + lines[i].computeSplitPoints() + } b.lines = append(lines[:limit], b.lines...) } -func (bs *bufferList) TypingStart(title, nick string) { +func (bs *BufferList) TypingStart(title, nick string) { idx := bs.idx(title) if idx < 0 { return @@ -411,7 +389,7 @@ func (bs *bufferList) TypingStart(title, nick string) { b.typings = append(b.typings, nick) } -func (bs *bufferList) TypingStop(title, nick string) { +func (bs *BufferList) TypingStop(title, nick string) { idx := bs.idx(title) if idx < 0 { return @@ -427,19 +405,19 @@ func (bs *bufferList) TypingStop(title, nick string) { } } -func (bs *bufferList) Current() (title string) { +func (bs *BufferList) Current() (title string) { return bs.list[bs.current].title } -func (bs *bufferList) CurrentOldestTime() (t *time.Time) { +func (bs *BufferList) CurrentOldestTime() (t *time.Time) { ls := bs.list[bs.current].lines if 0 < len(ls) { - t = &ls[0].at + t = &ls[0].At } return } -func (bs *bufferList) ScrollUp() { +func (bs *BufferList) ScrollUp() { b := &bs.list[bs.current] if b.isAtTop { return @@ -447,7 +425,7 @@ func (bs *bufferList) ScrollUp() { b.scrollAmt += bs.height / 2 } -func (bs *bufferList) ScrollDown() { +func (bs *BufferList) ScrollDown() { b := &bs.list[bs.current] b.scrollAmt -= bs.height / 2 @@ -456,12 +434,12 @@ func (bs *bufferList) ScrollDown() { } } -func (bs *bufferList) IsAtTop() bool { +func (bs *BufferList) IsAtTop() bool { b := &bs.list[bs.current] return b.isAtTop } -func (bs *bufferList) idx(title string) int { +func (bs *BufferList) idx(title string) int { if title == "" { return bs.current } @@ -475,13 +453,13 @@ func (bs *bufferList) idx(title string) int { return -1 } -func (bs *bufferList) Draw(screen tcell.Screen) { +func (bs *BufferList) Draw(screen tcell.Screen) { bs.list[bs.current].DrawLines(screen, bs.width, bs.height-3, bs.nickColWidth) bs.drawStatusBar(screen, bs.height-3) bs.drawTitleList(screen, bs.height-1) } -func (bs *bufferList) drawStatusBar(screen tcell.Screen, y int) { +func (bs *BufferList) drawStatusBar(screen tcell.Screen, y int) { st := tcell.StyleDefault.Dim(true) nicks := bs.list[bs.current].typings verb := " is typing..." @@ -517,7 +495,7 @@ func (bs *bufferList) drawStatusBar(screen tcell.Screen, y int) { } } -func (bs *bufferList) drawTitleList(screen tcell.Screen, y int) { +func (bs *BufferList) drawTitleList(screen tcell.Screen, y int) { var widths []int for _, b := range bs.list { width := StringWidth(b.title) @@ -604,24 +582,7 @@ func printTime(screen tcell.Screen, x int, y int, st tcell.Style, t time.Time) { screen.SetContent(x+4, y, mn1, nil, st) } -// 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 -} - -func ircColorCode(code int) string { +func IrcColorCode(code int) string { var c [3]rune c[0] = 0x03 c[1] = rune(code/10) + '0' diff --git a/ui/editor.go b/ui/editor.go index 33cf736..1b30c53 100644 --- a/ui/editor.go +++ b/ui/editor.go @@ -9,8 +9,8 @@ type Completion struct { CursorIdx int } -// editor is the text field where the user writes messages and commands. -type editor struct { +// Editor is the text field where the user writes messages and commands. +type Editor struct { // text contains the written runes. An empty slice means no text is written. text [][]rune @@ -38,8 +38,8 @@ type editor struct { completeIdx int } -func newEditor(width int, autoComplete func(cursorIdx int, text []rune) []Completion) editor { - return editor{ +func NewEditor(width int, autoComplete func(cursorIdx int, text []rune) []Completion) Editor { + return Editor{ text: [][]rune{{}}, textWidth: []int{0}, width: width, @@ -47,7 +47,7 @@ func newEditor(width int, autoComplete func(cursorIdx int, text []rune) []Comple } } -func (e *editor) Resize(width int) { +func (e *Editor) Resize(width int) { if width < e.width { e.cursorIdx = 0 e.offsetIdx = 0 @@ -56,21 +56,21 @@ func (e *editor) Resize(width int) { e.width = width } -func (e *editor) IsCommand() bool { +func (e *Editor) IsCommand() bool { return len(e.text[e.lineIdx]) != 0 && e.text[e.lineIdx][0] == '/' } -func (e *editor) TextLen() int { +func (e *Editor) TextLen() int { return len(e.text[e.lineIdx]) } -func (e *editor) PutRune(r rune) { +func (e *Editor) PutRune(r rune) { e.putRune(r) e.Right() e.autoCache = nil } -func (e *editor) putRune(r rune) { +func (e *Editor) putRune(r rune) { e.text[e.lineIdx] = append(e.text[e.lineIdx], ' ') copy(e.text[e.lineIdx][e.cursorIdx+1:], e.text[e.lineIdx][e.cursorIdx:]) e.text[e.lineIdx][e.cursorIdx] = r @@ -83,7 +83,7 @@ func (e *editor) putRune(r rune) { } } -func (e *editor) RemRune() (ok bool) { +func (e *Editor) RemRune() (ok bool) { ok = 0 < e.cursorIdx if !ok { return @@ -94,7 +94,7 @@ func (e *editor) RemRune() (ok bool) { return } -func (e *editor) RemRuneForward() (ok bool) { +func (e *Editor) RemRuneForward() (ok bool) { ok = e.cursorIdx < len(e.text[e.lineIdx]) if !ok { return @@ -104,7 +104,7 @@ func (e *editor) RemRuneForward() (ok bool) { return } -func (e *editor) remRuneAt(idx int) { +func (e *Editor) remRuneAt(idx int) { // TODO avoid looping twice rw := e.textWidth[idx+1] - e.textWidth[idx] for i := idx + 1; i < len(e.textWidth); i++ { @@ -117,7 +117,7 @@ func (e *editor) remRuneAt(idx int) { e.text[e.lineIdx] = e.text[e.lineIdx][:len(e.text[e.lineIdx])-1] } -func (e *editor) Flush() (content string) { +func (e *Editor) Flush() (content string) { content = string(e.text[e.lineIdx]) if len(e.text[len(e.text)-1]) == 0 { e.lineIdx = len(e.text) - 1 @@ -132,12 +132,12 @@ func (e *editor) Flush() (content string) { return } -func (e *editor) Right() { +func (e *Editor) Right() { e.right() e.autoCache = nil } -func (e *editor) right() { +func (e *Editor) right() { if e.cursorIdx == len(e.text[e.lineIdx]) { return } @@ -151,7 +151,7 @@ func (e *editor) right() { } } -func (e *editor) Left() { +func (e *Editor) Left() { if e.cursorIdx == 0 { return } @@ -165,7 +165,7 @@ func (e *editor) Left() { e.autoCache = nil } -func (e *editor) Home() { +func (e *Editor) Home() { if e.cursorIdx == 0 { return } @@ -174,7 +174,7 @@ func (e *editor) Home() { e.autoCache = nil } -func (e *editor) End() { +func (e *Editor) End() { if e.cursorIdx == len(e.text[e.lineIdx]) { return } @@ -185,7 +185,7 @@ func (e *editor) End() { e.autoCache = nil } -func (e *editor) Up() { +func (e *Editor) Up() { if e.lineIdx == 0 { return } @@ -197,7 +197,7 @@ func (e *editor) Up() { e.End() } -func (e *editor) Down() { +func (e *Editor) Down() { if e.lineIdx == len(e.text)-1 { if len(e.text[e.lineIdx]) == 0 { return @@ -213,7 +213,7 @@ func (e *editor) Down() { e.End() } -func (e *editor) AutoComplete() (ok bool) { +func (e *Editor) AutoComplete() (ok bool) { if e.autoCache == nil { e.autoCache = e.autoComplete(e.cursorIdx, e.text[e.lineIdx]) if e.autoCache == nil { @@ -234,7 +234,7 @@ func (e *editor) AutoComplete() (ok bool) { return true } -func (e *editor) computeTextWidth() { +func (e *Editor) computeTextWidth() { e.textWidth = e.textWidth[:1] rw := 0 for _, r := range e.text[e.lineIdx] { @@ -243,7 +243,7 @@ func (e *editor) computeTextWidth() { } } -func (e *editor) Draw(screen tcell.Screen, y int) { +func (e *Editor) Draw(screen tcell.Screen, y int) { st := tcell.StyleDefault x := 0 diff --git a/ui/style.go b/ui/style.go index f6e8c39..8672535 100644 --- a/ui/style.go +++ b/ui/style.go @@ -169,6 +169,14 @@ func (cb *colorBuffer) WriteRune(r rune) (ok int) { return } +const ( + ColorWhite = iota + ColorBlack + ColorBlue + ColorGreen + ColorRed +) + var ansiCodes = []tcell.Color{ // Taken from <https://modern.ircdocs.horse/formatting.html> tcell.ColorWhite, tcell.ColorBlack, tcell.ColorBlue, tcell.ColorGreen, @@ -19,8 +19,8 @@ type UI struct { exit atomic.Value // bool config Config - bs bufferList - e editor + bs BufferList + e Editor } func New(config Config) (ui *UI, err error) { @@ -52,11 +52,15 @@ func New(config Config) (ui *UI, err error) { ui.exit.Store(false) hmIdx := rand.Intn(len(homeMessages)) - ui.bs = newBufferList(w, h, ui.config.NickColWidth) + ui.bs = NewBufferList(w, h, ui.config.NickColWidth) ui.bs.Add(Home) - ui.bs.AddLine("", NewLineNow("--", homeMessages[hmIdx])) + ui.bs.AddLine("", false, Line{ + At: time.Now(), + Head: "--", + Body: homeMessages[hmIdx], + }) - ui.e = newEditor(w, ui.config.AutoComplete) + ui.e = NewEditor(w, ui.config.AutoComplete) ui.Resize() @@ -85,22 +89,18 @@ func (ui *UI) CurrentBufferOldestTime() (t *time.Time) { func (ui *UI) NextBuffer() { ui.bs.Next() - ui.draw() } func (ui *UI) PreviousBuffer() { ui.bs.Previous() - ui.draw() } func (ui *UI) ScrollUp() { ui.bs.ScrollUp() - ui.draw() } func (ui *UI) ScrollDown() { ui.bs.ScrollDown() - ui.draw() } func (ui *UI) IsAtTop() bool { @@ -108,37 +108,27 @@ func (ui *UI) IsAtTop() bool { } func (ui *UI) AddBuffer(title string) { - ok := ui.bs.Add(title) - if ok { - ui.draw() - } + _ = ui.bs.Add(title) } func (ui *UI) RemoveBuffer(title string) { - ok := ui.bs.Remove(title) - if ok { - ui.draw() - } + _ = ui.bs.Remove(title) } -func (ui *UI) AddLine(buffer string, line Line) { - ui.bs.AddLine(buffer, line) - ui.draw() +func (ui *UI) AddLine(buffer string, highlight bool, line Line) { + ui.bs.AddLine(buffer, highlight, line) } func (ui *UI) AddLines(buffer string, lines []Line) { ui.bs.AddLines(buffer, lines) - ui.draw() } func (ui *UI) TypingStart(buffer, nick string) { ui.bs.TypingStart(buffer, nick) - ui.draw() } func (ui *UI) TypingStop(buffer, nick string) { ui.bs.TypingStop(buffer, nick) - ui.draw() } func (ui *UI) InputIsCommand() bool { @@ -151,77 +141,56 @@ func (ui *UI) InputLen() int { func (ui *UI) InputRune(r rune) { ui.e.PutRune(r) - ui.draw() } func (ui *UI) InputRight() { ui.e.Right() - ui.draw() } func (ui *UI) InputLeft() { ui.e.Left() - ui.draw() } func (ui *UI) InputHome() { ui.e.Home() - ui.draw() } func (ui *UI) InputEnd() { ui.e.End() - ui.draw() } func (ui *UI) InputUp() { ui.e.Up() - ui.draw() } func (ui *UI) InputDown() { ui.e.Down() - ui.draw() } func (ui *UI) InputBackspace() (ok bool) { - ok = ui.e.RemRune() - if ok { - ui.draw() - } - return + return ui.e.RemRune() } func (ui *UI) InputDelete() (ok bool) { - ok = ui.e.RemRuneForward() - if ok { - ui.draw() - } - return + return ui.e.RemRuneForward() } func (ui *UI) InputAutoComplete() (ok bool) { - ok = ui.e.AutoComplete() - if ok { - ui.draw() - } - return + return ui.e.AutoComplete() } func (ui *UI) InputEnter() (content string) { - content = ui.e.Flush() - ui.draw() - return + return ui.e.Flush() } func (ui *UI) Resize() { w, h := ui.screen.Size() ui.e.Resize(w) ui.bs.Resize(w, h) - ui.draw() + ui.Draw() } -func (ui *UI) draw() { +func (ui *UI) Draw() { _, h := ui.screen.Size() ui.e.Draw(ui.screen, h-2) ui.bs.Draw(ui.screen) |