Some checks failed
Docs Deploy / build_and_deploy (push) Has been cancelled
Generate Docs / cli (push) Has been cancelled
Generate Config Doc / cli (push) Has been cancelled
Go formatting / go-formatting (push) Has been cancelled
Check links / markdown-link-check (push) Has been cancelled
Integration / pre-test (push) Has been cancelled
Integration / test on (push) Has been cancelled
Integration / status (push) Has been cancelled
Lint / Lint Go code (push) Has been cancelled
Test / test (ubuntu-latest) (push) Has been cancelled
339 lines
8 KiB
Go
339 lines
8 KiB
Go
package cliui
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sync"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cliui/bubbleconfirm"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cliui/clispinner"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cliui/entrywriter"
|
|
uilog "git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cliui/log"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/errors"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/events"
|
|
)
|
|
|
|
type sessionOptions struct {
|
|
stdin io.ReadCloser
|
|
stdout io.WriteCloser
|
|
stderr io.WriteCloser
|
|
|
|
spinnerStart bool
|
|
spinnerText string
|
|
|
|
ignoreEvents bool
|
|
verbosity uilog.Verbosity
|
|
}
|
|
|
|
// Session controls command line interaction with users.
|
|
type Session struct {
|
|
options sessionOptions
|
|
ev events.Bus
|
|
spinner clispinner.Spinner
|
|
out uilog.Output
|
|
wg *sync.WaitGroup
|
|
ended bool
|
|
skipUI bool
|
|
}
|
|
|
|
// Option configures session options.
|
|
type Option func(s *Session)
|
|
|
|
// WithStdout sets the starndard output for the session.
|
|
func WithStdout(stdout io.WriteCloser) Option {
|
|
return func(s *Session) {
|
|
s.options.stdout = stdout
|
|
}
|
|
}
|
|
|
|
// WithStderr sets base stderr for a Session.
|
|
func WithStderr(stderr io.WriteCloser) Option {
|
|
return func(s *Session) {
|
|
s.options.stderr = stderr
|
|
}
|
|
}
|
|
|
|
// WithStdin sets the starndard input for the session.
|
|
func WithStdin(stdin io.ReadCloser) Option {
|
|
return func(s *Session) {
|
|
s.options.stdin = stdin
|
|
}
|
|
}
|
|
|
|
// WithVerbosity sets a verbosity level for the Session.
|
|
func WithVerbosity(v uilog.Verbosity) Option {
|
|
return func(s *Session) {
|
|
s.options.verbosity = v
|
|
}
|
|
}
|
|
|
|
// IgnoreEvents configures the session to avoid displaying events.
|
|
// This is a compatibility option to be able to use the session and
|
|
// the events bus when models are used to manage CLI UI. The session
|
|
// won't handle the events when this option is present.
|
|
func IgnoreEvents() Option {
|
|
return func(s *Session) {
|
|
s.options.ignoreEvents = true
|
|
}
|
|
}
|
|
|
|
// StartSpinner forces spinner to be spinning right after creation.
|
|
func StartSpinner() Option {
|
|
return func(s *Session) {
|
|
s.options.spinnerStart = true
|
|
}
|
|
}
|
|
|
|
// StartSpinnerWithText forces spinner to be spinning right after creation
|
|
// with a custom status text.
|
|
func StartSpinnerWithText(text string) Option {
|
|
return func(s *Session) {
|
|
s.options.spinnerStart = true
|
|
s.options.spinnerText = text
|
|
}
|
|
}
|
|
|
|
func WithoutUserInteraction(yes bool) Option {
|
|
return func(s *Session) {
|
|
s.skipUI = yes
|
|
}
|
|
}
|
|
|
|
// New creates a new Session.
|
|
func New(options ...Option) *Session {
|
|
session := Session{
|
|
ev: events.NewBus(),
|
|
wg: &sync.WaitGroup{},
|
|
options: sessionOptions{
|
|
stdin: os.Stdin,
|
|
stdout: os.Stdout,
|
|
stderr: os.Stderr,
|
|
spinnerText: clispinner.DefaultText,
|
|
},
|
|
}
|
|
|
|
for _, apply := range options {
|
|
apply(&session)
|
|
}
|
|
|
|
logOptions := []uilog.Option{
|
|
uilog.WithStdout(session.options.stdout),
|
|
uilog.WithStderr(session.options.stderr),
|
|
}
|
|
|
|
if session.options.verbosity == uilog.VerbosityVerbose {
|
|
logOptions = append(logOptions, uilog.Verbose())
|
|
}
|
|
|
|
session.out = uilog.NewOutput(logOptions...)
|
|
|
|
if session.options.spinnerStart {
|
|
session.StartSpinner(session.options.spinnerText)
|
|
}
|
|
|
|
// The main loop that prints the events uses a wait group to block
|
|
// the session end until all the events are printed.
|
|
if !session.options.ignoreEvents {
|
|
session.wg.Add(1)
|
|
go session.handleEvents()
|
|
}
|
|
|
|
return &session
|
|
}
|
|
|
|
// EventBus returns the event bus of the session.
|
|
func (s Session) EventBus() events.Bus {
|
|
return s.ev
|
|
}
|
|
|
|
// Verbosity returns the verbosity level for the session output.
|
|
func (s Session) Verbosity() uilog.Verbosity {
|
|
return s.options.verbosity
|
|
}
|
|
|
|
// NewOutput returns a new logging output bound to the session.
|
|
// The new output will use the session's verbosity, stderr and stdout.
|
|
// Label and color arguments are used to prefix the output when the
|
|
// session verbosity is verbose.
|
|
func (s Session) NewOutput(label, color string) uilog.Output {
|
|
options := []uilog.Option{
|
|
uilog.WithStdout(s.options.stdout),
|
|
uilog.WithStderr(s.options.stderr),
|
|
}
|
|
|
|
if s.options.verbosity == uilog.VerbosityVerbose {
|
|
options = append(options, uilog.CustomVerbose(label, color))
|
|
}
|
|
|
|
return uilog.NewOutput(options...)
|
|
}
|
|
|
|
// SpinnerMessage change the spinner message.
|
|
func (s *Session) SpinnerMessage(text string) {
|
|
s.StopSpinner()
|
|
s.StartSpinner(text)
|
|
}
|
|
|
|
// StartSpinner starts the spinner.
|
|
func (s *Session) StartSpinner(text string) {
|
|
if s.options.ignoreEvents {
|
|
return
|
|
}
|
|
|
|
// Verbose mode must not render the spinner but instead
|
|
// it should just print the text to display next to the
|
|
// app label otherwise the verbose logs would be printed
|
|
// with an invalid format.
|
|
if s.options.verbosity == uilog.VerbosityVerbose {
|
|
fmt.Fprint(s.out.Stdout(), text)
|
|
return
|
|
}
|
|
|
|
if s.spinner == nil {
|
|
s.spinner = clispinner.New(clispinner.WithWriter(s.out.Stdout()))
|
|
}
|
|
|
|
s.spinner.SetText(text).Start()
|
|
}
|
|
|
|
// StopSpinner stops the spinner.
|
|
func (s Session) StopSpinner() {
|
|
if s.spinner == nil {
|
|
return
|
|
}
|
|
|
|
s.spinner.Stop()
|
|
}
|
|
|
|
// PauseSpinner pauses spinner and returns a function to restart the spinner.
|
|
func (s Session) PauseSpinner() (restart func()) {
|
|
isActive := s.spinner != nil && s.spinner.IsActive()
|
|
if isActive {
|
|
s.spinner.Stop()
|
|
}
|
|
|
|
return func() {
|
|
if isActive {
|
|
s.spinner.Start()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Printf prints formatted arbitrary message.
|
|
func (s Session) Printf(format string, a ...interface{}) error {
|
|
defer s.PauseSpinner()()
|
|
_, err := fmt.Fprintf(s.out.Stdout(), format, a...)
|
|
return err
|
|
}
|
|
|
|
// Println prints arbitrary message with line break.
|
|
func (s Session) Println(messages ...interface{}) error {
|
|
defer s.PauseSpinner()()
|
|
_, err := fmt.Fprintln(s.out.Stdout(), messages...)
|
|
return err
|
|
}
|
|
|
|
// Print prints arbitrary message.
|
|
func (s Session) Print(messages ...interface{}) error {
|
|
defer s.PauseSpinner()()
|
|
_, err := fmt.Fprint(s.out.Stdout(), messages...)
|
|
return err
|
|
}
|
|
|
|
// Ask asks questions in the terminal and collect answers.
|
|
func (s Session) Ask(questions ...bubbleconfirm.Question) error {
|
|
// If the flag yes was set true, we skip the user interaction
|
|
if s.skipUI {
|
|
return nil
|
|
}
|
|
defer s.PauseSpinner()()
|
|
// TODO provide writer from the session
|
|
return bubbleconfirm.Ask(questions...)
|
|
}
|
|
|
|
// ErrAbort is returned when the user aborts the operation.
|
|
var ErrAbort = errors.New("aborted or not confirmed")
|
|
|
|
// AskConfirm asks a yes/no question using a bubbletea dialog.
|
|
func (s Session) AskConfirm(message string) error {
|
|
if s.skipUI {
|
|
return nil
|
|
}
|
|
|
|
defer s.PauseSpinner()()
|
|
|
|
// Create and run the bubbletea program
|
|
p := tea.NewProgram(bubbleconfirm.NewModel(message))
|
|
|
|
// Run the program
|
|
m, err := p.Run()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Type assert to our model
|
|
confirmModel, ok := m.(bubbleconfirm.Model)
|
|
if !ok {
|
|
return errors.New("could not assert type to bubbleconfirm.Model")
|
|
}
|
|
|
|
// Check the result
|
|
if confirmModel.Choice() != bubbleconfirm.Yes {
|
|
return ErrAbort
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PrintTable prints table data.
|
|
func (s Session) PrintTable(header []string, entries ...[]string) error {
|
|
defer s.PauseSpinner()()
|
|
return entrywriter.MustWrite(s.out.Stdout(), header, entries...)
|
|
}
|
|
|
|
// End finishes the session by stopping the spinner and the event bus.
|
|
// Once the session is ended it should not be used anymore.
|
|
func (s *Session) End() {
|
|
if s.ended {
|
|
return
|
|
}
|
|
|
|
s.StopSpinner()
|
|
s.ev.Stop()
|
|
s.wg.Wait()
|
|
s.ended = true
|
|
}
|
|
|
|
func (s *Session) handleEvents() {
|
|
defer s.wg.Done()
|
|
|
|
stdout := s.out.Stdout()
|
|
|
|
for e := range s.ev.Events() {
|
|
switch e.ProgressIndication {
|
|
case events.IndicationStart:
|
|
s.StartSpinner(e.String())
|
|
case events.IndicationUpdate:
|
|
if s.spinner == nil {
|
|
// When the spinner is not initialized print the event
|
|
fmt.Fprintf(stdout, "%s\n", e)
|
|
} else {
|
|
// Otherwise update the spinner with a new text
|
|
s.spinner.SetText(e.String())
|
|
}
|
|
case events.IndicationFinish:
|
|
s.StopSpinner()
|
|
fmt.Fprintf(stdout, "%s\n", e)
|
|
case events.IndicationNone:
|
|
fallthrough
|
|
default:
|
|
// The text printed here won't be removed when the spinner stops
|
|
resume := s.PauseSpinner()
|
|
fmt.Fprintf(stdout, "%s\n", e)
|
|
resume()
|
|
}
|
|
}
|
|
}
|