Skip to content

Commit

Permalink
Refactor event handling
Browse files Browse the repository at this point in the history
Now event handlers can decide which handler will handle the next event.
This change simplifies logic to decide who should be handling events by
removing a unique, global, point of coordination that was not working
well on some situations (e.g. the container menu) and it was quite
fragile.

It is also easier now to decide what is going to be shown on the screen.

A thing that still needs to be solved is how to write unit tests for
event handlers.

It would also be good to find a design that will allow to get rid of
global variables.
  • Loading branch information
moncho committed Jul 21, 2018
1 parent 195b52d commit dfde37f
Show file tree
Hide file tree
Showing 21 changed files with 1,197 additions and 1,019 deletions.
213 changes: 157 additions & 56 deletions app/cmenu_events.go
@@ -1,177 +1,278 @@
package app

import (
"errors"
"fmt"
"strings"

"github.com/moncho/dry/appui"
"github.com/moncho/dry/docker"
"github.com/moncho/dry/ui"
"github.com/moncho/dry/ui/json"
termbox "github.com/nsf/termbox-go"
)

type cMenuEventHandler struct {
baseEventHandler
}

func (h *cMenuEventHandler) widget() appui.AppWidget {
return h.dry.widgetRegistry.ContainerMenu
}

func (h *cMenuEventHandler) handle(event termbox.Event) {
if h.forwardingEvents {
func (h *cMenuEventHandler) handle(event termbox.Event, f func(eventHandler)) {
if h.forwardingEvents() {
h.eventChan <- event
return
}
handled := false

handled := true
switch event.Key {

case termbox.KeyEsc:
handled = true
h.screen.Cursor.Reset()
h.dry.ShowContainers()
h.dry.SetViewMode(Main)
f(viewsToHandlers[Main])

case termbox.KeyEnter:
handled = true
err := h.widget().OnEvent(func(s string) error {
err := widgets.ContainerMenu.OnEvent(func(s string) error {
//s is a string made of two parts: an Id and a description
//separated by ":"
cd := strings.Split(s, ":")
if len(cd) != 2 {
return errors.New("Invalid command description: " + s)
}
id := cd[0]
command, err := docker.CommandFromDescription(cd[1])
if err != nil {
return err
}
h.handleCommand(id, command)
h.handleCommand(id, command, f)
return nil
})
if err != nil {
h.dry.appmessage(fmt.Sprintf("Could not run command: %s", err.Error()))
}
default:
handled = false
}

if !handled {
h.baseEventHandler.handle(event)
h.baseEventHandler.handle(event, f)
} else {
refreshScreen()
}
}

func (h *cMenuEventHandler) handleCommand(id string, command docker.Command) {
func (h *cMenuEventHandler) handleCommand(id string, command docker.Command, f func(eventHandler)) {

dry := h.dry
screen := h.screen

container := dry.dockerDaemon.ContainerByID(id)
switch command {
case docker.KILL:
prompt := appui.NewPrompt(
fmt.Sprintf("Do you want to kill container %s? (y/N)", id))
widgets.add(prompt)
h.setForwardEvents(true)

go func() {
events := ui.EventSource{
Events: h.eventChan,
EventHandledCallback: func(e termbox.Event) error {
return refreshScreen()
},
}
prompt.OnFocus(events)
conf, cancel := prompt.Text()
h.setForwardEvents(false)
widgets.remove(prompt)
if cancel || (conf != "y" && conf != "Y") {
return
}

dry.actionMessage(id, "Killing")
err := dry.dockerDaemon.Kill(id)
if err == nil {
dry.actionMessage(id, "killed")
widgets.ContainerMenu.ForContainer(id)
refreshScreen()
} else {
dry.errorMessage(id, "killing", err)
}

}()
case docker.RESTART:

prompt := appui.NewPrompt(
fmt.Sprintf("Do you want to restart container %s? (y/N)", id))
widgets.add(prompt)
h.setForwardEvents(true)

go func() {
if err := dry.dockerDaemon.RestartContainer(id); err != nil {
events := ui.EventSource{
Events: h.eventChan,
EventHandledCallback: func(e termbox.Event) error {
return refreshScreen()
},
}
prompt.OnFocus(events)
conf, cancel := prompt.Text()
h.setForwardEvents(false)
widgets.remove(prompt)
if cancel || (conf != "y" && conf != "Y") {

return
}

if err := dry.dockerDaemon.RestartContainer(id); err == nil {
widgets.ContainerMenu.ForContainer(id)
refreshScreen()
} else {
dry.appmessage(
fmt.Sprintf("Error restarting container %s, err: %s", id, err.Error()))
}

}()

case docker.STOP:

prompt := appui.NewPrompt(
fmt.Sprintf("Do you want to stop container %s? (y/N)", id))
widgets.add(prompt)
h.setForwardEvents(true)

go func() {
if err := dry.dockerDaemon.StopContainer(id); err != nil {
dry.appmessage(
fmt.Sprintf("Error stopping container %s, err: %s", id, err.Error()))
events := ui.EventSource{
Events: h.eventChan,
EventHandledCallback: func(e termbox.Event) error {
return refreshScreen()
},
}
prompt.OnFocus(events)
conf, cancel := prompt.Text()
h.setForwardEvents(false)
widgets.remove(prompt)
if cancel || (conf != "y" && conf != "Y") {

return
}

dry.actionMessage(id, "Stopping")
err := dry.dockerDaemon.StopContainer(id)
if err == nil {
widgets.ContainerMenu.ForContainer(id)
refreshScreen()
} else {
dry.errorMessage(id, "stopping", err)
}

}()
case docker.LOGS:
h.setForwardEvents(true)
prompt := logsPrompt()
dry.widgetRegistry.add(prompt)
widgets.add(prompt)
go func() {
events := ui.EventSource{
Events: h.eventChan,
Events: h.events(),
EventHandledCallback: func(e termbox.Event) error {
return refreshScreen()
},
}
prompt.OnFocus(events)
dry.widgetRegistry.remove(prompt)
widgets.remove(prompt)
since, canceled := prompt.Text()

if canceled {
h.setForwardEvents(false)
return
}

logs, err := h.dry.dockerDaemon.Logs(id, since)
if err == nil {
appui.Stream(logs, h.eventChan, func() {
h.setForwardEvents(false)
h.dry.SetViewMode(ContainerMenu)
h.closeViewChan <- struct{}{}
})
appui.Stream(logs, h.eventChan,
func() {
h.dry.SetViewMode(ContainerMenu)
f(h)
h.setForwardEvents(false)
refreshScreen()
})
} else {
h.dry.appmessage("Error showing container logs: " + err.Error())
h.setForwardEvents(false)
}
}()
case docker.RM:
prompt := appui.NewPrompt(
fmt.Sprintf("Do you want to remove container %s? (y/N)", id))
widgets.add(prompt)
h.setForwardEvents(true)

go func() {
events := ui.EventSource{
Events: h.eventChan,
EventHandledCallback: func(e termbox.Event) error {
return refreshScreen()
},
}
prompt.OnFocus(events)
conf, cancel := prompt.Text()
h.setForwardEvents(false)
widgets.remove(prompt)
if cancel || (conf != "y" && conf != "Y") {

return
}

dry.actionMessage(id, "Removing")
err := dry.dockerDaemon.Rm(id)
if err == nil {
dry.actionMessage(id, "removed")
f(viewsToHandlers[Main])
dry.SetViewMode(Main)
refreshScreen()
} else {
dry.errorMessage(id, "removing", err)
}

}()

case docker.STATS:

h.setForwardEvents(true)
h.dry.SetViewMode(NoView)
statsChan := dry.dockerDaemon.OpenChannel(container)
go statsScreen(container, statsChan, screen, h.eventChan,
func() {
h.setForwardEvents(false)
h.dry.SetViewMode(ContainerMenu)
h.closeViewChan <- struct{}{}
f(h)
h.setForwardEvents(false)
refreshScreen()
})

case docker.INSPECT:
h.setFocus(false)
container, err := h.dry.dockerDaemon.Inspect(id)
if err == nil {
go func() {
defer func() {
h.setFocus(true)
h.dry.SetViewMode(ContainerMenu)
h.closeViewChan <- struct{}{}
}()
v, err := json.NewViewer(
h.screen,
appui.DryTheme,
container)
if err != nil {
dry.appmessage(
fmt.Sprintf("Error inspecting container: %s", err.Error()))
return
}
v.Focus(h.eventChan)
}()
h.setForwardEvents(true)
err := inspect(
h.screen,
h.eventChan,
func(id string) (interface{}, error) {
return h.dry.dockerDaemon.Inspect(id)
},
func() {
h.dry.SetViewMode(ContainerMenu)
f(h)
h.setForwardEvents(false)
refreshScreen()
})(id)

if err != nil {
dry.appmessage(
fmt.Sprintf("Error inspecting container: %s", err.Error()))
return
}
case docker.HISTORY:
history, err := dry.dockerDaemon.History(container.ImageID)

if err == nil {
renderer := appui.NewDockerImageHistoryRenderer(history)
h.setFocus(false)
go appui.Less(renderer, screen, h.eventChan, h.closeViewChan)

go appui.Less(renderer, screen, h.eventChan, func() {
h.dry.SetViewMode(ContainerMenu)
f(h)
h.setForwardEvents(false)
refreshScreen()
})
} else {
dry.appmessage(
fmt.Sprintf("Error showing image history: %s", err.Error()))
Expand Down

0 comments on commit dfde37f

Please sign in to comment.