summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordelthas <delthas@dille.cc>2022-03-29 19:23:36 +0200
committerdelthas <delthas@dille.cc>2022-04-12 18:01:39 +0200
commit7a86dff763bff9dcf1ddb14a5db4bd590c13af4a (patch)
tree8b1ab4534297f959c3611bca376e36322211d49d
parentAdd 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.go16
-rw-r--r--commands.go22
-rw-r--r--doc/senpai.1.scd4
-rw-r--r--irc/events.go4
-rw-r--r--irc/session.go30
-rw-r--r--irc/tokens.go22
-rw-r--r--ui/buffers.go96
-rw-r--r--ui/ui.go12
8 files changed, 167 insertions, 39 deletions
diff --git a/app.go b/app.go
index c0f0112..e2fa566 100644
--- a/app.go
+++ b/app.go
@@ -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))
diff --git a/ui/ui.go b/ui/ui.go
index 077f599..bbdc425 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -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)
}