// SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s package model import ( "context" "sync" "time" ) const ( maxBuff = 10 keyEntryDelay = 100 * time.Millisecond // CommandBuffer represents a command buffer. CommandBuffer BufferKind = 1 << iota // FilterBuffer represents a filter buffer. FilterBuffer ) type ( // BufferKind indicates a buffer type. BufferKind int8 // BuffWatcher represents a command buffer listener. BuffWatcher interface { // BufferCompleted indicates input was accepted. BufferCompleted(text, suggestion string) // BufferChanged indicates the buffer was changed. BufferChanged(text, suggestion string) // BufferActive indicates the buff activity changed. BufferActive(state bool, kind BufferKind) } ) // CmdBuff represents user command input. type CmdBuff struct { buff []rune suggestion string listeners map[BuffWatcher]struct{} hotKey rune kind BufferKind active bool cancel context.CancelFunc mx sync.RWMutex } // NewCmdBuff returns a new command buffer. func NewCmdBuff(key rune, kind BufferKind) *CmdBuff { return &CmdBuff{ hotKey: key, kind: kind, buff: make([]rune, 0, maxBuff), listeners: make(map[BuffWatcher]struct{}), } } // InCmdMode checks if a command exists and the buffer is active. func (c *CmdBuff) InCmdMode() bool { c.mx.RLock() defer c.mx.RUnlock() if !c.active { return false } return len(c.buff) > 0 } // IsActive checks if command buffer is active. func (c *CmdBuff) IsActive() bool { c.mx.RLock() defer c.mx.RUnlock() return c.active } // SetActive toggles cmd buffer active state. func (c *CmdBuff) SetActive(b bool) { c.mx.Lock() c.active = b c.mx.Unlock() c.fireActive(c.active) } // GetText returns the current text. func (c *CmdBuff) GetText() string { c.mx.RLock() defer c.mx.RUnlock() return string(c.buff) } // GetKind returns the buffer kind. func (c *CmdBuff) GetKind() BufferKind { c.mx.RLock() defer c.mx.RUnlock() return c.kind } // GetSuggestion returns the current suggestion. func (c *CmdBuff) GetSuggestion() string { c.mx.RLock() defer c.mx.RUnlock() return c.suggestion } func (c *CmdBuff) hasCancel() bool { c.mx.RLock() defer c.mx.RUnlock() return c.cancel != nil } func (c *CmdBuff) setCancel(f context.CancelFunc) { c.mx.Lock() c.cancel = f c.mx.Unlock() } func (c *CmdBuff) resetCancel() { c.mx.Lock() c.cancel = nil c.mx.Unlock() } // SetText initializes the buffer with a command. func (c *CmdBuff) SetText(text, suggestion string, wipe bool) { c.mx.Lock() if wipe { c.buff, c.suggestion = []rune(text), suggestion } else { c.buff, c.suggestion = append(c.buff, []rune(text)...), suggestion } c.mx.Unlock() c.fireBufferCompleted(c.GetText(), c.GetSuggestion()) } // Add adds a new character to the buffer. func (c *CmdBuff) Add(r rune) { c.mx.Lock() c.buff = append(c.buff, r) c.mx.Unlock() c.fireBufferChanged(c.GetText(), c.GetSuggestion()) if c.hasCancel() { return } ctx, cancel := context.WithTimeout(context.Background(), keyEntryDelay) c.setCancel(cancel) go func() { <-ctx.Done() c.fireBufferCompleted(c.GetText(), c.GetSuggestion()) c.resetCancel() }() } // Delete removes the last character from the buffer. func (c *CmdBuff) Delete() { if c.Empty() { return } c.SetText(string(c.buff[:len(c.buff)-1]), "", true) c.fireBufferChanged(c.GetText(), c.GetSuggestion()) if c.hasCancel() { return } ctx, cancel := context.WithTimeout(context.Background(), 800*time.Millisecond) c.setCancel(cancel) go func() { <-ctx.Done() c.fireBufferCompleted(c.GetText(), c.GetSuggestion()) c.resetCancel() }() } // ClearText clears out command buffer. func (c *CmdBuff) ClearText(fire bool) { c.mx.Lock() c.buff, c.suggestion = c.buff[:0], "" c.mx.Unlock() if fire { c.fireBufferCompleted(c.GetText(), c.GetSuggestion()) } } // Reset clears out the command buffer and deactivates it. func (c *CmdBuff) Reset() { c.ClearText(true) c.SetActive(false) c.fireBufferCompleted(c.GetText(), c.GetSuggestion()) } // Empty returns true if no cmd, false otherwise. func (c *CmdBuff) Empty() bool { c.mx.RLock() defer c.mx.RUnlock() return len(c.buff) == 0 } // ---------------------------------------------------------------------------- // Event Listeners... // AddListener registers a cmd buffer listener. func (c *CmdBuff) AddListener(w BuffWatcher) { c.mx.Lock() c.listeners[w] = struct{}{} c.mx.Unlock() } // RemoveListener removes a listener. func (c *CmdBuff) RemoveListener(l BuffWatcher) { c.mx.Lock() delete(c.listeners, l) c.mx.Unlock() } func (c *CmdBuff) fireBufferCompleted(t, s string) { for l := range c.listeners { l.BufferCompleted(t, s) } } func (c *CmdBuff) fireBufferChanged(t, s string) { for l := range c.listeners { l.BufferChanged(t, s) } } func (c *CmdBuff) fireActive(b bool) { for l := range c.listeners { l.BufferActive(b, c.GetKind()) } }