k9s/internal/ui/prompt.go

248 lines
5.1 KiB
Go

package ui
import (
"fmt"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
)
const (
defaultPrompt = "%c> [::b]%s"
defaultSpacer = 4
)
var _ PromptModel = (*model.CmdBuff)(nil)
var _ Suggester = (*model.CmdBuff)(nil)
var _ PromptModel = (*model.FishBuff)(nil)
var _ Suggester = (*model.FishBuff)(nil)
type Suggester interface {
CurrentSuggestion() (string, bool)
NextSuggestion() (string, bool)
PrevSuggestion() (string, bool)
ClearSuggestions()
}
type PromptModel interface {
// AutoSuggests returns true if model implements auto suggestions.
AutoSuggests() bool
// Suggestions returns suggestions.
Suggestions() []string
// SetText sets the model text.
SetText(string)
// GetText returns the current text.
GetText() string
// ClearText clears out model text.
ClearText()
// Notify notifies all listener of current suggestions.
Notify()
// AddListener registers a command listener.
AddListener(model.BuffWatcher)
// RemoveListener removes a listener.
RemoveListener(model.BuffWatcher)
IsActive() bool
SetActive(bool)
Add(rune)
Delete()
}
// Prompt captures users free from command input.
type Prompt struct {
*tview.TextView
noIcons bool
icon rune
styles *config.Styles
model PromptModel
spacer int
}
// NewPrompt returns a new command view.
func NewPrompt(noIcons bool, styles *config.Styles) *Prompt {
c := Prompt{
styles: styles,
noIcons: noIcons,
TextView: tview.NewTextView(),
spacer: defaultSpacer,
}
if noIcons {
c.spacer--
}
c.SetWordWrap(true)
c.SetWrap(true)
c.SetDynamicColors(true)
c.SetBorder(true)
c.SetBorderPadding(0, 0, 1, 1)
c.SetBackgroundColor(styles.BgColor())
c.SetTextColor(styles.FgColor())
styles.AddListener(&c)
c.SetInputCapture(c.keyboard)
return &c
}
// SendKey sends an 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))
}
}
func (c *Prompt) SetModel(m PromptModel) {
if c.model != nil {
c.model.RemoveListener(c)
}
c.model = m
c.model.AddListener(c)
}
func (c *Prompt) keyboard(evt *tcell.EventKey) *tcell.EventKey {
m, ok := c.model.(Suggester)
if !ok {
return nil
}
switch evt.Key() {
case tcell.KeyBackspace2, tcell.KeyBackspace, tcell.KeyDelete:
c.model.Delete()
case tcell.KeyRune:
c.model.Add(evt.Rune())
case tcell.KeyEscape:
c.model.ClearText()
c.model.SetActive(false)
case tcell.KeyEnter, tcell.KeyCtrlE:
if curr, ok := m.CurrentSuggestion(); ok {
c.model.SetText(c.model.GetText() + curr)
}
c.model.SetActive(false)
case tcell.KeyCtrlW, tcell.KeyCtrlU:
c.model.ClearText()
case tcell.KeyDown:
if next, ok := m.NextSuggestion(); ok {
c.suggest(c.model.GetText(), next)
}
case tcell.KeyUp:
if prev, ok := m.PrevSuggestion(); ok {
c.suggest(c.model.GetText(), prev)
}
case tcell.KeyTab, tcell.KeyRight, tcell.KeyCtrlF:
if curr, ok := m.CurrentSuggestion(); ok {
c.model.SetText(c.model.GetText() + curr)
m.ClearSuggestions()
}
}
return evt
}
// StylesChanged notifies skin changed.
func (c *Prompt) StylesChanged(s *config.Styles) {
c.styles = s
c.SetBackgroundColor(s.BgColor())
c.SetTextColor(s.FgColor())
}
// InCmdMode returns true if command is active, false otherwise.
func (c *Prompt) InCmdMode() bool {
if c.model == nil {
return false
}
return c.model.IsActive()
}
func (c *Prompt) activate() {
c.SetCursorIndex(len(c.model.GetText()))
c.write(c.model.GetText(), "")
c.model.Notify()
}
func (c *Prompt) update(s string) {
c.Clear()
c.write(s, "")
}
func (c *Prompt) suggest(text, suggestion string) {
c.Clear()
c.write(text, suggestion)
}
func (c *Prompt) write(text, suggest string) {
c.SetCursorIndex(c.spacer + len(text))
txt := text
if suggest != "" {
txt += "[gray::-]" + suggest
}
fmt.Fprintf(c, defaultPrompt, c.icon, txt)
}
// ----------------------------------------------------------------------------
// Event Listener protocol...
// BufferChanged indicates the buffer was changed.
func (c *Prompt) BufferChanged(s string) {
c.update(s)
}
func (c *Prompt) SuggestionChanged(text, sugg string) {
c.Clear()
c.write(text, sugg)
}
// BufferActive indicates the buff activity changed.
func (c *Prompt) BufferActive(activate bool, kind model.BufferKind) {
if activate {
c.ShowCursor(true)
c.SetBorder(true)
c.SetTextColor(c.styles.FgColor())
c.SetBorderColor(colorFor(kind))
c.icon = c.iconFor(kind)
c.activate()
return
}
c.ShowCursor(false)
c.SetBorder(false)
c.SetBackgroundColor(c.styles.BgColor())
c.Clear()
}
func (c *Prompt) iconFor(k model.BufferKind) rune {
if c.noIcons {
return ' '
}
switch k {
case model.CommandBuffer:
return '🐶'
default:
return '🐩'
}
}
// ----------------------------------------------------------------------------
// Helpers...
func colorFor(k model.BufferKind) tcell.Color {
switch k {
case model.CommandBuffer:
return tcell.ColorAqua
default:
return tcell.ColorSeaGreen
}
}