diff --git a/README.md b/README.md index e984d3a..869fb10 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,16 @@ Getting started "C-8": "backspace", "": "delete", "": "space" + }, + "search": { + "": "cursor-left", + "": "cursor-right", + "": "clear-input", + "": "clear-input", + "": "backspace", + "C-8": "backspace", + "": "delete", + "": "space" } } } @@ -79,6 +89,7 @@ Default Key Mapping | mode | key | action | |---------|-----------|----------------------------| | command | `i` | insert mode | +| command | `/` | search mode | | command | `k` | move channel cursor up | | command | `j` | move channel cursor down | | command | `g` | move channel cursor top | @@ -95,3 +106,5 @@ Default Key Mapping | insert | `right` | move input cursor right | | insert | `enter` | send message | | insert | `esc` | command mode | +| search | `esc` | command mode | +| search | `enter` | command mode | diff --git a/components/channels.go b/components/channels.go index 3897b09..b311f83 100644 --- a/components/channels.go +++ b/components/channels.go @@ -195,6 +195,7 @@ func (c *Channels) MoveCursorBottom() { // ScrollUp enables us to scroll through the channel list when it overflows func (c *Channels) ScrollUp() { + // Is cursor at the top of the channel view? if c.CursorPosition == c.List.InnerBounds().Min.Y { if c.Offset > 0 { c.Offset-- @@ -206,6 +207,7 @@ func (c *Channels) ScrollUp() { // ScrollDown enables us to scroll through the channel list when it overflows func (c *Channels) ScrollDown() { + // Is the cursor at the bottom of the channel view? if c.CursorPosition == c.List.InnerBounds().Max.Y-1 { if c.Offset < len(c.List.Items)-1 { c.Offset++ @@ -215,6 +217,45 @@ func (c *Channels) ScrollDown() { } } +// Search will search through the channels to find a channel, +// when a match has been found the selected channel will then +// be the channel that has been found +func (c *Channels) Search(term string) { + for i, item := range c.List.Items { + if strings.Contains(item, term) { + + // The new position + newPos := i + + // Is the new position in range of the current view? + minRange := c.Offset + maxRange := c.Offset + c.List.InnerBounds().Max.Y - 1 + + if newPos < minRange { + // newPos is above, we need to scroll up. + c.SetSelectedChannel(i) + + // How much do we need to scroll to get it into range? + c.Offset = c.Offset - (minRange - newPos) + } else if newPos > maxRange { + // newPos is below, we need to scroll down + c.SetSelectedChannel(i) + + // How much do we need to scroll to get it into range? + c.Offset = c.Offset + (newPos - maxRange) + 1 + } else { + // newPos is inside range + c.SetSelectedChannel(i) + } + + // Set cursor to correct position + c.CursorPosition = (newPos - minRange) + 1 + + break + } + } +} + // SetNotification will be called when a new message arrives and will // render an notification icon in front of the channel name func (c *Channels) SetNotification(svc *service.SlackService, channelID string) { diff --git a/config/config.go b/config/config.go index dff7d9a..b446b48 100644 --- a/config/config.go +++ b/config/config.go @@ -28,6 +28,7 @@ func NewConfig(filepath string) (*Config, error) { KeyMap: map[string]keyMapping{ "command": { "i": "mode-insert", + "/": "mode-search", "k": "channel-up", "j": "channel-down", "g": "channel-top", @@ -51,6 +52,16 @@ func NewConfig(filepath string) (*Config, error) { "": "delete", "": "space", }, + "search": { + "": "cursor-left", + "": "cursor-right", + "": "clear-input", + "": "clear-input", + "": "backspace", + "C-8": "backspace", + "": "delete", + "": "space", + }, }, } diff --git a/context/context.go b/context/context.go index 361facb..cd770ae 100644 --- a/context/context.go +++ b/context/context.go @@ -14,6 +14,7 @@ import ( const ( CommandMode = "command" InsertMode = "insert" + SearchMode = "search" ) type AppContext struct { diff --git a/handlers/event.go b/handlers/event.go index 8948a88..38aef8d 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -28,6 +28,8 @@ var actionMap = map[string]func(*context.AppContext){ "quit": actionQuit, "mode-insert": actionInsertMode, "mode-command": actionCommandMode, + "mode-search": actionSearchMode, + "clear-input": actionClearInput, "channel-up": actionMoveCursorUpChannels, "channel-down": actionMoveCursorDownChannels, "channel-top": actionMoveCursorTopChannels, @@ -67,6 +69,8 @@ func anyKeyHandler(ctx *context.AppContext) { } else { if ctx.Mode == context.InsertMode && ev.Ch != 0 { actionInput(ctx.View, ev.Ch) + } else if ctx.Mode == context.SearchMode && ev.Ch != 0 { + actionSearch(ctx, ev.Ch) } } } @@ -133,6 +137,15 @@ func actionInput(view *views.View, key rune) { termui.Render(view.Input) } +func actionClearInput(ctx *context.AppContext) { + // Clear input + ctx.View.Input.Clear() + ctx.View.Refresh() + + // Set command mode + actionCommandMode(ctx) +} + func actionSpace(ctx *context.AppContext) { actionInput(ctx.View, ' ') } @@ -174,6 +187,23 @@ func actionSend(ctx *context.AppContext) { } } +func actionSearch(ctx *context.AppContext, key rune) { + go func() { + if timer != nil { + timer.Stop() + } + + actionInput(ctx.View, key) + + timer = time.NewTimer(time.Second / 4) + <-timer.C + + term := ctx.View.Input.GetText() + ctx.View.Channels.Search(term) + actionChangeChannel(ctx) + }() +} + // actionQuit will exit the program by using os.Exit, this is // done because we are using a custom termui EvtStream. Which // we won't be able to call termui.StopLoop() on. See main.go @@ -194,6 +224,12 @@ func actionCommandMode(ctx *context.AppContext) { termui.Render(ctx.View.Mode) } +func actionSearchMode(ctx *context.AppContext) { + ctx.Mode = context.SearchMode + ctx.View.Mode.Par.Text = "SEARCH" + termui.Render(ctx.View.Mode) +} + func actionGetMessages(ctx *context.AppContext) { ctx.View.Chat.GetMessages( ctx.Service,