commit 7a86dff

delthas  ·  2022-03-29 17:23:36 +0000 UTC
parent 25ae442
Implement SEARCH

Also refactor ui/ to support overlays, temporary anonmyous buffers.

See: https://github.com/emersion/soju/pull/39
8 files changed,  +167, -39
M app.go
M app.go
+16, -0
 1@@ -565,6 +565,8 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) {
 2 		if ok {
 3 			app.typing()
 4 		}
 5+	case tcell.KeyEscape:
 6+		app.win.CloseOverlay()
 7 	case tcell.KeyCR, tcell.KeyLF:
 8 		netID, buffer := app.win.CurrentBuffer()
 9 		input := app.win.InputEnter()
10@@ -599,6 +601,9 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) {
11 // requestHistory is a wrapper around irc.Session.RequestHistory to only request
12 // history when needed.
13 func (app *App) requestHistory() {
14+	if app.win.HasOverlay() {
15+		return
16+	}
17 	netID, buffer := app.win.CurrentBuffer()
18 	s := app.sessions[netID]
19 	if s == nil {
20@@ -854,6 +859,17 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
21 		if !bounds.IsZero() {
22 			app.messageBounds[boundKey{netID, ev.Target}] = bounds
23 		}
24+	case irc.SearchEvent:
25+		app.win.OpenOverlay()
26+		lines := make([]ui.Line, 0, len(ev.Messages))
27+		for _, m := range ev.Messages {
28+			_, line, _ := app.formatMessage(s, m)
29+			if line.IsZero() {
30+				continue
31+			}
32+			lines = append(lines, line)
33+		}
34+		app.win.AddLines("", ui.Overlay, lines, nil)
35 	case irc.ReadEvent:
36 		app.win.SetRead(netID, ev.Target, ev.Timestamp)
37 	case irc.BouncerNetworkEvent:
+22, -0
 1@@ -167,6 +167,13 @@ func init() {
 2 			Desc:      "remove effect of a ban from the user",
 3 			Handle:    commandDoUnban,
 4 		},
 5+		"SEARCH": {
 6+			AllowHome: true,
 7+			MaxArgs:   1,
 8+			Usage:     "<text>",
 9+			Desc:      "searches messages in a target",
10+			Handle:    commandDoSearch,
11+		},
12 	}
13 }
14 
15@@ -579,6 +586,21 @@ func commandDoUnban(app *App, args []string) (err error) {
16 	return nil
17 }
18 
19+func commandDoSearch(app *App, args []string) (err error) {
20+	if len(args) == 0 {
21+		app.win.CloseOverlay()
22+		return nil
23+	}
24+	text := args[0]
25+	netID, channel := app.win.CurrentBuffer()
26+	s := app.sessions[netID]
27+	if s == nil {
28+		return errOffline
29+	}
30+	s.Search(channel, text)
31+	return nil
32+}
33+
34 // implemented from https://golang.org/src/strings/strings.go?s=8055:8085#L310
35 func fieldsN(s string, n int) []string {
36 	s = strings.TrimSpace(s)
+4, -0
 1@@ -174,6 +174,10 @@ _name_ is matched case-insensitively.  It can be one of the following:
 2 *UNBAN* <nick> [channel]
 3 	Allow _nick_ to enter _channel_ again (the current channel if not given).
 4 
 5+*SEARCH* <text>
 6+	Search messages matching the given text, in the current channel or server.
 7+	This open a temporary list, which can be closed with the escape key.
 8+
 9 # SEE ALSO
10 
11 *senpai*(5)
+4, -0
 1@@ -99,6 +99,10 @@ type ReadEvent struct {
 2 	Timestamp time.Time
 3 }
 4 
 5+type SearchEvent struct {
 6+	Messages []MessageEvent
 7+}
 8+
 9 type BouncerNetworkEvent struct {
10 	ID   string
11 	Name string
+30, -0
 1@@ -61,6 +61,7 @@ var SupportedCapabilities = map[string]struct{}{
 2 	"draft/event-playback":     {},
 3 	"soju.im/bouncer-networks": {},
 4 	"soju.im/read":             {},
 5+	"soju.im/search":           {},
 6 }
 7 
 8 // Values taken by the "@+typing=" client tag.  TypingUnspec means the value or
 9@@ -134,6 +135,8 @@ type Session struct {
10 	chReqs         map[string]struct{}     // set of targets for which history is currently requested.
11 	targetsBatchID string                  // ID of the channel history targets batch being processed.
12 	targetsBatch   HistoryTargetsEvent     // channel history targets batch being processed.
13+	searchBatchID  string                  // ID of the search targets batch being processed.
14+	searchBatch    SearchEvent             // search batch being processed.
15 	monitors       map[string]struct{}     // set of users we want to monitor (and keep even if they are disconnected).
16 
17 	pendingChannels map[string]time.Time // set of join requests stamps for channels.
18@@ -344,6 +347,18 @@ func (s *Session) ChangeMode(channel, flags string, args []string) {
19 	s.out <- NewMessage("MODE", args...)
20 }
21 
22+func (s *Session) Search(target, text string) {
23+	if _, ok := s.enabledCaps["soju.im/search"]; !ok {
24+		return
25+	}
26+	attrs := make(map[string]string)
27+	attrs["text"] = text
28+	if target != "" {
29+		attrs["in"] = target
30+	}
31+	s.out <- NewMessage("SEARCH", formatTags(attrs))
32+}
33+
34 func splitChunks(s string, chunkLen int) (chunks []string) {
35 	if chunkLen <= 0 {
36 		return []string{s}
37@@ -578,6 +593,15 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
38 				return nil, nil
39 			}
40 			s.targetsBatch.Targets[target] = t
41+		} else if id == s.searchBatchID {
42+			ev, err := s.handleMessageRegistered(msg, true)
43+			if err != nil {
44+				return nil, err
45+			}
46+			if ev, ok := ev.(MessageEvent); ok {
47+				s.searchBatch.Messages = append(s.searchBatch.Messages, ev)
48+				return nil, nil
49+			}
50 		} else if b, ok := s.chBatches[id]; ok {
51 			ev, err := s.handleMessageRegistered(msg, true)
52 			if err != nil {
53@@ -1195,6 +1219,9 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
54 			case "draft/chathistory-targets":
55 				s.targetsBatchID = id
56 				s.targetsBatch = HistoryTargetsEvent{Targets: make(map[string]time.Time)}
57+			case "soju.im/search":
58+				s.searchBatchID = id
59+				s.searchBatch = SearchEvent{}
60 			}
61 		} else {
62 			if b, ok := s.chBatches[id]; ok {
63@@ -1205,6 +1232,9 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er
64 				s.targetsBatchID = ""
65 				delete(s.chReqs, "")
66 				return s.targetsBatch, nil
67+			} else if s.searchBatchID == id {
68+				s.searchBatchID = ""
69+				return s.searchBatch, nil
70 			}
71 		}
72 	case "NICK":
+14, -8
 1@@ -150,6 +150,19 @@ func parseTags(s string) (tags map[string]string) {
 2 	return
 3 }
 4 
 5+func formatTags(tags map[string]string) string {
 6+	var sb strings.Builder
 7+	for k, v := range tags {
 8+		sb.WriteString(k)
 9+		if v != "" {
10+			sb.WriteRune('=')
11+			sb.WriteString(escapeTagValue(v))
12+		}
13+		sb.WriteRune(';')
14+	}
15+	return sb.String()
16+}
17+
18 var (
19 	errEmptyMessage      = errors.New("empty message")
20 	errIncompleteMessage = errors.New("message is incomplete")
21@@ -306,14 +319,7 @@ func (msg *Message) String() string {
22 
23 	if msg.Tags != nil {
24 		sb.WriteRune('@')
25-		for k, v := range msg.Tags {
26-			sb.WriteString(k)
27-			if v != "" {
28-				sb.WriteRune('=')
29-				sb.WriteString(escapeTagValue(v))
30-			}
31-			sb.WriteRune(';')
32-		}
33+		sb.WriteString(formatTags(msg.Tags))
34 		sb.WriteRune(' ')
35 	}
36 
+65, -31
  1@@ -9,6 +9,8 @@ import (
  2 	"github.com/gdamore/tcell/v2"
  3 )
  4 
  5+const Overlay = "/overlay"
  6+
  7 func IsSplitRune(r rune) bool {
  8 	return r == ' ' || r == '\t'
  9 }
 10@@ -193,6 +195,7 @@ type buffer struct {
 11 
 12 type BufferList struct {
 13 	list    []buffer
 14+	overlay *buffer
 15 	current int
 16 	clicked int
 17 
 18@@ -219,7 +222,24 @@ func (bs *BufferList) ResizeTimeline(tlInnerWidth, tlHeight int) {
 19 	bs.tlHeight = tlHeight - 2
 20 }
 21 
 22+func (bs *BufferList) OpenOverlay() {
 23+	bs.overlay = &buffer{
 24+		netID:   "",
 25+		netName: "",
 26+		title:   Overlay,
 27+	}
 28+}
 29+
 30+func (bs *BufferList) CloseOverlay() {
 31+	bs.overlay = nil
 32+}
 33+
 34+func (bs *BufferList) HasOverlay() bool {
 35+	return bs.overlay != nil
 36+}
 37+
 38 func (bs *BufferList) To(i int) bool {
 39+	bs.overlay = nil
 40 	if i == bs.current {
 41 		return false
 42 	}
 43@@ -240,15 +260,19 @@ func (bs *BufferList) ShowBufferNumbers(enabled bool) {
 44 }
 45 
 46 func (bs *BufferList) Next() {
 47+	bs.overlay = nil
 48 	bs.current = (bs.current + 1) % len(bs.list)
 49-	bs.list[bs.current].highlights = 0
 50-	bs.list[bs.current].unread = false
 51+	b := bs.cur()
 52+	b.highlights = 0
 53+	b.unread = false
 54 }
 55 
 56 func (bs *BufferList) Previous() {
 57+	bs.overlay = nil
 58 	bs.current = (bs.current - 1 + len(bs.list)) % len(bs.list)
 59-	bs.list[bs.current].highlights = 0
 60-	bs.list[bs.current].unread = false
 61+	b := bs.cur()
 62+	b.highlights = 0
 63+	b.unread = false
 64 }
 65 
 66 func (bs *BufferList) Add(netID, netName, title string) (i int, added bool) {
 67@@ -295,7 +319,11 @@ func (bs *BufferList) Add(netID, netName, title string) (i int, added bool) {
 68 }
 69 
 70 func (bs *BufferList) Remove(netID, title string) bool {
 71-	idx := bs.idx(netID, title)
 72+	idx, b := bs.at(netID, title)
 73+	if b == bs.overlay {
 74+		bs.overlay = nil
 75+		return false
 76+	}
 77 	if idx < 0 {
 78 		return false
 79 	}
 80@@ -318,12 +346,12 @@ func (bs *BufferList) mergeLine(former *Line, addition Line) (keepLine bool) {
 81 }
 82 
 83 func (bs *BufferList) AddLine(netID, title string, notify NotifyType, line Line) {
 84-	idx := bs.idx(netID, title)
 85-	if idx < 0 {
 86+	_, b := bs.at(netID, title)
 87+	if b == nil {
 88 		return
 89 	}
 90+	current := bs.cur()
 91 
 92-	b := &bs.list[idx]
 93 	n := len(b.lines)
 94 	line.At = line.At.UTC()
 95 
 96@@ -340,27 +368,25 @@ func (bs *BufferList) AddLine(netID, title string, notify NotifyType, line Line)
 97 	} else {
 98 		line.computeSplitPoints()
 99 		b.lines = append(b.lines, line)
100-		if idx == bs.current && 0 < b.scrollAmt {
101+		if b == current && 0 < b.scrollAmt {
102 			b.scrollAmt += len(line.NewLines(bs.tlInnerWidth)) + 1
103 		}
104 	}
105 
106-	if notify != NotifyNone && idx != bs.current {
107+	if notify != NotifyNone && b != current {
108 		b.unread = true
109 	}
110-	if notify == NotifyHighlight && idx != bs.current {
111+	if notify == NotifyHighlight && b != current {
112 		b.highlights++
113 	}
114 }
115 
116 func (bs *BufferList) AddLines(netID, title string, before, after []Line) {
117-	idx := bs.idx(netID, title)
118-	if idx < 0 {
119+	_, b := bs.at(netID, title)
120+	if b == nil {
121 		return
122 	}
123 
124-	b := &bs.list[idx]
125-
126 	lines := make([]Line, 0, len(before)+len(b.lines)+len(after))
127 	for _, buf := range []*[]Line{&before, &b.lines, &after} {
128 		for _, line := range *buf {
129@@ -382,20 +408,18 @@ func (bs *BufferList) AddLines(netID, title string, before, after []Line) {
130 }
131 
132 func (bs *BufferList) SetTopic(netID, title string, topic string) {
133-	idx := bs.idx(netID, title)
134-	if idx < 0 {
135+	_, b := bs.at(netID, title)
136+	if b == nil {
137 		return
138 	}
139-	b := &bs.list[idx]
140 	b.topic = topic
141 }
142 
143 func (bs *BufferList) SetRead(netID, title string, timestamp time.Time) {
144-	idx := bs.idx(netID, title)
145-	if idx < 0 {
146+	_, b := bs.at(netID, title)
147+	if b == nil {
148 		return
149 	}
150-	b := &bs.list[idx]
151 	if len(b.lines) > 0 && !b.lines[len(b.lines)-1].At.After(timestamp) {
152 		b.highlights = 0
153 		b.unread = false
154@@ -406,7 +430,7 @@ func (bs *BufferList) SetRead(netID, title string, timestamp time.Time) {
155 }
156 
157 func (bs *BufferList) UpdateRead() (netID, title string, timestamp time.Time) {
158-	b := &bs.list[bs.current]
159+	b := bs.cur()
160 	var line *Line
161 	y := 0
162 	for i := len(b.lines) - 1; 0 <= i; i-- {
163@@ -429,7 +453,7 @@ func (bs *BufferList) Current() (netID, title string) {
164 }
165 
166 func (bs *BufferList) ScrollUp(n int) {
167-	b := &bs.list[bs.current]
168+	b := bs.cur()
169 	if b.isAtTop {
170 		return
171 	}
172@@ -437,7 +461,7 @@ func (bs *BufferList) ScrollUp(n int) {
173 }
174 
175 func (bs *BufferList) ScrollDown(n int) {
176-	b := &bs.list[bs.current]
177+	b := bs.cur()
178 	b.scrollAmt -= n
179 
180 	if b.scrollAmt < 0 {
181@@ -446,7 +470,7 @@ func (bs *BufferList) ScrollDown(n int) {
182 }
183 
184 func (bs *BufferList) ScrollUpHighlight() bool {
185-	b := &bs.list[bs.current]
186+	b := bs.cur()
187 	ymin := b.scrollAmt + bs.tlHeight
188 	y := 0
189 	for i := len(b.lines) - 1; 0 <= i; i-- {
190@@ -461,7 +485,7 @@ func (bs *BufferList) ScrollUpHighlight() bool {
191 }
192 
193 func (bs *BufferList) ScrollDownHighlight() bool {
194-	b := &bs.list[bs.current]
195+	b := bs.cur()
196 	yLastHighlight := 0
197 	y := 0
198 	for i := len(b.lines) - 1; 0 <= i && y < b.scrollAmt; i-- {
199@@ -476,18 +500,28 @@ func (bs *BufferList) ScrollDownHighlight() bool {
200 }
201 
202 func (bs *BufferList) IsAtTop() bool {
203-	b := &bs.list[bs.current]
204+	b := bs.cur()
205 	return b.isAtTop
206 }
207 
208-func (bs *BufferList) idx(netID, title string) int {
209+func (bs *BufferList) at(netID, title string) (int, *buffer) {
210+	if netID == "" && title == Overlay {
211+		return -1, bs.overlay
212+	}
213 	lTitle := strings.ToLower(title)
214 	for i, b := range bs.list {
215 		if b.netID == netID && strings.ToLower(b.title) == lTitle {
216-			return i
217+			return i, &bs.list[i]
218 		}
219 	}
220-	return -1
221+	return -1, nil
222+}
223+
224+func (bs *BufferList) cur() *buffer {
225+	if bs.overlay != nil {
226+		return bs.overlay
227+	}
228+	return &bs.list[bs.current]
229 }
230 
231 func (bs *BufferList) DrawVerticalBufferList(screen tcell.Screen, x0, y0, width, height int, offset *int) {
232@@ -599,7 +633,7 @@ func (bs *BufferList) DrawHorizontalBufferList(screen tcell.Screen, x0, y0, widt
233 func (bs *BufferList) DrawTimeline(screen tcell.Screen, x0, y0, nickColWidth int) {
234 	clearArea(screen, x0, y0, bs.tlInnerWidth+nickColWidth+9, bs.tlHeight+2)
235 
236-	b := &bs.list[bs.current]
237+	b := bs.cur()
238 
239 	xTopic := x0
240 	printString(screen, &xTopic, y0, Styled(b.topic, tcell.StyleDefault))
+12, -0
 1@@ -192,6 +192,18 @@ func (ui *UI) IsAtTop() bool {
 2 	return ui.bs.IsAtTop()
 3 }
 4 
 5+func (ui *UI) OpenOverlay() {
 6+	ui.bs.OpenOverlay()
 7+}
 8+
 9+func (ui *UI) CloseOverlay() {
10+	ui.bs.CloseOverlay()
11+}
12+
13+func (ui *UI) HasOverlay() bool {
14+	return ui.bs.HasOverlay()
15+}
16+
17 func (ui *UI) AddBuffer(netID, netName, title string) (i int, added bool) {
18 	return ui.bs.Add(netID, netName, title)
19 }