diff options
author | delthas <delthas@dille.cc> | 2022-03-29 19:23:36 +0200 |
---|---|---|
committer | delthas <delthas@dille.cc> | 2022-04-12 18:01:39 +0200 |
commit | 7a86dff763bff9dcf1ddb14a5db4bd590c13af4a (patch) | |
tree | 8b1ab4534297f959c3611bca376e36322211d49d | |
parent | Add a 15s keepalive to connections (diff) |
Implement SEARCH
Also refactor ui/ to support overlays, temporary anonmyous buffers.
See: https://github.com/emersion/soju/pull/39
-rw-r--r-- | app.go | 16 | ||||
-rw-r--r-- | commands.go | 22 | ||||
-rw-r--r-- | doc/senpai.1.scd | 4 | ||||
-rw-r--r-- | irc/events.go | 4 | ||||
-rw-r--r-- | irc/session.go | 30 | ||||
-rw-r--r-- | irc/tokens.go | 22 | ||||
-rw-r--r-- | ui/buffers.go | 96 | ||||
-rw-r--r-- | ui/ui.go | 12 |
8 files changed, 167 insertions, 39 deletions
@@ -565,6 +565,8 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) { if ok { app.typing() } + case tcell.KeyEscape: + app.win.CloseOverlay() case tcell.KeyCR, tcell.KeyLF: netID, buffer := app.win.CurrentBuffer() input := app.win.InputEnter() @@ -599,6 +601,9 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) { // requestHistory is a wrapper around irc.Session.RequestHistory to only request // history when needed. func (app *App) requestHistory() { + if app.win.HasOverlay() { + return + } netID, buffer := app.win.CurrentBuffer() s := app.sessions[netID] if s == nil { @@ -854,6 +859,17 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) { if !bounds.IsZero() { app.messageBounds[boundKey{netID, ev.Target}] = bounds } + case irc.SearchEvent: + app.win.OpenOverlay() + lines := make([]ui.Line, 0, len(ev.Messages)) + for _, m := range ev.Messages { + _, line, _ := app.formatMessage(s, m) + if line.IsZero() { + continue + } + lines = append(lines, line) + } + app.win.AddLines("", ui.Overlay, lines, nil) case irc.ReadEvent: app.win.SetRead(netID, ev.Target, ev.Timestamp) case irc.BouncerNetworkEvent: diff --git a/commands.go b/commands.go index 10ae19e..03e999a 100644 --- a/commands.go +++ b/commands.go @@ -167,6 +167,13 @@ func init() { Desc: "remove effect of a ban from the user", Handle: commandDoUnban, }, + "SEARCH": { + AllowHome: true, + MaxArgs: 1, + Usage: "<text>", + Desc: "searches messages in a target", + Handle: commandDoSearch, + }, } } @@ -579,6 +586,21 @@ func commandDoUnban(app *App, args []string) (err error) { return nil } +func commandDoSearch(app *App, args []string) (err error) { + if len(args) == 0 { + app.win.CloseOverlay() + return nil + } + text := args[0] + netID, channel := app.win.CurrentBuffer() + s := app.sessions[netID] + if s == nil { + return errOffline + } + s.Search(channel, text) + return nil +} + // implemented from https://golang.org/src/strings/strings.go?s=8055:8085#L310 func fieldsN(s string, n int) []string { s = strings.TrimSpace(s) diff --git a/doc/senpai.1.scd b/doc/senpai.1.scd index 76fee8a..8dc3ef1 100644 --- a/doc/senpai.1.scd +++ b/doc/senpai.1.scd @@ -174,6 +174,10 @@ _name_ is matched case-insensitively. It can be one of the following: *UNBAN* <nick> [channel] Allow _nick_ to enter _channel_ again (the current channel if not given). +*SEARCH* <text> + Search messages matching the given text, in the current channel or server. + This open a temporary list, which can be closed with the escape key. + # SEE ALSO *senpai*(5) diff --git a/irc/events.go b/irc/events.go index 7c33f70..7e92371 100644 --- a/irc/events.go +++ b/irc/events.go @@ -99,6 +99,10 @@ type ReadEvent struct { Timestamp time.Time } +type SearchEvent struct { + Messages []MessageEvent +} + type BouncerNetworkEvent struct { ID string Name string diff --git a/irc/session.go b/irc/session.go index ca2c2d9..c4c673a 100644 --- a/irc/session.go +++ b/irc/session.go @@ -61,6 +61,7 @@ var SupportedCapabilities = map[string]struct{}{ "draft/event-playback": {}, "soju.im/bouncer-networks": {}, "soju.im/read": {}, + "soju.im/search": {}, } // Values taken by the "@+typing=" client tag. TypingUnspec means the value or @@ -134,6 +135,8 @@ type Session struct { chReqs map[string]struct{} // set of targets for which history is currently requested. targetsBatchID string // ID of the channel history targets batch being processed. targetsBatch HistoryTargetsEvent // channel history targets batch being processed. + searchBatchID string // ID of the search targets batch being processed. + searchBatch SearchEvent // search batch being processed. monitors map[string]struct{} // set of users we want to monitor (and keep even if they are disconnected). pendingChannels map[string]time.Time // set of join requests stamps for channels. @@ -344,6 +347,18 @@ func (s *Session) ChangeMode(channel, flags string, args []string) { s.out <- NewMessage("MODE", args...) } +func (s *Session) Search(target, text string) { + if _, ok := s.enabledCaps["soju.im/search"]; !ok { + return + } + attrs := make(map[string]string) + attrs["text"] = text + if target != "" { + attrs["in"] = target + } + s.out <- NewMessage("SEARCH", formatTags(attrs)) +} + func splitChunks(s string, chunkLen int) (chunks []string) { if chunkLen <= 0 { return []string{s} @@ -578,6 +593,15 @@ func (s *Session) handleRegistered(msg Message) (Event, error) { return nil, nil } s.targetsBatch.Targets[target] = t + } else if id == s.searchBatchID { + ev, err := s.handleMessageRegistered(msg, true) + if err != nil { + return nil, err + } + if ev, ok := ev.(MessageEvent); ok { + s.searchBatch.Messages = append(s.searchBatch.Messages, ev) + return nil, nil + } } else if b, ok := s.chBatches[id]; ok { ev, err := s.handleMessageRegistered(msg, true) if err != nil { @@ -1195,6 +1219,9 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er case "draft/chathistory-targets": s.targetsBatchID = id s.targetsBatch = HistoryTargetsEvent{Targets: make(map[string]time.Time)} + case "soju.im/search": + s.searchBatchID = id + s.searchBatch = SearchEvent{} } } else { if b, ok := s.chBatches[id]; ok { @@ -1205,6 +1232,9 @@ func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, er s.targetsBatchID = "" delete(s.chReqs, "") return s.targetsBatch, nil + } else if s.searchBatchID == id { + s.searchBatchID = "" + return s.searchBatch, nil } } case "NICK": diff --git a/irc/tokens.go b/irc/tokens.go index aaffc08..7bcde99 100644 --- a/irc/tokens.go +++ b/irc/tokens.go @@ -150,6 +150,19 @@ func parseTags(s string) (tags map[string]string) { return } +func formatTags(tags map[string]string) string { + var sb strings.Builder + for k, v := range tags { + sb.WriteString(k) + if v != "" { + sb.WriteRune('=') + sb.WriteString(escapeTagValue(v)) + } + sb.WriteRune(';') + } + return sb.String() +} + var ( errEmptyMessage = errors.New("empty message") errIncompleteMessage = errors.New("message is incomplete") @@ -306,14 +319,7 @@ func (msg *Message) String() string { if msg.Tags != nil { sb.WriteRune('@') - for k, v := range msg.Tags { - sb.WriteString(k) - if v != "" { - sb.WriteRune('=') - sb.WriteString(escapeTagValue(v)) - } - sb.WriteRune(';') - } + sb.WriteString(formatTags(msg.Tags)) sb.WriteRune(' ') } diff --git a/ui/buffers.go b/ui/buffers.go index 6222fbe..5ed3414 100644 --- a/ui/buffers.go +++ b/ui/buffers.go @@ -9,6 +9,8 @@ import ( "github.com/gdamore/tcell/v2" ) +const Overlay = "/overlay" + func IsSplitRune(r rune) bool { return r == ' ' || r == '\t' } @@ -193,6 +195,7 @@ type buffer struct { type BufferList struct { list []buffer + overlay *buffer current int clicked int @@ -219,7 +222,24 @@ func (bs *BufferList) ResizeTimeline(tlInnerWidth, tlHeight int) { bs.tlHeight = tlHeight - 2 } +func (bs *BufferList) OpenOverlay() { + bs.overlay = &buffer{ + netID: "", + netName: "", + title: Overlay, + } +} + +func (bs *BufferList) CloseOverlay() { + bs.overlay = nil +} + +func (bs *BufferList) HasOverlay() bool { + return bs.overlay != nil +} + func (bs *BufferList) To(i int) bool { + bs.overlay = nil if i == bs.current { return false } @@ -240,15 +260,19 @@ func (bs *BufferList) ShowBufferNumbers(enabled bool) { } func (bs *BufferList) Next() { + bs.overlay = nil bs.current = (bs.current + 1) % len(bs.list) - bs.list[bs.current].highlights = 0 - bs.list[bs.current].unread = false + b := bs.cur() + b.highlights = 0 + b.unread = false } func (bs *BufferList) Previous() { + bs.overlay = nil bs.current = (bs.current - 1 + len(bs.list)) % len(bs.list) - bs.list[bs.current].highlights = 0 - bs.list[bs.current].unread = false + b := bs.cur() + b.highlights = 0 + b.unread = false } func (bs *BufferList) Add(netID, netName, title string) (i int, added bool) { @@ -295,7 +319,11 @@ func (bs *BufferList) Add(netID, netName, title string) (i int, added bool) { } func (bs *BufferList) Remove(netID, title string) bool { - idx := bs.idx(netID, title) + idx, b := bs.at(netID, title) + if b == bs.overlay { + bs.overlay = nil + return false + } if idx < 0 { return false } @@ -318,12 +346,12 @@ func (bs *BufferList) mergeLine(former *Line, addition Line) (keepLine bool) { } func (bs *BufferList) AddLine(netID, title string, notify NotifyType, line Line) { - idx := bs.idx(netID, title) - if idx < 0 { + _, b := bs.at(netID, title) + if b == nil { return } + current := bs.cur() - b := &bs.list[idx] n := len(b.lines) line.At = line.At.UTC() @@ -340,27 +368,25 @@ func (bs *BufferList) AddLine(netID, title string, notify NotifyType, line Line) } else { line.computeSplitPoints() b.lines = append(b.lines, line) - if idx == bs.current && 0 < b.scrollAmt { + if b == current && 0 < b.scrollAmt { b.scrollAmt += len(line.NewLines(bs.tlInnerWidth)) + 1 } } - if notify != NotifyNone && idx != bs.current { + if notify != NotifyNone && b != current { b.unread = true } - if notify == NotifyHighlight && idx != bs.current { + if notify == NotifyHighlight && b != current { b.highlights++ } } func (bs *BufferList) AddLines(netID, title string, before, after []Line) { - idx := bs.idx(netID, title) - if idx < 0 { + _, b := bs.at(netID, title) + if b == nil { return } - b := &bs.list[idx] - lines := make([]Line, 0, len(before)+len(b.lines)+len(after)) for _, buf := range []*[]Line{&before, &b.lines, &after} { for _, line := range *buf { @@ -382,20 +408,18 @@ func (bs *BufferList) AddLines(netID, title string, before, after []Line) { } func (bs *BufferList) SetTopic(netID, title string, topic string) { - idx := bs.idx(netID, title) - if idx < 0 { + _, b := bs.at(netID, title) + if b == nil { return } - b := &bs.list[idx] b.topic = topic } func (bs *BufferList) SetRead(netID, title string, timestamp time.Time) { - idx := bs.idx(netID, title) - if idx < 0 { + _, b := bs.at(netID, title) + if b == nil { return } - b := &bs.list[idx] if len(b.lines) > 0 && !b.lines[len(b.lines)-1].At.After(timestamp) { b.highlights = 0 b.unread = false @@ -406,7 +430,7 @@ func (bs *BufferList) SetRead(netID, title string, timestamp time.Time) { } func (bs *BufferList) UpdateRead() (netID, title string, timestamp time.Time) { - b := &bs.list[bs.current] + b := bs.cur() var line *Line y := 0 for i := len(b.lines) - 1; 0 <= i; i-- { @@ -429,7 +453,7 @@ func (bs *BufferList) Current() (netID, title string) { } func (bs *BufferList) ScrollUp(n int) { - b := &bs.list[bs.current] + b := bs.cur() if b.isAtTop { return } @@ -437,7 +461,7 @@ func (bs *BufferList) ScrollUp(n int) { } func (bs *BufferList) ScrollDown(n int) { - b := &bs.list[bs.current] + b := bs.cur() b.scrollAmt -= n if b.scrollAmt < 0 { @@ -446,7 +470,7 @@ func (bs *BufferList) ScrollDown(n int) { } func (bs *BufferList) ScrollUpHighlight() bool { - b := &bs.list[bs.current] + b := bs.cur() ymin := b.scrollAmt + bs.tlHeight y := 0 for i := len(b.lines) - 1; 0 <= i; i-- { @@ -461,7 +485,7 @@ func (bs *BufferList) ScrollUpHighlight() bool { } func (bs *BufferList) ScrollDownHighlight() bool { - b := &bs.list[bs.current] + b := bs.cur() yLastHighlight := 0 y := 0 for i := len(b.lines) - 1; 0 <= i && y < b.scrollAmt; i-- { @@ -476,18 +500,28 @@ func (bs *BufferList) ScrollDownHighlight() bool { } func (bs *BufferList) IsAtTop() bool { - b := &bs.list[bs.current] + b := bs.cur() return b.isAtTop } -func (bs *BufferList) idx(netID, title string) int { +func (bs *BufferList) at(netID, title string) (int, *buffer) { + if netID == "" && title == Overlay { + return -1, bs.overlay + } lTitle := strings.ToLower(title) for i, b := range bs.list { if b.netID == netID && strings.ToLower(b.title) == lTitle { - return i + return i, &bs.list[i] } } - return -1 + return -1, nil +} + +func (bs *BufferList) cur() *buffer { + if bs.overlay != nil { + return bs.overlay + } + return &bs.list[bs.current] } func (bs *BufferList) DrawVerticalBufferList(screen tcell.Screen, x0, y0, width, height int, offset *int) { @@ -599,7 +633,7 @@ func (bs *BufferList) DrawHorizontalBufferList(screen tcell.Screen, x0, y0, widt func (bs *BufferList) DrawTimeline(screen tcell.Screen, x0, y0, nickColWidth int) { clearArea(screen, x0, y0, bs.tlInnerWidth+nickColWidth+9, bs.tlHeight+2) - b := &bs.list[bs.current] + b := bs.cur() xTopic := x0 printString(screen, &xTopic, y0, Styled(b.topic, tcell.StyleDefault)) @@ -192,6 +192,18 @@ func (ui *UI) IsAtTop() bool { return ui.bs.IsAtTop() } +func (ui *UI) OpenOverlay() { + ui.bs.OpenOverlay() +} + +func (ui *UI) CloseOverlay() { + ui.bs.CloseOverlay() +} + +func (ui *UI) HasOverlay() bool { + return ui.bs.HasOverlay() +} + func (ui *UI) AddBuffer(netID, netName, title string) (i int, added bool) { return ui.bs.Add(netID, netName, title) } |