diff --git a/app/cmenu_events.go b/app/cmenu_events.go index 0cfcdfaa..57cc6c82 100644 --- a/app/cmenu_events.go +++ b/app/cmenu_events.go @@ -1,13 +1,13 @@ 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" ) @@ -15,50 +15,50 @@ 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 @@ -66,112 +66,213 @@ func (h *cMenuEventHandler) handleCommand(id string, command docker.Command) { 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())) diff --git a/app/container_events.go b/app/container_events.go index 529c0c97..075ec56e 100644 --- a/app/container_events.go +++ b/app/container_events.go @@ -7,53 +7,63 @@ import ( "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 commandToExecute struct { +type commandRunner struct { command docker.Command container *docker.Container } type containersScreenEventHandler struct { baseEventHandler + widget appui.AppWidget } -func (h *containersScreenEventHandler) widget() appui.AppWidget { - return h.dry.widgetRegistry.ContainerList -} - -func (h *containersScreenEventHandler) handle(event termbox.Event) { - if h.forwardingEvents { +func (h *containersScreenEventHandler) handle(event termbox.Event, f func(eventHandler)) { + if h.forwardingEvents() { h.eventChan <- event return } - focus, handled := handleKey(h, event.Key) + handled := h.handleKey(event.Key, f) if !handled { - focus, handled = handleCharacter(h, event.Ch) + handled = h.handleCharacter(event.Ch, f) } if handled { - h.setFocus(focus) - if h.hasFocus() { - refreshScreen() - } + refreshScreen() } else { - h.baseEventHandler.handle(event) + h.baseEventHandler.handle(event, f) } } -func (h *containersScreenEventHandler) handleCommand(command commandToExecute) { - - closeView := true +func (h *containersScreenEventHandler) handleCommand(command commandRunner, f func(eventHandler)) { dry := h.dry screen := h.screen - id := command.container.ID switch command.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 { @@ -61,26 +71,71 @@ func (h *containersScreenEventHandler) handleCommand(command commandToExecute) { } 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() { + 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 { 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 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 + } + if err := dry.dockerDaemon.StopContainer(id); err != nil { dry.appmessage( fmt.Sprintf("Error stopping container %s, err: %s", id, err.Error())) } + }() + case docker.LOGS: - closeView = false - h.setForwardEvents(true) prompt := logsPrompt() - dry.widgetRegistry.add(prompt) + widgets.add(prompt) + h.setForwardEvents(true) go func() { events := ui.EventSource{ Events: h.eventChan, @@ -89,7 +144,7 @@ func (h *containersScreenEventHandler) handleCommand(command commandToExecute) { }, } prompt.OnFocus(events) - dry.widgetRegistry.remove(prompt) + widgets.remove(prompt) since, canceled := prompt.Text() if canceled { @@ -100,17 +155,38 @@ func (h *containersScreenEventHandler) handleCommand(command commandToExecute) { logs, err := h.dry.dockerDaemon.Logs(id, since) if err == nil { appui.Stream(logs, h.eventChan, func() { - h.setForwardEvents(false) h.dry.SetViewMode(Main) - h.closeViewChan <- struct{}{} + f(viewsToHandlers[Main]) + 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 { @@ -118,210 +194,255 @@ func (h *containersScreenEventHandler) handleCommand(command commandToExecute) { } else { dry.errorMessage(id, "removing", err) } + }() case docker.STATS: c := dry.dockerDaemon.ContainerByID(id) if c == nil || !docker.IsContainerRunning(c) { dry.appmessage( - fmt.Sprintf("Container with id %s not found or is not running", id)) + fmt.Sprintf("Container with id %s not found or not running", id)) } else { statsChan := dry.dockerDaemon.OpenChannel(c) - closeView = false + h.setForwardEvents(true) + h.dry.SetViewMode(NoView) go statsScreen(command.container, statsChan, screen, h.eventChan, func() { - h.setForwardEvents(false) h.dry.SetViewMode(Main) - h.closeViewChan <- struct{}{} + f(viewsToHandlers[Main]) + h.setForwardEvents(false) + refreshScreen() }) } case docker.INSPECT: - container, err := h.dry.dockerDaemon.Inspect(id) - if err == nil { - go func() { - defer func() { - 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) - }() - closeView = false + h.setForwardEvents(true) + err := inspect( + h.screen, + h.eventChan, + func(id string) (interface{}, error) { + return h.dry.dockerDaemon.Inspect(id) + }, + func() { + h.dry.SetViewMode(Main) + h.setForwardEvents(false) + f(h) + refreshScreen() + })(id) + + if err != nil { + dry.appmessage( + fmt.Sprintf("Error inspecting container: %s", err.Error())) + return } + case docker.HISTORY: history, err := dry.dockerDaemon.History(command.container.ImageID) if err == nil { - closeView = false + renderer := appui.NewDockerImageHistoryRenderer(history) - go appui.Less(renderer, screen, h.eventChan, h.closeViewChan) + go appui.Less(renderer, screen, h.eventChan, func() { + h.dry.SetViewMode(Main) + f(viewsToHandlers[Main]) + h.setForwardEvents(false) + }) } else { dry.appmessage( fmt.Sprintf("Error showing image history: %s", err.Error())) } } - if closeView { - h.closeViewChan <- struct{}{} - } } -func handleCharacter(h *containersScreenEventHandler, key rune) (focus, handled bool) { - focus = true - handled = false +func (h *containersScreenEventHandler) handleCharacter(key rune, f func(eventHandler)) bool { + handled := true dry := h.dry switch key { case '%': //filter containers - handled = true - showFilterInput(h) + h.setForwardEvents(true) + applyFilter := func(filter string, canceled bool) { + if !canceled { + h.widget.Filter(filter) + } + h.setForwardEvents(false) + } + showFilterInput(newEventSource(h.eventChan), applyFilter) + case 'e', 'E': //remove - handled = true - if err := h.widget().OnEvent( + if err := h.widget.OnEvent( func(id string) error { - h.dry.appmessage("Removing container " + id) - container := dry.dockerDaemon.ContainerByID(id) if container == nil { return fmt.Errorf("Container with id %s not found", id) } - //Since a command is created the focus is handled by handleCommand - //Fixes #24 - focus = false - h.handleCommand(commandToExecute{ + h.handleCommand(commandRunner{ docker.RM, container, - }) + }, f) return nil }); err != nil { h.dry.appmessage("There was an error removing the container: " + err.Error()) } case 'i', 'I': //inspect - handled = true - if err := h.widget().OnEvent( + if err := h.widget.OnEvent( func(id string) error { container := dry.dockerDaemon.ContainerByID(id) if container == nil { return fmt.Errorf("Container with id %s not found", id) } - //Since a command is created the focus is handled by handleCommand - //Fixes #24 - focus = false - h.handleCommand(commandToExecute{ + h.handleCommand(commandRunner{ docker.INSPECT, container, - }) + }, f) return nil }); err != nil { h.dry.appmessage("There was an error inspecting the container: " + err.Error()) } case 'l', 'L': //logs - handled = true - if err := h.widget().OnEvent( + if err := h.widget.OnEvent( func(id string) error { container := dry.dockerDaemon.ContainerByID(id) if container == nil { return fmt.Errorf("Container with id %s not found", id) } - h.handleCommand(commandToExecute{ + h.handleCommand(commandRunner{ docker.LOGS, container, - }) + }, f) return nil }); err != nil { h.dry.appmessage("There was an error showing logs: " + err.Error()) } case 's', 'S': //stats - handled = true - if err := h.widget().OnEvent( + if err := h.widget.OnEvent( func(id string) error { container := dry.dockerDaemon.ContainerByID(id) if container == nil { return fmt.Errorf("Container with id %s not found", id) } - //Since a command is created the focus is handled by handleCommand - //Fixes #24 - focus = false - h.handleCommand(commandToExecute{ + h.handleCommand(commandRunner{ docker.STATS, container, - }) + }, f) return nil }); err != nil { h.dry.appmessage("There was an error showing stats: " + err.Error()) } + default: + handled = false } - return focus, handled + return handled } -func handleKey(h *containersScreenEventHandler, key termbox.Key) (bool, bool) { - focus := true +func (h *containersScreenEventHandler) handleKey(key termbox.Key, f func(eventHandler)) bool { handled := true cursor := h.screen.Cursor switch key { case termbox.KeyF1: //sort - h.widget().Sort() + h.widget.Sort() case termbox.KeyF2: //show all containers cursor.Reset() - h.dry.widgetRegistry.ContainerList.ToggleShowAllContainers() + widgets.ContainerList.ToggleShowAllContainers() case termbox.KeyF5: // refresh h.dry.appmessage("Refreshing container list") h.dry.dockerDaemon.Refresh(func(e error) { if e == nil { - h.widget().Unmount() + h.widget.Unmount() refreshScreen() } else { h.dry.appmessage("There was an error refreshing: " + e.Error()) } }) case termbox.KeyCtrlE: //remove all stopped - if confirmation, err := appui.ReadLine("All stopped containers will be removed. Do you want to continue? (y/N) "); err == nil { - h.screen.ClearAndFlush() - if confirmation == "Y" || confirmation == "y" { - h.dry.RemoveAllStoppedContainers() + prompt := appui.NewPrompt( + "All stopped containers will be removed. Do you want to continue? (y/N) ") + 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 + } + go func() { + h.dry.appmessage("Removing all stopped containers") + if count, err := h.dry.dockerDaemon.RemoveAllStoppedContainers(); err == nil { + h.dry.appmessage(fmt.Sprintf("Removed %d stopped containers", count)) + } else { + h.dry.appmessage( + fmt.Sprintf( + "Error removing all stopped containers: %s", err.Error())) + } + refreshScreen() + }() + }() + case termbox.KeyCtrlK: //kill - if err := h.widget().OnEvent( + if err := h.widget.OnEvent( func(id string) error { - h.dry.appmessage("Killing container " + id) - return h.dry.dockerDaemon.Kill(id) + container := h.dry.dockerDaemon.ContainerByID(id) + if container == nil { + return fmt.Errorf("Container with id %s not found", id) + } + h.handleCommand(commandRunner{ + docker.KILL, + container, + }, f) + return nil }); err != nil { - h.dry.appmessage("There was an error killing the container: " + err.Error()) + h.dry.appmessage("There was an error killing container: " + err.Error()) } case termbox.KeyCtrlR: //start - if err := h.widget().OnEvent( + if err := h.widget.OnEvent( func(id string) error { - h.dry.appmessage("Restarting container " + id) - - return h.dry.dockerDaemon.RestartContainer(id) + container := h.dry.dockerDaemon.ContainerByID(id) + if container == nil { + return fmt.Errorf("Container with id %s not found", id) + } + h.handleCommand(commandRunner{ + docker.RESTART, + container, + }, f) + return nil }); err != nil { - h.dry.appmessage("There was an error refreshing: " + err.Error()) + h.dry.appmessage("There was an error restarting: " + err.Error()) } case termbox.KeyCtrlT: //stop - if err := h.widget().OnEvent( + if err := h.widget.OnEvent( func(id string) error { - h.dry.appmessage("Stopping container " + id) - return h.dry.dockerDaemon.StopContainer(id) + container := h.dry.dockerDaemon.ContainerByID(id) + if container == nil { + return fmt.Errorf("Container with id %s not found", id) + } + h.handleCommand(commandRunner{ + docker.STOP, + container, + }, f) + return nil }); err != nil { - h.dry.appmessage("There was an error killing the container: " + err.Error()) + h.dry.appmessage("There was an error stopping container: " + err.Error()) } case termbox.KeyEnter: //Container menu showMenu := func(id string) error { h.screen.Cursor.Reset() - h.dry.ShowContainerMenu(id) + widgets.ContainerMenu.ForContainer(id) + h.dry.SetViewMode(ContainerMenu) + f(viewsToHandlers[ContainerMenu]) return refreshScreen() } - if err := h.widget().OnEvent(showMenu); err != nil { + if err := h.widget.OnEvent(showMenu); err != nil { h.dry.appmessage(err.Error()) } @@ -329,31 +450,28 @@ func handleKey(h *containersScreenEventHandler, key termbox.Key) (bool, bool) { handled = false } - return focus, handled + return handled } //statsScreen shows container stats on the screen //TODO move to appui -func statsScreen(container *docker.Container, stats *docker.StatsChannel, screen *ui.Screen, keyboardQueue chan termbox.Event, closeCallback func()) { - defer closeCallback() - screen.Clear() - +func statsScreen(container *docker.Container, stats *docker.StatsChannel, screen *ui.Screen, events <-chan termbox.Event, closeCallback func()) { if !docker.IsContainerRunning(container) { return } + defer closeCallback() + screen.ClearAndFlush() info, infoLines := appui.NewContainerInfo(container) screen.Render(1, info) - var mutex = &sync.Mutex{} - screen.Flush() - + var mutex sync.Mutex s := stats.Stats header := appui.NewMonitorTableHeader() header.SetX(0) header.SetWidth(ui.ActiveScreen.Dimensions.Width) - header.SetY(infoLines + 2) + header.SetY(infoLines + 3) statsRow := appui.NewContainerStatsRow(container, header) statsRow.SetX(0) @@ -363,17 +481,18 @@ func statsScreen(container *docker.Container, stats *docker.StatsChannel, screen loop: for { select { - case event := <-keyboardQueue: - switch event.Type { - case termbox.EventKey: - if event.Key == termbox.KeyEsc { - //the lock is acquired before breaking the loop - mutex.Lock() - s = nil - } + case event := <-events: + if event.Type == termbox.EventKey && event.Key == termbox.KeyEsc { + //the lock is acquired before breaking the loop + mutex.Lock() + s = nil } - case stat := <-s: + + case stat, ok := <-s: { + if !ok { + break loop + } mutex.Lock() statsRow.Update(container, stat) top, _ := appui.NewDockerTop( diff --git a/app/df_events.go b/app/df_events.go index 886ca2a4..47f6bb56 100644 --- a/app/df_events.go +++ b/app/df_events.go @@ -1,7 +1,11 @@ package app import ( + "fmt" + "time" + "github.com/moncho/dry/appui" + "github.com/moncho/dry/ui" termbox "github.com/nsf/termbox-go" ) @@ -13,36 +17,54 @@ type diskUsageScreenEventHandler struct { baseEventHandler } -func (h *diskUsageScreenEventHandler) widget() appui.AppWidget { - return nil -} - -func (h *diskUsageScreenEventHandler) handle(event termbox.Event) { +func (h *diskUsageScreenEventHandler) handle(event termbox.Event, f func(eventHandler)) { + if h.forwardingEvents() { + h.eventChan <- event + return + } handled := false - ignored := false switch event.Key { case termbox.KeyArrowUp | termbox.KeyArrowDown: //To avoid the base handler handling this - ignored = true handled = true } switch event.Ch { case 'p', 'P': handled = true - if confirmation, err := appui.ReadLine(confirmation); err == nil { - h.screen.ClearAndFlush() - if confirmation == "Y" || confirmation == "y" { - h.dry.Prune() + + rw := appui.NewPrompt(confirmation) + h.setForwardEvents(true) + widgets.add(rw) + go func() { + events := ui.EventSource{ + Events: h.eventChan, + EventHandledCallback: func(e termbox.Event) error { + return refreshScreen() + }, } - } - } - if handled { - h.setFocus(true) - if !ignored { refreshScreen() - } - } else { - h.baseEventHandler.handle(event) + + rw.OnFocus(events) + widgets.remove(rw) + confirmation, canceled := rw.Text() + h.setForwardEvents(false) + if canceled || (confirmation != "y" && confirmation != "Y") { + return + } + pr, err := h.dry.dockerDaemon.Prune() + if err == nil { + h.dry.cache.Add(pruneReport, pr, 30*time.Second) + refreshScreen() + } else { + h.dry.appmessage( + fmt.Sprintf( + "Error running prune. %s", err)) + } + + }() + } + if !handled { + h.baseEventHandler.handle(event, f) } } diff --git a/app/dry.go b/app/dry.go index dca8998b..088ad6ba 100644 --- a/app/dry.go +++ b/app/dry.go @@ -5,9 +5,7 @@ import ( "sync" "time" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/swarm" drydocker "github.com/moncho/dry/docker" "github.com/moncho/dry/ui" cache "github.com/patrickmn/go-cache" @@ -22,32 +20,20 @@ type state struct { //Dry represents the application. type Dry struct { - widgetRegistry *WidgetRegistry dockerDaemon drydocker.ContainerDaemon dockerEvents <-chan events.Message dockerEventsDone chan<- struct{} - info types.Info output chan string state *state //cache is a potential replacement for state cache *cache.Cache } -//changeViewMode changes the view mode of dry and refreshes the screen -func (d *Dry) changeViewMode(newViewMode viewMode) { - d.SetViewMode(newViewMode) - refreshScreen() -} - //SetViewMode changes the view mode of dry func (d *Dry) SetViewMode(newViewMode viewMode) { d.state.Lock() defer d.state.Unlock() - //If the new view is one of the main screens, it must be - //considered as the view to go back to. - if newViewMode.isMainScreen() { - d.state.previousViewMode = newViewMode - } + d.state.viewMode = newViewMode } @@ -67,18 +53,6 @@ func (d *Dry) Ok() (bool, error) { return d.dockerDaemon.Ok() } -//Prune runs docker prune -func (d *Dry) Prune() { - pr, err := d.dockerDaemon.Prune() - if err == nil { - d.cache.Add(pruneReport, pr, 30*time.Second) - } else { - d.appmessage( - fmt.Sprintf( - "Error running prune. %s", err)) - } -} - //PruneReport returns docker prune report, if any available func (d *Dry) PruneReport() *drydocker.PruneReport { if pr, ok := d.cache.Get(pruneReport); ok { @@ -87,164 +61,6 @@ func (d *Dry) PruneReport() *drydocker.PruneReport { return nil } -//RemoveAllStoppedContainers removes all stopped containers -func (d *Dry) RemoveAllStoppedContainers() { - d.appmessage(fmt.Sprintf("Removing all stopped containers")) - if count, err := d.dockerDaemon.RemoveAllStoppedContainers(); err == nil { - d.appmessage(fmt.Sprintf("Removed %d stopped containers", count)) - } else { - d.appmessage( - fmt.Sprintf( - "Error removing all stopped containers. %s", err)) - } -} - -//RemoveDanglingImages removes dangling images -func (d *Dry) RemoveDanglingImages() { - - d.appmessage("Removing dangling images") - if count, err := d.dockerDaemon.RemoveDanglingImages(); err == nil { - d.appmessage(fmt.Sprintf("Removed %d dangling images", count)) - } else { - d.appmessage( - fmt.Sprintf( - "Error removing dangling images. %s", err)) - } -} - -//RemoveImage removes the Docker image with the given id -func (d *Dry) RemoveImage(id string, force bool) { - shortID := drydocker.TruncateID(id) - d.appmessage(fmt.Sprintf("Removing image: %s", shortID)) - if _, err := d.dockerDaemon.Rmi(id, force); err == nil { - d.appmessage(fmt.Sprintf("Removed image: %s", shortID)) - } else { - d.appmessage(fmt.Sprintf("Error removing image %s: %s", shortID, err.Error())) - } -} - -//RemoveNetwork removes the Docker network with the given id -func (d *Dry) RemoveNetwork(id string) { - shortID := drydocker.TruncateID(id) - d.appmessage(fmt.Sprintf("Removing network: %s", shortID)) - if err := d.dockerDaemon.RemoveNetwork(id); err == nil { - d.appmessage(fmt.Sprintf("Removed network: %s", shortID)) - } else { - d.appmessage(fmt.Sprintf("Error network image %s: %s", shortID, err.Error())) - } -} - -//Rm removes the container with the given id -func (d *Dry) Rm(id string) { - shortID := drydocker.TruncateID(id) - d.actionMessage(shortID, "Removing") - if err := d.dockerDaemon.Rm(id); err == nil { - d.actionMessage(shortID, "Removed") - } else { - d.errorMessage(shortID, "removing", err) - } -} - -//ServiceInspect returns information about the service with the given ID -func (d *Dry) ServiceInspect(id string) (*swarm.Service, error) { - return d.dockerDaemon.Service(id) -} - -//ShowMainView changes the state of dry to show the main view, main views are -//the container list, the image list or the network list -func (d *Dry) ShowMainView() { - d.changeViewMode(d.state.previousViewMode) -} - -//ShowContainerMenu changes the state of dry to show the commands menu for the -//given container -func (d *Dry) ShowContainerMenu(cID string) { - d.widgetRegistry.ContainerMenu.ForContainer(cID) - d.changeViewMode(ContainerMenu) -} - -//ShowContainers changes the state of dry to show the container list -func (d *Dry) ShowContainers() { - d.changeViewMode(Main) -} - -//ShowDiskUsage changes the state of dry to show docker disk usage -func (d *Dry) ShowDiskUsage() { - d.changeViewMode(DiskUsage) -} - -//ShowDockerEvents changes the state of dry to show the log of docker events -func (d *Dry) ShowDockerEvents() { - d.changeViewMode(EventsMode) -} - -//ShowHelp changes the state of dry to show the extended help -func (d *Dry) ShowHelp() { - d.changeViewMode(HelpMode) -} - -//ShowImages changes the state of dry to show the list of Docker images reported -//by the daemon -func (d *Dry) ShowImages() { - d.changeViewMode(Images) -} - -//ShowInfo retrieves Docker Host info. -func (d *Dry) ShowInfo() error { - info, err := d.dockerDaemon.Info() - if err == nil { - d.changeViewMode(InfoMode) - d.info = info - return nil - } - return err - -} - -//ShowMonitor changes the state of dry to show the containers monitor -func (d *Dry) ShowMonitor() { - d.changeViewMode(Monitor) -} - -//ShowNetworks changes the state of dry to show the list of Docker networks reported -//by the daemon -func (d *Dry) ShowNetworks() { - d.changeViewMode(Networks) -} - -//ShowNodes changes the state of dry to show the node list -func (d *Dry) ShowNodes() { - d.changeViewMode(Nodes) -} - -//ShowServices changes the state of dry to show the service list -func (d *Dry) ShowServices() { - d.changeViewMode(Services) -} - -//ShowStacks changes the state of dry to show the stack list -func (d *Dry) ShowStacks() { - d.changeViewMode(Stacks) -} - -//ShowServiceTasks changes the state of dry to show the given service task list -func (d *Dry) ShowServiceTasks(serviceID string) { - d.widgetRegistry.ServiceTasks.ForService(serviceID) - d.changeViewMode(ServiceTasks) -} - -//ShowStackTasks changes the state of dry to show the given stack task list -func (d *Dry) ShowStackTasks(task string) { - d.widgetRegistry.StackTasks.ForStack(task) - d.changeViewMode(StackTasks) -} - -//ShowTasks changes the state of dry to show the given node task list -func (d *Dry) ShowTasks(nodeID string) { - d.widgetRegistry.NodeTasks.ForNode(nodeID) - d.changeViewMode(Tasks) -} - func (d *Dry) startDry() { de := dockerEventsListener{d} de.init() @@ -286,7 +102,8 @@ func newDry(screen *ui.Screen, d *drydocker.DockerDaemon) (*Dry, error) { previousViewMode: Main, } app := &Dry{} - app.widgetRegistry = NewWidgetRegistry(d) + widgets = newWidgetRegistry(d) + viewsToHandlers = initHandlers(app, screen) app.state = state app.dockerDaemon = d app.output = make(chan string) diff --git a/app/events.go b/app/events.go index f1b77301..5f435643 100644 --- a/app/events.go +++ b/app/events.go @@ -1,6 +1,7 @@ package app import ( + "fmt" "sync" "github.com/moncho/dry/appui" @@ -23,154 +24,254 @@ var viewsToHandlers = map[viewMode]eventHandler{ StackTasks: &stackTasksScreenEventHandler{}, } -var defaultHandler eventHandler - //eventHandler interface to handle termbox events type eventHandler interface { - getEventChan() chan termbox.Event - //handle handles a termbox event - handle(event termbox.Event) - //hasFocus returns true while the handler is processing events - hasFocus() bool - init(dry *Dry, - screen *ui.Screen, - keyboardQueueForView chan termbox.Event, - viewClosedChan chan struct{}) - setForwardEvents(forwardEvents bool) - widget() appui.AppWidget - widgetRegistry() *WidgetRegistry + events() chan termbox.Event + //handle handles the given termbox event, the given func can be + //used to set the handler of the next event + handle(event termbox.Event, nextHandler func(eventHandler)) } type baseEventHandler struct { - dry *Dry - screen *ui.Screen - eventChan chan termbox.Event - closeViewChan chan struct{} - focus bool - forwardingEvents bool - + dry *Dry + screen *ui.Screen + eventChan chan termbox.Event + forwarding bool sync.RWMutex } -func (b *baseEventHandler) getEventChan() chan termbox.Event { +func (b *baseEventHandler) events() chan termbox.Event { return b.eventChan } -func (b *baseEventHandler) handle(event termbox.Event) { +func (b *baseEventHandler) forwardingEvents() bool { + b.RLock() + defer b.RUnlock() + return b.forwarding +} + +func (b *baseEventHandler) setForwardEvents(t bool) { + b.Lock() + defer b.Unlock() + b.forwarding = t +} + +func (b *baseEventHandler) handle(event termbox.Event, f func(eventHandler)) { dry := b.dry screen := b.screen cursor := screen.Cursor - focus := true + refresh := true switch event.Key { case termbox.KeyArrowUp: //cursor up cursor.ScrollCursorUp() case termbox.KeyArrowDown: // cursor down cursor.ScrollCursorDown() - case termbox.KeyF8: // docker events - dry.ShowDiskUsage() + case termbox.KeyF8: // disk usage + f(viewsToHandlers[DiskUsage]) + dry.SetViewMode(DiskUsage) case termbox.KeyF9: // docker events - dry.ShowDockerEvents() - focus = false - go appui.Less(renderDry(dry), screen, b.eventChan, b.closeViewChan) + refresh = false + view := dry.viewMode() + dry.SetViewMode(EventsMode) + eh := &eventHandlerForwarder{ + eventChan: make(chan termbox.Event), + } + f(eh) + + renderer := appui.NewDockerEventsRenderer(dry.dockerDaemon.EventLog().Events()) + + go appui.Less(renderer, screen, eh.eventChan, func() { + dry.SetViewMode(view) + f(viewsToHandlers[view]) + refreshScreen() + }) case termbox.KeyF10: // docker info - dry.ShowInfo() - focus = false - go appui.Less(renderDry(dry), screen, b.eventChan, b.closeViewChan) + refresh = false + + view := dry.viewMode() + dry.SetViewMode(InfoMode) + + info, err := dry.dockerDaemon.Info() + if err == nil { + eh := &eventHandlerForwarder{ + eventChan: make(chan termbox.Event), + } + f(eh) + + renderer := appui.NewDockerInfoRenderer(info) + + go appui.Less(renderer, screen, eh.eventChan, func() { + dry.SetViewMode(view) + f(viewsToHandlers[view]) + refreshScreen() + }) + } else { + dry.appmessage( + fmt.Sprintf( + "There was an error retrieving Docker information: %s", err.Error())) + } } switch event.Ch { case '?', 'h', 'H': //help - focus = false - dry.ShowHelp() - go appui.Less(renderDry(dry), screen, b.eventChan, b.closeViewChan) + refresh = false + + view := dry.viewMode() + eh := &eventHandlerForwarder{ + eventChan: make(chan termbox.Event), + } + f(eh) + go appui.Less(ui.StringRenderer(help), screen, eh.eventChan, func() { + dry.SetViewMode(view) + f(viewsToHandlers[view]) + refreshScreen() + }) case '1': cursor.Reset() - dry.ShowContainers() + f(viewsToHandlers[Main]) + dry.SetViewMode(Main) case '2': cursor.Reset() - dry.ShowImages() + f(viewsToHandlers[Images]) + dry.SetViewMode(Images) case '3': cursor.Reset() - dry.ShowNetworks() + f(viewsToHandlers[Networks]) + dry.SetViewMode(Networks) case '4': cursor.Reset() - dry.ShowNodes() + f(viewsToHandlers[Nodes]) + dry.SetViewMode(Nodes) case '5': cursor.Reset() - dry.ShowServices() + f(viewsToHandlers[Services]) + dry.SetViewMode(Services) case '6': cursor.Reset() - dry.ShowStacks() + f(viewsToHandlers[Stacks]) + dry.SetViewMode(Stacks) case 'm', 'M': //monitor mode cursor.Reset() - dry.ShowMonitor() + f(viewsToHandlers[Monitor]) + dry.SetViewMode(Monitor) case 'g': //Cursor to the top cursor.Reset() case 'G': //Cursor to the bottom cursor.Bottom() } - - b.setFocus(focus) - if b.hasFocus() { + if refresh { refreshScreen() } -} - -func (b *baseEventHandler) hasFocus() bool { - b.RLock() - defer b.RUnlock() - return b.focus -} - -func (b *baseEventHandler) init(dry *Dry, - screen *ui.Screen, - keyboardQueueForView chan termbox.Event, - closeViewChan chan struct{}) { - b.dry = dry - b.screen = screen - b.eventChan = keyboardQueueForView - b.closeViewChan = closeViewChan -} -func (b *baseEventHandler) setFocus(focus bool) { - b.Lock() - defer b.Unlock() - b.focus = focus } -func (b *baseEventHandler) setForwardEvents(forward bool) { - b.forwardingEvents = forward -} +func initHandlers(dry *Dry, screen *ui.Screen) map[viewMode]eventHandler { + return map[viewMode]eventHandler{ + ContainerMenu: &cMenuEventHandler{ + baseEventHandler{ + dry: dry, + screen: screen, + eventChan: make(chan termbox.Event), + }, + }, + Images: &imagesScreenEventHandler{ + baseEventHandler{ + dry: dry, + screen: screen, + eventChan: make(chan termbox.Event), + }, + widgets.ImageList, + }, + Networks: &networksScreenEventHandler{ + baseEventHandler{ + dry: dry, + screen: screen, + eventChan: make(chan termbox.Event), + }, + widgets.Networks, + }, + DiskUsage: &diskUsageScreenEventHandler{ + baseEventHandler{ + dry: dry, + screen: screen, + eventChan: make(chan termbox.Event), + }, + }, + Main: &containersScreenEventHandler{ + baseEventHandler{ + dry: dry, + screen: screen, + eventChan: make(chan termbox.Event), + }, + widgets.ContainerList, + }, + Monitor: &monitorScreenEventHandler{ + baseEventHandler{ + dry: dry, + screen: screen, + eventChan: make(chan termbox.Event), + }, + widgets.Monitor, + }, + Nodes: &nodesScreenEventHandler{ + baseEventHandler{ + dry: dry, + screen: screen, + eventChan: make(chan termbox.Event), + }, + widgets.Nodes, + }, + Tasks: &taskScreenEventHandler{ + baseEventHandler{ + dry: dry, + screen: screen, + eventChan: make(chan termbox.Event), + }, + widgets.NodeTasks, + }, + Services: &servicesScreenEventHandler{ + baseEventHandler{ + dry: dry, + screen: screen, + eventChan: make(chan termbox.Event), + }, + widgets.ServiceList, + }, + ServiceTasks: &serviceTasksScreenEventHandler{ + baseEventHandler{ + dry: dry, + screen: screen, + eventChan: make(chan termbox.Event), + }, + widgets.ServiceTasks, + }, + Stacks: &stacksScreenEventHandler{ + baseEventHandler{ + dry: dry, + screen: screen, + eventChan: make(chan termbox.Event), + }, + widgets.Stacks, + }, + StackTasks: &stackTasksScreenEventHandler{ + baseEventHandler{ + dry: dry, + screen: screen, + eventChan: make(chan termbox.Event), + }, + widgets.StackTasks, + }, + } -func (b *baseEventHandler) widget() appui.AppWidget { - return nil } -func (b *baseEventHandler) widgetRegistry() *WidgetRegistry { - return b.dry.widgetRegistry +type eventHandlerForwarder struct { + eventChan chan termbox.Event } -type eventHandlerFactory struct { - dry *Dry - screen *ui.Screen - keyboardQueueForView chan termbox.Event - viewClosed chan struct{} - handlers map[viewMode]eventHandler - once sync.Once +func (b *eventHandlerForwarder) events() chan termbox.Event { + return b.eventChan } -//handlerFor creates eventHandlers -func (eh *eventHandlerFactory) handlerFor(view viewMode) eventHandler { - - eh.once.Do(func() { - defaultHandler = &baseEventHandler{} - defaultHandler.init(eh.dry, eh.screen, eh.keyboardQueueForView, eh.viewClosed) - eh.handlers = viewsToHandlers - for _, handler := range eh.handlers { - handler.init(eh.dry, eh.screen, eh.keyboardQueueForView, eh.viewClosed) - } - }) - if handler, ok := eh.handlers[view]; ok { - return handler - } - return defaultHandler +func (b *eventHandlerForwarder) handle(event termbox.Event, f func(eventHandler)) { + b.eventChan <- event } diff --git a/app/filter_event.go b/app/filter_event.go index 0995fdce..b80103d0 100644 --- a/app/filter_event.go +++ b/app/filter_event.go @@ -3,28 +3,14 @@ package app import ( "github.com/moncho/dry/appui" "github.com/moncho/dry/ui" - termbox "github.com/nsf/termbox-go" ) -func showFilterInput(eh eventHandler) { +func showFilterInput(es ui.EventSource, onDone func(string, bool)) { rw := appui.NewPrompt("Filter? (blank to remove current filter)") - eh.setForwardEvents(true) - eh.widgetRegistry().add(rw) + widgets.add(rw) go func() { - events := ui.EventSource{ - Events: eh.getEventChan(), - EventHandledCallback: func(e termbox.Event) error { - return refreshScreen() - }, - } - rw.OnFocus(events) - eh.widgetRegistry().remove(rw) - filter, canceled := rw.Text() - eh.setForwardEvents(false) - - if canceled { - return - } - eh.widget().Filter(filter) + rw.OnFocus(es) + widgets.remove(rw) + onDone(rw.Text()) }() } diff --git a/app/image_events.go b/app/image_events.go index 03662b76..1dd1d3b2 100644 --- a/app/image_events.go +++ b/app/image_events.go @@ -5,124 +5,210 @@ import ( "github.com/docker/docker/api/types" "github.com/moncho/dry/appui" + drydocker "github.com/moncho/dry/docker" "github.com/moncho/dry/ui" termbox "github.com/nsf/termbox-go" ) type imagesScreenEventHandler struct { baseEventHandler + widget *appui.DockerImagesWidget } -func (h *imagesScreenEventHandler) widget() appui.AppWidget { - return h.dry.widgetRegistry.ImageList -} - -func (h *imagesScreenEventHandler) handle(event termbox.Event) { - if h.forwardingEvents { +func (h *imagesScreenEventHandler) handle(event termbox.Event, f func(eventHandler)) { + if h.forwardingEvents() { h.eventChan <- event return } - - handled, keepFocus := h.handleKeyEvent(event.Key) + handled := h.handleKeyEvent(event.Key, f) if !handled { - handled, keepFocus = h.handleChEvent(event.Ch) + handled = h.handleChEvent(event.Ch, f) } if handled { - h.setFocus(keepFocus) - if h.hasFocus() { - refreshScreen() - } + refreshScreen() } else { - h.baseEventHandler.handle(event) + h.baseEventHandler.handle(event, f) } } -func (h *imagesScreenEventHandler) handleKeyEvent(key termbox.Key) (bool, bool) { - dry := h.dry - screen := h.screen - keepFocus := true +func (h *imagesScreenEventHandler) handleKeyEvent(key termbox.Key, f func(eventHandler)) bool { handled := true switch key { case termbox.KeyF1: //sort - h.dry.widgetRegistry.ImageList.Sort() + h.widget.Sort() case termbox.KeyF5: // refresh - h.widget().Unmount() + h.widget.Unmount() case termbox.KeyCtrlD: //remove dangling images - dry.RemoveDanglingImages() + prompt := appui.NewPrompt("Do you want to remove dangling images? (y/N)") + widgets.add(prompt) + h.setForwardEvents(true) + refreshScreen() + 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 + } + + h.dry.appmessage("Removing dangling images") + if count, err := h.dry.dockerDaemon.RemoveDanglingImages(); err == nil { + h.dry.appmessage(fmt.Sprintf("Removed %d dangling images", count)) + } else { + h.dry.appmessage( + fmt.Sprintf( + "Error removing dangling images. %s", err)) + } + refreshScreen() + + }() + case termbox.KeyCtrlE: //remove image - rmImage := func(id string) error { - dry.RemoveImage(id, false) - return nil - } - if err := h.widget().OnEvent(rmImage); err != nil { - dry.appmessage( - fmt.Sprintf("Error removing image: %s", err.Error())) - } + + prompt := appui.NewPrompt("Do you want to remove the selected image? (y/N)") + widgets.add(prompt) + h.setForwardEvents(true) + refreshScreen() + 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 + } + + rmImage := func(id string) error { + shortID := drydocker.TruncateID(id) + if _, err := h.dry.dockerDaemon.Rmi(id, false); err == nil { + h.dry.appmessage(fmt.Sprintf("Removed image: %s", shortID)) + } else { + h.dry.appmessage(fmt.Sprintf("Error removing image %s: %s", shortID, err.Error())) + } + return nil + } + if err := h.widget.OnEvent(rmImage); err != nil { + h.dry.appmessage( + fmt.Sprintf("Error removing image: %s", err.Error())) + } + refreshScreen() + + }() case termbox.KeyCtrlF: //force remove image - rmImage := func(id string) error { - dry.RemoveImage(id, true) - return nil - } - if err := h.widget().OnEvent(rmImage); err != nil { - dry.appmessage( - fmt.Sprintf("Error forcing image removal: %s", err.Error())) - } - case termbox.KeyEnter: //inspect image - inspectImage := func(id string) error { - image, err := h.dry.dockerDaemon.InspectImage(id) - if err != nil { - return err + prompt := appui.NewPrompt("Do you want to remove the selected image? (y/N)") + widgets.add(prompt) + h.setForwardEvents(true) + refreshScreen() + go func() { + events := ui.EventSource{ + Events: h.eventChan, + EventHandledCallback: func(e termbox.Event) error { + return refreshScreen() + }, } - keepFocus = false - renderer := appui.NewJSONRenderer(image) - go appui.Less(renderer, screen, h.eventChan, h.closeViewChan) - return nil - } - if err := h.widget().OnEvent(inspectImage); err != nil { - dry.appmessage( + prompt.OnFocus(events) + conf, cancel := prompt.Text() + h.setForwardEvents(false) + widgets.remove(prompt) + if cancel || (conf != "y" && conf != "Y") { + return + } + + rmImage := func(id string) error { + shortID := drydocker.TruncateID(id) + if _, err := h.dry.dockerDaemon.Rmi(id, true); err == nil { + h.dry.appmessage(fmt.Sprintf("Removed image: %s", shortID)) + } else { + h.dry.appmessage(fmt.Sprintf("Error removing image %s: %s", shortID, err.Error())) + } + return nil + } + if err := h.widget.OnEvent(rmImage); err != nil { + h.dry.appmessage( + fmt.Sprintf("Error forcing image removal: %s", err.Error())) + } + refreshScreen() + + }() + + case termbox.KeyEnter: //inspect image + h.setForwardEvents(true) + inspectImage := inspect( + h.screen, + h.eventChan, + func(id string) (interface{}, error) { + return h.dry.dockerDaemon.InspectImage(id) + }, + func() { + h.setForwardEvents(false) + h.dry.SetViewMode(Images) + f(h) + refreshScreen() + }) + + if err := h.widget.OnEvent(inspectImage); err != nil { + h.dry.appmessage( fmt.Sprintf("Error inspecting image: %s", err.Error())) } default: handled = false } - return handled, keepFocus + return handled } -func (h *imagesScreenEventHandler) handleChEvent(ch rune) (bool, bool) { +func (h *imagesScreenEventHandler) handleChEvent(ch rune, f func(eventHandler)) bool { dry := h.dry - screen := h.screen - keepFocus := true handled := true switch ch { case '2': //Ignore since dry is already on the images screen case 'i', 'I': //image history - history := func(id string) error { - history, err := h.dry.dockerDaemon.History(id) - if err != nil { - return err + showHistory := func(id string) error { + history, err := dry.dockerDaemon.History(id) + + if err == nil { + h.setForwardEvents(true) + renderer := appui.NewDockerImageHistoryRenderer(history) + + go appui.Less(renderer, h.screen, h.eventChan, func() { + h.dry.SetViewMode(Images) + f(h) + h.setForwardEvents(false) + refreshScreen() + }) } - keepFocus = false - renderer := appui.NewDockerImageHistoryRenderer(history) - go appui.Less(renderer, screen, h.eventChan, h.closeViewChan) - return nil + return err } - if err := h.widget().OnEvent(history); err != nil { + if err := h.widget.OnEvent(showHistory); err != nil { dry.appmessage(err.Error()) } case 'r', 'R': //Run container runImage := func(id string) error { + h.setForwardEvents(true) + defer h.setForwardEvents(false) image, err := h.dry.dockerDaemon.ImageByID(id) if err != nil { return err } rw := appui.NewImageRunWidget(image) - h.setForwardEvents(true) - dry.widgetRegistry.add(rw) + widgets.add(rw) go func(image types.ImageSummary) { events := ui.EventSource{ Events: h.eventChan, @@ -131,28 +217,33 @@ func (h *imagesScreenEventHandler) handleChEvent(ch rune) (bool, bool) { }, } rw.OnFocus(events) - dry.widgetRegistry.remove(rw) + widgets.remove(rw) runCommand, canceled := rw.Text() - h.setForwardEvents(false) if canceled { return } if err := dry.dockerDaemon.RunImage(image, runCommand); err != nil { dry.appmessage(err.Error()) } + }(image) return nil } - if err := h.widget().OnEvent(runImage); err != nil { + if err := h.widget.OnEvent(runImage); err != nil { dry.appmessage( fmt.Sprintf("Error running image: %s", err.Error())) } case '%': - handled = true - showFilterInput(h) + h.setForwardEvents(true) + applyFilter := func(filter string, canceled bool) { + if !canceled { + h.widget.Filter(filter) + } + h.setForwardEvents(false) + } + showFilterInput(newEventSource(h.eventChan), applyFilter) default: handled = false - } - return handled, keepFocus + return handled } diff --git a/app/loop.go b/app/loop.go index 5a89ad90..1bb39f8a 100644 --- a/app/loop.go +++ b/app/loop.go @@ -4,32 +4,13 @@ import ( "sync" "time" - "github.com/moncho/dry/appui" "github.com/moncho/dry/ui" "github.com/nsf/termbox-go" log "github.com/sirupsen/logrus" ) -type refresh func() error - -var refreshScreen refresh - -type focusTracker struct { - sync.Mutex - focus bool -} - -func (f *focusTracker) set(b bool) { - f.Lock() - defer f.Unlock() - f.focus = b -} - -func (f *focusTracker) hasFocus() bool { - f.Lock() - defer f.Unlock() - return f.focus -} +var refreshScreen func() error +var widgets *widgetRegistry //RenderLoop renders dry until it quits // nolint: gocyclo @@ -37,40 +18,30 @@ func RenderLoop(dry *Dry, screen *ui.Screen) { if ok, _ := dry.Ok(); !ok { return } - termuiEvents, done := ui.EventChannel() - keyboardQueue := make(chan termbox.Event) + //eventChan is a buffered channel so main loop can receive a new + //event while the previous one is being handled + eventChan := make(chan termbox.Event, 1) - viewClosed := make(chan struct{}) //On receive dry is rendered renderChan := make(chan struct{}) - //tracks if the main loop has the focus (and responds to events), - //or if events have to be delegated. - focus := &focusTracker{focus: true} - + var closingLock sync.RWMutex refreshScreen = func() error { - if focus.hasFocus() { - renderChan <- struct{}{} - } + closingLock.RLock() + defer closingLock.RUnlock() + renderChan <- struct{}{} return nil } - keyboardQueueForView := make(chan termbox.Event) dryOutputChan := dry.OuputChannel() - statusBar := ui.NewExpiringMessageWidget(0, ui.ActiveScreen.Dimensions.Width, appui.DryTheme) - eventHandlerFactory := &eventHandlerFactory{ - dry: dry, - screen: screen, - keyboardQueueForView: keyboardQueueForView, - viewClosed: viewClosed} defer close(done) - defer close(keyboardQueue) - defer close(keyboardQueueForView) - defer close(viewClosed) + defer close(eventChan) //make the global refreshScreen a noop before closing defer func() { + closingLock.Lock() + defer closingLock.Unlock() refreshScreen = func() error { return nil } @@ -83,55 +54,34 @@ func RenderLoop(dry *Dry, screen *ui.Screen) { for range renderChan { if !screen.Closing() { screen.Clear() - Render(dry, screen, statusBar) + render(dry, screen) } } }() refreshScreen() - //timer and status bar are shown if the main loop has the focus - go func(focus *focusTracker) { + go func() { + statusBar := widgets.MessageBar for { select { case dryMessage, ok := <-dryOutputChan: if ok { - if focus.hasFocus() { - statusBar.Message(dryMessage, 10*time.Second) - statusBar.Render() - } else { - //stop the status bar until the focus is retrieved - statusBar.Pause() - } + statusBar.Message(dryMessage, 10*time.Second) + statusBar.Render() } else { return } } } - }(focus) - - go func() { - for range viewClosed { - focus.set(true) - dry.ShowMainView() - } }() go func() { - for event := range keyboardQueue { - if focus.hasFocus() { - handler := eventHandlerFactory.handlerFor(dry.viewMode()) - if handler != nil { - handler.handle(event) - focus.set(handler.hasFocus()) - } - } else { - //Whoever has the focus, handles the event - select { - case keyboardQueueForView <- event: - default: - } - } + handler := viewsToHandlers[dry.viewMode()] + for event := range eventChan { + handler.handle(event, func(eh eventHandler) { + handler = eh + }) } }() @@ -143,18 +93,15 @@ loop: break loop case termbox.EventKey: //Ctrl+C breaks the loop (and exits dry) no matter what - if event.Key == termbox.KeyCtrlC || (focus.hasFocus() && (event.Ch == 'q' || event.Ch == 'Q')) { + if event.Key == termbox.KeyCtrlC || event.Ch == 'q' || event.Ch == 'Q' { break loop } else { - select { - case keyboardQueue <- event: - default: - } + eventChan <- event } case termbox.EventResize: ui.Resize() //Reload dry ui elements - dry.widgetRegistry = NewWidgetRegistry(dry.dockerDaemon) + widgets = newWidgetRegistry(dry.dockerDaemon) } } diff --git a/app/misc.go b/app/misc.go new file mode 100644 index 00000000..e542d45a --- /dev/null +++ b/app/misc.go @@ -0,0 +1,36 @@ +package app + +import ( + "github.com/moncho/dry/appui" + "github.com/moncho/dry/ui" + termbox "github.com/nsf/termbox-go" +) + +func logsPrompt() *appui.Prompt { + return appui.NewPrompt("Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes) or leave empty") +} + +func newEventSource(events <-chan termbox.Event) ui.EventSource { + return ui.EventSource{ + Events: events, + EventHandledCallback: func(e termbox.Event) error { + return refreshScreen() + }, + } +} + +func inspect( + screen *ui.Screen, + events <-chan termbox.Event, + inspect func(id string) (interface{}, error), + onClose func()) func(id string) error { + return func(id string) error { + inspected, err := inspect(id) + if err != nil { + return err + } + renderer := appui.NewJSONRenderer(inspected) + go appui.Less(renderer, screen, events, onClose) + return nil + } +} diff --git a/app/monitor_events.go b/app/monitor_events.go index 6442de45..6bca0a1e 100644 --- a/app/monitor_events.go +++ b/app/monitor_events.go @@ -7,36 +7,33 @@ import ( type monitorScreenEventHandler struct { baseEventHandler + widget *appui.Monitor } -func (h *monitorScreenEventHandler) widget() appui.AppWidget { - return h.dry.widgetRegistry.Monitor -} - -func (h *monitorScreenEventHandler) handle(event termbox.Event) { +func (h *monitorScreenEventHandler) handle(event termbox.Event, f func(eventHandler)) { handled := false cursor := h.screen.Cursor switch event.Key { case termbox.KeyArrowUp: //cursor up handled = true cursor.ScrollCursorUp() - h.widget().OnEvent(nil) + h.widget.OnEvent(nil) case termbox.KeyArrowDown: // cursor down handled = true cursor.ScrollCursorDown() - h.widget().OnEvent(nil) + h.widget.OnEvent(nil) } if !handled { switch event.Ch { case 'g': //Cursor to the top handled = true cursor.Reset() - h.widget().OnEvent(nil) + h.widget.OnEvent(nil) case 'G': //Cursor to the bottom handled = true cursor.Bottom() - h.widget().OnEvent(nil) + h.widget.OnEvent(nil) case 'H', 'h', 'q', '1', '2', '3', '4', '5': handled = false @@ -46,8 +43,6 @@ func (h *monitorScreenEventHandler) handle(event termbox.Event) { } } if !handled { - h.baseEventHandler.handle(event) - } else { - h.setFocus(true) + h.baseEventHandler.handle(event, f) } } diff --git a/app/network_events.go b/app/network_events.go index 9e2b153b..5bef2e4a 100644 --- a/app/network_events.go +++ b/app/network_events.go @@ -4,63 +4,90 @@ import ( "fmt" "github.com/moncho/dry/appui" + drydocker "github.com/moncho/dry/docker" + "github.com/moncho/dry/ui" termbox "github.com/nsf/termbox-go" ) type networksScreenEventHandler struct { baseEventHandler + widget *appui.DockerNetworksWidget } -func (h *networksScreenEventHandler) widget() appui.AppWidget { - return h.dry.widgetRegistry.Networks -} - -func (h *networksScreenEventHandler) handle(event termbox.Event) { - if h.forwardingEvents { +func (h *networksScreenEventHandler) handle(event termbox.Event, f func(eh eventHandler)) { + if h.forwardingEvents() { h.eventChan <- event return } - focus := true dry := h.dry screen := h.screen - handled := false + handled := true switch event.Key { case termbox.KeyF1: //sort - handled = true - h.widget().Sort() + h.widget.Sort() + refreshScreen() case termbox.KeyF5: // refresh - handled = true h.dry.appmessage("Refreshing network list") - h.widget().Unmount() - + h.widget.Unmount() + refreshScreen() case termbox.KeyEnter: //inspect - handled = true - inspectNetwork := func(id string) error { - network, err := h.dry.dockerDaemon.NetworkInspect(id) - if err != nil { - return err - } - focus = false - renderer := appui.NewJSONRenderer(network) - go appui.Less(renderer, screen, h.eventChan, h.closeViewChan) - return nil - } - if err := h.widget().OnEvent(inspectNetwork); err != nil { + inspectNetwork := inspect(screen, h.eventChan, + func(id string) (interface{}, error) { + return h.dry.dockerDaemon.NetworkInspect(id) + }, + func() { + h.dry.SetViewMode(Images) + f(viewsToHandlers[Images]) + h.setForwardEvents(false) + refreshScreen() + }) + h.setForwardEvents(true) + if err := h.widget.OnEvent(inspectNetwork); err != nil { dry.appmessage( fmt.Sprintf("Error inspecting image: %s", err.Error())) } case termbox.KeyCtrlE: //remove network - handled = true - rmNetwork := func(id string) error { - dry.RemoveNetwork(id) - return nil - } - if err := h.widget().OnEvent(rmNetwork); err != nil { - dry.appmessage( - fmt.Sprintf("Error removing network: %s", err.Error())) - } + prompt := appui.NewPrompt("Do you want to remove the selected network? (y/N)") + widgets.add(prompt) + h.setForwardEvents(true) + refreshScreen() + 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 + } + + rmNetwork := func(id string) error { + shortID := drydocker.TruncateID(id) + if err := h.dry.dockerDaemon.RemoveNetwork(id); err == nil { + h.dry.appmessage(fmt.Sprintf("Removed network: %s", shortID)) + } else { + h.dry.appmessage(fmt.Sprintf("Error network image %s: %s", shortID, err.Error())) + } + + return nil + } + if err := h.widget.OnEvent(rmNetwork); err != nil { + dry.appmessage( + fmt.Sprintf("Error removing network: %s", err.Error())) + } + refreshScreen() + + }() + + default: + handled = false } if !handled { switch event.Ch { @@ -69,15 +96,17 @@ func (h *networksScreenEventHandler) handle(event termbox.Event) { handled = true case '%': handled = true - showFilterInput(h) + h.setForwardEvents(true) + applyFilter := func(filter string, canceled bool) { + if !canceled { + h.widget.Filter(filter) + } + h.setForwardEvents(false) + } + showFilterInput(newEventSource(h.eventChan), applyFilter) } } - if handled { - h.setFocus(focus) - if h.hasFocus() { - refreshScreen() - } - } else { - h.baseEventHandler.handle(event) + if !handled { + h.baseEventHandler.handle(event, f) } } diff --git a/app/node_events.go b/app/node_events.go index 43e59889..94031eef 100644 --- a/app/node_events.go +++ b/app/node_events.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/moncho/dry/appui" + "github.com/moncho/dry/appui/swarm" "github.com/moncho/dry/docker" "github.com/moncho/dry/ui" termbox "github.com/nsf/termbox-go" @@ -11,33 +12,29 @@ import ( type nodesScreenEventHandler struct { baseEventHandler + widget *swarm.NodesWidget } -func (h *nodesScreenEventHandler) widget() appui.AppWidget { - return h.dry.widgetRegistry.Nodes -} - -func (h *nodesScreenEventHandler) handle(event termbox.Event) { - if h.forwardingEvents { +func (h *nodesScreenEventHandler) handle(event termbox.Event, f func(eventHandler)) { + if h.forwardingEvents() { h.eventChan <- event return } handled := false - focus := true switch event.Key { case termbox.KeyF1: //sort handled = true - h.dry.widgetRegistry.Nodes.Sort() + widgets.Nodes.Sort() case termbox.KeyF5: // refresh - h.widget().Unmount() + h.widget.Unmount() handled = true case termbox.KeyCtrlA: dry := h.dry rw := appui.NewPrompt("Changing node availability, please type one of ('active'|'pause'|'drain')") h.setForwardEvents(true) handled = true - dry.widgetRegistry.add(rw) + widgets.add(rw) go func() { events := ui.EventSource{ Events: h.eventChan, @@ -46,7 +43,7 @@ func (h *nodesScreenEventHandler) handle(event termbox.Event) { }, } rw.OnFocus(events) - dry.widgetRegistry.remove(rw) + widgets.remove(rw) availability, canceled := rw.Text() h.setForwardEvents(false) if canceled { @@ -70,32 +67,37 @@ func (h *nodesScreenEventHandler) handle(event termbox.Event) { } return refreshScreen() } - h.widget().OnEvent(changeNode) + h.widget.OnEvent(changeNode) }() case termbox.KeyEnter: showServices := func(nodeID string) error { h.screen.Cursor.Reset() - h.dry.ShowTasks(nodeID) + widgets.NodeTasks.ForNode(nodeID) + h.dry.SetViewMode(Tasks) + f(viewsToHandlers[Tasks]) return refreshScreen() } - h.widget().OnEvent(showServices) + h.widget.OnEvent(showServices) handled = true - } if !handled { switch event.Ch { case '%': handled = true - showFilterInput(h) + h.setForwardEvents(true) + applyFilter := func(filter string, canceled bool) { + if !canceled { + h.widget.Filter(filter) + } + h.setForwardEvents(false) + } + showFilterInput(newEventSource(h.eventChan), applyFilter) } } if !handled { - h.baseEventHandler.handle(event) + h.baseEventHandler.handle(event, f) } else { - h.setFocus(focus) - if h.hasFocus() { - refreshScreen() - } + refreshScreen() } } diff --git a/app/nodetasks_events.go b/app/nodetasks_events.go index 700c7758..182de4bc 100644 --- a/app/nodetasks_events.go +++ b/app/nodetasks_events.go @@ -3,55 +3,66 @@ package app import ( "fmt" - "github.com/moncho/dry/appui" + "github.com/moncho/dry/appui/swarm" termbox "github.com/nsf/termbox-go" ) type taskScreenEventHandler struct { baseEventHandler + widget *swarm.NodeTasksWidget } -func (h *taskScreenEventHandler) widget() appui.AppWidget { - return h.dry.widgetRegistry.NodeTasks -} - -func (h *taskScreenEventHandler) handle(event termbox.Event) { - if h.forwardingEvents { +func (h *taskScreenEventHandler) handle(event termbox.Event, f func(eventHandler)) { + if h.forwardingEvents() { h.eventChan <- event return } - handled := false - focus := true + handled := true switch event.Key { case termbox.KeyEsc: - handled = true - h.dry.ShowNodes() + f(viewsToHandlers[Nodes]) + h.dry.SetViewMode(Nodes) case termbox.KeyF1: //sort - handled = true - h.dry.widgetRegistry.NodeTasks.Sort() + widgets.NodeTasks.Sort() case termbox.KeyF5: // refresh - h.widget().Unmount() - handled = true + h.widget.Unmount() case termbox.KeyEnter: - handled = true - focus = false - if err := h.widget().OnEvent(inspectTask(h.dry, h.screen, h.eventChan, h.closeViewChan)); err != nil { + h.setForwardEvents(true) + if err := h.widget.OnEvent( + inspect( + h.screen, + h.eventChan, + func(id string) (interface{}, error) { + return h.dry.dockerDaemon.Task(id) + }, + func() { + h.dry.SetViewMode(Tasks) + f(h) + h.setForwardEvents(false) + })); err != nil { h.dry.appmessage( fmt.Sprintf("Error inspecting stack: %s", err.Error())) } - + default: + handled = false } if !handled { switch event.Ch { case '%': handled = true - showFilterInput(h) + h.setForwardEvents(true) + applyFilter := func(filter string, canceled bool) { + if !canceled { + h.widget.Filter(filter) + } + h.setForwardEvents(false) + } + showFilterInput(newEventSource(h.eventChan), applyFilter) } } if !handled { - h.baseEventHandler.handle(event) + h.baseEventHandler.handle(event, f) } else { - h.setFocus(focus) refreshScreen() } diff --git a/app/render.go b/app/render.go index f61e25f8..96c7e93f 100644 --- a/app/render.go +++ b/app/render.go @@ -11,14 +11,14 @@ import ( var cancelMonitorWidget context.CancelFunc -//Render renders dry on the given screen -func Render(d *Dry, screen *ui.Screen, statusBar *ui.ExpiringMessageWidget) { +//render renders dry on the given screen +func render(d *Dry, screen *ui.Screen) { var bufferers []gizaktermui.Bufferer var count int var keymap string var viewRenderer ui.Renderer - di := d.widgetRegistry.DockerInfo + di := widgets.DockerInfo bufferers = append(bufferers, di) //if the monitor widget is active it is now cancelled since (most likely) the view is going to change now @@ -29,7 +29,7 @@ func Render(d *Dry, screen *ui.Screen, statusBar *ui.ExpiringMessageWidget) { switch d.viewMode() { case ContainerMenu: { - cMenu := d.widgetRegistry.ContainerMenu + cMenu := widgets.ContainerMenu if err := cMenu.Mount(); err != nil { screen.Render(1, err.Error()) } @@ -41,7 +41,7 @@ func Render(d *Dry, screen *ui.Screen, statusBar *ui.ExpiringMessageWidget) { } case Main: { - containersWidget := d.widgetRegistry.ContainerList + containersWidget := widgets.ContainerList if err := containersWidget.Mount(); err != nil { screen.Render(1, err.Error()) } @@ -53,7 +53,7 @@ func Render(d *Dry, screen *ui.Screen, statusBar *ui.ExpiringMessageWidget) { case Images: { - widget := d.widgetRegistry.ImageList + widget := widgets.ImageList if err := widget.Mount(); err != nil { screen.Render(1, err.Error()) } @@ -65,7 +65,7 @@ func Render(d *Dry, screen *ui.Screen, statusBar *ui.ExpiringMessageWidget) { } case Networks: { - widget := d.widgetRegistry.Networks + widget := widgets.Networks if err := widget.Mount(); err != nil { screen.Render(1, err.Error()) } @@ -75,7 +75,7 @@ func Render(d *Dry, screen *ui.Screen, statusBar *ui.ExpiringMessageWidget) { } case Nodes: { - nodes := d.widgetRegistry.Nodes + nodes := widgets.Nodes if err := nodes.Mount(); err != nil { screen.Render(1, err.Error()) } @@ -85,7 +85,7 @@ func Render(d *Dry, screen *ui.Screen, statusBar *ui.ExpiringMessageWidget) { } case Services: { - servicesWidget := d.widgetRegistry.ServiceList + servicesWidget := widgets.ServiceList if err := servicesWidget.Mount(); err != nil { screen.Render(1, err.Error()) } @@ -95,7 +95,7 @@ func Render(d *Dry, screen *ui.Screen, statusBar *ui.ExpiringMessageWidget) { } case Tasks: { - tasks := d.widgetRegistry.NodeTasks + tasks := widgets.NodeTasks if err := tasks.Mount(); err != nil { screen.Render(1, err.Error()) } @@ -105,7 +105,7 @@ func Render(d *Dry, screen *ui.Screen, statusBar *ui.ExpiringMessageWidget) { } case ServiceTasks: { - tasks := d.widgetRegistry.ServiceTasks + tasks := widgets.ServiceTasks if err := tasks.Mount(); err != nil { screen.Render(1, err.Error()) } @@ -115,7 +115,7 @@ func Render(d *Dry, screen *ui.Screen, statusBar *ui.ExpiringMessageWidget) { } case Stacks: { - stacks := d.widgetRegistry.Stacks + stacks := widgets.Stacks if err := stacks.Mount(); err != nil { screen.Render(1, err.Error()) } @@ -125,7 +125,7 @@ func Render(d *Dry, screen *ui.Screen, statusBar *ui.ExpiringMessageWidget) { } case StackTasks: { - tasks := d.widgetRegistry.StackTasks + tasks := widgets.StackTasks if err := tasks.Mount(); err != nil { screen.Render(1, err.Error()) } @@ -136,8 +136,8 @@ func Render(d *Dry, screen *ui.Screen, statusBar *ui.ExpiringMessageWidget) { case DiskUsage: { if du, err := d.dockerDaemon.DiskUsage(); err == nil { - d.widgetRegistry.DiskUsage.PrepareToRender(&du, d.PruneReport()) - viewRenderer = d.widgetRegistry.DiskUsage + widgets.DiskUsage.PrepareToRender(&du, d.PruneReport()) + viewRenderer = widgets.DiskUsage } else { screen.Render(1, @@ -147,7 +147,7 @@ func Render(d *Dry, screen *ui.Screen, statusBar *ui.ExpiringMessageWidget) { } case Monitor: { - monitor := d.widgetRegistry.Monitor + monitor := widgets.Monitor monitor.Mount() ctx, cancel := context.WithCancel(context.Background()) monitor.RenderLoop(ctx) @@ -160,37 +160,19 @@ func Render(d *Dry, screen *ui.Screen, statusBar *ui.ExpiringMessageWidget) { updateCursorPosition(screen.Cursor, count) bufferers = append(bufferers, footer(keymap)) - statusBar.Render() + widgets.MessageBar.Render() screen.RenderBufferer(bufferers...) if viewRenderer != nil { screen.RenderRenderer(appui.MainScreenHeaderSize, viewRenderer) } - for _, widget := range d.widgetRegistry.activeWidgets { + for _, widget := range widgets.activeWidgets { screen.RenderBufferer(widget) } screen.Flush() } -//renderDry returns a Renderer with dry's current content -func renderDry(d *Dry) ui.Renderer { - var output ui.Renderer - switch d.viewMode() { - case EventsMode: - output = appui.NewDockerEventsRenderer(d.dockerDaemon.EventLog().Events()) - case HelpMode: - output = ui.StringRenderer(help) - case InfoMode: - output = appui.NewDockerInfoRenderer(d.info) - default: - { - output = ui.StringRenderer("Dry is not ready yet for rendering, be patient...") - } - } - return output -} - func footer(mapping string) *termui.MarkupPar { par := termui.NewParFromMarkupText(appui.DryTheme, mapping) diff --git a/app/service_events.go b/app/service_events.go index f5300ee8..a13be26d 100644 --- a/app/service_events.go +++ b/app/service_events.go @@ -5,44 +5,37 @@ import ( "strconv" "github.com/moncho/dry/appui" + "github.com/moncho/dry/appui/swarm" "github.com/moncho/dry/ui" - "github.com/moncho/dry/ui/json" termbox "github.com/nsf/termbox-go" ) type servicesScreenEventHandler struct { baseEventHandler + widget *swarm.ServicesWidget } -func (h *servicesScreenEventHandler) widget() appui.AppWidget { - return h.dry.widgetRegistry.ServiceList -} - -func (h *servicesScreenEventHandler) handle(event termbox.Event) { - if h.forwardingEvents { +func (h *servicesScreenEventHandler) handle(event termbox.Event, f func(eventHandler)) { + if h.forwardingEvents() { h.eventChan <- event return } - handled := false - focus := true + handled := true dry := h.dry switch event.Key { case termbox.KeyF1: // refresh - h.dry.widgetRegistry.ServiceList.Sort() - handled = true + widgets.ServiceList.Sort() case termbox.KeyF5: // refresh h.dry.appmessage("Refreshing the service list") - if err := h.widget().Unmount(); err != nil { + if err := h.widget.Unmount(); err != nil { h.dry.appmessage("There was an error refreshing the service list: " + err.Error()) } - handled = true case termbox.KeyCtrlR: rw := appui.NewPrompt("The selected service will be removed. Do you want to proceed? y/N") h.setForwardEvents(true) - handled = true - dry.widgetRegistry.add(rw) + widgets.add(rw) go func() { events := ui.EventSource{ Events: h.eventChan, @@ -51,7 +44,7 @@ func (h *servicesScreenEventHandler) handle(event termbox.Event) { }, } rw.OnFocus(events) - dry.widgetRegistry.remove(rw) + widgets.remove(rw) confirmation, canceled := rw.Text() h.setForwardEvents(false) if canceled || (confirmation != "y" && confirmation != "Y") { @@ -62,7 +55,7 @@ func (h *servicesScreenEventHandler) handle(event termbox.Event) { refreshScreen() return err } - if err := h.widget().OnEvent(removeService); err != nil { + if err := h.widget.OnEvent(removeService); err != nil { h.dry.appmessage("There was an error removing the service: " + err.Error()) } }() @@ -71,8 +64,7 @@ func (h *servicesScreenEventHandler) handle(event termbox.Event) { rw := appui.NewPrompt("Scale service. Number of replicas?") h.setForwardEvents(true) - handled = true - dry.widgetRegistry.add(rw) + widgets.add(rw) go func() { events := ui.EventSource{ Events: h.eventChan, @@ -81,7 +73,7 @@ func (h *servicesScreenEventHandler) handle(event termbox.Event) { }, } rw.OnFocus(events) - dry.widgetRegistry.remove(rw) + widgets.remove(rw) replicas, canceled := rw.Text() h.setForwardEvents(false) if canceled { @@ -103,7 +95,7 @@ func (h *servicesScreenEventHandler) handle(event termbox.Event) { refreshScreen() return err } - if err := h.widget().OnEvent(scaleService); err != nil { + if err := h.widget.OnEvent(scaleService); err != nil { h.dry.appmessage("There was an error scaling the service: " + err.Error()) } }() @@ -111,44 +103,43 @@ func (h *servicesScreenEventHandler) handle(event termbox.Event) { case termbox.KeyEnter: showTasks := func(serviceID string) error { h.screen.Cursor.Reset() - h.dry.ShowServiceTasks(serviceID) + widgets.ServiceTasks.ForService(serviceID) + f(viewsToHandlers[ServiceTasks]) + dry.SetViewMode(ServiceTasks) return refreshScreen() } - h.widget().OnEvent(showTasks) - handled = true + h.widget.OnEvent(showTasks) + default: + handled = false } switch event.Ch { case '%': handled = true - showFilterInput(h) + h.setForwardEvents(true) + applyFilter := func(filter string, canceled bool) { + if !canceled { + h.widget.Filter(filter) + } + h.setForwardEvents(false) + } + showFilterInput(newEventSource(h.eventChan), applyFilter) case 'i' | 'I': handled = true + h.setForwardEvents(true) + inspectService := inspect( + h.screen, + h.eventChan, + func(id string) (interface{}, error) { + return h.dry.dockerDaemon.Service(id) + }, + func() { + h.setForwardEvents(false) + h.dry.SetViewMode(Services) + f(h) + refreshScreen() + }) - inspectService := func(serviceID string) error { - service, err := h.dry.ServiceInspect(serviceID) - if err == nil { - go func() { - defer func() { - h.closeViewChan <- struct{}{} - }() - v, err := json.NewViewer( - h.screen, - appui.DryTheme, - service) - if err != nil { - dry.appmessage( - fmt.Sprintf("Error inspecting service: %s", err.Error())) - return - } - v.Focus(h.eventChan) - }() - return nil - } - return err - } - if err := h.widget().OnEvent(inspectService); err == nil { - focus = false - } else { + if err := h.widget.OnEvent(inspectService); err != nil { h.dry.appmessage("There was an error inspecting the service: " + err.Error()) } @@ -157,16 +148,10 @@ func (h *servicesScreenEventHandler) handle(event termbox.Event) { prompt := logsPrompt() h.setForwardEvents(true) handled = true - dry.widgetRegistry.add(prompt) + widgets.add(prompt) go func() { - events := ui.EventSource{ - Events: h.eventChan, - EventHandledCallback: func(e termbox.Event) error { - return refreshScreen() - }, - } - prompt.OnFocus(events) - dry.widgetRegistry.remove(prompt) + prompt.OnFocus(newEventSource(h.eventChan)) + widgets.remove(prompt) since, canceled := prompt.Text() if canceled { @@ -180,13 +165,14 @@ func (h *servicesScreenEventHandler) handle(event termbox.Event) { appui.Stream(logs, h.eventChan, func() { h.setForwardEvents(false) - h.closeViewChan <- struct{}{} + h.dry.SetViewMode(Services) + f(h) }) return nil } return err } - if err := h.widget().OnEvent(showServiceLogs); err != nil { + if err := h.widget.OnEvent(showServiceLogs); err != nil { h.dry.appmessage("There was an error showing service logs: " + err.Error()) h.setForwardEvents(false) @@ -194,15 +180,8 @@ func (h *servicesScreenEventHandler) handle(event termbox.Event) { }() } if !handled { - h.baseEventHandler.handle(event) + h.baseEventHandler.handle(event, f) } else { - h.setFocus(focus) - if h.hasFocus() { - refreshScreen() - } + refreshScreen() } } - -func logsPrompt() *appui.Prompt { - return appui.NewPrompt("Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes) or leave empty") -} diff --git a/app/servicetasks_events.go b/app/servicetasks_events.go index 853f4564..09cf4f9d 100644 --- a/app/servicetasks_events.go +++ b/app/servicetasks_events.go @@ -3,74 +3,70 @@ package app import ( "fmt" - "github.com/moncho/dry/appui" - "github.com/moncho/dry/ui" + "github.com/moncho/dry/appui/swarm" termbox "github.com/nsf/termbox-go" ) type serviceTasksScreenEventHandler struct { baseEventHandler + widget *swarm.ServiceTasksWidget } -func (h *serviceTasksScreenEventHandler) widget() appui.AppWidget { - return h.dry.widgetRegistry.ServiceTasks -} - -func (h *serviceTasksScreenEventHandler) handle(event termbox.Event) { - if h.forwardingEvents { +func (h *serviceTasksScreenEventHandler) handle(event termbox.Event, f func(eventHandler)) { + if h.forwardingEvents() { h.eventChan <- event return } - handled := false - focus := true + handled := true switch event.Key { case termbox.KeyEsc: - handled = true - h.dry.ShowServices() + + f(viewsToHandlers[Services]) + h.dry.SetViewMode(Services) case termbox.KeyF1: //sort - handled = true - h.dry.widgetRegistry.ServiceTasks.Sort() + widgets.ServiceTasks.Sort() case termbox.KeyF5: // refresh - h.widget().Unmount() - handled = true + h.widget.Unmount() + case termbox.KeyEnter: - handled = true - focus = false - if err := h.widget().OnEvent(inspectTask(h.dry, h.screen, h.eventChan, h.closeViewChan)); err != nil { + if err := h.widget.OnEvent( + inspect( + h.screen, + h.eventChan, + func(id string) (interface{}, error) { + return h.dry.dockerDaemon.Task(id) + }, + func() { + h.dry.SetViewMode(ServiceTasks) + h.setForwardEvents(false) + f(h) + })); err != nil { h.dry.appmessage( fmt.Sprintf("Error inspecting stack: %s", err.Error())) } + default: + handled = false } if !handled { switch event.Ch { case '%': handled = true - showFilterInput(h) + h.setForwardEvents(true) + applyFilter := func(filter string, canceled bool) { + if !canceled { + h.widget.Filter(filter) + } + h.setForwardEvents(false) + } + showFilterInput(newEventSource(h.eventChan), applyFilter) } } if !handled { - h.baseEventHandler.handle(event) + h.baseEventHandler.handle(event, f) } else { - h.setFocus(focus) refreshScreen() } } - -func inspectTask( - dry *Dry, - screen *ui.Screen, - eventChan chan termbox.Event, - closeViewChan chan struct{}) func(id string) error { - return func(id string) error { - stack, err := dry.dockerDaemon.Task(id) - if err != nil { - return err - } - renderer := appui.NewJSONRenderer(stack) - go appui.Less(renderer, screen, eventChan, closeViewChan) - return nil - } -} diff --git a/app/stack_events.go b/app/stack_events.go index 7809dfbe..1e0a0d96 100644 --- a/app/stack_events.go +++ b/app/stack_events.go @@ -4,46 +4,41 @@ import ( "fmt" "github.com/moncho/dry/appui" + "github.com/moncho/dry/appui/swarm" "github.com/moncho/dry/ui" termbox "github.com/nsf/termbox-go" ) type stacksScreenEventHandler struct { baseEventHandler + widget *swarm.StacksWidget } -func (h *stacksScreenEventHandler) widget() appui.AppWidget { - return h.dry.widgetRegistry.Stacks -} - -func (h *stacksScreenEventHandler) handle(event termbox.Event) { - if h.forwardingEvents { +func (h *stacksScreenEventHandler) handle(event termbox.Event, f func(eventHandler)) { + if h.forwardingEvents() { h.eventChan <- event return } - focus := true - handled := false + handled := true switch event.Key { case termbox.KeyF1: //sort - handled = true - h.dry.widgetRegistry.Stacks.Sort() + + widgets.Stacks.Sort() case termbox.KeyF5: // refresh - handled = true h.dry.appmessage("Refreshing stack list") - h.widget().Unmount() + h.widget.Unmount() case termbox.KeyEnter: //inspect - handled = true showTasks := func(stack string) error { - h.dry.ShowStackTasks(stack) + widgets.StackTasks.ForStack(stack) + h.dry.SetViewMode(StackTasks) return refreshScreen() } - h.widget().OnEvent(showTasks) + h.widget.OnEvent(showTasks) case termbox.KeyCtrlR: //remove stack rw := appui.NewPrompt("The selected stack will be removed. Do you want to proceed? y/N") h.setForwardEvents(true) - handled = true - h.dry.widgetRegistry.add(rw) + widgets.add(rw) go func() { events := ui.EventSource{ Events: h.eventChan, @@ -52,7 +47,7 @@ func (h *stacksScreenEventHandler) handle(event termbox.Event) { }, } rw.OnFocus(events) - h.dry.widgetRegistry.remove(rw) + widgets.remove(rw) confirmation, canceled := rw.Text() h.setForwardEvents(false) if canceled || (confirmation != "y" && confirmation != "Y") { @@ -66,10 +61,12 @@ func (h *stacksScreenEventHandler) handle(event termbox.Event) { refreshScreen() return err } - if err := h.widget().OnEvent(removeStack); err != nil { + if err := h.widget.OnEvent(removeStack); err != nil { h.dry.appmessage("There was an error removing the stack: " + err.Error()) } }() + default: + handled = false } if !handled { switch event.Ch { @@ -78,15 +75,19 @@ func (h *stacksScreenEventHandler) handle(event termbox.Event) { handled = true case '%': handled = true - showFilterInput(h) + h.setForwardEvents(true) + applyFilter := func(filter string, canceled bool) { + if !canceled { + h.widget.Filter(filter) + } + h.setForwardEvents(false) + } + showFilterInput(newEventSource(h.eventChan), applyFilter) } } if handled { - h.setFocus(focus) - if h.hasFocus() { - refreshScreen() - } + refreshScreen() } else { - h.baseEventHandler.handle(event) + h.baseEventHandler.handle(event, f) } } diff --git a/app/stacktasks_events.go b/app/stacktasks_events.go index ddc7962e..4e71b228 100644 --- a/app/stacktasks_events.go +++ b/app/stacktasks_events.go @@ -3,56 +3,67 @@ package app import ( "fmt" - "github.com/moncho/dry/appui" + "github.com/moncho/dry/appui/swarm" termbox "github.com/nsf/termbox-go" ) type stackTasksScreenEventHandler struct { baseEventHandler + widget *swarm.StacksTasksWidget } -func (h *stackTasksScreenEventHandler) widget() appui.AppWidget { - return h.dry.widgetRegistry.StackTasks -} - -func (h *stackTasksScreenEventHandler) handle(event termbox.Event) { - if h.forwardingEvents { +func (h *stackTasksScreenEventHandler) handle(event termbox.Event, f func(eventHandler)) { + if h.forwardingEvents() { h.eventChan <- event return } - handled := false - focus := true + handled := true switch event.Key { case termbox.KeyEsc: - handled = true - h.dry.ShowStacks() + f(viewsToHandlers[Stacks]) + h.dry.SetViewMode(Stacks) case termbox.KeyF1: //sort - handled = true - h.widget().Sort() + h.widget.Sort() case termbox.KeyF5: // refresh - handled = true h.dry.appmessage("Refreshing stack tasks list") - h.widget().Unmount() + h.widget.Unmount() case termbox.KeyEnter: - handled = true - focus = false - if err := h.widget().OnEvent(inspectTask(h.dry, h.screen, h.eventChan, h.closeViewChan)); err != nil { + h.setForwardEvents(true) + if err := h.widget.OnEvent( + inspect( + h.screen, + h.eventChan, + func(id string) (interface{}, error) { + return h.dry.dockerDaemon.Task(id) + }, + func() { + h.dry.SetViewMode(StackTasks) + h.setForwardEvents(false) + f(h) + })); err != nil { h.dry.appmessage( fmt.Sprintf("Error inspecting stack: %s", err.Error())) } - + default: + handled = false } switch event.Ch { case '%': handled = true - showFilterInput(h) + h.setForwardEvents(true) + applyFilter := func(filter string, canceled bool) { + if !canceled { + h.widget.Filter(filter) + } + h.setForwardEvents(false) + } + showFilterInput(newEventSource(h.eventChan), applyFilter) } if !handled { - h.baseEventHandler.handle(event) + h.baseEventHandler.handle(event, f) } else { - h.setFocus(focus) refreshScreen() } diff --git a/app/view.go b/app/view.go index 53a00da9..40baa43d 100644 --- a/app/view.go +++ b/app/view.go @@ -20,14 +20,5 @@ const ( StackTasks Tasks ContainerMenu + NoView ) - -//isMainScreen returns true if this viewMode is one of the main screens of dry -func (v viewMode) isMainScreen() bool { - switch v { - case ContainerMenu, Main, Networks, Images, Monitor, Nodes, Services, Stacks, ServiceTasks, StackTasks, Tasks: - return true - default: - return false - } -} diff --git a/app/view_test.go b/app/view_test.go deleted file mode 100644 index e2313c97..00000000 --- a/app/view_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package app - -import "testing" - -func Test_viewMode_isMainScreen(t *testing.T) { - tests := []struct { - name string - v viewMode - want bool - }{ - { - "main screen", - Main, - true, - }, - { - "non main screen", - HelpMode, - false, - }, - } - for _, tt := range tests { - if got := tt.v.isMainScreen(); got != tt.want { - t.Errorf("%q. viewMode.isMainScreen() = %v, want %v", tt.name, got, tt.want) - } - } -} diff --git a/app/widget_registry.go b/app/widget_registry.go index 2e0b1213..ad7e88f1 100644 --- a/app/widget_registry.go +++ b/app/widget_registry.go @@ -1,6 +1,8 @@ package app import ( + "sync" + "github.com/moncho/dry/appui" "github.com/moncho/dry/appui/swarm" "github.com/moncho/dry/docker" @@ -8,36 +10,37 @@ import ( "github.com/moncho/dry/ui/termui" ) -//WidgetRegistry holds two sets of widgets: +//widgetRegistry holds two sets of widgets: // * those registered in the the registry when it was created, that // can be reused. These are the individually named widgets found on // this struct. // * a list of widgets to be rendered on the next rendering. -type WidgetRegistry struct { - ContainerList *appui.ContainersWidget - ContainerMenu *appui.ContainerMenuWidget - DiskUsage *appui.DockerDiskUsageRenderer - DockerInfo *appui.DockerInfo - ImageList *appui.DockerImagesWidget - Monitor *appui.Monitor - Networks *appui.DockerNetworksWidget - Nodes *swarm.NodesWidget - NodeTasks *swarm.NodeTasksWidget - ServiceTasks *swarm.ServiceTasksWidget - ServiceList *swarm.ServicesWidget - Stacks *swarm.StacksWidget - StackTasks *swarm.StacksTasksWidget - activeWidgets map[string]termui.Widget - widgetForViewMap map[viewMode]termui.Widget +type widgetRegistry struct { + ContainerList *appui.ContainersWidget + ContainerMenu *appui.ContainerMenuWidget + DiskUsage *appui.DockerDiskUsageRenderer + DockerInfo *appui.DockerInfo + ImageList *appui.DockerImagesWidget + Monitor *appui.Monitor + Networks *appui.DockerNetworksWidget + Nodes *swarm.NodesWidget + NodeTasks *swarm.NodeTasksWidget + ServiceTasks *swarm.ServiceTasksWidget + ServiceList *swarm.ServicesWidget + Stacks *swarm.StacksWidget + StackTasks *swarm.StacksTasksWidget + MessageBar *ui.ExpiringMessageWidget + activeWidgets map[string]termui.Widget + sync.Mutex } //NewWidgetRegistry creates the WidgetCatalog -func NewWidgetRegistry(daemon docker.ContainerDaemon) *WidgetRegistry { +func newWidgetRegistry(daemon docker.ContainerDaemon) *widgetRegistry { di := appui.NewDockerInfo(daemon) di.SetX(0) di.SetY(1) di.SetWidth(ui.ActiveScreen.Dimensions.Width) - w := WidgetRegistry{ + w := widgetRegistry{ DockerInfo: di, ContainerList: appui.NewContainersWidget(daemon, appui.MainScreenHeaderSize), ContainerMenu: appui.NewContainerMenuWidget(daemon, appui.MainScreenHeaderSize), @@ -52,39 +55,24 @@ func NewWidgetRegistry(daemon docker.ContainerDaemon) *WidgetRegistry { Stacks: swarm.NewStacksWidget(daemon, appui.MainScreenHeaderSize), StackTasks: swarm.NewStacksTasksWidget(daemon, appui.MainScreenHeaderSize), activeWidgets: make(map[string]termui.Widget), + MessageBar: ui.NewExpiringMessageWidget(0, ui.ActiveScreen.Dimensions.Width, appui.DryTheme), } - initWidgetForViewMap(&w) - return &w } -func (wr *WidgetRegistry) widgetForView(v viewMode) termui.Widget { - return wr.widgetForViewMap[v] -} - -func (wr *WidgetRegistry) add(w termui.Widget) { +func (wr *widgetRegistry) add(w termui.Widget) { + wr.Lock() + defer wr.Unlock() if err := w.Mount(); err == nil { wr.activeWidgets[w.Name()] = w } } -func (wr *WidgetRegistry) remove(w termui.Widget) { +func (wr *widgetRegistry) remove(w termui.Widget) { + wr.Lock() + defer wr.Unlock() if err := w.Unmount(); err == nil { delete(wr.activeWidgets, w.Name()) } } - -func initWidgetForViewMap(wr *WidgetRegistry) { - viewMap := make(map[viewMode]termui.Widget) - viewMap[ContainerMenu] = wr.ContainerMenu - viewMap[Main] = wr.ContainerList - viewMap[Networks] = wr.Networks - viewMap[Images] = wr.ImageList - viewMap[Monitor] = wr.Monitor - viewMap[Nodes] = wr.Nodes - viewMap[Services] = wr.ServiceList - viewMap[Stacks] = wr.Stacks - wr.widgetForViewMap = viewMap - -}