From 0eecef2da61a9fdd67350effe7e1e2c343adf902 Mon Sep 17 00:00:00 2001 From: Ken Hibino Date: Sat, 21 May 2022 09:47:21 -0700 Subject: [PATCH] (cli): Refactor key event handlers --- tools/asynq/cmd/dash/dash.go | 212 ++-------------- tools/asynq/cmd/dash/key_event.go | 405 ++++++++++++++++++++++++++++++ 2 files changed, 430 insertions(+), 187 deletions(-) create mode 100644 tools/asynq/cmd/dash/key_event.go diff --git a/tools/asynq/cmd/dash/dash.go b/tools/asynq/cmd/dash/dash.go index e9198ce..8fba112 100644 --- a/tools/asynq/cmd/dash/dash.go +++ b/tools/asynq/cmd/dash/dash.go @@ -55,8 +55,9 @@ type redisInfo struct { } type Options struct { - DebugMode bool - UseRealData bool + DebugMode bool + UseRealData bool + PollInterval time.Duration } var baseStyle = tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset) @@ -64,19 +65,18 @@ var baseStyle = tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell func Run(opts Options) { s, err := tcell.NewScreen() if err != nil { - fmt.Println("failed to create a screen: %v", err) + fmt.Printf("failed to create a screen: %v\n", err) os.Exit(1) } if err := s.Init(); err != nil { - fmt.Println("failed to initialize screen: %v", err) + fmt.Printf("failed to initialize screen: %v\n", err) os.Exit(1) } - - inspector := asynq.NewInspector(asynq.RedisClientOpt{Addr: ":6379"}) - // Set default text style s.SetStyle(baseStyle) + inspector := asynq.NewInspector(asynq.RedisClientOpt{Addr: ":6379"}) + // channels to send/receive data fetched asynchronously var ( errorCh = make(chan error) @@ -86,7 +86,6 @@ func Run(opts Options) { tasksCh = make(chan []*asynq.TaskInfo) redisInfoCh = make(chan *redisInfo) ) - go fetchQueues(inspector, queuesCh, errorCh, opts) var state State // contained in this goroutine only; do not share @@ -96,18 +95,28 @@ func Run(opts Options) { eventCh := make(chan tcell.Event) done := make(chan struct{}) - const interval = 2 * time.Second - ticker := time.NewTicker(interval) + opts.PollInterval = 2 * time.Second + ticker := time.NewTicker(opts.PollInterval) defer ticker.Stop() + h := keyEventHandler{ + s: s, + state: &state, + opts: opts, + done: done, + ticker: ticker, + inspector: inspector, + errorCh: errorCh, + queueCh: queueCh, + queuesCh: queuesCh, + groupsCh: groupsCh, + tasksCh: tasksCh, + redisInfoCh: redisInfoCh, + } + // TODO: Double check that we are not leaking goroutine with this one. go s.ChannelEvents(eventCh, done) - quit := func() { - s.Fini() - close(done) - os.Exit(0) - } for { // Update screen s.Show() @@ -119,178 +128,7 @@ func Run(opts Options) { case *tcell.EventResize: s.Sync() case *tcell.EventKey: - // Esc and 'q' key have "go back" semantics - if ev.Key() == tcell.KeyEscape || ev.Rune() == 'q' { - if state.view == viewTypeHelp { - state.view = state.prevView // exit help - drawDash(s, &state, opts) - } else if state.view == viewTypeQueueDetails { - state.view = viewTypeQueues - drawDash(s, &state, opts) - } else { - quit() - } - } else if ev.Key() == tcell.KeyCtrlC { - quit() - } else if ev.Key() == tcell.KeyCtrlL { - s.Sync() - } else if (ev.Key() == tcell.KeyDown || ev.Rune() == 'j') && state.view == viewTypeQueues { - if state.queueTableRowIdx < len(state.queues) { - state.queueTableRowIdx++ - } else { - state.queueTableRowIdx = 0 // loop back - } - drawDash(s, &state, opts) - } else if (ev.Key() == tcell.KeyUp || ev.Rune() == 'k') && state.view == viewTypeQueues { - if state.queueTableRowIdx == 0 { - state.queueTableRowIdx = len(state.queues) - } else { - state.queueTableRowIdx-- - } - drawDash(s, &state, opts) - } else if (ev.Key() == tcell.KeyDown || ev.Rune() == 'j') && state.view == viewTypeQueueDetails { - if shouldShowGroupTable(&state) { - if state.groupTableRowIdx < groupPageSize(s) { - state.groupTableRowIdx++ - } else { - state.groupTableRowIdx = 0 // loop back - } - } else { - if state.taskTableRowIdx < len(state.tasks) { - state.taskTableRowIdx++ - } else { - state.taskTableRowIdx = 0 // loop back - } - } - drawDash(s, &state, opts) - } else if (ev.Key() == tcell.KeyUp || ev.Rune() == 'k') && state.view == viewTypeQueueDetails { - if shouldShowGroupTable(&state) { - if state.groupTableRowIdx == 0 { - state.groupTableRowIdx = groupPageSize(s) - } else { - state.groupTableRowIdx-- - } - } else { - if state.taskTableRowIdx == 0 { - state.taskTableRowIdx = len(state.tasks) - } else { - state.taskTableRowIdx-- - } - } - drawDash(s, &state, opts) - } else if ev.Key() == tcell.KeyEnter { - switch state.view { - case viewTypeQueues: - if state.queueTableRowIdx != 0 { - state.selectedQueue = state.queues[state.queueTableRowIdx-1] - state.view = viewTypeQueueDetails - state.taskState = asynq.TaskStateActive - state.tasks = nil - state.pageNum = 1 - go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, - taskPageSize(s), state.pageNum, tasksCh, errorCh) - ticker.Reset(interval) - drawDash(s, &state, opts) - } - case viewTypeQueueDetails: - if shouldShowGroupTable(&state) && state.groupTableRowIdx != 0 { - state.selectedGroup = state.groups[state.groupTableRowIdx-1] - state.tasks = nil - state.pageNum = 1 - go fetchAggregatingTasks(inspector, state.selectedQueue.Queue, state.selectedGroup.Group, - taskPageSize(s), state.pageNum, tasksCh, errorCh) - ticker.Reset(interval) - drawDash(s, &state, opts) - } - - } - } else if ev.Rune() == '?' { - state.prevView = state.view - state.view = viewTypeHelp - drawDash(s, &state, opts) - } else if ev.Key() == tcell.KeyF1 && state.view != viewTypeQueues { - go fetchQueues(inspector, queuesCh, errorCh, opts) - ticker.Reset(interval) - state.view = viewTypeQueues - drawDash(s, &state, opts) - } else if ev.Key() == tcell.KeyF2 && state.view != viewTypeServers { - //TODO Start data fetch and reset ticker - state.view = viewTypeServers - drawDash(s, &state, opts) - } else if ev.Key() == tcell.KeyF3 && state.view != viewTypeSchedulers { - //TODO Start data fetch and reset ticker - state.view = viewTypeSchedulers - drawDash(s, &state, opts) - } else if ev.Key() == tcell.KeyF4 && state.view != viewTypeRedis { - go fetchRedisInfo(redisInfoCh, errorCh) - ticker.Reset(interval) - state.view = viewTypeRedis - drawDash(s, &state, opts) - } else if (ev.Key() == tcell.KeyRight || ev.Rune() == 'l') && state.view == viewTypeQueueDetails { - state.taskState = nextTaskState(state.taskState) - state.pageNum = 1 - state.taskTableRowIdx = 0 - state.tasks = nil - state.selectedGroup = nil - if shouldShowGroupTable(&state) { - go fetchGroups(inspector, state.selectedQueue.Queue, groupsCh, errorCh) - } else { - go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, - taskPageSize(s), state.pageNum, tasksCh, errorCh) - } - ticker.Reset(interval) - drawDash(s, &state, opts) - } else if (ev.Key() == tcell.KeyLeft || ev.Rune() == 'h') && state.view == viewTypeQueueDetails { - state.taskState = prevTaskState(state.taskState) - state.pageNum = 1 - state.taskTableRowIdx = 0 - state.tasks = nil - state.selectedGroup = nil - if shouldShowGroupTable(&state) { - go fetchGroups(inspector, state.selectedQueue.Queue, groupsCh, errorCh) - } else { - go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, - taskPageSize(s), state.pageNum, tasksCh, errorCh) - } - ticker.Reset(interval) - drawDash(s, &state, opts) - } else if ev.Rune() == 'n' && state.view == viewTypeQueueDetails { - if shouldShowGroupTable(&state) { - pageSize := groupPageSize(s) - total := len(state.groups) - start := (state.pageNum - 1) * pageSize - end := start + pageSize - if end <= total { - state.pageNum++ - drawDash(s, &state, opts) - } - } else { - pageSize := taskPageSize(s) - totalCount := getTaskCount(state.selectedQueue, state.taskState) - if (state.pageNum-1)*pageSize+len(state.tasks) < totalCount { - state.pageNum++ - go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, - pageSize, state.pageNum, tasksCh, errorCh) - ticker.Reset(interval) - } - } - } else if ev.Rune() == 'p' && state.view == viewTypeQueueDetails { - if shouldShowGroupTable(&state) { - pageSize := groupPageSize(s) - start := (state.pageNum - 1) * pageSize - if start > 0 { - state.pageNum-- - drawDash(s, &state, opts) - } - } else { - if state.pageNum > 1 { - state.pageNum-- - go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, - taskPageSize(s), state.pageNum, tasksCh, errorCh) - ticker.Reset(interval) - } - } - } + h.HandleKeyEvent(ev) } case <-ticker.C: diff --git a/tools/asynq/cmd/dash/key_event.go b/tools/asynq/cmd/dash/key_event.go new file mode 100644 index 0000000..41bc092 --- /dev/null +++ b/tools/asynq/cmd/dash/key_event.go @@ -0,0 +1,405 @@ +// Copyright 2022 Kentaro Hibino. All rights reserved. +// Use of this source code is governed by a MIT license +// that can be found in the LICENSE file. + +package dash + +import ( + "os" + "time" + + "github.com/gdamore/tcell/v2" + "github.com/hibiken/asynq" +) + +type keyEventHandler struct { + s tcell.Screen + state *State + opts Options + done chan struct{} + + ticker *time.Ticker + inspector *asynq.Inspector + + errorCh chan error + queueCh chan *asynq.QueueInfo + queuesCh chan []*asynq.QueueInfo + groupsCh chan []*asynq.GroupInfo + tasksCh chan []*asynq.TaskInfo + redisInfoCh chan *redisInfo +} + +func (h *keyEventHandler) quit() { + h.s.Fini() + close(h.done) + os.Exit(0) +} + +func (h *keyEventHandler) HandleKeyEvent(ev *tcell.EventKey) { + if ev.Key() == tcell.KeyEscape || ev.Rune() == 'q' { + h.goBack() // Esc and 'q' key have "go back" semantics + } else if ev.Key() == tcell.KeyCtrlC { + h.quit() + } else if ev.Key() == tcell.KeyCtrlL { + h.s.Sync() + } else if ev.Key() == tcell.KeyDown || ev.Rune() == 'j' { + h.handleDownKey() + } else if ev.Key() == tcell.KeyUp || ev.Rune() == 'k' { + h.handleUpKey() + } else if ev.Key() == tcell.KeyRight || ev.Rune() == 'l' { + h.handleRightKey() + } else if ev.Key() == tcell.KeyLeft || ev.Rune() == 'h' { + h.handleLeftKey() + } else if ev.Key() == tcell.KeyEnter { + h.handleEnterKey() + } else if ev.Rune() == '?' { + h.showHelp() + } else if ev.Key() == tcell.KeyF1 { + h.showQueues() + } else if ev.Key() == tcell.KeyF2 { + h.showServers() + } else if ev.Key() == tcell.KeyF3 { + h.showSchedulers() + } else if ev.Key() == tcell.KeyF4 { + h.showRedisInfo() + } else if ev.Rune() == 'n' { + h.nextPage() + } else if ev.Rune() == 'p' { + h.prevPage() + } +} + +func (h *keyEventHandler) goBack() { + var ( + s = h.s + state = h.state + opts = h.opts + ) + if state.view == viewTypeHelp { + state.view = state.prevView // exit help + drawDash(s, state, opts) + } else if state.view == viewTypeQueueDetails { + state.view = viewTypeQueues + drawDash(s, state, opts) + } else { + h.quit() + } +} + +func (h *keyEventHandler) handleDownKey() { + switch h.state.view { + case viewTypeQueues: + h.downKeyQueues() + case viewTypeQueueDetails: + h.downKeyQueueDetails() + } +} + +func (h *keyEventHandler) downKeyQueues() { + if h.state.queueTableRowIdx < len(h.state.queues) { + h.state.queueTableRowIdx++ + } else { + h.state.queueTableRowIdx = 0 // loop back + } + drawDash(h.s, h.state, h.opts) +} + +func (h *keyEventHandler) downKeyQueueDetails() { + s, state, opts := h.s, h.state, h.opts + if shouldShowGroupTable(state) { + if state.groupTableRowIdx < groupPageSize(s) { + state.groupTableRowIdx++ + } else { + state.groupTableRowIdx = 0 // loop back + } + } else { + if state.taskTableRowIdx < len(state.tasks) { + state.taskTableRowIdx++ + } else { + state.taskTableRowIdx = 0 // loop back + } + } + drawDash(s, state, opts) +} + +func (h *keyEventHandler) handleUpKey() { + switch h.state.view { + case viewTypeQueues: + h.upKeyQueues() + case viewTypeQueueDetails: + h.upKeyQueueDetails() + } +} + +func (h *keyEventHandler) upKeyQueues() { + s, state, opts := h.s, h.state, h.opts + if state.queueTableRowIdx == 0 { + state.queueTableRowIdx = len(state.queues) + } else { + state.queueTableRowIdx-- + } + drawDash(s, state, opts) +} + +func (h *keyEventHandler) upKeyQueueDetails() { + s, state, opts := h.s, h.state, h.opts + if shouldShowGroupTable(state) { + if state.groupTableRowIdx == 0 { + state.groupTableRowIdx = groupPageSize(s) + } else { + state.groupTableRowIdx-- + } + } else { + if state.taskTableRowIdx == 0 { + state.taskTableRowIdx = len(state.tasks) + } else { + state.taskTableRowIdx-- + } + } + drawDash(s, state, opts) +} + +func (h *keyEventHandler) handleEnterKey() { + switch h.state.view { + case viewTypeQueues: + h.enterKeyQueues() + case viewTypeQueueDetails: + h.enterKeyQueueDetails() + } +} + +func (h *keyEventHandler) enterKeyQueues() { + var ( + s = h.s + state = h.state + opts = h.opts + inspector = h.inspector + ticker = h.ticker + errorCh = h.errorCh + tasksCh = h.tasksCh + ) + if state.queueTableRowIdx != 0 { + state.selectedQueue = state.queues[state.queueTableRowIdx-1] + state.view = viewTypeQueueDetails + state.taskState = asynq.TaskStateActive + state.tasks = nil + state.pageNum = 1 + go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, + taskPageSize(s), state.pageNum, tasksCh, errorCh) + ticker.Reset(opts.PollInterval) + drawDash(s, state, opts) + } +} + +func (h *keyEventHandler) enterKeyQueueDetails() { + var ( + s = h.s + state = h.state + opts = h.opts + inspector = h.inspector + ticker = h.ticker + errorCh = h.errorCh + tasksCh = h.tasksCh + ) + if shouldShowGroupTable(state) && state.groupTableRowIdx != 0 { + state.selectedGroup = state.groups[state.groupTableRowIdx-1] + state.tasks = nil + state.pageNum = 1 + go fetchAggregatingTasks(inspector, state.selectedQueue.Queue, state.selectedGroup.Group, + taskPageSize(s), state.pageNum, tasksCh, errorCh) + ticker.Reset(opts.PollInterval) + drawDash(s, state, opts) + } +} + +func (h *keyEventHandler) handleLeftKey() { + var ( + s = h.s + state = h.state + opts = h.opts + inspector = h.inspector + ticker = h.ticker + errorCh = h.errorCh + tasksCh = h.tasksCh + groupsCh = h.groupsCh + ) + if state.view == viewTypeQueueDetails { + state.taskState = prevTaskState(state.taskState) + state.pageNum = 1 + state.taskTableRowIdx = 0 + state.tasks = nil + state.selectedGroup = nil + if shouldShowGroupTable(state) { + go fetchGroups(inspector, state.selectedQueue.Queue, groupsCh, errorCh) + } else { + go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, + taskPageSize(s), state.pageNum, tasksCh, errorCh) + } + ticker.Reset(opts.PollInterval) + drawDash(s, state, opts) + } +} + +func (h *keyEventHandler) handleRightKey() { + var ( + s = h.s + state = h.state + opts = h.opts + inspector = h.inspector + ticker = h.ticker + errorCh = h.errorCh + tasksCh = h.tasksCh + groupsCh = h.groupsCh + ) + if state.view == viewTypeQueueDetails { + state.taskState = nextTaskState(state.taskState) + state.pageNum = 1 + state.taskTableRowIdx = 0 + state.tasks = nil + state.selectedGroup = nil + if shouldShowGroupTable(state) { + go fetchGroups(inspector, state.selectedQueue.Queue, groupsCh, errorCh) + } else { + go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, + taskPageSize(s), state.pageNum, tasksCh, errorCh) + } + ticker.Reset(opts.PollInterval) + drawDash(s, state, opts) + } +} + +func (h *keyEventHandler) nextPage() { + var ( + s = h.s + state = h.state + opts = h.opts + inspector = h.inspector + ticker = h.ticker + errorCh = h.errorCh + tasksCh = h.tasksCh + ) + if state.view == viewTypeQueueDetails { + if shouldShowGroupTable(state) { + pageSize := groupPageSize(s) + total := len(state.groups) + start := (state.pageNum - 1) * pageSize + end := start + pageSize + if end <= total { + state.pageNum++ + drawDash(s, state, opts) + } + } else { + pageSize := taskPageSize(s) + totalCount := getTaskCount(state.selectedQueue, state.taskState) + if (state.pageNum-1)*pageSize+len(state.tasks) < totalCount { + state.pageNum++ + go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, + pageSize, state.pageNum, tasksCh, errorCh) + ticker.Reset(opts.PollInterval) + } + } + } +} + +func (h *keyEventHandler) prevPage() { + var ( + s = h.s + state = h.state + opts = h.opts + inspector = h.inspector + ticker = h.ticker + errorCh = h.errorCh + tasksCh = h.tasksCh + ) + if state.view == viewTypeQueueDetails { + if shouldShowGroupTable(state) { + pageSize := groupPageSize(s) + start := (state.pageNum - 1) * pageSize + if start > 0 { + state.pageNum-- + drawDash(s, state, opts) + } + } else { + if state.pageNum > 1 { + state.pageNum-- + go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, + taskPageSize(s), state.pageNum, tasksCh, errorCh) + ticker.Reset(opts.PollInterval) + } + } + } +} + +func (h *keyEventHandler) showQueues() { + var ( + s = h.s + state = h.state + inspector = h.inspector + queuesCh = h.queuesCh + errorCh = h.errorCh + opts = h.opts + ticker = h.ticker + ) + if state.view != viewTypeQueues { + go fetchQueues(inspector, queuesCh, errorCh, opts) + ticker.Reset(opts.PollInterval) + state.view = viewTypeQueues + drawDash(s, state, opts) + } +} + +func (h *keyEventHandler) showServers() { + var ( + s = h.s + state = h.state + opts = h.opts + ) + if state.view != viewTypeServers { + //TODO Start data fetch and reset ticker + state.view = viewTypeServers + drawDash(s, state, opts) + } +} + +func (h *keyEventHandler) showSchedulers() { + var ( + s = h.s + state = h.state + opts = h.opts + ) + if state.view != viewTypeSchedulers { + //TODO Start data fetch and reset ticker + state.view = viewTypeSchedulers + drawDash(s, state, opts) + } +} + +func (h *keyEventHandler) showRedisInfo() { + var ( + s = h.s + state = h.state + opts = h.opts + redisInfoCh = h.redisInfoCh + errorCh = h.errorCh + ticker = h.ticker + ) + if state.view != viewTypeRedis { + go fetchRedisInfo(redisInfoCh, errorCh) + ticker.Reset(opts.PollInterval) + state.view = viewTypeRedis + drawDash(s, state, opts) + } +} + +func (h *keyEventHandler) showHelp() { + var ( + s = h.s + state = h.state + opts = h.opts + ) + if state.view != viewTypeHelp { + state.prevView = state.view + state.view = viewTypeHelp + drawDash(s, state, opts) + } +}