(cli): Refactor key event handlers

This commit is contained in:
Ken Hibino
2022-05-21 09:47:21 -07:00
parent 8b8b17650f
commit 0eecef2da6
2 changed files with 430 additions and 187 deletions

View File

@@ -55,8 +55,9 @@ type redisInfo struct {
} }
type Options struct { type Options struct {
DebugMode bool DebugMode bool
UseRealData bool UseRealData bool
PollInterval time.Duration
} }
var baseStyle = tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset) 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) { func Run(opts Options) {
s, err := tcell.NewScreen() s, err := tcell.NewScreen()
if err != nil { 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) os.Exit(1)
} }
if err := s.Init(); err != nil { 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) os.Exit(1)
} }
inspector := asynq.NewInspector(asynq.RedisClientOpt{Addr: ":6379"})
// Set default text style // Set default text style
s.SetStyle(baseStyle) s.SetStyle(baseStyle)
inspector := asynq.NewInspector(asynq.RedisClientOpt{Addr: ":6379"})
// channels to send/receive data fetched asynchronously // channels to send/receive data fetched asynchronously
var ( var (
errorCh = make(chan error) errorCh = make(chan error)
@@ -86,7 +86,6 @@ func Run(opts Options) {
tasksCh = make(chan []*asynq.TaskInfo) tasksCh = make(chan []*asynq.TaskInfo)
redisInfoCh = make(chan *redisInfo) redisInfoCh = make(chan *redisInfo)
) )
go fetchQueues(inspector, queuesCh, errorCh, opts) go fetchQueues(inspector, queuesCh, errorCh, opts)
var state State // contained in this goroutine only; do not share var state State // contained in this goroutine only; do not share
@@ -96,18 +95,28 @@ func Run(opts Options) {
eventCh := make(chan tcell.Event) eventCh := make(chan tcell.Event)
done := make(chan struct{}) done := make(chan struct{})
const interval = 2 * time.Second opts.PollInterval = 2 * time.Second
ticker := time.NewTicker(interval) ticker := time.NewTicker(opts.PollInterval)
defer ticker.Stop() 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. // TODO: Double check that we are not leaking goroutine with this one.
go s.ChannelEvents(eventCh, done) go s.ChannelEvents(eventCh, done)
quit := func() {
s.Fini()
close(done)
os.Exit(0)
}
for { for {
// Update screen // Update screen
s.Show() s.Show()
@@ -119,178 +128,7 @@ func Run(opts Options) {
case *tcell.EventResize: case *tcell.EventResize:
s.Sync() s.Sync()
case *tcell.EventKey: case *tcell.EventKey:
// Esc and 'q' key have "go back" semantics h.HandleKeyEvent(ev)
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)
}
}
}
} }
case <-ticker.C: case <-ticker.C:

View File

@@ -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)
}
}