diff --git a/README.md b/README.md index 90c8a6d..1aa8671 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ A terminal ui for signal-cli, written in Go. * configurable contact [colors](config/README.md#configure-contact-colors) * can use [fzf](https://github.com/junegunn/fzf) to fuzzy-find files to attach * support for groups! (but not creating new groups) +* quickly filter messages by providing a regex pattern ### Dependencies @@ -61,6 +62,7 @@ If you are updating from a previous version, I recommend deleting your conversat * `K` - Previous Contact * `a` - Attach file (sent with next message) * `A` - Use fzf to attach a file +* `/` - Filter conversation by providing a pattern * `i` - Insert Mode * `CTRL+L` - Clear input field (also clears staged attachments) * `I` - Compose (opens $EDITOR and lets you make a fancy message) diff --git a/model/model.go b/model/model.go index d14dee2..cdde553 100644 --- a/model/model.go +++ b/model/model.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "sort" "time" @@ -252,6 +253,22 @@ func (c *Conversation) String() string { return out } +// Filter redners the conversation, but filters out any messages that don't have a regex match +// TODO: eliminate code duplication with String() +func (c *Conversation) Filter(pattern string) string { + if pattern == "" { + return c.String() + } + out := "" + for _, k := range c.MessageOrder { + s := c.Messages[k].String() + if found, err := regexp.MatchString(pattern, s); found == true || err != nil { + out += s + } + } + return out +} + // AddMessage appends a message to the conversation func (c *Conversation) AddMessage(message *Message) { c.addMessage(message) diff --git a/widgets/attach.go b/widgets/attach.go index e310178..09537e9 100644 --- a/widgets/attach.go +++ b/widgets/attach.go @@ -72,7 +72,7 @@ func NewAttachInput(parent *ChatWindow) *CommandInput { // FZFFile opens up FZF and fuzzy-searches for a file func FZFFile() (string, error) { - cmd := exec.Command("fzf") + cmd := exec.Command("fzf", "--prompt=attach: ", "--margin=10%,10%,10%,10%", "--border") cmd.Stdin = os.Stdin cmd.Stderr = os.Stderr buf := bytes.NewBuffer([]byte{}) diff --git a/widgets/chatwindow.go b/widgets/chatwindow.go index c233846..9366d62 100644 --- a/widgets/chatwindow.go +++ b/widgets/chatwindow.go @@ -188,28 +188,22 @@ func (c *ChatWindow) FocusMe() { c.app.SetFocus(c) } -// ShowContactSearch opens a contact search panel -func (c *ChatWindow) ShowContactSearch() { - log.Debug("SHOWING CONTACT SEARCH") - p := NewContactSearch(c) - c.searchPanel = p - c.SetRows(0, 3, p.maxHeight) +// ShowAttachInput opens a commandPanel to choose a file to attach +func (c *ChatWindow) ShowAttachInput() { + c.HideCommandInput() // only one at a time + log.Debug("SHOWING ATTACH INPUT") + p := NewAttachInput(c) + c.commandPanel = p + c.SetRows(0, 3, 1) c.AddItem(p, 2, 0, 1, 2, 0, 0, false) c.app.SetFocus(p) } -// HideSearch hides any current search panel -func (c *ChatWindow) HideSearch() { - log.Debug("HIDING SEARCH") - c.RemoveItem(c.searchPanel) - c.SetRows(0, 3) - c.FocusMe() -} - -// ShowAttachInput opens a commandPanel to choose a file to attach -func (c *ChatWindow) ShowAttachInput() { - log.Debug("SHOWING CONTACT SEARCH") - p := NewAttachInput(c) +// ShowFilterInput opens a commandPanel to filter the conversation +func (c *ChatWindow) ShowFilterInput() { + c.HideCommandInput() // only one at a time + log.Debug("SHOWING FILTER INPUT") + p := NewFilterInput(c) c.commandPanel = p c.SetRows(0, 3, 1) c.AddItem(p, 2, 0, 1, 2, 0, 0, false) @@ -220,7 +214,12 @@ func (c *ChatWindow) ShowAttachInput() { func (c *ChatWindow) HideCommandInput() { log.Debug("HIDING COMMAND INPUT") c.RemoveItem(c.commandPanel) + // TODO: this should happen automatically when i clear a FilterInput + // maybe command panel should be an interface with a "Close" method + // that does stuff like this + c.conversationPanel.Filter("") c.SetRows(0, 3) + c.update() c.FocusMe() } @@ -232,7 +231,7 @@ func (c *ChatWindow) ShowStatusBar() { // HideStatusBar stops showing the status bar func (c *ChatWindow) HideStatusBar() { - c.RemoveItem(c.statusBar) // do we actually need to do this? + c.RemoveItem(c.statusBar) c.SetRows(0, 3) } @@ -441,59 +440,6 @@ func (c *ChatWindow) update() { } } -type SearchPanel struct { - *tview.Grid - list *tview.TextView - input *SearchInput - parent *ChatWindow - maxHeight int -} - -func (p *SearchPanel) Close() { - p.parent.HideSearch() -} - -func NewContactSearch(parent *ChatWindow) *SearchPanel { - maxHeight := 10 - p := &SearchPanel{ - Grid: tview.NewGrid().SetRows(maxHeight-3, 1), - list: tview.NewTextView(), - parent: parent, - maxHeight: maxHeight, - } - //contactList := parent.siggo.Contacts().SortedByName() - p.input = NewSearchInput(p) - p.AddItem(p.list, 0, 0, 1, 1, 0, 0, false) - p.AddItem(p.input, 1, 0, 1, 1, 0, 0, true) - p.SetBorder(true) - p.SetTitle("search contacts...") - return p -} - -type SearchInput struct { - *tview.InputField - parent *SearchPanel -} - -func NewSearchInput(parent *SearchPanel) *SearchInput { - si := &SearchInput{ - InputField: tview.NewInputField(), - parent: parent, - } - si.SetLabel("> ") - si.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - // Setup keys - log.Debugf("Key Event : %v mods: %v rune: %v", event.Key(), event.Modifiers(), event.Rune()) - switch event.Key() { - case tcell.KeyESC: - si.parent.Close() - return nil - } - return event - }) - return si -} - type StatusBar struct { *tview.TextView parent *ChatWindow @@ -558,6 +504,9 @@ func NewChatWindow(siggo *model.Siggo, app *tview.Application) *ChatWindow { case 97: // a w.ShowAttachInput() return nil + case 47: // / + w.ShowFilterInput() + return nil case 65: // a w.FancyAttach() return nil @@ -589,10 +538,11 @@ func NewChatWindow(siggo *model.Siggo, app *tview.Application) *ChatWindow { case tcell.KeyESC: w.NormalMode() w.HideStatusBar() + w.HideCommandInput() return nil - case tcell.KeyCtrlT: - w.ShowContactSearch() - return nil + //case tcell.KeyCtrlT: + //w.ShowContactSearch() + //return nil case tcell.KeyCtrlN: w.NextUnreadMessage() return nil diff --git a/widgets/conversation.go b/widgets/conversation.go index 12eeaec..01656c0 100644 --- a/widgets/conversation.go +++ b/widgets/conversation.go @@ -3,19 +3,23 @@ package widgets import ( "fmt" - "github.com/derricw/siggo/model" "github.com/rivo/tview" + log "github.com/sirupsen/logrus" + + "github.com/derricw/siggo/model" ) type ConversationPanel struct { *tview.TextView hideTitle bool hidePhoneNumber bool + // only show messages matching a filter + filter string } func (p *ConversationPanel) Update(conv *model.Conversation) { p.Clear() - p.SetText(conv.String()) + p.SetText(conv.Filter(p.filter)) if !p.hideTitle { if !p.hidePhoneNumber { p.SetTitle(fmt.Sprintf("%s <%s>", conv.Contact.String(), conv.Contact.Number)) @@ -30,6 +34,11 @@ func (p *ConversationPanel) Clear() { p.SetText("") } +func (p *ConversationPanel) Filter(s string) { + log.Debugf("filtering converstaion: %s", s) + p.filter = s +} + func NewConversationPanel(siggo *model.Siggo) *ConversationPanel { c := &ConversationPanel{ TextView: tview.NewTextView(), diff --git a/widgets/filter.go b/widgets/filter.go new file mode 100644 index 0000000..5d65b32 --- /dev/null +++ b/widgets/filter.go @@ -0,0 +1,38 @@ +package widgets + +import ( + "github.com/gdamore/tcell" + "github.com/rivo/tview" + log "github.com/sirupsen/logrus" +) + +// NewFilterInput is a command input that lets you filter the current conversation +func NewFilterInput(parent *ChatWindow) *CommandInput { + ci := &CommandInput{ + InputField: tview.NewInputField(), + parent: parent, + } + ci.SetLabel("/") + //ci.SetText("") + ci.SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor) + ci.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + // Setup keys + log.Debugf("Key Event : %v mods: %v rune: %v", event.Key(), event.Modifiers(), event.Rune()) + switch event.Key() { + case tcell.KeyESC: + ci.parent.conversationPanel.Filter("") + ci.parent.HideCommandInput() + return nil + case tcell.KeyEnter: + s := ci.GetText() + if s != "" { + ci.parent.conversationPanel.Filter(s) + } + ci.parent.NormalMode() + ci.parent.update() + return nil + } + return event + }) + return ci +}