ctop/menus.go

495 lines
11 KiB
Go
Raw Permalink Normal View History

2017-01-05 00:13:17 +01:00
package main
import (
2017-12-13 02:20:14 +01:00
"fmt"
2020-01-03 13:25:54 +01:00
"strings"
2017-12-13 02:20:14 +01:00
"time"
2017-02-07 04:33:09 +01:00
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/container"
2017-01-05 00:13:17 +01:00
"github.com/bcicen/ctop/widgets"
2017-02-15 08:40:16 +01:00
"github.com/bcicen/ctop/widgets/menu"
2017-01-05 00:13:17 +01:00
ui "github.com/gizak/termui"
"github.com/pkg/browser"
2017-01-05 00:13:17 +01:00
)
// MenuFn executes a menu window, returning the next menu or nil
type MenuFn func() MenuFn
2017-02-15 08:40:16 +01:00
var helpDialog = []menu.Item{
2018-01-10 22:10:10 +01:00
{"<enter> - open container menu", ""},
{"", ""},
{"[a] - toggle display of all containers", ""},
{"[f] - filter displayed containers", ""},
{"[h] - open this help dialog", ""},
{"[H] - toggle ctop header", ""},
{"[s] - select container sort field", ""},
{"[r] - reverse container sort order", ""},
2018-01-10 22:10:10 +01:00
{"[o] - open single view", ""},
{"[l] - view container logs ([t] to toggle timestamp when open)", ""},
2018-10-25 20:52:59 +02:00
{"[e] - exec shell", ""},
{"[w] - open browser (first port is http)", ""},
2020-01-03 14:21:57 +01:00
{"[c] - configure columns", ""},
{"[S] - save current configuration to file", ""},
{"[q] - exit ctop", ""},
2017-01-05 00:13:17 +01:00
}
func HelpMenu() MenuFn {
ui.Clear()
2017-02-18 04:37:00 +01:00
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
2017-02-15 08:40:16 +01:00
m := menu.NewMenu()
2017-01-05 00:13:17 +01:00
m.BorderLabel = "Help"
2017-02-15 08:40:16 +01:00
m.AddItems(helpDialog...)
2018-02-02 16:21:33 +01:00
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
ui.Clear()
ui.Render(m)
})
2017-01-05 00:13:17 +01:00
ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop()
})
ui.Loop()
return nil
2017-01-05 00:13:17 +01:00
}
func FilterMenu() MenuFn {
ui.DefaultEvtStream.ResetHandlers()
2017-02-18 04:37:00 +01:00
defer ui.DefaultEvtStream.ResetHandlers()
2017-01-21 19:15:29 +01:00
i := widgets.NewInput()
i.BorderLabel = "Filter"
i.SetY(ui.TermHeight() - i.Height)
i.Data = config.GetVal("filterStr")
2017-01-21 19:15:29 +01:00
ui.Render(i)
// refresh container rows on input
stream := i.Stream()
go func() {
for s := range stream {
config.Update("filterStr", s)
2017-03-08 01:10:38 +01:00
RefreshDisplay()
ui.Render(i)
}
}()
2017-01-21 19:15:29 +01:00
i.InputHandlers()
ui.Handle("/sys/kbd/<escape>", func(ui.Event) {
config.Update("filterStr", "")
ui.StopLoop()
})
2017-01-21 19:15:29 +01:00
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
2017-02-07 04:33:09 +01:00
config.Update("filterStr", i.Data)
2017-01-21 19:15:29 +01:00
ui.StopLoop()
})
ui.Loop()
return nil
2017-01-21 19:15:29 +01:00
}
func SortMenu() MenuFn {
ui.Clear()
2017-02-18 04:37:00 +01:00
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
2017-02-15 08:40:16 +01:00
m := menu.NewMenu()
2017-01-05 00:13:17 +01:00
m.Selectable = true
m.SortItems = true
2017-01-05 00:13:17 +01:00
m.BorderLabel = "Sort Field"
for _, field := range container.SortFields() {
2017-02-15 08:40:16 +01:00
m.AddItems(menu.Item{field, ""})
}
// set cursor position to current sort field
2017-02-19 04:54:24 +01:00
m.SetCursor(config.GetVal("sortField"))
HandleKeys("up", m.Up)
HandleKeys("down", m.Down)
HandleKeys("exit", ui.StopLoop)
2017-01-05 00:13:17 +01:00
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
2020-01-03 00:02:53 +01:00
config.Update("sortField", m.SelectedValue())
ui.StopLoop()
})
ui.Render(m)
ui.Loop()
return nil
}
func ColumnsMenu() MenuFn {
2020-01-03 13:25:54 +01:00
const (
enabledStr = "[X]"
disabledStr = "[ ]"
padding = 2
)
2020-01-03 00:02:53 +01:00
ui.Clear()
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
m := menu.NewMenu()
m.Selectable = true
m.SortItems = false
m.BorderLabel = "Columns"
m.SubText = "Re-order: <Page Up> / <Page Down>"
2020-01-03 00:02:53 +01:00
rebuild := func() {
2020-01-03 13:25:54 +01:00
// get padding for right alignment of enabled status
var maxLen int
for _, col := range config.GlobalColumns {
if len(col.Label) > maxLen {
maxLen = len(col.Label)
}
}
maxLen += padding
// rebuild menu items
2020-01-03 00:02:53 +01:00
m.ClearItems()
for _, col := range config.GlobalColumns {
2020-01-03 13:25:54 +01:00
txt := col.Label + strings.Repeat(" ", maxLen-len(col.Label))
if col.Enabled {
txt += enabledStr
} else {
txt += disabledStr
}
2020-01-03 00:02:53 +01:00
m.AddItems(menu.Item{col.Name, txt})
}
}
upFn := func() {
config.ColumnLeft(m.SelectedValue())
m.Up()
rebuild()
}
downFn := func() {
config.ColumnRight(m.SelectedValue())
m.Down()
rebuild()
}
toggleFn := func() {
config.ColumnToggle(m.SelectedValue())
rebuild()
}
rebuild()
HandleKeys("up", m.Up)
HandleKeys("down", m.Down)
HandleKeys("enter", toggleFn)
HandleKeys("pgup", upFn)
HandleKeys("pgdown", downFn)
ui.Handle("/sys/kbd/x", func(ui.Event) { toggleFn() })
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { toggleFn() })
HandleKeys("exit", func() {
cSource, err := cursor.cSuper.Get()
if err == nil {
for _, c := range cSource.All() {
c.RecreateWidgets()
}
}
2017-01-05 00:13:17 +01:00
ui.StopLoop()
})
ui.Render(m)
2017-01-05 00:13:17 +01:00
ui.Loop()
return nil
2017-01-05 00:13:17 +01:00
}
2017-11-20 12:09:36 +01:00
func ContainerMenu() MenuFn {
2017-11-20 12:09:36 +01:00
c := cursor.Selected()
if c == nil {
return nil
2017-11-20 12:09:36 +01:00
}
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
m := menu.NewMenu()
m.Selectable = true
m.BorderLabel = "Menu"
2018-01-10 22:10:10 +01:00
items := []menu.Item{
menu.Item{Val: "single", Label: "[o] single view"},
menu.Item{Val: "logs", Label: "[l] log view"},
2018-01-10 22:10:10 +01:00
}
2017-11-20 12:09:36 +01:00
if c.Meta["state"] == "running" {
items = append(items, menu.Item{Val: "stop", Label: "[s] stop"})
items = append(items, menu.Item{Val: "pause", Label: "[p] pause"})
items = append(items, menu.Item{Val: "restart", Label: "[r] restart"})
items = append(items, menu.Item{Val: "exec", Label: "[e] exec shell"})
if c.Meta["Web Port"] != "" {
items = append(items, menu.Item{Val: "browser", Label: "[w] open in browser"})
}
2017-11-20 12:09:36 +01:00
}
if c.Meta["state"] == "exited" || c.Meta["state"] == "created" {
items = append(items, menu.Item{Val: "start", Label: "[s] start"})
items = append(items, menu.Item{Val: "remove", Label: "[R] remove"})
2017-11-20 12:09:36 +01:00
}
if c.Meta["state"] == "paused" {
items = append(items, menu.Item{Val: "unpause", Label: "[p] unpause"})
}
items = append(items, menu.Item{Val: "cancel", Label: "[c] cancel"})
2017-11-20 12:09:36 +01:00
m.AddItems(items...)
ui.Render(m)
HandleKeys("up", m.Up)
HandleKeys("down", m.Down)
var selected string
// shortcuts
ui.Handle("/sys/kbd/o", func(ui.Event) {
selected = "single"
ui.StopLoop()
})
ui.Handle("/sys/kbd/l", func(ui.Event) {
selected = "logs"
ui.StopLoop()
})
if c.Meta["state"] != "paused" {
ui.Handle("/sys/kbd/s", func(ui.Event) {
if c.Meta["state"] == "running" {
selected = "stop"
} else {
selected = "start"
}
ui.StopLoop()
})
}
if c.Meta["state"] != "exited" && c.Meta["state"] != "created" {
ui.Handle("/sys/kbd/p", func(ui.Event) {
if c.Meta["state"] == "paused" {
selected = "unpause"
} else {
selected = "pause"
}
ui.StopLoop()
})
}
if c.Meta["state"] == "running" {
ui.Handle("/sys/kbd/e", func(ui.Event) {
selected = "exec"
ui.StopLoop()
})
ui.Handle("/sys/kbd/r", func(ui.Event) {
selected = "restart"
ui.StopLoop()
})
if c.Meta["Web Port"] != "" {
ui.Handle("/sys/kbd/w", func(ui.Event) {
selected = "browser"
})
}
}
ui.Handle("/sys/kbd/R", func(ui.Event) {
selected = "remove"
ui.StopLoop()
})
ui.Handle("/sys/kbd/c", func(ui.Event) {
ui.StopLoop()
})
2017-11-20 12:09:36 +01:00
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
2020-01-03 00:02:53 +01:00
selected = m.SelectedValue()
ui.StopLoop()
2017-11-20 12:09:36 +01:00
})
ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop()
})
ui.Loop()
var nextMenu MenuFn
switch selected {
case "single":
nextMenu = SingleView
case "logs":
nextMenu = LogMenu
case "exec":
nextMenu = ExecShell
case "browser":
nextMenu = OpenInBrowser
case "start":
nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start)
case "stop":
nextMenu = Confirm(confirmTxt("stop", c.GetMeta("name")), c.Stop)
case "remove":
nextMenu = Confirm(confirmTxt("remove", c.GetMeta("name")), c.Remove)
case "pause":
nextMenu = Confirm(confirmTxt("pause", c.GetMeta("name")), c.Pause)
case "unpause":
nextMenu = Confirm(confirmTxt("unpause", c.GetMeta("name")), c.Unpause)
case "restart":
nextMenu = Confirm(confirmTxt("restart", c.GetMeta("name")), c.Restart)
}
return nextMenu
2017-11-20 12:09:36 +01:00
}
2017-11-25 19:30:50 +01:00
func LogMenu() MenuFn {
2017-11-25 19:30:50 +01:00
c := cursor.Selected()
if c == nil {
return nil
2017-11-25 19:30:50 +01:00
}
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
logs, quit := logReader(c)
m := widgets.NewTextView(logs)
2018-05-10 11:53:59 +02:00
m.BorderLabel = fmt.Sprintf("Logs [%s]", c.GetMeta("name"))
2017-11-25 19:30:50 +01:00
ui.Render(m)
2017-11-28 14:55:29 +01:00
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
m.Resize()
})
ui.Handle("/sys/kbd/t", func(ui.Event) {
m.Toggle()
2017-11-28 14:55:29 +01:00
})
2017-11-25 19:30:50 +01:00
ui.Handle("/sys/kbd/", func(ui.Event) {
quit <- true
ui.StopLoop()
})
ui.Loop()
return nil
2017-11-25 19:30:50 +01:00
}
2018-10-25 20:52:59 +02:00
func ExecShell() MenuFn {
2018-10-07 15:46:32 +02:00
c := cursor.Selected()
if c == nil {
return nil
}
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
// Detect and execute default shell in container.
// Execute Ash shell command: /bin/sh -c
// Reset colors: printf '\e[0m\e[?25h'
// Clear screen
// Run default shell for the user. It's configured in /etc/passwd and looks like root:x:0:0:root:/root:/bin/bash:
// 1. Get current user id: id -un
// 2. Find user's line in /etc/passwd by grep
// 3. Extract default user's shell by cutting seven's column separated by :
// 4. Execute the shell path with eval
if err := c.Exec([]string{"/bin/sh", "-c", "printf '\\e[0m\\e[?25h' && clear && eval `grep ^$(id -un): /etc/passwd | cut -d : -f 7-`"}); err != nil {
log.StatusErr(err)
2018-10-25 20:52:59 +02:00
}
2018-10-07 15:46:32 +02:00
return nil
}
func OpenInBrowser() MenuFn {
c := cursor.Selected()
if c == nil {
return nil
}
webPort := c.Meta.Get("Web Port")
if webPort == "" {
return nil
}
link := "http://" + webPort + "/"
browser.OpenURL(link)
return nil
}
// Create a confirmation dialog with a given description string and
// func to perform if confirmed
func Confirm(txt string, fn func()) MenuFn {
menu := func() MenuFn {
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
m := menu.NewMenu()
m.Selectable = true
m.BorderLabel = "Confirm"
m.SubText = txt
items := []menu.Item{
menu.Item{Val: "cancel", Label: "[c]ancel"},
menu.Item{Val: "yes", Label: "[y]es"},
}
var response bool
m.AddItems(items...)
ui.Render(m)
yes := func() {
response = true
ui.StopLoop()
}
no := func() {
response = false
ui.StopLoop()
}
HandleKeys("up", m.Up)
HandleKeys("down", m.Down)
HandleKeys("exit", no)
ui.Handle("/sys/kbd/c", func(ui.Event) { no() })
ui.Handle("/sys/kbd/y", func(ui.Event) { yes() })
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
2020-01-03 00:02:53 +01:00
switch m.SelectedValue() {
case "cancel":
no()
case "yes":
yes()
}
})
ui.Loop()
if response {
fn()
}
return nil
}
return menu
}
type toggleLog struct {
timestamp time.Time
message string
}
func (t *toggleLog) Toggle(on bool) string {
if on {
return fmt.Sprintf("%s %s", t.timestamp.Format("2006-01-02T15:04:05.999Z07:00"), t.message)
}
return t.message
}
func logReader(container *container.Container) (logs chan widgets.ToggleText, quit chan bool) {
2017-11-25 19:30:50 +01:00
logCollector := container.Logs()
stream := logCollector.Stream()
logs = make(chan widgets.ToggleText)
2017-11-25 19:30:50 +01:00
quit = make(chan bool)
go func() {
for {
select {
2017-11-28 14:40:43 +01:00
case log := <-stream:
logs <- &toggleLog{timestamp: log.Timestamp, message: log.Message}
2017-11-28 14:40:43 +01:00
case <-quit:
2017-11-25 19:30:50 +01:00
logCollector.Stop()
close(logs)
return
}
}
}()
return
}
func confirmTxt(a, n string) string { return fmt.Sprintf("%s container %s?", a, n) }