func actionMoveCursorUpChannels(ctx *context.AppContext) {
	go func() {
		if scrollTimer != nil {
			scrollTimer.Stop()
		}
		ctx.View.Channels.MoveCursorUp()
		termui.Render(ctx.View.Channels)
		scrollTimer = time.NewTimer(time.Second / 4)
		<-scrollTimer.C
		actionChangeChannel(ctx)
	}()
}
actionMoveCursorUpChannels will execute the actionChangeChannel
function. A timer is implemented to support fast scrolling through
the list without executing the actionChangeChannel event
package handlers
import (
	"fmt"
	"log"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"
	"github.com/0xAX/notificator"
	"github.com/erroneousboat/termui"
	termbox "github.com/nsf/termbox-go"
	"github.com/slack-go/slack"
	"github.com/erroneousboat/slack-term/components"
	"github.com/erroneousboat/slack-term/config"
	"github.com/erroneousboat/slack-term/context"
	"github.com/erroneousboat/slack-term/views"
)
var scrollTimer *time.Timer
var notifyTimer *time.Timer
var actionMap = map[string]func(*context.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,
	"channel-search-next": actionSearchNextChannels,
	"channel-search-prev": actionSearchPrevChannels,
	"channel-jump":        actionJumpChannels,
	"thread-up":           actionMoveCursorUpThreads,
	"thread-down":         actionMoveCursorDownThreads,
	"chat-up":             actionScrollUpChat,
	"chat-down":           actionScrollDownChat,
	"help":                actionHelp,
}
func Initialize(ctx *context.AppContext) {
	eventHandler(ctx)
	messageHandler(ctx)
	go actionSetPresenceAll(ctx)
}
func eventHandler(ctx *context.AppContext) {
	go func() {
		for {
			ctx.EventQueue <- termbox.PollEvent()
		}
	}()
	go func() {
		for {
			ev := <-ctx.EventQueue
			handleTermboxEvents(ctx, ev)
			handleMoreTermboxEvents(ctx, ev)
			if ctx.Debug {
				ctx.View.Debug.Println(
					"event received",
				)
			}
		}
	}()
}
func handleTermboxEvents(ctx *context.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 *context.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 *context.AppContext) {
	go func() {
		for {
			select {
			case rtmEvent := <-ctx.Service.RTM.IncomingEvents:
				switch ev := rtmEvent.Data.(type) {
				case *slack.MessageEvent:
					msg, err := ctx.Service.CreateMessageFromMessageEvent(ev, ev.Channel)
					if err != nil {
						continue
					}
					if ev.Channel == ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID {
						var threadTimestamp string
						if ev.ThreadTimestamp != "" {
							threadTimestamp = ev.ThreadTimestamp
						} else if ev.PreviousMessage != nil && ev.PreviousMessage.ThreadTimestamp != "" {
							threadTimestamp = ev.PreviousMessage.ThreadTimestamp
						} else {
							threadTimestamp = ""
						}
						if threadTimestamp != "" {
							ctx.View.Chat.AddReply(threadTimestamp, msg)
						} else if threadTimestamp == "" && ctx.Focus == context.ChatFocus {
							ctx.View.Chat.AddMessage(msg)
						}
						if ctx.View.Chat.IsNewThread(threadTimestamp) {
							actionChangeChannel(ctx)
						} else {
							termui.Render(ctx.View.Chat)
						}
					}
					if ev.User != ctx.Service.CurrentUserID {
						actionNewMessage(ctx, ev)
					}
				case *slack.PresenceChangeEvent:
					actionSetPresence(ctx, ev.User, ev.Presence)
				case *slack.RTMError:
					ctx.View.Debug.Println(
						ev.Error(),
					)
				}
			}
		}
	}()
}
func actionKeyEvent(ctx *context.AppContext, ev termbox.Event) {
	keyStr := getKeyString(ev)
	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)
		} else if ctx.Mode == context.SearchMode && ev.Ch != 0 {
			actionSearch(ctx, ev.Ch)
		}
	}
}
func actionResizeEvent(ctx *context.AppContext, ev termbox.Event) {
	if termui.TermWidth() < 25 || termui.TermHeight() < 5 {
		return
	}
	termui.Body.Width = termui.TermWidth()
	ctx.View.Channels.List.Height = termui.TermHeight() - ctx.View.Input.Par.Height
	ctx.View.Chat.List.Height = termui.TermHeight() - ctx.View.Input.Par.Height
	ctx.View.Debug.List.Height = termui.TermHeight() - ctx.View.Input.Par.Height
	termui.Body.Align()
	termui.Render(termui.Body)
}
func actionRedrawGrid(ctx *context.AppContext, threads bool, debug bool) {
	termui.Clear()
	termui.Body = termui.NewGrid()
	termui.Body.X = 0
	termui.Body.Y = 0
	termui.Body.BgColor = termui.ThemeAttr("bg")
	termui.Body.Width = termui.TermWidth()
	columns := []*termui.Row{
		termui.NewCol(ctx.Config.SidebarWidth, 0, ctx.View.Channels),
	}
	if threads && debug {
		columns = append(
			columns,
			[]*termui.Row{
				termui.NewCol(ctx.Config.MainWidth-ctx.Config.ThreadsWidth-3, 0, ctx.View.Chat),
				termui.NewCol(ctx.Config.ThreadsWidth, 0, ctx.View.Threads),
				termui.NewCol(3, 0, ctx.View.Debug),
			}...,
		)
	} else if threads {
		columns = append(
			columns,
			[]*termui.Row{
				termui.NewCol(ctx.Config.MainWidth-ctx.Config.ThreadsWidth, 0, ctx.View.Chat),
				termui.NewCol(ctx.Config.ThreadsWidth, 0, ctx.View.Threads),
			}...,
		)
	} else if debug {
		columns = append(
			columns,
			[]*termui.Row{
				termui.NewCol(ctx.Config.MainWidth-5, 0, ctx.View.Chat),
				termui.NewCol(ctx.Config.MainWidth-6, 0, ctx.View.Debug),
			}...,
		)
	} else {
		columns = append(
			columns,
			[]*termui.Row{
				termui.NewCol(ctx.Config.MainWidth, 0, ctx.View.Chat),
			}...,
		)
	}
	termui.Body.AddRows(
		termui.NewRow(columns...),
		termui.NewRow(
			termui.NewCol(ctx.Config.SidebarWidth, 0, ctx.View.Mode),
			termui.NewCol(ctx.Config.MainWidth, 0, ctx.View.Input),
		),
	)
	termui.Body.Align()
	termui.Render(termui.Body)
}
func actionInput(view *views.View, key rune) {
	view.Input.Insert(key)
	termui.Render(view.Input)
}
func actionClearInput(ctx *context.AppContext) {
	ctx.View.Input.Clear()
	ctx.View.Refresh()
	actionCommandMode(ctx)
}
func actionSpace(ctx *context.AppContext) {
	actionInput(ctx.View, ' ')
}
func actionBackSpace(ctx *context.AppContext) {
	ctx.View.Input.Backspace()
	termui.Render(ctx.View.Input)
}
func actionDelete(ctx *context.AppContext) {
	ctx.View.Input.Delete()
	termui.Render(ctx.View.Input)
}
func actionMoveCursorRight(ctx *context.AppContext) {
	ctx.View.Input.MoveCursorRight()
	termui.Render(ctx.View.Input)
}
func actionMoveCursorLeft(ctx *context.AppContext) {
	ctx.View.Input.MoveCursorLeft()
	termui.Render(ctx.View.Input)
}
func actionSend(ctx *context.AppContext) {
	if !ctx.View.Input.IsEmpty() {
		message := ctx.View.Input.GetText()
		ctx.View.Input.Clear()
		termui.Render(ctx.View.Input)
		isCmd, err := ctx.Service.SendCommand(
			ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
			message,
		)
		if err != nil {
			ctx.View.Debug.Println(
				err.Error(),
			)
		}
		if !isCmd {
			if ctx.Focus == context.ChatFocus {
				err := ctx.Service.SendMessage(
					ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
					message,
				)
				if err != nil {
					ctx.View.Debug.Println(
						err.Error(),
					)
				}
			}
			if ctx.Focus == context.ThreadFocus {
				err := ctx.Service.SendReply(
					ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
					ctx.View.Threads.ChannelItems[ctx.View.Threads.SelectedChannel].ID,
					message,
				)
				if err != nil {
					ctx.View.Debug.Println(
						err.Error(),
					)
				}
			}
		}
		channelItem := ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel]
		if channelItem.Notification {
			ctx.Service.MarkAsRead(channelItem)
			ctx.View.Channels.MarkAsRead(ctx.View.Channels.SelectedChannel)
		}
		termui.Render(ctx.View.Channels)
	}
}
func actionSearch(ctx *context.AppContext, key rune) {
	actionInput(ctx.View, key)
	go func() {
		if scrollTimer != nil {
			scrollTimer.Stop()
		}
		scrollTimer = time.NewTimer(time.Second / 4)
		<-scrollTimer.C
		term := ctx.View.Input.GetText()
		ctx.View.Channels.Search(term)
		actionChangeChannel(ctx)
	}()
}
func actionQuit(ctx *context.AppContext) {
	termbox.Close()
	os.Exit(0)
}
func actionInsertMode(ctx *context.AppContext) {
	ctx.Mode = context.InsertMode
	ctx.View.Mode.SetInsertMode()
}
func actionCommandMode(ctx *context.AppContext) {
	ctx.Mode = context.CommandMode
	ctx.View.Mode.SetCommandMode()
}
func actionSearchMode(ctx *context.AppContext) {
	ctx.Mode = context.SearchMode
	ctx.View.Mode.SetSearchMode()
}
func actionGetMessages(ctx *context.AppContext) {
	msgs, _, err := ctx.Service.GetMessages(
		ctx.View.Channels.ChannelItems[ctx.View.Channels.SelectedChannel].ID,
		ctx.View.Chat.GetMaxItems(),
	)
	if err != nil {
		termbox.Close()
		log.Println(err)
		os.Exit(0)
	}
	ctx.View.Chat.SetMessages(msgs)
	termui.Render(ctx.View.Chat)
}