commit 7fae7f7
Hubert Hirtz
·
2020-10-21 14:22:28 +0000 UTC
parent 75d5148
Vertical channel list
7 files changed,
+231,
-213
M
app.go
+34,
-25
1@@ -3,7 +3,6 @@ package senpai
2 import (
3 "crypto/tls"
4 "fmt"
5- "hash/fnv"
6 "os/exec"
7 "strings"
8 "time"
9@@ -45,6 +44,7 @@ func NewApp(cfg Config) (app *App, err error) {
10 if err != nil {
11 return
12 }
13+ app.win.SetPrompt(">")
14
15 app.initWindow()
16
17@@ -261,19 +261,31 @@ func (app *App) handleUIEvent(ev tcell.Event) {
18 case tcell.KeyRight:
19 if ev.Modifiers() == tcell.ModAlt {
20 app.win.NextBuffer()
21+ app.updatePrompt()
22 } else {
23 app.win.InputRight()
24 }
25 case tcell.KeyLeft:
26 if ev.Modifiers() == tcell.ModAlt {
27 app.win.PreviousBuffer()
28+ app.updatePrompt()
29 } else {
30 app.win.InputLeft()
31 }
32 case tcell.KeyUp:
33- app.win.InputUp()
34+ if ev.Modifiers() == tcell.ModAlt {
35+ app.win.PreviousBuffer()
36+ } else {
37+ app.win.InputUp()
38+ }
39+ app.updatePrompt()
40 case tcell.KeyDown:
41- app.win.InputDown()
42+ if ev.Modifiers() == tcell.ModAlt {
43+ app.win.NextBuffer()
44+ } else {
45+ app.win.InputDown()
46+ }
47+ app.updatePrompt()
48 case tcell.KeyHome:
49 app.win.InputHome()
50 case tcell.KeyEnd:
51@@ -282,11 +294,13 @@ func (app *App) handleUIEvent(ev tcell.Event) {
52 ok := app.win.InputBackspace()
53 if ok {
54 app.typing()
55+ app.updatePrompt()
56 }
57 case tcell.KeyDelete:
58 ok := app.win.InputDelete()
59 if ok {
60 app.typing()
61+ app.updatePrompt()
62 }
63 case tcell.KeyTab:
64 ok := app.win.InputAutoComplete()
65@@ -305,9 +319,11 @@ func (app *App) handleUIEvent(ev tcell.Event) {
66 Body: fmt.Sprintf("%q: %s", input, err),
67 })
68 }
69+ app.updatePrompt()
70 case tcell.KeyRune:
71 app.win.InputRune(ev.Rune())
72 app.typing()
73+ app.updatePrompt()
74 default:
75 return
76 }
77@@ -442,22 +458,22 @@ func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line,
78 headColor := ui.ColorWhite
79 if isFromSelf && isQuery {
80 head = "\u2192 " + ev.Target
81- headColor = app.identColor(ev.Target)
82+ headColor = ui.IdentColor(ev.Target)
83 } else if isAction || isNotice {
84 head = "*"
85 } else {
86- headColor = app.identColor(head)
87+ headColor = ui.IdentColor(head)
88 }
89
90 body := strings.TrimSuffix(ev.Content, "\x01")
91 if isNotice && isAction {
92- c := ircColorSequence(app.identColor(ev.User.Name))
93+ c := ircColorSequence(ui.IdentColor(ev.User.Name))
94 body = fmt.Sprintf("(%s%s\x0F:%s)", c, ev.User.Name, body[7:])
95 } else if isAction {
96- c := ircColorSequence(app.identColor(ev.User.Name))
97+ c := ircColorSequence(ui.IdentColor(ev.User.Name))
98 body = fmt.Sprintf("%s%s\x0F%s", c, ev.User.Name, body[7:])
99 } else if isNotice {
100- c := ircColorSequence(app.identColor(ev.User.Name))
101+ c := ircColorSequence(ui.IdentColor(ev.User.Name))
102 body = fmt.Sprintf("(%s%s\x0F: %s)", c, ev.User.Name, body)
103 }
104
105@@ -471,6 +487,16 @@ func (app *App) formatMessage(ev irc.MessageEvent) (buffer string, line ui.Line,
106 return
107 }
108
109+func (app *App) updatePrompt() {
110+ buffer := app.win.CurrentBuffer()
111+ command := app.win.InputIsCommand()
112+ if buffer == Home || command {
113+ app.win.SetPrompt(">")
114+ } else {
115+ app.win.SetPrompt(app.s.Nick())
116+ }
117+}
118+
119 func ircColorSequence(code int) string {
120 var c [3]rune
121 c[0] = 0x03
122@@ -479,23 +505,6 @@ func ircColorSequence(code int) string {
123 return string(c[:])
124 }
125
126-// see <https://modern.ircdocs.horse/formatting.html>
127-var identColorBlacklist = []int{1, 8, 16, 27, 28, 88, 89, 90, 91}
128-
129-func (app *App) identColor(s string) (code int) {
130- h := fnv.New32()
131- _, _ = h.Write([]byte(s))
132-
133- code = int(h.Sum32()) % (99 - len(identColorBlacklist))
134- for _, c := range identColorBlacklist {
135- if c <= code {
136- code++
137- }
138- }
139-
140- return
141-}
142-
143 func cleanMessage(s string) string {
144 var res strings.Builder
145 var sb ui.StyleBuffer
+2,
-0
1@@ -872,6 +872,8 @@ func (s *Session) handle(msg Message) (err error) {
2 delete(s.users, nickCf)
3 s.users[newNickCf] = formerUser
4 u = formerUser.Name.Copy()
5+ } else {
6+ break
7 }
8
9 if nickCf == s.nickCf {
+89,
-175
1@@ -1,8 +1,6 @@
2 package ui
3
4 import (
5- "fmt"
6- "math"
7 "strings"
8 "time"
9
10@@ -169,101 +167,31 @@ type buffer struct {
11 isAtTop bool
12 }
13
14-func (b *buffer) DrawLines(screen tcell.Screen, x0, width, height, nickColWidth int) {
15- st := tcell.StyleDefault
16- for x := 0; x < width; x++ {
17- for y := 0; y < height; y++ {
18- screen.SetContent(x, y, ' ', nil, st)
19- }
20- }
21-
22- y0 := b.scrollAmt + height
23- for i := len(b.lines) - 1; 0 <= i; i-- {
24- if y0 < 0 {
25- break
26- }
27-
28- x1 := x0 + 9 + nickColWidth
29-
30- line := &b.lines[i]
31- nls := line.NewLines(width - x1)
32- y0 -= len(nls) + 1
33- if height <= y0 {
34- continue
35- }
36-
37- if i == 0 || b.lines[i-1].At.Truncate(time.Minute) != line.At.Truncate(time.Minute) {
38- printTime(screen, x0, y0, st.Bold(true), line.At.Local())
39- }
40-
41- head := truncate(line.Head, nickColWidth, "\u2026")
42- x := x0 + 7 + nickColWidth - StringWidth(head)
43- st = st.Foreground(colorFromCode(line.HeadColor))
44- if line.Highlight {
45- st = st.Reverse(true)
46- }
47- screen.SetContent(x-1, y0, ' ', nil, st)
48- screen.SetContent(x0+7+nickColWidth, y0, ' ', nil, st)
49- printString(screen, &x, y0, st, head)
50- st = st.Reverse(false).Foreground(tcell.ColorDefault)
51-
52- x = x1
53- y := y0
54-
55- var sb StyleBuffer
56- sb.Reset()
57- for i, r := range line.Body {
58- if 0 < len(nls) && i == nls[0] {
59- x = x1
60- y++
61- nls = nls[1:]
62- if height < y {
63- break
64- }
65- }
66-
67- if y != y0 && x == x1 && IsSplitRune(r) {
68- continue
69- }
70-
71- if st, ok := sb.WriteRune(r); ok != 0 {
72- if 1 < ok {
73- screen.SetContent(x, y, ',', nil, st)
74- x++
75- }
76- screen.SetContent(x, y, r, nil, st)
77- x += runeWidth(r)
78- }
79- }
80-
81- sb.Reset()
82- }
83-
84- b.isAtTop = 0 <= y0
85-}
86-
87 type BufferList struct {
88 list []buffer
89 current int
90- status string
91
92- width int
93- height int
94+ tlWidth int
95+ tlHeight int
96 nickColWidth int
97 }
98
99-func NewBufferList(width, height, nickColWidth int) BufferList {
100+func NewBufferList(tlWidth, tlHeight, nickColWidth int) BufferList {
101 return BufferList{
102 list: []buffer{},
103- width: width,
104- height: height,
105+ tlWidth: tlWidth,
106+ tlHeight: tlHeight,
107 nickColWidth: nickColWidth,
108 }
109 }
110
111-func (bs *BufferList) Resize(width, height int) {
112- bs.width = width
113- bs.height = height
114+func (bs *BufferList) ResizeTimeline(tlWidth, tlHeight, nickColWidth int) {
115+ bs.tlWidth = tlWidth
116+ bs.tlHeight = tlHeight
117+}
118+
119+func (bs *BufferList) tlInnerWidth() int {
120+ return bs.tlWidth - bs.nickColWidth - 9
121 }
122
123 func (bs *BufferList) Next() {
124@@ -326,7 +254,7 @@ func (bs *BufferList) AddLine(title string, highlight bool, line Line) {
125 line.computeSplitPoints()
126 b.lines = append(b.lines, line)
127 if idx == bs.current && 0 < b.scrollAmt {
128- b.scrollAmt += len(line.NewLines(bs.width-9-bs.nickColWidth)) + 1
129+ b.scrollAmt += len(line.NewLines(bs.tlInnerWidth())) + 1
130 }
131 }
132
133@@ -363,10 +291,6 @@ func (bs *BufferList) AddLines(title string, lines []Line) {
134 b.lines = append(lines[:limit], b.lines...)
135 }
136
137-func (bs *BufferList) SetStatus(status string) {
138- bs.status = status
139-}
140-
141 func (bs *BufferList) Current() (title string) {
142 return bs.list[bs.current].title
143 }
144@@ -384,12 +308,12 @@ func (bs *BufferList) ScrollUp() {
145 if b.isAtTop {
146 return
147 }
148- b.scrollAmt += bs.height / 2
149+ b.scrollAmt += bs.tlHeight / 2
150 }
151
152 func (bs *BufferList) ScrollDown() {
153 b := &bs.list[bs.current]
154- b.scrollAmt -= bs.height / 2
155+ b.scrollAmt -= bs.tlHeight / 2
156
157 if b.scrollAmt < 0 {
158 b.scrollAmt = 0
159@@ -415,79 +339,28 @@ func (bs *BufferList) idx(title string) int {
160 return -1
161 }
162
163-func (bs *BufferList) Draw(screen tcell.Screen) {
164- bs.list[bs.current].DrawLines(screen, 0, bs.width, bs.height-2, bs.nickColWidth)
165- bs.drawStatusBar(screen, bs.height-2)
166- bs.drawTitleList(screen, bs.height-1)
167-}
168-
169-func (bs *BufferList) drawStatusBar(screen tcell.Screen, y int) {
170- st := tcell.StyleDefault.Dim(true)
171-
172- for x := 0; x < bs.width; x++ {
173- screen.SetContent(x, y, 0x2500, nil, st)
174- }
175-
176- if bs.status == "" {
177- return
178- }
179-
180- x := 2
181- screen.SetContent(1, y, 0x2524, nil, st)
182- printString(screen, &x, y, st, bs.status)
183- screen.SetContent(x, y, 0x251c, nil, st)
184-}
185-
186-func (bs *BufferList) drawTitleList(screen tcell.Screen, y int) {
187- var widths []int
188- for _, b := range bs.list {
189- width := StringWidth(b.title)
190- if 0 < b.highlights {
191- width += int(math.Log10(float64(b.highlights))) + 3
192- }
193- widths = append(widths, width)
194- }
195-
196+func (bs *BufferList) DrawVerticalBufferList(screen tcell.Screen, x0, y0, width, height int) {
197+ width--
198 st := tcell.StyleDefault
199
200- for x := 0; x < bs.width; x++ {
201- screen.SetContent(x, y, ' ', nil, st)
202- }
203-
204- x := (bs.width - widths[bs.current]) / 2
205- printString(screen, &x, y, st.Underline(true), bs.list[bs.current].title)
206- x += 2
207-
208- i := (bs.current + 1) % len(bs.list)
209- for x < bs.width && i != bs.current {
210- b := &bs.list[i]
211- st = tcell.StyleDefault
212- if b.unread {
213- st = st.Bold(true)
214- }
215- printString(screen, &x, y, st, b.title)
216- if 0 < b.highlights {
217- st = st.Foreground(tcell.ColorRed).Reverse(true)
218+ for y := y0; y < y0+height; y++ {
219+ for x := x0; x < x0+width; x++ {
220 screen.SetContent(x, y, ' ', nil, st)
221- x++
222- printNumber(screen, &x, y, st, b.highlights)
223- screen.SetContent(x, y, ' ', nil, st)
224- x++
225 }
226- x += 2
227- i = (i + 1) % len(bs.list)
228+ screen.SetContent(x0+width, y, 0x2502, nil, st.Dim(true))
229 }
230
231- i = (bs.current - 1 + len(bs.list)) % len(bs.list)
232- x = (bs.width - widths[bs.current]) / 2
233- for 0 < x && i != bs.current {
234- x -= widths[i] + 2
235- b := &bs.list[i]
236+ for i, b := range bs.list {
237 st = tcell.StyleDefault
238+ x := x0
239+ y := y0 + i
240 if b.unread {
241 st = st.Bold(true)
242+ } else if y == bs.current {
243+ st = st.Underline(true)
244 }
245- printString(screen, &x, y, st, b.title)
246+ title := truncate(b.title, width, "\u2026")
247+ printString(screen, &x, y, st, title)
248 if 0 < b.highlights {
249 st = st.Foreground(tcell.ColorRed).Reverse(true)
250 screen.SetContent(x, y, ' ', nil, st)
251@@ -496,33 +369,74 @@ func (bs *BufferList) drawTitleList(screen tcell.Screen, y int) {
252 screen.SetContent(x, y, ' ', nil, st)
253 x++
254 }
255- x -= widths[i]
256- i = (i - 1 + len(bs.list)) % len(bs.list)
257+ y++
258 }
259 }
260
261-func printString(screen tcell.Screen, x *int, y int, st tcell.Style, s string) {
262- for _, r := range s {
263- screen.SetContent(*x, y, r, nil, st)
264- *x += runeWidth(r)
265+func (bs *BufferList) DrawTimeline(screen tcell.Screen, x0, y0, nickColWidth int) {
266+ st := tcell.StyleDefault
267+ for x := x0; x < x0+bs.tlWidth; x++ {
268+ for y := y0; y < y0+bs.tlHeight; y++ {
269+ screen.SetContent(x, y, ' ', nil, st)
270+ }
271 }
272-}
273
274-func printNumber(screen tcell.Screen, x *int, y int, st tcell.Style, n int) {
275- s := fmt.Sprintf("%d", n)
276- printString(screen, x, y, st, s)
277-}
278+ b := &bs.list[bs.current]
279+ yi := b.scrollAmt + y0 + bs.tlHeight
280+ for i := len(b.lines) - 1; 0 <= i; i-- {
281+ if yi < 0 {
282+ break
283+ }
284+
285+ x1 := x0 + 9 + nickColWidth
286+
287+ line := &b.lines[i]
288+ nls := line.NewLines(bs.tlInnerWidth())
289+ yi -= len(nls) + 1
290+ if y0+bs.tlHeight <= yi {
291+ continue
292+ }
293+
294+ if i == 0 || b.lines[i-1].At.Truncate(time.Minute) != line.At.Truncate(time.Minute) {
295+ printTime(screen, x0, yi, st.Bold(true), line.At.Local())
296+ }
297+
298+ identSt := st.Foreground(colorFromCode(line.HeadColor)).Reverse(line.Highlight)
299+ printIdent(screen, x0+7, yi, nickColWidth, identSt, line.Head)
300+
301+ x := x1
302+ y := yi
303+
304+ var sb StyleBuffer
305+ sb.Reset()
306+ for i, r := range line.Body {
307+ if 0 < len(nls) && i == nls[0] {
308+ x = x1
309+ y++
310+ nls = nls[1:]
311+ if bs.tlHeight < y {
312+ break
313+ }
314+ }
315+
316+ if y != yi && x == x1 && IsSplitRune(r) {
317+ continue
318+ }
319+
320+ if st, ok := sb.WriteRune(r); ok != 0 {
321+ if 1 < ok {
322+ screen.SetContent(x, y, ',', nil, st)
323+ x++
324+ }
325+ screen.SetContent(x, y, r, nil, st)
326+ x += runeWidth(r)
327+ }
328+ }
329+
330+ sb.Reset()
331+ }
332
333-func printTime(screen tcell.Screen, x int, y int, st tcell.Style, t time.Time) {
334- hr0 := rune(t.Hour()/10) + '0'
335- hr1 := rune(t.Hour()%10) + '0'
336- mn0 := rune(t.Minute()/10) + '0'
337- mn1 := rune(t.Minute()%10) + '0'
338- screen.SetContent(x+0, y, hr0, nil, st)
339- screen.SetContent(x+1, y, hr1, nil, st)
340- screen.SetContent(x+2, y, ':', nil, st)
341- screen.SetContent(x+3, y, mn0, nil, st)
342- screen.SetContent(x+4, y, mn1, nil, st)
343+ b.isAtTop = y0 <= yi
344 }
345
346 func IrcColorCode(code int) string {
+40,
-0
1@@ -0,0 +1,40 @@
2+package ui
3+
4+import (
5+ "fmt"
6+ "time"
7+
8+ "github.com/gdamore/tcell/v2"
9+)
10+
11+func printIdent(screen tcell.Screen, x, y, width int, st tcell.Style, s string) {
12+ s = truncate(s, width, "\u2026")
13+ x += width - StringWidth(s)
14+ screen.SetContent(x-1, y, ' ', nil, st)
15+ printString(screen, &x, y, st, s)
16+ screen.SetContent(x, y, ' ', nil, st)
17+}
18+
19+func printString(screen tcell.Screen, x *int, y int, st tcell.Style, s string) {
20+ for _, r := range s {
21+ screen.SetContent(*x, y, r, nil, st)
22+ *x += runeWidth(r)
23+ }
24+}
25+
26+func printNumber(screen tcell.Screen, x *int, y int, st tcell.Style, n int) {
27+ s := fmt.Sprintf("%d", n)
28+ printString(screen, x, y, st, s)
29+}
30+
31+func printTime(screen tcell.Screen, x int, y int, st tcell.Style, t time.Time) {
32+ hr0 := rune(t.Hour()/10) + '0'
33+ hr1 := rune(t.Hour()%10) + '0'
34+ mn0 := rune(t.Minute()/10) + '0'
35+ mn1 := rune(t.Minute()%10) + '0'
36+ screen.SetContent(x+0, y, hr0, nil, st)
37+ screen.SetContent(x+1, y, hr1, nil, st)
38+ screen.SetContent(x+2, y, ':', nil, st)
39+ screen.SetContent(x+3, y, mn0, nil, st)
40+ screen.SetContent(x+4, y, mn1, nil, st)
41+}
+5,
-5
1@@ -242,20 +242,20 @@ func (e *Editor) computeTextWidth() {
2 }
3 }
4
5-func (e *Editor) Draw(screen tcell.Screen, y int) {
6+func (e *Editor) Draw(screen tcell.Screen, x0, y int) {
7 st := tcell.StyleDefault
8
9- x := 0
10+ x := x0
11 i := e.offsetIdx
12
13- for i < len(e.text[e.lineIdx]) && x < e.width {
14+ for i < len(e.text[e.lineIdx]) && x < x0+e.width {
15 r := e.text[e.lineIdx][i]
16 screen.SetContent(x, y, r, nil, st)
17 x += runeWidth(r)
18 i++
19 }
20
21- for x < e.width {
22+ for x < x0+e.width {
23 screen.SetContent(x, y, ' ', nil, st)
24 x++
25 }
26@@ -265,7 +265,7 @@ func (e *Editor) Draw(screen tcell.Screen, y int) {
27 if e.cursorIdx+1 < len(e.textWidth) {
28 curEnd = e.textWidth[e.cursorIdx+1] - e.textWidth[e.offsetIdx]
29 }
30- for x := curStart; x < curEnd; x++ {
31+ for x := x0 + curStart; x < x0+curEnd; x++ {
32 screen.ShowCursor(x, y)
33 }
34 }
+19,
-0
1@@ -1,6 +1,8 @@
2 package ui
3
4 import (
5+ "hash/fnv"
6+
7 "github.com/gdamore/tcell/v2"
8 "github.com/mattn/go-runewidth"
9 )
10@@ -223,3 +225,20 @@ func colorFromCode(code int) (color tcell.Color) {
11 }
12 return
13 }
14+
15+// see <https://modern.ircdocs.horse/formatting.html>
16+var identColorBlacklist = []int{1, 8, 16, 27, 28, 88, 89, 90, 91}
17+
18+func IdentColor(s string) (code int) {
19+ h := fnv.New32()
20+ _, _ = h.Write([]byte(s))
21+
22+ code = int(h.Sum32()) % (99 - len(identColorBlacklist))
23+ for _, c := range identColorBlacklist {
24+ if c <= code {
25+ code++
26+ }
27+ }
28+
29+ return
30+}
M
ui/ui.go
+42,
-8
1@@ -18,8 +18,10 @@ type UI struct {
2 exit atomic.Value // bool
3 config Config
4
5- bs BufferList
6- e Editor
7+ bs BufferList
8+ e Editor
9+ prompt string
10+ status string
11 }
12
13 func New(config Config) (ui *UI, err error) {
14@@ -115,7 +117,11 @@ func (ui *UI) AddLines(buffer string, lines []Line) {
15 }
16
17 func (ui *UI) SetStatus(status string) {
18- ui.bs.SetStatus(status)
19+ ui.status = status
20+}
21+
22+func (ui *UI) SetPrompt(prompt string) {
23+ ui.prompt = prompt
24 }
25
26 func (ui *UI) InputIsCommand() bool {
27@@ -172,13 +178,41 @@ func (ui *UI) InputEnter() (content string) {
28
29 func (ui *UI) Resize() {
30 w, h := ui.screen.Size()
31- ui.e.Resize(w)
32- ui.bs.Resize(w, h-1)
33+ ui.e.Resize(w - 25 - ui.config.NickColWidth)
34+ ui.bs.ResizeTimeline(w-16, h-2, ui.config.NickColWidth)
35 }
36
37 func (ui *UI) Draw() {
38- _, h := ui.screen.Size()
39- ui.e.Draw(ui.screen, h-1)
40- ui.bs.Draw(ui.screen)
41+ w, h := ui.screen.Size()
42+
43+ ui.e.Draw(ui.screen, 25+ui.config.NickColWidth, h-1)
44+
45+ ui.bs.DrawTimeline(ui.screen, 16, 0, ui.config.NickColWidth)
46+ ui.bs.DrawVerticalBufferList(ui.screen, 0, 0, 16, h)
47+ ui.drawStatusBar(16, h-2, w-16)
48+
49+ for x := 16; x < 25+ui.config.NickColWidth; x++ {
50+ ui.screen.SetContent(x, h-1, ' ', nil, tcell.StyleDefault)
51+ }
52+ st := tcell.StyleDefault.Foreground(colorFromCode(IdentColor(ui.prompt)))
53+ printIdent(ui.screen, 23, h-1, ui.config.NickColWidth, st, ui.prompt)
54+
55 ui.screen.Show()
56 }
57+
58+func (ui *UI) drawStatusBar(x0, y, width int) {
59+ st := tcell.StyleDefault.Dim(true)
60+
61+ for x := x0; x < x0+width; x++ {
62+ ui.screen.SetContent(x, y, ' ', nil, st)
63+ }
64+
65+ if ui.status == "" {
66+ return
67+ }
68+
69+ x := x0 + 5 + ui.config.NickColWidth
70+ printString(ui.screen, &x, y, st, "--")
71+ x += 2
72+ printString(ui.screen, &x, y, st, ui.status)
73+}