304 lines
6.4 KiB
Go
304 lines
6.4 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright Authors of K9s
|
|
|
|
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/derailed/k9s/internal/config"
|
|
"github.com/derailed/k9s/internal/model"
|
|
"github.com/derailed/tcell/v2"
|
|
"github.com/derailed/tview"
|
|
)
|
|
|
|
const (
|
|
defaultPrompt = "%c> [::b]%s"
|
|
defaultSpacer = 4
|
|
)
|
|
|
|
var (
|
|
_ PromptModel = (*model.FishBuff)(nil)
|
|
_ Suggester = (*model.FishBuff)(nil)
|
|
)
|
|
|
|
// Suggester provides suggestions.
|
|
type Suggester interface {
|
|
// CurrentSuggestion returns the current suggestion.
|
|
CurrentSuggestion() (string, bool)
|
|
|
|
// NextSuggestion returns the next suggestion.
|
|
NextSuggestion() (string, bool)
|
|
|
|
// PrevSuggestion returns the prev suggestion.
|
|
PrevSuggestion() (string, bool)
|
|
|
|
// ClearSuggestions clear out all suggestions.
|
|
ClearSuggestions()
|
|
}
|
|
|
|
// PromptModel represents a prompt buffer.
|
|
type PromptModel interface {
|
|
// SetText sets the model text.
|
|
SetText(txt, sug string)
|
|
|
|
// GetText returns the current text.
|
|
GetText() string
|
|
|
|
// GetSuggestion returns the current suggestion.
|
|
GetSuggestion() string
|
|
|
|
// ClearText clears out model text.
|
|
ClearText(fire bool)
|
|
|
|
// Notify notifies all listener of current suggestions.
|
|
Notify(bool)
|
|
|
|
// AddListener registers a command listener.
|
|
AddListener(model.BuffWatcher)
|
|
|
|
// RemoveListener removes a listener.
|
|
RemoveListener(model.BuffWatcher)
|
|
|
|
// IsActive returns true if prompt is active.
|
|
IsActive() bool
|
|
|
|
// SetActive sets whether the prompt is active or not.
|
|
SetActive(bool)
|
|
|
|
// Add adds a new char to the prompt.
|
|
Add(rune)
|
|
|
|
// Delete deletes the last prompt character.
|
|
Delete()
|
|
}
|
|
|
|
// Prompt captures users free from command input.
|
|
type Prompt struct {
|
|
*tview.TextView
|
|
|
|
app *App
|
|
noIcons bool
|
|
icon rune
|
|
styles *config.Styles
|
|
model PromptModel
|
|
spacer int
|
|
mx sync.RWMutex
|
|
}
|
|
|
|
// NewPrompt returns a new command view.
|
|
func NewPrompt(app *App, noIcons bool, styles *config.Styles) *Prompt {
|
|
p := Prompt{
|
|
app: app,
|
|
styles: styles,
|
|
noIcons: noIcons,
|
|
TextView: tview.NewTextView(),
|
|
spacer: defaultSpacer,
|
|
}
|
|
if noIcons {
|
|
p.spacer--
|
|
}
|
|
p.SetWordWrap(true)
|
|
p.SetWrap(true)
|
|
p.SetDynamicColors(true)
|
|
p.SetBorder(true)
|
|
p.SetBorderPadding(0, 0, 1, 1)
|
|
p.SetBackgroundColor(styles.K9s.Prompt.BgColor.Color())
|
|
p.SetTextColor(styles.K9s.Prompt.FgColor.Color())
|
|
styles.AddListener(&p)
|
|
p.SetInputCapture(p.keyboard)
|
|
|
|
return &p
|
|
}
|
|
|
|
// SendKey sends a keyboard event (testing only!).
|
|
func (p *Prompt) SendKey(evt *tcell.EventKey) {
|
|
p.keyboard(evt)
|
|
}
|
|
|
|
// SendStrokes (testing only!)
|
|
func (p *Prompt) SendStrokes(s string) {
|
|
for _, r := range s {
|
|
p.keyboard(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone))
|
|
}
|
|
}
|
|
|
|
// Deactivate sets the prompt as inactive.
|
|
func (p *Prompt) Deactivate() {
|
|
if p.model != nil {
|
|
p.model.ClearText(true)
|
|
p.model.SetActive(false)
|
|
}
|
|
}
|
|
|
|
// SetModel sets the prompt buffer model.
|
|
func (p *Prompt) SetModel(m PromptModel) {
|
|
if p.model != nil {
|
|
p.model.RemoveListener(p)
|
|
}
|
|
p.model = m
|
|
p.model.AddListener(p)
|
|
}
|
|
|
|
func (p *Prompt) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
|
m, ok := p.model.(Suggester)
|
|
if !ok {
|
|
return evt
|
|
}
|
|
|
|
// nolint:exhaustive
|
|
switch evt.Key() {
|
|
case tcell.KeyBackspace2, tcell.KeyBackspace, tcell.KeyDelete:
|
|
p.model.Delete()
|
|
|
|
case tcell.KeyRune:
|
|
p.model.Add(evt.Rune())
|
|
|
|
case tcell.KeyEscape:
|
|
p.model.ClearText(true)
|
|
p.model.SetActive(false)
|
|
|
|
case tcell.KeyEnter, tcell.KeyCtrlE:
|
|
p.model.SetText(p.model.GetText(), "")
|
|
p.model.SetActive(false)
|
|
|
|
case tcell.KeyCtrlW, tcell.KeyCtrlU:
|
|
p.model.ClearText(true)
|
|
|
|
case tcell.KeyUp:
|
|
if s, ok := m.NextSuggestion(); ok {
|
|
p.model.SetText(p.model.GetText(), s)
|
|
}
|
|
|
|
case tcell.KeyDown:
|
|
if s, ok := m.PrevSuggestion(); ok {
|
|
p.model.SetText(p.model.GetText(), s)
|
|
}
|
|
|
|
case tcell.KeyTab, tcell.KeyRight, tcell.KeyCtrlF:
|
|
if s, ok := m.CurrentSuggestion(); ok {
|
|
p.model.SetText(p.model.GetText()+s, "")
|
|
m.ClearSuggestions()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// StylesChanged notifies skin changed.
|
|
func (p *Prompt) StylesChanged(s *config.Styles) {
|
|
p.styles = s
|
|
p.SetBackgroundColor(s.K9s.Prompt.BgColor.Color())
|
|
p.SetTextColor(s.K9s.Prompt.FgColor.Color())
|
|
}
|
|
|
|
// InCmdMode returns true if command is active, false otherwise.
|
|
func (p *Prompt) InCmdMode() bool {
|
|
if p.model == nil {
|
|
return false
|
|
}
|
|
return p.model.IsActive()
|
|
}
|
|
|
|
func (p *Prompt) activate() {
|
|
p.Clear()
|
|
p.SetCursorIndex(len(p.model.GetText()))
|
|
p.write(p.model.GetText(), p.model.GetSuggestion())
|
|
p.model.Notify(false)
|
|
}
|
|
|
|
func (p *Prompt) Clear() {
|
|
p.mx.Lock()
|
|
defer p.mx.Unlock()
|
|
|
|
p.TextView.Clear()
|
|
}
|
|
|
|
func (p *Prompt) Draw(sc tcell.Screen) {
|
|
p.mx.RLock()
|
|
defer p.mx.RUnlock()
|
|
|
|
p.TextView.Draw(sc)
|
|
}
|
|
|
|
func (p *Prompt) update(text, suggestion string) {
|
|
p.Clear()
|
|
p.write(text, suggestion)
|
|
}
|
|
|
|
func (p *Prompt) write(text, suggest string) {
|
|
p.mx.Lock()
|
|
defer p.mx.Unlock()
|
|
|
|
p.SetCursorIndex(p.spacer + len(text))
|
|
txt := text
|
|
if suggest != "" {
|
|
txt += fmt.Sprintf("[%s::-]%s", p.styles.Prompt().SuggestColor, suggest)
|
|
}
|
|
fmt.Fprintf(p, defaultPrompt, p.icon, txt)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Event Listener protocol...
|
|
|
|
// BufferCompleted indicates input was accepted.
|
|
func (p *Prompt) BufferCompleted(text, suggestion string) {
|
|
p.update(text, suggestion)
|
|
}
|
|
|
|
// BufferChanged indicates the buffer was changed.
|
|
func (p *Prompt) BufferChanged(text, suggestion string) {
|
|
p.update(text, suggestion)
|
|
}
|
|
|
|
// SuggestionChanged notifies the suggestion changed.
|
|
func (p *Prompt) SuggestionChanged(text, suggestion string) {
|
|
p.update(text, suggestion)
|
|
}
|
|
|
|
// BufferActive indicates the buff activity changed.
|
|
func (p *Prompt) BufferActive(activate bool, kind model.BufferKind) {
|
|
if activate {
|
|
p.ShowCursor(true)
|
|
p.SetBorder(true)
|
|
p.SetTextColor(p.styles.FgColor())
|
|
p.SetBorderColor(p.colorFor(kind))
|
|
p.icon = p.iconFor(kind)
|
|
p.activate()
|
|
return
|
|
}
|
|
|
|
p.ShowCursor(false)
|
|
p.SetBorder(false)
|
|
p.SetBackgroundColor(p.styles.BgColor())
|
|
p.Clear()
|
|
}
|
|
|
|
func (p *Prompt) iconFor(k model.BufferKind) rune {
|
|
if p.noIcons {
|
|
return ' '
|
|
}
|
|
|
|
// nolint:exhaustive
|
|
switch k {
|
|
case model.CommandBuffer:
|
|
return '🐶'
|
|
default:
|
|
return '🐩'
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Helpers...
|
|
|
|
func (p *Prompt) colorFor(k model.BufferKind) tcell.Color {
|
|
// nolint:exhaustive
|
|
switch k {
|
|
case model.CommandBuffer:
|
|
return p.styles.Prompt().Border.CommandColor.Color()
|
|
default:
|
|
return p.styles.Prompt().Border.DefaultColor.Color()
|
|
}
|
|
}
|