From f9c41c2f95d7bcf93fd3767fb8d08992dc6299ad Mon Sep 17 00:00:00 2001 From: Chris Marshall Date: Thu, 27 Oct 2016 12:51:06 -0400 Subject: [PATCH 01/13] Standardize action interface to accept AppContexts --- handlers/event.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/handlers/event.go b/handlers/event.go index 0961ae5..77f7ed4 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -135,24 +135,24 @@ func actionInput(view *views.View, key rune) { termui.Render(view.Input) } -func actionBackSpace(view *views.View) { - view.Input.Backspace() - termui.Render(view.Input) +func actionBackSpace(ctx *context.AppContext) { + ctx.View.Input.Backspace() + termui.Render(ctx.View.Input) } -func actionDelete(view *views.View) { - view.Input.Delete() - termui.Render(view.Input) +func actionDelete(ctx *context.AppContext) { + ctx.View.Input.Delete() + termui.Render(ctx.View.Input) } -func actionMoveCursorRight(view *views.View) { - view.Input.MoveCursorRight() - termui.Render(view.Input) +func actionMoveCursorRight(ctx *context.AppContext) { + ctx.View.Input.MoveCursorRight() + termui.Render(ctx.View.Input) } -func actionMoveCursorLeft(view *views.View) { - view.Input.MoveCursorLeft() - termui.Render(view.Input) +func actionMoveCursorLeft(ctx *context.AppContext) { + ctx.View.Input.MoveCursorLeft() + termui.Render(ctx.View.Input) } func actionSend(ctx *context.AppContext) { @@ -172,7 +172,7 @@ func actionSend(ctx *context.AppContext) { } } -func actionQuit() { +func actionQuit(*context.AppContext) { termui.StopLoop() } From c5f561793e62424386f27aef6c4a86ee29b4828b Mon Sep 17 00:00:00 2001 From: Chris Marshall Date: Thu, 27 Oct 2016 12:51:33 -0400 Subject: [PATCH 02/13] Define key mapping maps for lookup on key presses --- handlers/event.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/handlers/event.go b/handlers/event.go index 77f7ed4..44bdf19 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -15,6 +15,39 @@ func RegisterEventHandlers(ctx *context.AppContext) { termui.Handle("/sys/wnd/resize", resizeHandler(ctx)) } +var keyMapping = map[termbox.Key]string{ + termbox.KeyPgup: "pg-up", + termbox.KeyCtrlB: "ctrl-b", + termbox.KeyCtrlU: "ctrl-u", + termbox.KeyPgdn: "pg-dn", + termbox.KeyCtrlF: "ctrl-f", + termbox.KeyCtrlD: "ctrl-d", + termbox.KeyEsc: "esc", + termbox.KeyEnter: "enter", + termbox.KeyBackspace: "backspace", + termbox.KeyBackspace2: "backspace", + termbox.KeyDelete: "del", + termbox.KeyArrowRight: "right", + termbox.KeyArrowLeft: "left", +} + +var actionMap = map[string]func(*context.AppContext){ + "backspace": actionBackSpace, + "delete": actionDelete, + "cursor-right": actionMoveCursorRight, + "cursor-left": actionMoveCursorLeft, + "send": actionSend, + "quit": actionQuit, + "insert": actionInsertMode, + "normal": actionCommandMode, + "channel-up": actionMoveCursorUpChannels, + "channel-down": actionMoveCursorDownChannels, + "channel-top": actionMoveCursorTopChannels, + "channel-bottom": actionMoveCursorBottomChannels, + "chat-up": actionScrollUpChat, + "chat-down": actionScrollDownChat, +} + func anyKeyHandler(ctx *context.AppContext) { go func() { for { From 58e1b942b44b2f5b99b86c599640763bc952e640 Mon Sep 17 00:00:00 2001 From: Chris Marshall Date: Thu, 27 Oct 2016 12:52:54 -0400 Subject: [PATCH 03/13] Read key mappings from config file And set the standard defaults --- README.md | 32 ++++++++++++++++++++++++++++---- config/config.go | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 444d731..82ec23c 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,32 @@ Getting started "theme": "light", // optional: set the width of the sidebar (between 1 and 11), default is 1 - "sidebar_width": 3 + "sidebar_width": 3, + + // optional: define custom key mappings + // (shown are the default key mappings) + "keys": { + "normal": { + "i": "insert", + "k": "channel-up", + "j": "channel-down", + "gg": "channel-top", + "G": "channel-bottom", + "pg-up": "chat-up", + "ctrl-b": "chat-up", + "ctrl-u": "chat-up", + "pg-down": "chat-down", + "ctrl-f": "chat-down", + "ctrl-d": "chat-down", + "q": "quit" + }, + "insert": { + "left": "cursor-left", + "right": "cursor-right", + "enter": "send", + "esc": "normal" + } + } } ``` @@ -44,8 +69,8 @@ Getting started $ slack-term -config [path-to-config-file] ``` -Usage ------ +Default Key Mapping +------------------- | mode | key | action | |--------|-----------|----------------------------| @@ -60,7 +85,6 @@ Usage | normal | `pg-down` | scroll chat pane down | | normal | `ctrl-f` | scroll chat pane down | | normal | `ctrl-d` | scroll chat pane down | -| normal | `pg-down` | scroll chat pane down | | normal | `q` | quit | | insert | `left` | move input cursor left | | insert | `right` | move input cursor right | diff --git a/config/config.go b/config/config.go index f01f2a3..d59bcd6 100644 --- a/config/config.go +++ b/config/config.go @@ -10,18 +10,45 @@ import ( // Config is the definition of a Config struct type Config struct { - SlackToken string `json:"slack_token"` - Theme string `json:"theme"` - SidebarWidth int `json:"sidebar_width"` - MainWidth int `json:"-"` + SlackToken string `json:"slack_token"` + Theme string `json:"theme"` + SidebarWidth int `json:"sidebar_width"` + MainWidth int `json:"-"` + KeyMapping map[string]keyMapping `json:"keys"` } +type keyMapping map[string]string + // NewConfig loads the config file and returns a Config struct func NewConfig(filepath string) (*Config, error) { cfg := Config{ Theme: "dark", SidebarWidth: 1, MainWidth: 11, + KeyMapping: map[string]keyMapping{ + "normal": keyMapping{ + "i": "insert", + "k": "channel-up", + "j": "channel-down", + "g": "channel-top", + "G": "channel-bottom", + "pg-up": "chat-up", + "ctrl-b": "chat-up", + "ctrl-u": "chat-up", + "pg-down": "chat-down", + "ctrl-f": "chat-down", + "ctrl-d": "chat-down", + "q": "quit", + }, + "insert": keyMapping{ + "left": "cursor-left", + "right": "cursor-right", + "enter": "send", + "esc": "normal", + "backspace": "backspace", + "del": "delete", + }, + }, } file, err := os.Open(filepath) From b7feeede86842a1ae1e179e1241ce4c391d14e42 Mon Sep 17 00:00:00 2001 From: Chris Marshall Date: Thu, 27 Oct 2016 12:53:37 -0400 Subject: [PATCH 04/13] Read key mappings from configs and execute actions --- context/context.go | 2 +- handlers/event.go | 74 +++++++++++++++------------------------------- 2 files changed, 25 insertions(+), 51 deletions(-) diff --git a/context/context.go b/context/context.go index 573d058..490a888 100644 --- a/context/context.go +++ b/context/context.go @@ -11,7 +11,7 @@ import ( ) const ( - CommandMode = "command" + CommandMode = "normal" InsertMode = "insert" ) diff --git a/handlers/event.go b/handlers/event.go index 44bdf19..454ab93 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -1,6 +1,8 @@ package handlers import ( + "fmt" + "github.com/gizak/termui" "github.com/nlopes/slack" termbox "github.com/nsf/termbox-go" @@ -53,56 +55,28 @@ func anyKeyHandler(ctx *context.AppContext) { for { ev := termbox.PollEvent() - if ev.Type == termbox.EventKey { - if ctx.Mode == context.CommandMode { - switch ev.Key { - case termbox.KeyPgup: - actionScrollUpChat(ctx) - case termbox.KeyCtrlB: - actionScrollUpChat(ctx) - case termbox.KeyCtrlU: - actionScrollUpChat(ctx) - case termbox.KeyPgdn: - actionScrollDownChat(ctx) - case termbox.KeyCtrlF: - actionScrollDownChat(ctx) - case termbox.KeyCtrlD: - actionScrollDownChat(ctx) - default: - switch ev.Ch { - case 'q': - actionQuit() - case 'j': - actionMoveCursorDownChannels(ctx) - case 'k': - actionMoveCursorUpChannels(ctx) - case 'g': - actionMoveCursorTopChannels(ctx) - case 'G': - actionMoveCursorBottomChannels(ctx) - case 'i': - actionInsertMode(ctx) - } - } - } else if ctx.Mode == context.InsertMode { - switch ev.Key { - case termbox.KeyEsc: - actionCommandMode(ctx) - case termbox.KeyEnter: - actionSend(ctx) - case termbox.KeySpace: - actionInput(ctx.View, ' ') - case termbox.KeyBackspace, termbox.KeyBackspace2: - actionBackSpace(ctx.View) - case termbox.KeyDelete: - actionDelete(ctx.View) - case termbox.KeyArrowRight: - actionMoveCursorRight(ctx.View) - case termbox.KeyArrowLeft: - actionMoveCursorLeft(ctx.View) - default: - actionInput(ctx.View, ev.Ch) - } + if ev.Type != termbox.EventKey { + continue + } + + mappedKey := keyMapping[ev.Key] + if mappedKey == "" { + mappedKey = fmt.Sprintf("%c", ev.Ch) + } + + mappedActionName := ctx.Config.KeyMapping[ctx.Mode][mappedKey] + action := actionMap[mappedActionName] + if action != nil { + action(ctx) + continue + } + + if ctx.Mode == context.InsertMode { + switch ev.Key { + case termbox.KeySpace: + actionInput(ctx.View, ' ') + default: + actionInput(ctx.View, ev.Ch) } } } From ad948508f434b6583d140ec9ec0d6dfd77dc2cfb Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Sat, 29 Oct 2016 17:57:58 +0200 Subject: [PATCH 05/13] Make keys mappable --- README.md | 47 +++++++++--------- config/config.go | 45 ++++++++--------- context/context.go | 2 +- handlers/event.go | 121 ++++++++++++++++++++++++++++++--------------- main.go | 2 +- 5 files changed, 130 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 7b5cbfb..ee897c0 100644 --- a/README.md +++ b/README.md @@ -35,27 +35,30 @@ Getting started // optional: define custom key mappings // (shown are the default key mappings) - "keys": { - "normal": { - "i": "insert", - "k": "channel-up", - "j": "channel-down", - "gg": "channel-top", - "G": "channel-bottom", - "pg-up": "chat-up", - "ctrl-b": "chat-up", - "ctrl-u": "chat-up", - "pg-down": "chat-down", - "ctrl-f": "chat-down", - "ctrl-d": "chat-down", - "q": "quit" - }, - "insert": { - "left": "cursor-left", - "right": "cursor-right", - "enter": "send", - "esc": "normal" - } + "key-map": { + "command": { + "i": "mode-insert", + "k": "channel-up", + "j": "channel-down", + "g": "channel-top", + "G": "channel-bottom", + "": "chat-up", + "C-b": "chat-up", + "C-u": "chat-up", + "": "chat-down", + "C-f": "chat-down", + "C-d": "chat-down", + "q": "quit", + }, + "insert": { + "": "cursor-left", + "": "cursor-right", + "": "send", + "": "mode-command", + "": "backspace", + "": "delete", + "": "space", + } } } ``` @@ -77,7 +80,7 @@ Default Key Mapping | normal | `i` | insert mode | | normal | `k` | move channel cursor up | | normal | `j` | move channel cursor down | -| normal | `gg` | move channel cursor top | +| normal | `g` | move channel cursor top | | normal | `G` | move channel cursor bottom | | normal | `pg-up` | scroll chat pane up | | normal | `ctrl-b` | scroll chat pane up | diff --git a/config/config.go b/config/config.go index d59bcd6..122ac0f 100644 --- a/config/config.go +++ b/config/config.go @@ -14,7 +14,7 @@ type Config struct { Theme string `json:"theme"` SidebarWidth int `json:"sidebar_width"` MainWidth int `json:"-"` - KeyMapping map[string]keyMapping `json:"keys"` + KeyMap map[string]keyMapping `json:"key-map"` } type keyMapping map[string]string @@ -25,28 +25,29 @@ func NewConfig(filepath string) (*Config, error) { Theme: "dark", SidebarWidth: 1, MainWidth: 11, - KeyMapping: map[string]keyMapping{ - "normal": keyMapping{ - "i": "insert", - "k": "channel-up", - "j": "channel-down", - "g": "channel-top", - "G": "channel-bottom", - "pg-up": "chat-up", - "ctrl-b": "chat-up", - "ctrl-u": "chat-up", - "pg-down": "chat-down", - "ctrl-f": "chat-down", - "ctrl-d": "chat-down", - "q": "quit", + KeyMap: map[string]keyMapping{ + "command": { + "i": "mode-insert", + "k": "channel-up", + "j": "channel-down", + "g": "channel-top", + "G": "channel-bottom", + "": "chat-up", + "C-b": "chat-up", + "C-u": "chat-up", + "": "chat-down", + "C-f": "chat-down", + "C-d": "chat-down", + "q": "quit", }, - "insert": keyMapping{ - "left": "cursor-left", - "right": "cursor-right", - "enter": "send", - "esc": "normal", - "backspace": "backspace", - "del": "delete", + "insert": { + "": "cursor-left", + "": "cursor-right", + "": "send", + "": "mode-command", + "": "backspace", + "": "delete", + "": "space", }, }, } diff --git a/context/context.go b/context/context.go index 490a888..573d058 100644 --- a/context/context.go +++ b/context/context.go @@ -11,7 +11,7 @@ import ( ) const ( - CommandMode = "normal" + CommandMode = "command" InsertMode = "insert" ) diff --git a/handlers/event.go b/handlers/event.go index 454ab93..df4d096 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -1,7 +1,7 @@ package handlers import ( - "fmt" + "strconv" "github.com/gizak/termui" "github.com/nlopes/slack" @@ -11,37 +11,19 @@ import ( "github.com/erroneousboat/slack-term/views" ) -func RegisterEventHandlers(ctx *context.AppContext) { - anyKeyHandler(ctx) - incomingMessageHandler(ctx) - termui.Handle("/sys/wnd/resize", resizeHandler(ctx)) -} - -var keyMapping = map[termbox.Key]string{ - termbox.KeyPgup: "pg-up", - termbox.KeyCtrlB: "ctrl-b", - termbox.KeyCtrlU: "ctrl-u", - termbox.KeyPgdn: "pg-dn", - termbox.KeyCtrlF: "ctrl-f", - termbox.KeyCtrlD: "ctrl-d", - termbox.KeyEsc: "esc", - termbox.KeyEnter: "enter", - termbox.KeyBackspace: "backspace", - termbox.KeyBackspace2: "backspace", - termbox.KeyDelete: "del", - termbox.KeyArrowRight: "right", - termbox.KeyArrowLeft: "left", -} - +// actionMap binds specific action names to the function counterparts, +// these action names can then be used to bind them to specific keys +// in the Config. var actionMap = map[string]func(*context.AppContext){ + "space": actionSpace, "backspace": actionBackSpace, "delete": actionDelete, "cursor-right": actionMoveCursorRight, "cursor-left": actionMoveCursorLeft, "send": actionSend, "quit": actionQuit, - "insert": actionInsertMode, - "normal": actionCommandMode, + "mode-insert": actionInsertMode, + "mode-command": actionCommandMode, "channel-up": actionMoveCursorUpChannels, "channel-down": actionMoveCursorDownChannels, "channel-top": actionMoveCursorTopChannels, @@ -50,6 +32,12 @@ var actionMap = map[string]func(*context.AppContext){ "chat-down": actionScrollDownChat, } +func RegisterEventHandlers(ctx *context.AppContext) { + anyKeyHandler(ctx) + incomingMessageHandler(ctx) + termui.Handle("/sys/wnd/resize", resizeHandler(ctx)) +} + func anyKeyHandler(ctx *context.AppContext) { go func() { for { @@ -59,23 +47,20 @@ func anyKeyHandler(ctx *context.AppContext) { continue } - mappedKey := keyMapping[ev.Key] - if mappedKey == "" { - mappedKey = fmt.Sprintf("%c", ev.Ch) - } + keyStr := getKeyString(ev) - mappedActionName := ctx.Config.KeyMapping[ctx.Mode][mappedKey] - action := actionMap[mappedActionName] - if action != nil { - action(ctx) - continue - } - - if ctx.Mode == context.InsertMode { - switch ev.Key { - case termbox.KeySpace: - actionInput(ctx.View, ' ') - default: + // Get the action name (actionStr) from the key that + // has been pressed. If this is found try to uncover + // the associated function with this key and execute + // it. + actionStr, ok := ctx.Config.KeyMap[ctx.Mode][keyStr] + if ok { + action, ok := actionMap[actionStr] + if ok { + action(ctx) + } + } else { + if ctx.Mode == context.InsertMode && ev.Ch != 0 { actionInput(ctx.View, ev.Ch) } } @@ -142,6 +127,10 @@ func actionInput(view *views.View, key rune) { termui.Render(view.Input) } +func actionSpace(ctx *context.AppContext) { + actionInput(ctx.View, ' ') +} + func actionBackSpace(ctx *context.AppContext) { ctx.View.Input.Backspace() termui.Render(ctx.View.Input) @@ -262,3 +251,53 @@ func actionScrollDownChat(ctx *context.AppContext) { ctx.View.Chat.ScrollDown() termui.Render(ctx.View.Chat) } + +// GetKeyString will return a string that resembles the key event from +// termbox. This is blatanly copied from termui because it is an unexported +// function. +// +// See: +// - https://github.com/gizak/termui/blob/a7e3aeef4cdf9fa2edb723b1541cb69b7bb089ea/events.go#L31-L72 +// - https://github.com/nsf/termbox-go/blob/master/api_common.go +func getKeyString(e termbox.Event) string { + var ek string + + k := string(e.Ch) + pre := "" + mod := "" + + if e.Mod == termbox.ModAlt { + mod = "M-" + } + if e.Ch == 0 { + if e.Key > 0xFFFF-12 { + k = "" + } else if e.Key > 0xFFFF-25 { + ks := []string{"", "", "", "", "", "", "", "", "", ""} + k = ks[0xFFFF-int(e.Key)-12] + } + + if e.Key <= 0x7F { + pre = "C-" + k = string('a' - 1 + int(e.Key)) + kmap := map[termbox.Key][2]string{ + termbox.KeyCtrlSpace: {"C-", ""}, + termbox.KeyBackspace: {"", ""}, + termbox.KeyTab: {"", ""}, + termbox.KeyEnter: {"", ""}, + termbox.KeyEsc: {"", ""}, + termbox.KeyCtrlBackslash: {"C-", "\\"}, + termbox.KeyCtrlSlash: {"C-", "/"}, + termbox.KeySpace: {"", ""}, + termbox.KeyCtrl8: {"C-", "8"}, + } + if sk, ok := kmap[e.Key]; ok { + pre = sk[0] + k = sk[1] + } + } + } + + ek = pre + mod + k + return ek +} diff --git a/main.go b/main.go index f81b571..62d7330 100644 --- a/main.go +++ b/main.go @@ -32,7 +32,7 @@ var ( flgConfig string flgUsage bool - VERSION = "v0.1.0" + VERSION = "v0.2.0" ) func init() { From 7e57b42c911e9a774cbf010766b4f23252d4a317 Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Sat, 29 Oct 2016 22:48:27 +0200 Subject: [PATCH 06/13] Add timeout when changing channels Fixes #25 --- handlers/event.go | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/handlers/event.go b/handlers/event.go index df4d096..e3350ca 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -2,6 +2,7 @@ package handlers import ( "strconv" + "time" "github.com/gizak/termui" "github.com/nlopes/slack" @@ -11,6 +12,8 @@ import ( "github.com/erroneousboat/slack-term/views" ) +var timer *time.Timer + // actionMap binds specific action names to the function counterparts, // these action names can then be used to bind them to specific keys // in the Config. @@ -193,19 +196,36 @@ func actionGetMessages(ctx *context.AppContext) { termui.Render(ctx.View.Chat) } -func actionGetChannels(ctx *context.AppContext) { - ctx.View.Channels.GetChannels(ctx.Service) - termui.Render(ctx.View.Channels) -} - func actionMoveCursorUpChannels(ctx *context.AppContext) { - ctx.View.Channels.MoveCursorUp() - actionChangeChannel(ctx) + go func() { + if timer != nil { + timer.Stop() + } + + ctx.View.Channels.MoveCursorUp() + termui.Render(ctx.View.Channels) + + timer = time.NewTimer(time.Second / 4) + <-timer.C + + actionChangeChannel(ctx) + }() } func actionMoveCursorDownChannels(ctx *context.AppContext) { - ctx.View.Channels.MoveCursorDown() - actionChangeChannel(ctx) + go func() { + if timer != nil { + timer.Stop() + } + + ctx.View.Channels.MoveCursorDown() + termui.Render(ctx.View.Channels) + + timer = time.NewTimer(time.Second / 4) + <-timer.C + + actionChangeChannel(ctx) + }() } func actionMoveCursorTopChannels(ctx *context.AppContext) { From b3e361c4da0632ee428e0d062872a39f5d18b5fb Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Sat, 29 Oct 2016 23:59:16 +0200 Subject: [PATCH 07/13] Add read mark for channels Fixes #12 --- components/channels.go | 4 ++++ handlers/event.go | 3 +++ service/slack.go | 26 +++++++++++++++++++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/components/channels.go b/components/channels.go index 35a0fc1..e778dc2 100644 --- a/components/channels.go +++ b/components/channels.go @@ -220,3 +220,7 @@ func (c *Channels) ClearNewMessageIndicator() { c.List.Items[c.SelectedChannel] = channelName[0] } } + +func (c *Channels) SetReadMark(svc *service.SlackService) { + svc.SetChannelReadMark(svc.SlackChannels[c.SelectedChannel]) +} diff --git a/handlers/event.go b/handlers/event.go index e3350ca..6178ad2 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -253,6 +253,9 @@ func actionChangeChannel(ctx *context.AppContext) { ctx.Service.Channels[ctx.View.Channels.SelectedChannel].Name, ) + // Set read mark + ctx.View.Channels.SetReadMark(ctx.Service) + termui.Render(ctx.View.Channels) termui.Render(ctx.View.Chat) } diff --git a/service/slack.go b/service/slack.go index 09ba59f..12b075c 100644 --- a/service/slack.go +++ b/service/slack.go @@ -107,6 +107,29 @@ func (s *SlackService) GetChannels() []Channel { return chans } +// SetChannelReadMark will set the read mark for a channel, group, and im +// channel based on the current time. +func (s *SlackService) SetChannelReadMark(channel interface{}) { + switch channel := channel.(type) { + case slack.Channel: + s.Client.SetChannelReadMark( + channel.ID, fmt.Sprintf("%f", + float64(time.Now().Unix())), + ) + case slack.Group: + s.Client.SetGroupReadMark( + channel.ID, fmt.Sprintf("%f", + float64(time.Now().Unix())), + ) + case slack.IM: + s.Client.MarkIMChannel( + channel.ID, fmt.Sprintf("%f", + float64(time.Now().Unix())), + ) + } +} + +// SendMessage will send a message to a particular channel func (s *SlackService) SendMessage(channel string, message string) { // https://godoc.org/github.com/nlopes/slack#PostMessageParameters postParams := slack.PostMessageParameters{ @@ -117,6 +140,8 @@ func (s *SlackService) SendMessage(channel string, message string) { s.Client.PostMessage(channel, message, postParams) } +// GetMessages will get messages for a channel, group or im channel delimited +// by a count. func (s *SlackService) GetMessages(channel interface{}, count int) []string { // https://api.slack.com/methods/channels.history historyParams := slack.HistoryParameters{ @@ -135,7 +160,6 @@ func (s *SlackService) GetMessages(channel interface{}, count int) []string { log.Fatal(err) // FIXME } case slack.Group: - // TODO: json: cannot unmarshal number into Go value of type string history, err = s.Client.GetGroupHistory(chnType.ID, historyParams) if err != nil { log.Fatal(err) // FIXME From 5d11b6b785b89b824bf7a0a4c9cc337cef0c01a8 Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Sun, 30 Oct 2016 14:26:12 +0100 Subject: [PATCH 08/13] Add help page Fixes #11 --- README.md | 39 ++++++++++++++++++++------------------- components/chat.go | 34 ++++++++++++++++++++++++++++++++++ config/config.go | 3 ++- handlers/event.go | 6 ++++++ main.go | 5 ++--- 5 files changed, 64 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index ee897c0..873faff 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Getting started // optional: define custom key mappings // (shown are the default key mappings) - "key-map": { + "key_map": { "command": { "i": "mode-insert", "k": "channel-up", @@ -75,21 +75,22 @@ Getting started Default Key Mapping ------------------- -| mode | key | action | -|--------|-----------|----------------------------| -| normal | `i` | insert mode | -| normal | `k` | move channel cursor up | -| normal | `j` | move channel cursor down | -| normal | `g` | move channel cursor top | -| normal | `G` | move channel cursor bottom | -| normal | `pg-up` | scroll chat pane up | -| normal | `ctrl-b` | scroll chat pane up | -| normal | `ctrl-u` | scroll chat pane up | -| normal | `pg-down` | scroll chat pane down | -| normal | `ctrl-f` | scroll chat pane down | -| normal | `ctrl-d` | scroll chat pane down | -| normal | `q` | quit | -| insert | `left` | move input cursor left | -| insert | `right` | move input cursor right | -| insert | `enter` | send message | -| insert | `esc` | normal mode | +| mode | key | action | +|---------|-----------|----------------------------| +| command | `i` | insert mode | +| command | `k` | move channel cursor up | +| command | `j` | move channel cursor down | +| command | `g` | move channel cursor top | +| command | `G` | move channel cursor bottom | +| command | `pg-up` | scroll chat pane up | +| command | `ctrl-b` | scroll chat pane up | +| command | `ctrl-u` | scroll chat pane up | +| command | `pg-down` | scroll chat pane down | +| command | `ctrl-f` | scroll chat pane down | +| command | `ctrl-d` | scroll chat pane down | +| command | `q` | quit | +| command | `f1` | help | +| insert | `left` | move input cursor left | +| insert | `right` | move input cursor right | +| insert | `enter` | send message | +| insert | `esc` | command mode | diff --git a/components/chat.go b/components/chat.go index 8712379..8e11248 100644 --- a/components/chat.go +++ b/components/chat.go @@ -1,11 +1,14 @@ package components import ( + "fmt" "html" + "sort" "strings" "github.com/gizak/termui" + "github.com/erroneousboat/slack-term/config" "github.com/erroneousboat/slack-term/service" ) @@ -208,3 +211,34 @@ func (c *Chat) ScrollDown() { func (c *Chat) SetBorderLabel(label string) { c.List.BorderLabel = label } + +// Help shows the usage and key bindings in the chat pane +func (c *Chat) Help(cfg *config.Config) { + help := []string{ + "slack-term - slack client for your terminal", + "", + "USAGE:", + " slack-term -config [path-to-config]", + "", + "KEY BINDINGS:", + "", + } + + for mode, mapping := range cfg.KeyMap { + help = append(help, fmt.Sprintf(" %s", strings.ToUpper(mode))) + help = append(help, "") + + var keys []string + for k := range mapping { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + help = append(help, fmt.Sprintf(" %-12s%-15s", k, mapping[k])) + } + help = append(help, "") + } + + c.List.Items = help +} diff --git a/config/config.go b/config/config.go index 122ac0f..862eb65 100644 --- a/config/config.go +++ b/config/config.go @@ -14,7 +14,7 @@ type Config struct { Theme string `json:"theme"` SidebarWidth int `json:"sidebar_width"` MainWidth int `json:"-"` - KeyMap map[string]keyMapping `json:"key-map"` + KeyMap map[string]keyMapping `json:"key_map"` } type keyMapping map[string]string @@ -39,6 +39,7 @@ func NewConfig(filepath string) (*Config, error) { "C-f": "chat-down", "C-d": "chat-down", "q": "quit", + "": "help", }, "insert": { "": "cursor-left", diff --git a/handlers/event.go b/handlers/event.go index 6178ad2..a8f5857 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -33,6 +33,7 @@ var actionMap = map[string]func(*context.AppContext){ "channel-bottom": actionMoveCursorBottomChannels, "chat-up": actionScrollUpChat, "chat-down": actionScrollDownChat, + "help": actionHelp, } func RegisterEventHandlers(ctx *context.AppContext) { @@ -275,6 +276,11 @@ func actionScrollDownChat(ctx *context.AppContext) { termui.Render(ctx.View.Chat) } +func actionHelp(ctx *context.AppContext) { + ctx.View.Chat.Help(ctx.Config) + termui.Render(ctx.View.Chat) +} + // GetKeyString will return a string that resembles the key event from // termbox. This is blatanly copied from termui because it is an unexported // function. diff --git a/main.go b/main.go index 62d7330..8021575 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,8 @@ import ( ) const ( - USAGE = `NAME: + VERSION = "v0.2.0" + USAGE = `NAME: slack-term - slack client for your terminal USAGE: @@ -31,8 +32,6 @@ GLOBAL OPTIONS: var ( flgConfig string flgUsage bool - - VERSION = "v0.2.0" ) func init() { From e85d6b5ec1900d8921a75172bad5b268ae92ff5b Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Sun, 30 Oct 2016 15:38:13 +0100 Subject: [PATCH 09/13] Show edited messages Fixes #27 --- service/slack.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/service/slack.go b/service/slack.go index 12b075c..7e5a0d1 100644 --- a/service/slack.go +++ b/service/slack.go @@ -259,6 +259,12 @@ func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent var msgs []string var name string + // Append (edited) when and edit message is received + if message.SubType == "message_changed" { + message = &slack.MessageEvent{Msg: *message.SubMessage} + message.Text = fmt.Sprintf("%s (edited)", message.Text) + } + // Get username from cache name, ok := s.UserCache[message.User] From 156128a12754c3d9e15c94e72ee2985283e44ff3 Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Sun, 30 Oct 2016 16:02:11 +0100 Subject: [PATCH 10/13] Add Channel topic next to channel name in chat window Fixes #45 --- components/chat.go | 19 ++++++++++++++----- handlers/event.go | 2 +- service/slack.go | 11 ++++++----- views/chat.go | 2 +- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/components/chat.go b/components/chat.go index 8e11248..0182d66 100644 --- a/components/chat.go +++ b/components/chat.go @@ -19,7 +19,7 @@ type Chat struct { } // CreateChat is the constructor for the Chat struct -func CreateChat(svc *service.SlackService, inputHeight int, selectedChannel interface{}, selectedChannelName string) *Chat { +func CreateChat(svc *service.SlackService, inputHeight int, selectedSlackChannel interface{}, selectedChannel service.Channel) *Chat { chat := &Chat{ List: termui.NewList(), Offset: 0, @@ -28,8 +28,8 @@ func CreateChat(svc *service.SlackService, inputHeight int, selectedChannel inte chat.List.Height = termui.TermHeight() - inputHeight chat.List.Overflow = "wrap" - chat.GetMessages(svc, selectedChannel) - chat.SetBorderLabel(selectedChannelName) + chat.GetMessages(svc, selectedSlackChannel) + chat.SetBorderLabel(selectedChannel) return chat } @@ -208,8 +208,17 @@ func (c *Chat) ScrollDown() { } // SetBorderLabel will set Label of the Chat pane to the specified string -func (c *Chat) SetBorderLabel(label string) { - c.List.BorderLabel = label +func (c *Chat) SetBorderLabel(channel service.Channel) { + var channelName string + if channel.Topic != "" { + channelName = fmt.Sprintf("%s - %s", + channel.Name, + channel.Topic, + ) + } else { + channelName = channel.Name + } + c.List.BorderLabel = channelName } // Help shows the usage and key bindings in the chat pane diff --git a/handlers/event.go b/handlers/event.go index a8f5857..0e55058 100644 --- a/handlers/event.go +++ b/handlers/event.go @@ -251,7 +251,7 @@ func actionChangeChannel(ctx *context.AppContext) { // Set channel name for the Chat pane ctx.View.Chat.SetBorderLabel( - ctx.Service.Channels[ctx.View.Channels.SelectedChannel].Name, + ctx.Service.Channels[ctx.View.Channels.SelectedChannel], ) // Set read mark diff --git a/service/slack.go b/service/slack.go index 7e5a0d1..c269507 100644 --- a/service/slack.go +++ b/service/slack.go @@ -19,8 +19,9 @@ type SlackService struct { } type Channel struct { - ID string - Name string + ID string + Name string + Topic string } // NewSlackService is the constructor for the SlackService and will initialize @@ -71,7 +72,7 @@ func (s *SlackService) GetChannels() []Channel { } for _, chn := range slackChans { s.SlackChannels = append(s.SlackChannels, chn) - chans = append(chans, Channel{chn.ID, chn.Name}) + chans = append(chans, Channel{chn.ID, chn.Name, chn.Topic.Value}) } // Groups @@ -81,7 +82,7 @@ func (s *SlackService) GetChannels() []Channel { } for _, grp := range slackGroups { s.SlackChannels = append(s.SlackChannels, grp) - chans = append(chans, Channel{grp.ID, grp.Name}) + chans = append(chans, Channel{grp.ID, grp.Name, grp.Topic.Value}) } // IM @@ -97,7 +98,7 @@ func (s *SlackService) GetChannels() []Channel { // to the UserCache, so we skip it name, ok := s.UserCache[im.User] if ok { - chans = append(chans, Channel{im.ID, name}) + chans = append(chans, Channel{im.ID, name, ""}) s.SlackChannels = append(s.SlackChannels, im) } } diff --git a/views/chat.go b/views/chat.go index e44af31..831162e 100644 --- a/views/chat.go +++ b/views/chat.go @@ -23,7 +23,7 @@ func CreateChatView(svc *service.SlackService) *View { svc, input.Par.Height, svc.SlackChannels[channels.SelectedChannel], - svc.Channels[channels.SelectedChannel].Name, + svc.Channels[channels.SelectedChannel], ) mode := components.CreateMode() From 57145a4ec40a142013e9c77fd8971cf3e72e0638 Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Mon, 31 Oct 2016 09:59:00 +0100 Subject: [PATCH 11/13] Add secondary backspace to default config --- config/config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.go b/config/config.go index 862eb65..dff7d9a 100644 --- a/config/config.go +++ b/config/config.go @@ -47,6 +47,7 @@ func NewConfig(filepath string) (*Config, error) { "": "send", "": "mode-command", "": "backspace", + "C-8": "backspace", "": "delete", "": "space", }, From 85bc84e0d20f8e528c6a135bfbb2252e72efaf03 Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Mon, 31 Oct 2016 21:26:12 +0100 Subject: [PATCH 12/13] Fix some slack integrations Fixes #13 For instance the github integration doesn't have a discoverable username, so this will become unkown. Some integrations with attachment fields, title and text should be tested for the correct order. --- service/slack.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/service/slack.go b/service/slack.go index c269507..a948685 100644 --- a/service/slack.go +++ b/service/slack.go @@ -260,7 +260,7 @@ func (s *SlackService) CreateMessageFromMessageEvent(message *slack.MessageEvent var msgs []string var name string - // Append (edited) when and edit message is received + // Append (edited) when an edited message is received if message.SubType == "message_changed" { message = &slack.MessageEvent{Msg: *message.SubMessage} message.Text = fmt.Sprintf("%s (edited)", message.Text) @@ -335,6 +335,15 @@ func createMessageFromAttachments(atts []slack.Attachment) []string { ), ) } + + if att.Text != "" { + msgs = append(msgs, att.Text) + } + + if att.Title != "" { + msgs = append(msgs, att.Title) + } } + return msgs } From a6c8e15fdcde49b444182586ca2530b1f5949d85 Mon Sep 17 00:00:00 2001 From: erroneousboat Date: Mon, 31 Oct 2016 21:38:24 +0100 Subject: [PATCH 13/13] Update readme --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 873faff..8709325 100644 --- a/README.md +++ b/README.md @@ -27,14 +27,13 @@ Getting started { "slack_token": "yourslacktokenhere", - // optional: add the following to use light theme, default is dark + // OPTIONAL: add the following to use light theme, default is dark "theme": "light", - // optional: set the width of the sidebar (between 1 and 11), default is 1 + // OPTIONAL: set the width of the sidebar (between 1 and 11), default is 1 "sidebar_width": 3, - // optional: define custom key mappings - // (shown are the default key mappings) + // OPTIONAL: define custom key mappings, defaults are: "key_map": { "command": { "i": "mode-insert", @@ -49,6 +48,7 @@ Getting started "C-f": "chat-down", "C-d": "chat-down", "q": "quit", + "": "help" }, "insert": { "": "cursor-left", @@ -56,6 +56,7 @@ Getting started "": "send", "": "mode-command", "": "backspace", + "C-8": "backspace", "": "delete", "": "space", }