package main import ( "os" "strconv" "time" "github.com/gizak/termui" "github.com/nlopes/slack" termbox "github.com/nsf/termbox-go" ) 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. var actionMap = map[string]func(*AppContext){ "space": actionSpace, "backspace": actionBackSpace, "delete": actionDelete, "cursor-right": actionMoveCursorRight, "cursor-left": actionMoveCursorLeft, "send": actionSend, "quit": actionQuit, "mode-insert": actionInsertMode, "mode-command": actionCommandMode, "mode-search": actionSearchMode, "clear-input": actionClearInput, "channel-up": actionMoveCursorUpChannels, "channel-down": actionMoveCursorDownChannels, "channel-top": actionMoveCursorTopChannels, "channel-bottom": actionMoveCursorBottomChannels, "chat-up": actionScrollUpChat, "chat-down": actionScrollDownChat, "help": actionHelp, } func RegisterEventHandlers(ctx *AppContext) { eventHandler(ctx) messageHandler(ctx) } func eventHandler(ctx *AppContext) { go func() { for { ctx.EventQueue <- termbox.PollEvent() } }() go func() { for { ev := <-ctx.EventQueue handleTermboxEvents(ctx, ev) handleMoreTermboxEvents(ctx, ev) } }() } func handleTermboxEvents(ctx *AppContext, ev termbox.Event) bool { switch ev.Type { case termbox.EventKey: actionKeyEvent(ctx, ev) case termbox.EventResize: actionResizeEvent(ctx, ev) } return true } func handleMoreTermboxEvents(ctx *AppContext, ev termbox.Event) bool { for { select { case ev := <-ctx.EventQueue: ok := handleTermboxEvents(ctx, ev) if !ok { return false } default: return true } } } func messageHandler(ctx *AppContext) { go func() { for { select { case msg := <-ctx.Service.RTM.IncomingEvents: switch ev := msg.Data.(type) { case *slack.MessageEvent: // Construct message msg := ctx.Service.CreateMessageFromMessageEvent(ev) // Add message to the selected channel if ev.Channel == ctx.Service.Channels[ctx.View.Channels.SelectedChannel].ID { // reverse order of messages, mainly done // when attachments are added to message for i := len(msg) - 1; i >= 0; i-- { ctx.View.Chat.AddMessage(msg[i]) } termui.Render(ctx.View.Chat) // TODO: set Chat.Offset to 0, to automatically scroll // down? } // Set new message indicator for channel, I'm leaving // this here because I also want to be notified when // I'm currently in a channel but not in the terminal // window (tmux). But only create a notification when // it comes from someone else but the current user. if ev.User != ctx.Service.CurrentUserID { actionNewMessage(ctx, ev.Channel) } case *slack.PresenceChangeEvent: actionSetPresence(ctx, ev.User, ev.Presence) } } } }() } func actionKeyEvent(ctx *AppContext, ev termbox.Event) { keyStr := getKeyString(ev) // 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 == InsertMode && ev.Ch != 0 { actionInput(ctx.View, ev.Ch) } else if ctx.Mode == SearchMode && ev.Ch != 0 { actionSearch(ctx, ev.Ch) } } } func actionResizeEvent(ctx *AppContext, ev termbox.Event) { termui.Body.Width = termui.TermWidth() termui.Body.Align() termui.Render(termui.Body) } func actionInput(view *View, key rune) { view.Input.Insert(key) termui.Render(view.Input) } func actionClearInput(ctx *AppContext) { // Clear input ctx.View.Input.Clear() ctx.View.Refresh() // Set command mode actionCommandMode(ctx) } func actionSpace(ctx *AppContext) { actionInput(ctx.View, ' ') } func actionBackSpace(ctx *AppContext) { ctx.View.Input.Backspace() termui.Render(ctx.View.Input) } func actionDelete(ctx *AppContext) { ctx.View.Input.Delete() termui.Render(ctx.View.Input) } func actionMoveCursorRight(ctx *AppContext) { ctx.View.Input.MoveCursorRight() termui.Render(ctx.View.Input) } func actionMoveCursorLeft(ctx *AppContext) { ctx.View.Input.MoveCursorLeft() termui.Render(ctx.View.Input) } func actionSend(ctx *AppContext) { if !ctx.View.Input.IsEmpty() { // Clear message before sending, to combat // quick succession of actionSend message := ctx.View.Input.GetText() ctx.View.Input.Clear() ctx.View.Refresh() ctx.View.Input.SendMessage( ctx.Service, ctx.Service.Channels[ctx.View.Channels.SelectedChannel].ID, message, ) } } func actionSearch(ctx *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 // for the customEvtStream and why this is done. func actionQuit(ctx *AppContext) { termbox.Close() os.Exit(0) } func actionInsertMode(ctx *AppContext) { ctx.Mode = InsertMode ctx.View.Mode.Par.Text = "INSERT" termui.Render(ctx.View.Mode) } func actionCommandMode(ctx *AppContext) { ctx.Mode = CommandMode ctx.View.Mode.Par.Text = "NORMAL" termui.Render(ctx.View.Mode) } func actionSearchMode(ctx *AppContext) { ctx.Mode = SearchMode ctx.View.Mode.Par.Text = "SEARCH" termui.Render(ctx.View.Mode) } func actionGetMessages(ctx *AppContext) { ctx.View.Chat.GetMessages( ctx.Service, ctx.Service.Channels[ctx.View.Channels.SelectedChannel], ) termui.Render(ctx.View.Chat) } func actionMoveCursorUpChannels(ctx *AppContext) { 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 *AppContext) { 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 *AppContext) { ctx.View.Channels.MoveCursorTop() actionChangeChannel(ctx) } func actionMoveCursorBottomChannels(ctx *AppContext) { ctx.View.Channels.MoveCursorBottom() actionChangeChannel(ctx) } func actionChangeChannel(ctx *AppContext) { // Clear messages from Chat pane ctx.View.Chat.ClearMessages() // Get message for the new channel ctx.View.Chat.GetMessages( ctx.Service, ctx.Service.SlackChannels[ctx.View.Channels.SelectedChannel], ) // Set channel name for the Chat pane ctx.View.Chat.SetBorderLabel( ctx.Service.Channels[ctx.View.Channels.SelectedChannel], ) // Set read mark ctx.View.Channels.SetReadMark(ctx.Service) termui.Render(ctx.View.Channels) termui.Render(ctx.View.Chat) } func actionNewMessage(ctx *AppContext, channelID string) { ctx.View.Channels.SetNotification(ctx.Service, channelID) termui.Render(ctx.View.Channels) } func actionSetPresence(ctx *AppContext, channelID string, presence string) { ctx.View.Channels.SetPresence(ctx.Service, channelID, presence) termui.Render(ctx.View.Channels) } func actionScrollUpChat(ctx *AppContext) { ctx.View.Chat.ScrollUp() termui.Render(ctx.View.Chat) } func actionScrollDownChat(ctx *AppContext) { ctx.View.Chat.ScrollDown() termui.Render(ctx.View.Chat) } func actionHelp(ctx *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. // // 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 }