package cliuimodel import ( "container/list" "fmt" "strings" "time" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cliui/colors" "git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cliui/icons" "git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/events" ) // EventMsg defines a message for events. type EventMsg struct { events.Event Start time.Time Duration time.Duration } // NewStatusEvents returns a new events model. func NewStatusEvents(bus events.Provider, maxHistory int) StatusEvents { return StatusEvents{ events: list.New(), spinner: NewSpinner(), maxHistory: maxHistory, bus: bus, } } // StatusEvents defines a model for status events. // The model renders a view that can be divided in three sections. // The first one displays the "static" events which are the ones // that are not status events. The second section displays a spinner // with the status event that is in progress, and the third one // displays a list with the past status events. type StatusEvents struct { static []events.Event events *list.List spinner spinner.Model maxHistory int bus events.Provider } func (m *StatusEvents) ClearEvents() { m.static = nil m.events.Init() } func (m StatusEvents) Wait() tea.Cmd { return tea.Batch(m.spinner.Tick, m.WaitEvent) } func (m StatusEvents) WaitEvent() tea.Msg { e := <-m.bus.Events() return EventMsg{ Start: time.Now(), Event: e, } } func (m StatusEvents) Update(msg tea.Msg) (StatusEvents, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case EventMsg: if msg.InProgress() { // Save the duration of the current ongoing event before setting a new one if e := m.events.Front(); e != nil { evt := e.Value.(EventMsg) evt.Duration = time.Since(evt.Start) e.Value = evt } // Add the event to the queue m.events.PushFront(msg) // Only show a reduced history of events if m.events.Len() > m.maxHistory { m.events.Remove(m.events.Back()) } } else { // Events that have no progress status are considered static, // so they will be printed without the spinner and won't be // removed from the output until the view is removed. m.static = append(m.static, msg.Event) } // Return a command to wait for the next event cmd = m.Wait() default: // Update the spinner state and get a new tick command m.spinner, cmd = m.spinner.Update(msg) } return m, cmd } func (m StatusEvents) View() string { var view strings.Builder // Display static events first for _, evt := range m.static { view.WriteString(evt.String()) if !strings.HasSuffix(evt.Message, "\n") { view.WriteRune('\n') } } // Make sure there is a line between the static and status events if m.static != nil && m.events.Len() > 0 { view.WriteRune('\n') } // Display status events if m.events.Len() > 0 { for e := m.events.Front(); e != nil; e = e.Next() { evt := e.Value.(EventMsg) // The first event is displayed using a spinner if e.Prev() == nil { fmt.Fprintf(&view, "%s%s\n", m.spinner.View(), evt) if e.Next() != nil { view.WriteRune('\n') } continue } // Display finished status event d := evt.Duration.Round(time.Second) s := strings.TrimSuffix(evt.String(), "...") fmt.Fprintf(&view, "%s %s %s\n", icons.OK, s, colors.Faint(d.String())) } } return view.String() } // NewEvents returns a new events model. func NewEvents(bus events.Provider) Events { return Events{ events: list.New(), bus: bus, spinner: NewSpinner(), } } // Events defines a model for events. // The model renders a view that prints all received events one after // the other. Status events are displayed with a spinner and removed // from the list once they finish. type Events struct { events *list.List bus events.Provider spinner spinner.Model } func (m *Events) ClearEvents() { m.events.Init() } func (m Events) Wait() tea.Cmd { // Check if the last added event is a status event // and if so make sure that the spinner is updated. if e := m.events.Back(); e != nil { if evt := e.Value.(events.Event); evt.InProgress() { return tea.Batch(m.spinner.Tick, m.WaitEvent) } } // By default, just wait until the next event is received return m.WaitEvent } func (m Events) WaitEvent() tea.Msg { e := <-m.bus.Events() return EventMsg{ Event: e, Start: time.Now(), } } func (m Events) Update(msg tea.Msg) (Events, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case EventMsg: // Remove the last event if is a status one. // Status events must always be the last event in the list so the // spinner is displayed at the bottom and not in between events. // They are removed when another status event is received. if e := m.events.Back(); e != nil { if evt := e.Value.(events.Event); evt.InProgress() { m.events.Remove(e) } } // Append event at the end of the list m.events.PushBack(msg.Event) // Return a command to wait for the next event cmd = m.Wait() default: // Update the spinner state and get a new tick command m.spinner, cmd = m.spinner.Update(msg) } return m, cmd } func (m Events) View() string { var ( view strings.Builder group string ) // Display the list of events for e := m.events.Front(); e != nil; e = e.Next() { evt := e.Value.(events.Event) // Add an empty line when the event group changes but omit it // for the first event to avoid adding an initial empty line. if group != evt.Group && e.Prev() != nil { // Update the group being displayed group = evt.Group view.WriteRune('\n') } if e.Next() == nil && evt.InProgress() { // When the event is the last one and is a status event display a spinner... fmt.Fprintf(&view, "\n%s%s", m.spinner.View(), evt) } else { // Otherwise display the event without the spinner view.WriteString(evt.String()) } // Make sure that events have an EOL, so they are displayed right below each other if !strings.HasSuffix(evt.Message, "\n") { view.WriteRune('\n') } } return view.String() }