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
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:]
+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 {