dynamo-browse/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go
Leon Mika 79692302af
Issue 23: Added progress indicators and cancellation (#34)
- Wrapped all table operations in a new foreground job context, which mediates foreground tasks.
- Added cancellation support and partial results for table read operations.
- Added the "mark" command, which can mark, unmark & toggle marked items
- Added support for alias arguments.
- Removed the "unmark" command, and replaced it as an alias to the "marked" command
- Fixed seg faults raised when there is no table shown in the result set.
2022-10-10 10:15:25 +11:00

153 lines
4.1 KiB
Go

package statusandprompt
import (
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils"
"log"
)
// StatusAndPrompt is a resizing model which displays a submodel and a status bar. When the start prompt
// event is received, focus will be torn away and the user will be given a prompt the enter text.
type StatusAndPrompt struct {
model layout.ResizingModel
style Style
modeLine string
statusMessage string
spinner spinner.Model
spinnerVisible bool
pendingInput *events.PromptForInputMsg
textInput textinput.Model
width int
}
type Style struct {
ModeLine lipgloss.Style
}
func New(model layout.ResizingModel, initialMsg string, style Style) *StatusAndPrompt {
textInput := textinput.New()
return &StatusAndPrompt{
model: model,
style: style,
statusMessage: initialMsg,
modeLine: "",
spinner: spinner.New(spinner.WithSpinner(spinner.Line)),
textInput: textInput,
}
}
func (s *StatusAndPrompt) Init() tea.Cmd {
return tea.Batch(
s.model.Init(),
)
}
func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cc utils.CmdCollector
switch msg := msg.(type) {
case events.ErrorMsg:
s.statusMessage = "Error: " + msg.Error()
case events.StatusMsg:
s.statusMessage = string(msg)
case events.WrappedStatusMsg:
s.statusMessage = string(msg.Message)
cc.Add(func() tea.Msg { return msg.Next })
case events.ForegroundJobUpdate:
if msg.JobRunning {
s.spinnerVisible = true
s.statusMessage = msg.JobStatus
cc.Add(s.spinner.Tick)
} else {
s.spinnerVisible = false
}
case events.ModeMessage:
s.modeLine = string(msg)
case events.MessageWithStatus:
if hasModeMessage, ok := msg.(events.MessageWithMode); ok {
s.modeLine = hasModeMessage.ModeMessage()
}
s.statusMessage = msg.StatusMessage()
case events.PromptForInputMsg:
if s.pendingInput != nil {
// ignore, already in an input
return s, nil
}
s.textInput.Prompt = msg.Prompt
s.textInput.Focus()
s.textInput.SetValue("")
s.pendingInput = &msg
return s, nil
case tea.KeyMsg:
if s.pendingInput != nil {
switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc:
s.pendingInput = nil
case tea.KeyEnter:
pendingInput := s.pendingInput
s.pendingInput = nil
return s, func() tea.Msg {
m := pendingInput.OnDone(s.textInput.Value())
log.Printf("return msg type = %T", m)
return m
}
default:
if msg.Type == tea.KeyRunes {
msg.Runes = sliceutils.Filter(msg.Runes, func(r rune) bool { return r != '\x0d' && r != '\x0a' })
}
newTextInput, cmd := s.textInput.Update(msg)
s.textInput = newTextInput
return s, cmd
}
} else {
s.statusMessage = ""
}
}
if s.spinnerVisible {
s.spinner = cc.Collect(s.spinner.Update(msg)).(spinner.Model)
}
s.model = cc.Collect(s.model.Update(msg)).(layout.ResizingModel)
return s, cc.Cmd()
}
func (s *StatusAndPrompt) InPrompt() bool {
return s.pendingInput != nil
}
func (s *StatusAndPrompt) View() string {
return lipgloss.JoinVertical(lipgloss.Top, s.model.View(), s.viewStatus())
}
func (s *StatusAndPrompt) Resize(w, h int) layout.ResizingModel {
s.width = w
submodelHeight := h - lipgloss.Height(s.viewStatus())
s.model = s.model.Resize(w, submodelHeight)
return s
}
func (s *StatusAndPrompt) viewStatus() string {
modeLine := s.style.ModeLine.Render(lipgloss.PlaceHorizontal(s.width, lipgloss.Left, s.modeLine, lipgloss.WithWhitespaceChars(" ")))
var statusLine string
if s.pendingInput != nil {
statusLine = s.textInput.View()
} else {
statusLine = s.statusMessage
}
if s.spinnerVisible {
statusLine = lipgloss.JoinHorizontal(lipgloss.Left, s.spinner.View(), " ", statusLine)
}
return lipgloss.JoinVertical(lipgloss.Top, modeLine, statusLine)
}