// SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s package ui import ( "os" "sync" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" "github.com/derailed/tcell/v2" "github.com/derailed/tview" "github.com/rs/zerolog/log" ) // App represents an application. type App struct { *tview.Application Configurator Main *Pages flash *model.Flash actions KeyActions views map[string]tview.Primitive cmdBuff *model.FishBuff running bool mx sync.RWMutex } // NewApp returns a new app. func NewApp(cfg *config.Config, context string) *App { a := App{ Application: tview.NewApplication(), actions: make(KeyActions), Configurator: Configurator{Config: cfg}, Main: NewPages(), flash: model.NewFlash(model.DefaultFlashDelay), cmdBuff: model.NewFishBuff(':', model.CommandBuffer), } a.ReloadStyles() a.views = map[string]tview.Primitive{ "menu": NewMenu(a.Styles), "logo": NewLogo(a.Styles), "prompt": NewPrompt(&a, a.Config.K9s.UI.NoIcons, a.Styles), "crumbs": NewCrumbs(a.Styles), } return &a } // Init initializes the application. func (a *App) Init() { a.bindKeys() a.Prompt().SetModel(a.cmdBuff) a.cmdBuff.AddListener(a) a.Styles.AddListener(a) a.SetRoot(a.Main, true).EnableMouse(a.Config.K9s.UI.EnableMouse) } // QueueUpdate queues up a ui action. func (a *App) QueueUpdate(f func()) { if a.Application == nil { return } go func() { a.Application.QueueUpdate(f) }() } // QueueUpdateDraw queues up a ui action and redraw the ui. func (a *App) QueueUpdateDraw(f func()) { if a.Application == nil { return } go func() { a.Application.QueueUpdateDraw(f) }() } // IsRunning checks if app is actually running. func (a *App) IsRunning() bool { a.mx.RLock() defer a.mx.RUnlock() return a.running } // SetRunning sets the app run state. func (a *App) SetRunning(f bool) { a.mx.Lock() defer a.mx.Unlock() a.running = f } // BufferCompleted indicates input was accepted. func (a *App) BufferCompleted(_, _ string) {} // BufferChanged indicates the buffer was changed. func (a *App) BufferChanged(_, _ string) {} // BufferActive indicates the buff activity changed. func (a *App) BufferActive(state bool, kind model.BufferKind) { flex, ok := a.Main.GetPrimitive("main").(*tview.Flex) if !ok { return } if state && flex.ItemAt(1) != a.Prompt() { flex.AddItemAtIndex(1, a.Prompt(), 3, 1, false) } else if !state && flex.ItemAt(1) == a.Prompt() { flex.RemoveItemAtIndex(1) a.SetFocus(flex) } } // SuggestionChanged notifies of update to command suggestions. func (a *App) SuggestionChanged(ss []string) {} // 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") } } // ReloadStyles reloads skin file. func (a *App) ReloadStyles() { a.RefreshStyles() } // Conn returns an api server connection. func (a *App) Conn() client.Connection { return a.Config.GetConnection() } func (a *App) bindKeys() { a.actions = KeyActions{ KeyColon: NewKeyAction("Cmd", a.activateCmd, false), tcell.KeyCtrlR: NewKeyAction("Redraw", a.redrawCmd, false), tcell.KeyCtrlC: NewKeyAction("Quit", a.quitCmd, false), tcell.KeyCtrlU: NewSharedKeyAction("Clear Filter", a.clearCmd, false), tcell.KeyCtrlQ: NewSharedKeyAction("Clear Filter", a.clearCmd, false), } } // BailOut exits the application. func (a *App) BailOut() { a.Stop() os.Exit(0) } // ResetPrompt reset the prompt model and marks buffer as active. func (a *App) ResetPrompt(m PromptModel) { a.Prompt().SetModel(m) a.SetFocus(a.Prompt()) m.SetActive(true) } // ResetCmd clear out user command. func (a *App) ResetCmd() { a.cmdBuff.Reset() } // ActivateCmd toggle command mode. func (a *App) ActivateCmd(b bool) { a.cmdBuff.SetActive(b) } // GetCmd retrieves user command. func (a *App) GetCmd() string { return a.cmdBuff.GetText() } // CmdBuff returns the app cmd model. func (a *App) CmdBuff() *model.FishBuff { return a.cmdBuff } // HasCmd check if cmd buffer is active and has a command. func (a *App) HasCmd() bool { return a.cmdBuff.IsActive() && !a.cmdBuff.Empty() } func (a *App) quitCmd(evt *tcell.EventKey) *tcell.EventKey { if a.InCmdMode() { return evt } if !a.Config.K9s.NoExitOnCtrlC { a.BailOut() } // overwrite the default ctrl-c behavior of tview return nil } // InCmdMode check if command mode is active. func (a *App) InCmdMode() bool { return a.Prompt().InCmdMode() } // HasAction checks if key matches a registered binding. func (a *App) HasAction(key tcell.Key) (KeyAction, bool) { act, ok := a.actions[key] return act, ok } // GetActions returns a collection of actions. func (a *App) GetActions() KeyActions { return a.actions } // AddActions returns the application actions. func (a *App) AddActions(aa KeyActions) { for k, v := range aa { a.actions[k] = v } } // Views return the application root views. func (a *App) Views() map[string]tview.Primitive { return a.views } func (a *App) clearCmd(evt *tcell.EventKey) *tcell.EventKey { if !a.cmdBuff.IsActive() { return evt } a.cmdBuff.ClearText(true) return nil } func (a *App) activateCmd(evt *tcell.EventKey) *tcell.EventKey { if a.InCmdMode() { return evt } a.ResetPrompt(a.cmdBuff) a.cmdBuff.ClearText(true) return nil } // RedrawCmd forces a redraw. func (a *App) redrawCmd(evt *tcell.EventKey) *tcell.EventKey { a.QueueUpdateDraw(func() {}) return evt } // View Accessors... // Crumbs return app crumbs. func (a *App) Crumbs() *Crumbs { return a.views["crumbs"].(*Crumbs) } // Logo return the app logo. func (a *App) Logo() *Logo { return a.views["logo"].(*Logo) } // Prompt returns command prompt. func (a *App) Prompt() *Prompt { return a.views["prompt"].(*Prompt) } // Menu returns app menu. func (a *App) Menu() *Menu { return a.views["menu"].(*Menu) } // Flash returns a flash model. func (a *App) Flash() *model.Flash { return a.flash } // ---------------------------------------------------------------------------- // Helpers... // AsKey converts rune to keyboard key.,. func AsKey(evt *tcell.EventKey) tcell.Key { if evt.Key() != tcell.KeyRune { return evt.Key() } key := tcell.Key(evt.Rune()) if evt.Modifiers() == tcell.ModAlt { key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers())) } return key }