k9s/internal/view/app.go

479 lines
11 KiB
Go

package view
import (
"context"
"errors"
"fmt"
"time"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/watch"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
const (
splashTime = 1
clusterRefresh = time.Duration(5 * time.Second)
statusIndicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%"
)
// App represents an application view.
type App struct {
*ui.App
Content *PageStack
command *Command
factory *watch.Factory
version string
showHeader bool
cancelFn context.CancelFunc
}
// NewApp returns a K9s app instance.
func NewApp(cfg *config.Config) *App {
a := App{
App: ui.NewApp(cfg.K9s.CurrentCluster),
Content: NewPageStack(),
}
a.Config = cfg
a.InitBench(cfg.K9s.CurrentCluster)
a.Views()["statusIndicator"] = ui.NewStatusIndicator(a.App, a.Styles)
a.Views()["clusterInfo"] = NewClusterInfo(&a, client.NewMetricsServer(cfg.GetConnection()))
return &a
}
// ActiveView returns the currently active view.
func (a *App) ActiveView() model.Component {
return a.Content.GetPrimitive("main").(model.Component)
}
// PrevCmd pops the command stack.
func (a *App) PrevCmd(evt *tcell.EventKey) *tcell.EventKey {
if !a.Content.IsLast() {
a.Content.Pop()
}
return nil
}
// Init initializes the application.
func (a *App) Init(version string, rate int) error {
ctx := context.WithValue(context.Background(), internal.KeyApp, a)
if err := a.Content.Init(ctx); err != nil {
return err
}
a.Content.Stack.AddListener(a.Crumbs())
a.Content.Stack.AddListener(a.Menu())
a.version = version
a.App.Init()
a.CmdBuff().AddListener(a)
a.bindKeys()
if a.Conn() == nil {
return errors.New("No client connection detected")
}
ns, err := a.Conn().Config().CurrentNamespaceName()
if err != nil {
log.Info().Msg("No namespace specified using all namespaces")
}
a.factory = watch.NewFactory(a.Conn())
a.initFactory(ns)
a.command = NewCommand(a)
if err := a.command.Init(); err != nil {
return err
}
a.clusterInfo().init(version)
if a.Config.K9s.GetHeadless() {
a.refreshIndicator()
}
main := tview.NewFlex().SetDirection(tview.FlexRow)
main.AddItem(a.statusIndicator(), 1, 1, false)
main.AddItem(a.Content, 0, 10, true)
main.AddItem(a.Crumbs(), 2, 1, false)
main.AddItem(a.Flash(), 2, 1, false)
a.Main.AddPage("main", main, true, false)
a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
a.toggleHeader(!a.Config.K9s.GetHeadless())
a.Styles.AddListener(a)
return nil
}
// StylesChanged notifies the skin changed.
func (a *App) StylesChanged(s *config.Styles) {
a.Main.SetBackgroundColor(s.BgColor())
if f, ok := a.Main.GetPrimitive("main").(*tview.Flex); ok {
f.SetBackgroundColor(s.BgColor())
if h, ok := f.ItemAt(0).(*tview.Flex); ok {
h.SetBackgroundColor(s.BgColor())
} else {
log.Error().Msgf("Header not found")
}
} else {
log.Error().Msgf("Main not found")
}
}
func (a *App) bindKeys() {
a.AddActions(ui.KeyActions{
ui.KeyH: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
ui.KeyHelp: ui.NewSharedKeyAction("Help", a.helpCmd, false),
tcell.KeyCtrlA: ui.NewSharedKeyAction("Aliases", a.aliasCmd, false),
tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, false),
tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", a.clearCmd, false),
})
}
// BufferChanged indicates the buffer was changed.
func (a *App) BufferChanged(s string) {}
// BufferActive indicates the buff activity changed.
func (a *App) BufferActive(state bool, _ ui.BufferKind) {
flex, ok := a.Main.GetPrimitive("main").(*tview.Flex)
if !ok {
return
}
if state && flex.ItemAt(1) != a.Cmd() {
flex.AddItemAtIndex(1, a.Cmd(), 3, 1, false)
} else if !state && flex.ItemAt(1) == a.Cmd() {
flex.RemoveItemAtIndex(1)
}
a.Draw()
}
func (a *App) toggleHeader(flag bool) {
a.showHeader = flag
flex, ok := a.Main.GetPrimitive("main").(*tview.Flex)
if !ok {
log.Fatal().Msg("Expecting valid flex view")
}
if a.showHeader {
flex.RemoveItemAtIndex(0)
flex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false)
} else {
flex.RemoveItemAtIndex(0)
flex.AddItemAtIndex(0, a.statusIndicator(), 1, 1, false)
a.refreshIndicator()
}
}
func (a *App) buildHeader() tview.Primitive {
header := tview.NewFlex()
header.SetBackgroundColor(a.Styles.BgColor())
header.SetBorderPadding(0, 0, 1, 1)
header.SetDirection(tview.FlexColumn)
if !a.showHeader {
return header
}
header.AddItem(a.clusterInfo(), 40, 1, false)
header.AddItem(a.Menu(), 0, 1, false)
header.AddItem(a.Logo(), 26, 1, false)
return header
}
// Halt stop the application event loop.
func (a *App) Halt() {
if a.cancelFn != nil {
a.cancelFn()
}
}
// Resume restarts the app event loop.
func (a *App) Resume() {
var ctx context.Context
ctx, a.cancelFn = context.WithCancel(context.Background())
go a.clusterUpdater(ctx)
if err := a.StylesUpdater(ctx, a); err != nil {
log.Error().Err(err).Msgf("Styles update failed")
}
}
func (a *App) clusterUpdater(ctx context.Context) {
for {
select {
case <-ctx.Done():
log.Debug().Msg("Cluster updater canceled!")
return
case <-time.After(clusterRefresh):
a.QueueUpdateDraw(func() {
a.refreshClusterInfo()
})
}
}
}
// BOZO!! Refact to use model/view strategy.
func (a *App) refreshClusterInfo() {
if !a.showHeader {
a.refreshIndicator()
} else {
a.clusterInfo().refresh()
}
}
func (a *App) refreshIndicator() {
mx := client.NewMetricsServer(a.Conn())
cluster := model.NewCluster(a.Conn(), mx)
var cmx client.ClusterMetrics
nos, nmx, err := fetchResources(a)
if err != nil {
log.Error().Err(err).Msgf("unable to refresh cluster statusIndicator")
return
}
if err := cluster.Metrics(nos, nmx, &cmx); err != nil {
log.Error().Err(err).Msgf("unable to refresh cluster statusIndicator")
return
}
cpu := render.AsPerc(cmx.PercCPU)
if cpu == "0" {
cpu = render.NAValue
}
mem := render.AsPerc(cmx.PercMEM)
if mem == "0" {
mem = render.NAValue
}
a.statusIndicator().SetPermanent(fmt.Sprintf(
statusIndicatorFmt,
a.version,
cluster.ClusterName(),
cluster.UserName(),
cluster.Version(),
cpu,
mem,
))
}
func (a *App) switchNS(ns string) bool {
if ns == render.ClusterScope {
ns = render.AllNamespaces
}
if err := a.Config.SetActiveNamespace(ns); err != nil {
log.Error().Err(err).Msg("Config Set NS failed!")
return false
}
a.factory.SetActive(ns)
return true
}
func (a *App) switchCtx(name string, loadPods bool) error {
log.Debug().Msgf("Switching Context %q", name)
a.Halt()
defer a.Resume()
{
ns, err := a.Conn().Config().CurrentNamespaceName()
if err != nil {
log.Warn().Msg("No namespace specified in context. Using K9s config")
}
a.initFactory(ns)
if err := a.command.Reset(); err != nil {
return err
}
a.Config.Reset()
if err := a.Config.Save(); err != nil {
log.Error().Err(err).Msg("Config save failed!")
}
a.Flash().Infof("Switching context to %s", name)
if err := a.gotoResource("pods", true); loadPods && err != nil {
a.Flash().Err(err)
}
a.refreshClusterInfo()
a.ReloadStyles(name)
}
return nil
}
func (a *App) initFactory(ns string) {
a.factory.Terminate()
a.factory.Init()
a.factory.SetActive(ns)
}
// BailOut exists the application.
func (a *App) BailOut() {
a.factory.Terminate()
a.App.BailOut()
}
// Run starts the application loop
func (a *App) Run() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
a.Halt()
if err := a.StylesUpdater(ctx, a); err != nil {
log.Error().Err(err).Msg("Unable to track skin changes")
}
go func() {
<-time.After(splashTime * time.Second)
a.QueueUpdateDraw(func() {
a.Main.SwitchToPage("main")
})
}()
if err := a.command.defaultCmd(); err != nil {
panic(err)
}
if err := a.Application.Run(); err != nil {
panic(err)
}
}
// Status reports a new app status for display.
func (a *App) Status(l ui.FlashLevel, msg string) {
a.Flash().Info(msg)
a.setIndicator(l, msg)
a.setLogo(l, msg)
a.Draw()
}
// ClearStatus reset log back to normal.
func (a *App) ClearStatus() {
a.Logo().Reset()
a.Flash().Clear()
a.Draw()
}
func (a *App) setLogo(l ui.FlashLevel, msg string) {
switch l {
case ui.FlashErr:
a.Logo().Err(msg)
case ui.FlashWarn:
a.Logo().Warn(msg)
case ui.FlashInfo:
a.Logo().Info(msg)
default:
a.Logo().Reset()
}
a.Draw()
}
func (a *App) setIndicator(l ui.FlashLevel, msg string) {
switch l {
case ui.FlashErr:
a.statusIndicator().Err(msg)
case ui.FlashWarn:
a.statusIndicator().Warn(msg)
case ui.FlashInfo:
a.statusIndicator().Info(msg)
default:
a.statusIndicator().Reset()
}
a.Draw()
}
func (a *App) toggleHeaderCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.Cmd().InCmdMode() {
return evt
}
a.showHeader = !a.showHeader
a.toggleHeader(a.showHeader)
a.Draw()
return nil
}
func (a *App) clearCmd(evt *tcell.EventKey) *tcell.EventKey {
if !a.CmdBuff().IsActive() {
return evt
}
a.CmdBuff().Clear()
return nil
}
func (a *App) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.CmdBuff().IsActive() && !a.CmdBuff().Empty() {
if err := a.gotoResource(a.GetCmd(), true); err != nil {
log.Error().Err(err).Msgf("Goto resource for %q failed", a.GetCmd())
a.Flash().Err(err)
return nil
}
a.ResetCmd()
return nil
}
a.ActivateCmd(false)
return evt
}
func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey {
if _, ok := a.Content.GetPrimitive("main").(*Help); ok {
return evt
}
if a.Content.Top() != nil && a.Content.Top().Name() == helpTitle {
a.Content.Pop()
return nil
}
if err := a.inject(NewHelp()); err != nil {
a.Flash().Err(err)
}
return nil
}
func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey {
if _, ok := a.Content.GetPrimitive("main").(*Alias); ok {
return evt
}
if a.Content.Top() != nil && a.Content.Top().Name() == aliasTitle {
a.Content.Pop()
return nil
}
if err := a.inject(NewAlias("aliases")); err != nil {
a.Flash().Err(err)
}
return nil
}
func (a *App) gotoResource(res string, clearStack bool) error {
return a.command.run(res, clearStack)
}
func (a *App) inject(c model.Component) error {
ctx := context.WithValue(context.Background(), internal.KeyApp, a)
if err := c.Init(ctx); err != nil {
return fmt.Errorf("component init failed for %q %v", c.Name(), err)
}
a.Content.Push(c)
return nil
}
func (a *App) clusterInfo() *ClusterInfo {
return a.Views()["clusterInfo"].(*ClusterInfo)
}
func (a *App) statusIndicator() *ui.StatusIndicator {
return a.Views()["statusIndicator"].(*ui.StatusIndicator)
}