commit 5f7dc12
sewn
·
2026-07-28 19:06:13 +0000 UTC
parent 466484d
Add selecting a single message in timeline
4 files changed,
+145,
-14
M
app.go
+8,
-0
1@@ -728,6 +728,7 @@ func (app *App) handleMouseEvent(ev vaxis.Mouse) {
2 })
3 }
4 } else {
5+ app.win.SelectMessageAt(y)
6 app.win.Click(x, y, ev)
7 }
8 }
9@@ -907,6 +908,11 @@ func (app *App) handleAction(action string, args ...string) {
10 }
11 case "close-overlay":
12 app.win.CloseOverlay()
13+ app.win.ClearMessageSelection()
14+ case "message-select-previous":
15+ app.win.SelectMessagePrevious()
16+ case "message-select-next":
17+ app.win.SelectMessageNext()
18 case "toggle-topic":
19 app.win.ToggleTopic()
20 case "toggle-channel-list":
21@@ -984,6 +990,8 @@ var defaultCommands = map[string][]string{
22 "Page_Down": {"scroll-down"},
23 "Control+n": {"buffer-next"},
24 "Control+p": {"buffer-previous"},
25+ "Control+Up": {"message-select-previous"},
26+ "Control+Down": {"message-select-next"},
27 "Alt+Right": {"buffer-next"},
28 "Shift+Right": {"buffer-next-unread"},
29 "Control+Right": {"cursor-right-word"},
+5,
-1
1@@ -261,7 +261,11 @@ shortcuts {
2 | auto-complete
3 : open/select the auto-completion dialog/item
4 | close-overlay
5-: close any open overlay buffer (e.g. search results)
6+: close any open overlay buffer (e.g. search results), and clear any message selection
7+| message-select-previous
8+: select the previous message in the timeline
9+| message-select-next
10+: select the next message in the timeline
11 | toggle-channel-list
12 : show/hide the vertical channel list
13 | toggle-member-list
+112,
-13
1@@ -243,6 +243,8 @@ type buffer struct {
2 scrollAmt int // offset in lines from the bottom
3 topicOffset int // offset in clusters that are skipped when rendering topic text
4 isAtTop bool
5+
6+ selected int
7 }
8
9 type BufferList struct {
10@@ -284,9 +286,10 @@ func (bs *BufferList) ResizeTimeline(tlInnerWidth, tlHeight, textWidth int) {
11
12 func (bs *BufferList) OpenOverlay() {
13 bs.overlay = &buffer{
14- netID: "",
15- netName: "",
16- title: Overlay,
17+ netID: "",
18+ netName: "",
19+ title: Overlay,
20+ selected: -1,
21 }
22 }
23
24@@ -398,9 +401,10 @@ func (bs *BufferList) Add(netID, netName, title string) (i int, added bool) {
25 }
26
27 b := buffer{
28- netID: netID,
29- netName: netName,
30- title: title,
31+ netID: netID,
32+ netName: netName,
33+ title: title,
34+ selected: -1,
35 }
36 if i == len(bs.list) {
37 bs.list = append(bs.list, b)
38@@ -545,6 +549,10 @@ func (bs *BufferList) AddLine(netID, title string, line Line) {
39 }
40 }
41
42+ if b.selected >= len(b.lines) {
43+ b.selected = -1
44+ }
45+
46 if line.Notify != NotifyNone && (!bs.focused || b != current) {
47 b.unread = true
48 }
49@@ -647,6 +655,9 @@ func (bs *BufferList) AddLines(netID, title string, before, after []Line) {
50 }
51 }
52 b.lines = lines
53+ if b.selected >= len(b.lines) {
54+ b.selected = -1
55+ }
56 if b == bs.cur() && b.unreadSkip == optionalUnset && len(b.lines) > 0 {
57 if b.unreadRuler.IsZero() || !b.lines[len(b.lines)-1].At.After(b.unreadRuler) {
58 b.unreadSkip = optionalTrue
59@@ -756,7 +767,7 @@ func (bs *BufferList) SetRead(netID, title string, timestamp time.Time) {
60 func (bs *BufferList) UpdateRead() (netID, title string, timestamp time.Time) {
61 b := bs.cur()
62 var l *Line
63- bs.forEachLine(b, func(line *Line, y int) bool {
64+ bs.forEachLine(b, func(line *Line, i, y int) bool {
65 l = line
66 if y >= b.scrollAmt && line.Readable {
67 return true
68@@ -803,7 +814,7 @@ func (bs *BufferList) ScrollDown(n int) {
69 func (bs *BufferList) ScrollUpHighlight() bool {
70 b := bs.cur()
71 ymin := b.scrollAmt + bs.tlHeight
72- return bs.forEachLine(b, func(line *Line, y int) bool {
73+ return bs.forEachLine(b, func(line *Line, i, y int) bool {
74 if ymin <= y && line.Highlight {
75 b.scrollAmt = y - bs.tlHeight + 1
76 return true
77@@ -815,7 +826,7 @@ func (bs *BufferList) ScrollUpHighlight() bool {
78 func (bs *BufferList) ScrollDownHighlight() bool {
79 b := bs.cur()
80 yLastHighlight := 0
81- bs.forEachLine(b, func(line *Line, y int) bool {
82+ bs.forEachLine(b, func(line *Line, i, y int) bool {
83 if y >= b.scrollAmt {
84 return true
85 }
86@@ -828,6 +839,83 @@ func (bs *BufferList) ScrollDownHighlight() bool {
87 return b.scrollAmt != 0
88 }
89
90+func (bs *BufferList) SelectPrevious() {
91+ b := bs.cur()
92+ if len(b.lines) == 0 {
93+ return
94+ }
95+ if b.selected < 0 {
96+ b.selected = len(b.lines) - 1
97+ } else if b.selected > 0 {
98+ b.selected--
99+ }
100+ bs.scrollToSelected()
101+}
102+
103+func (bs *BufferList) SelectNext() {
104+ b := bs.cur()
105+ if b.selected < 0 {
106+ return
107+ }
108+ b.selected++
109+ if b.selected >= len(b.lines) {
110+ b.selected = -1
111+ }
112+ bs.scrollToSelected()
113+}
114+
115+func (bs *BufferList) scrollToSelected() {
116+ b := bs.cur()
117+ if b.selected < 0 {
118+ return
119+ }
120+ bs.forEachLine(b, func(line *Line, i, y int) bool {
121+ if i != b.selected {
122+ return false
123+ }
124+ rows := line.Rows(bs.ui.vx, bs.textWidth)
125+ top := y + rows - 1
126+ if top >= b.scrollAmt+bs.tlHeight {
127+ b.scrollAmt = top - bs.tlHeight + 1
128+ } else if y < b.scrollAmt {
129+ b.scrollAmt = y
130+ }
131+ return true
132+ })
133+}
134+
135+func (bs *BufferList) ClearSelection() {
136+ bs.cur().selected = -1
137+}
138+
139+func (bs *BufferList) Selected() *Line {
140+ b := bs.cur()
141+ if b.selected < 0 || b.selected >= len(b.lines) {
142+ return nil
143+ }
144+ return &b.lines[b.selected]
145+}
146+
147+func (bs *BufferList) SelectAt(y0, screenY int) {
148+ b := bs.cur()
149+ if !bs.ui.hideTopic {
150+ y0 += 2
151+ }
152+ target := b.scrollAmt + y0 + bs.tlHeight - screenY - 1
153+ idx := -1
154+ bs.forEachLine(b, func(line *Line, i, y int) bool {
155+ rows := line.Rows(bs.ui.vx, bs.textWidth)
156+ if target >= y && target < y+rows {
157+ idx = i
158+ return true
159+ }
160+ return false
161+ })
162+ if idx >= 0 {
163+ bs.cur().selected = idx
164+ }
165+}
166+
167 func (bs *BufferList) ScrollTopicLeft(n int) {
168 b := bs.cur()
169 b.topicOffset -= n
170@@ -878,7 +966,7 @@ func (bs *BufferList) cur() *buffer {
171 return &bs.list[bs.current]
172 }
173
174-func (bs *BufferList) forEachLine(b *buffer, f func(line *Line, y int) bool) bool {
175+func (bs *BufferList) forEachLine(b *buffer, f func(line *Line, i, y int) bool) bool {
176 rulerDrawn := b.unreadSkip != optionalFalse || b.unreadRuler.IsZero() || b.title == ""
177 y := 0
178 for i := len(b.lines) - 1; 0 <= i; i-- {
179@@ -887,7 +975,7 @@ func (bs *BufferList) forEachLine(b *buffer, f func(line *Line, y int) bool) boo
180 rulerDrawn = true
181 y++
182 }
183- if f(line, y) {
184+ if f(line, i, y) {
185 return true
186 }
187 y += line.Rows(bs.ui.vx, bs.textWidth)
188@@ -1274,6 +1362,7 @@ func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
189
190 line := &b.lines[i]
191 nls := line.NewLines(bs.ui.vx, bs.textWidth)
192+ selected := i == b.selected
193
194 if !rulerDrawn {
195 isRead := !line.At.After(b.unreadRuler)
196@@ -1313,6 +1402,9 @@ func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
197 st := vaxis.Style{
198 Attribute: vaxis.AttrBold,
199 }
200+ if selected {
201+ st.Attribute |= vaxis.AttrReverse
202+ }
203 // as a special case, always draw the first visible message date, even if it is a continuation line
204 yd := cY
205 if yd < y0 {
206@@ -1326,10 +1418,13 @@ func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
207 yp := cY - b.lines[i-1].Rows(bs.ui.vx, bs.textWidth)
208 showTime = i == 0 || bs.shouldShowDate(b, i-1, yp, y0)
209 }
210- if showTime {
211+ if showTime || selected {
212 st := vaxis.Style{
213 Foreground: bs.ui.config.Colors.Gray,
214 }
215+ if selected {
216+ st.Attribute |= vaxis.AttrReverse
217+ }
218 printTime(vx, x0, cY, st, line.At.Local())
219 }
220 }
221@@ -1402,7 +1497,11 @@ func (bs *BufferList) DrawTimeline(ui *UI, x0, y0, nickColWidth int) {
222
223 xb := x
224 if y >= y0 {
225- dx, di := printCluster(vx, x, y, -1, l, style)
226+ drawStyle := style
227+ if selected {
228+ drawStyle.Attribute |= vaxis.AttrReverse
229+ }
230+ dx, di := printCluster(vx, x, y, -1, l, drawStyle)
231 x += dx
232 lbi += len(string(l[:di]))
233 l = l[di:]
M
ui/ui.go
+20,
-0
1@@ -334,6 +334,26 @@ func (ui *UI) ClickMember(i int) {
2 ui.memberClicked = i
3 }
4
5+func (ui *UI) SelectMessagePrevious() {
6+ ui.bs.SelectPrevious()
7+}
8+
9+func (ui *UI) SelectMessageNext() {
10+ ui.bs.SelectNext()
11+}
12+
13+func (ui *UI) ClearMessageSelection() {
14+ ui.bs.ClearSelection()
15+}
16+
17+func (ui *UI) SelectMessageAt(y int) {
18+ ui.bs.SelectAt(0, y)
19+}
20+
21+func (ui *UI) SelectedMessage() *Line {
22+ return ui.bs.Selected()
23+}
24+
25 func (ui *UI) Click(x, y int, event vaxis.Mouse) {
26 for _, ev := range ui.clickEvents {
27 if x >= ev.xb && x < ev.xe && y == ev.y {