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
+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))
M
ui/ui.go
+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 }