256 lines
5.6 KiB
Go
256 lines
5.6 KiB
Go
|
package statusbar
|
|||
|
|
|||
|
import (
|
|||
|
"fmt"
|
|||
|
"lazykimi/pkg/theme"
|
|||
|
"strings"
|
|||
|
"time"
|
|||
|
|
|||
|
"github.com/charmbracelet/bubbles/v2/spinner"
|
|||
|
tea "github.com/charmbracelet/bubbletea/v2"
|
|||
|
"github.com/charmbracelet/lipgloss/v2"
|
|||
|
)
|
|||
|
|
|||
|
type FlashType string
|
|||
|
|
|||
|
const (
|
|||
|
FlashTypeError FlashType = "error"
|
|||
|
FlashTypeSuccess FlashType = "success"
|
|||
|
)
|
|||
|
|
|||
|
func FlashSuccess(msg string) tea.Cmd {
|
|||
|
return func() tea.Msg {
|
|||
|
return EventFlashSuccess(msg)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func FlashError(msg string) tea.Cmd {
|
|||
|
return func() tea.Msg {
|
|||
|
return EventFlashError(msg)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
type (
|
|||
|
EventFlashSuccess string
|
|||
|
EventFlashError string
|
|||
|
)
|
|||
|
|
|||
|
// Model represents the application status bar
|
|||
|
type Model struct {
|
|||
|
// Layout
|
|||
|
width int
|
|||
|
styles Style
|
|||
|
|
|||
|
// Left sections
|
|||
|
messagesCount int
|
|||
|
modelName string
|
|||
|
markdownMode bool
|
|||
|
|
|||
|
// Right sections
|
|||
|
Spinner spinner.Model
|
|||
|
isGenerating bool
|
|||
|
startTime time.Time
|
|||
|
flashType FlashType
|
|||
|
flashMessage string
|
|||
|
}
|
|||
|
|
|||
|
// Style represents the styling for the status bar
|
|||
|
type Style struct {
|
|||
|
base lipgloss.Style
|
|||
|
divider lipgloss.Style
|
|||
|
message lipgloss.Style
|
|||
|
model lipgloss.Style
|
|||
|
mode lipgloss.Style
|
|||
|
flash map[FlashType]lipgloss.Style
|
|||
|
bar lipgloss.Style
|
|||
|
}
|
|||
|
|
|||
|
// New creates a new status bar component
|
|||
|
func New(width int) Model {
|
|||
|
s := spinner.New()
|
|||
|
s.Spinner = spinner.Moon
|
|||
|
|
|||
|
// Initialize base styles
|
|||
|
baseStyle := lipgloss.NewStyle().Bold(true).Padding(0, 1)
|
|||
|
|
|||
|
return Model{
|
|||
|
width: width,
|
|||
|
Spinner: s,
|
|||
|
styles: Style{
|
|||
|
base: baseStyle,
|
|||
|
divider: lipgloss.NewStyle().
|
|||
|
Foreground(lipgloss.Color("#666666")).
|
|||
|
Padding(0, 1),
|
|||
|
message: baseStyle.Foreground(theme.Yellow),
|
|||
|
model: baseStyle.Foreground(theme.Blue),
|
|||
|
mode: baseStyle.Foreground(theme.Green),
|
|||
|
flash: map[FlashType]lipgloss.Style{
|
|||
|
FlashTypeError: baseStyle.Foreground(theme.Red),
|
|||
|
FlashTypeSuccess: baseStyle.Foreground(theme.Green),
|
|||
|
},
|
|||
|
bar: lipgloss.NewStyle().
|
|||
|
BorderStyle(lipgloss.NormalBorder()).
|
|||
|
BorderForeground(theme.Gray).
|
|||
|
BorderTop(true).
|
|||
|
Width(width),
|
|||
|
},
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// SetWidth updates the width of the status bar
|
|||
|
func (s *Model) SetWidth(width int) {
|
|||
|
s.width = width
|
|||
|
}
|
|||
|
|
|||
|
// SetLoading sets loading state
|
|||
|
func (s *Model) SetGenerating(loading bool) {
|
|||
|
s.isGenerating = loading
|
|||
|
if loading {
|
|||
|
s.startTime = time.Now()
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func (s *Model) ClearFlashMessage() {
|
|||
|
s.flashType = ""
|
|||
|
s.flashMessage = ""
|
|||
|
}
|
|||
|
|
|||
|
// SetError sets an error message
|
|||
|
func (s *Model) setFlashMessage(t FlashType, msg string) {
|
|||
|
s.flashType = t
|
|||
|
s.flashMessage = msg
|
|||
|
}
|
|||
|
|
|||
|
// SetMessageCount sets the message count
|
|||
|
func (s *Model) SetMessageCount(count int) {
|
|||
|
s.messagesCount = count
|
|||
|
}
|
|||
|
|
|||
|
// SetModelName sets the model name
|
|||
|
func (s *Model) SetModelName(name string) {
|
|||
|
s.modelName = name
|
|||
|
}
|
|||
|
|
|||
|
// SetMarkdownMode sets the markdown mode
|
|||
|
func (s *Model) SetMarkdownMode(enable bool) {
|
|||
|
s.markdownMode = enable
|
|||
|
}
|
|||
|
|
|||
|
// Init implements tea.Model
|
|||
|
func (s *Model) Init() tea.Cmd {
|
|||
|
return s.Spinner.Tick
|
|||
|
}
|
|||
|
|
|||
|
// Update implements tea.Model
|
|||
|
func (s Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||
|
var cmd tea.Cmd
|
|||
|
var cmds []tea.Cmd
|
|||
|
|
|||
|
switch msg := msg.(type) {
|
|||
|
case tea.WindowSizeMsg:
|
|||
|
s.SetWidth(msg.Width)
|
|||
|
case spinner.TickMsg:
|
|||
|
s.Spinner, cmd = s.Spinner.Update(msg)
|
|||
|
cmds = append(cmds, cmd)
|
|||
|
case EventFlashError:
|
|||
|
s.setFlashMessage(FlashTypeError, string(msg))
|
|||
|
case EventFlashSuccess:
|
|||
|
s.setFlashMessage(FlashTypeSuccess, string(msg))
|
|||
|
}
|
|||
|
return s, tea.Batch(cmds...)
|
|||
|
}
|
|||
|
|
|||
|
// View implements tea.Model
|
|||
|
func (s *Model) View() string {
|
|||
|
// Render left and right sections
|
|||
|
leftBar := s.joinSections(s.getLeftSections())
|
|||
|
rightBar := s.joinSections(s.getRightSections())
|
|||
|
|
|||
|
// Calculate and handle available space
|
|||
|
availableWidth := s.width
|
|||
|
leftWidth := lipgloss.Width(leftBar)
|
|||
|
rightWidth := lipgloss.Width(rightBar)
|
|||
|
spacerWidth := availableWidth - leftWidth - rightWidth
|
|||
|
|
|||
|
var statusBar string
|
|||
|
// Handle different space scenarios
|
|||
|
switch {
|
|||
|
case spacerWidth >= 0:
|
|||
|
// Enough space for everything
|
|||
|
spacer := strings.Repeat(" ", spacerWidth)
|
|||
|
statusBar = fmt.Sprintf("%s%s%s", leftBar, spacer, rightBar)
|
|||
|
case availableWidth >= leftWidth:
|
|||
|
// Only show left bar
|
|||
|
statusBar = leftBar
|
|||
|
default:
|
|||
|
// Not enough space - truncate left bar
|
|||
|
statusBar = lipgloss.NewStyle().
|
|||
|
MaxWidth(availableWidth).
|
|||
|
Render(leftBar)
|
|||
|
}
|
|||
|
|
|||
|
// Apply the final bar styling
|
|||
|
return s.styles.bar.Width(s.width).Render(statusBar)
|
|||
|
}
|
|||
|
|
|||
|
// getLeftSections returns the rendered left sections of the status bar
|
|||
|
func (s *Model) getLeftSections() []string {
|
|||
|
mode := "PLAIN"
|
|||
|
if s.markdownMode {
|
|||
|
mode = "MARKDOWN"
|
|||
|
}
|
|||
|
|
|||
|
return []string{
|
|||
|
s.styles.message.Render(fmt.Sprintf("💬 %d", s.messagesCount)),
|
|||
|
s.styles.model.Render(fmt.Sprintf("🤖 %s", s.modelName)),
|
|||
|
s.styles.mode.Render(mode),
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// getRightSections returns the rendered right sections of the status bar
|
|||
|
func (s *Model) getRightSections() []string {
|
|||
|
var sections []string
|
|||
|
|
|||
|
// Generation status
|
|||
|
if s.isGenerating {
|
|||
|
duration := time.Since(s.startTime).Round(time.Millisecond)
|
|||
|
sections = append(sections,
|
|||
|
s.styles.flash[FlashTypeSuccess].Render(
|
|||
|
fmt.Sprintf("⚡ %s %.1fs", s.Spinner.View(), duration.Seconds()),
|
|||
|
),
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
// Flash message
|
|||
|
if s.flashMessage != "" {
|
|||
|
var icon string
|
|||
|
switch s.flashType {
|
|||
|
case FlashTypeError:
|
|||
|
icon = "×"
|
|||
|
case FlashTypeSuccess:
|
|||
|
icon = "✓"
|
|||
|
}
|
|||
|
sections = append(sections,
|
|||
|
s.styles.flash[s.flashType].Render(
|
|||
|
fmt.Sprintf("%s %s", icon, s.flashMessage),
|
|||
|
),
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
return sections
|
|||
|
}
|
|||
|
|
|||
|
// joinSections joins sections with a dot separator
|
|||
|
func (s *Model) joinSections(sections []string) string {
|
|||
|
if len(sections) == 0 {
|
|||
|
return ""
|
|||
|
}
|
|||
|
|
|||
|
separator := s.styles.divider.SetString("┃").String()
|
|||
|
result := sections[0]
|
|||
|
for _, section := range sections[1:] {
|
|||
|
result = lipgloss.JoinHorizontal(lipgloss.Center, result, separator, section)
|
|||
|
}
|
|||
|
return result
|
|||
|
}
|