// Package ui implements the bubbletea TUI — the "Focused Forge" interface // used by admins who want deep y/n configuration without leaving the // terminal. // // Layout (tmux-style): // // ┌─────────────────────────────────────────┐ // │ 🔮 SORCERY-Go · Casting: wget, openssl │ header // ├─────────────────────────────────────────┤ // │ [scrollable build log] │ viewport // │ ... │ // ├─────────────────────────────────────────┤ // │ [LOAD 2.41] [JOBS 3/12] ▓▓▓▓▓░░░ 42% │ status bar // └─────────────────────────────────────────┘ package ui import ( "fmt" "strings" "github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // Model holds the entire TUI state. type Model struct { casting []string completed int total int logs viewport.Model progress progress.Model width int height int logLines []string } var ( statusStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFFFFF")). Background(lipgloss.Color("#353535")). Bold(true) logStyle = lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), true, false, false, false). BorderForeground(lipgloss.Color("#555555")) ) // NewModel returns the initial TUI state. func NewModel(total int) Model { vp := viewport.New(80, 20) p := progress.New(progress.WithDefaultGradient()) return Model{ total: total, logs: vp, progress: p, casting: []string{}, } } // Init satisfies tea.Model. func (m Model) Init() tea.Cmd { return nil } // Update handles messages from the engine. func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: if msg.String() == "q" || msg.String() == "ctrl+c" { return m, tea.Quit } case LogMsg: m.logLines = append(m.logLines, string(msg)) m.logs.SetContent(strings.Join(m.logLines, "\n")) case CastCompleteMsg: m.completed++ pct := float64(m.completed) / float64(m.total) return m, m.progress.SetPercent(pct) case tea.WindowSizeMsg: m.width, m.height = msg.Width, msg.Height m.logs.Width = msg.Width m.logs.Height = msg.Height - 4 } var cmd tea.Cmd m.logs, cmd = m.logs.Update(msg) return m, cmd } // View renders the screen. func (m Model) View() string { header := fmt.Sprintf(" 🔮 SORCERY-Go · Casting: %s", strings.Join(m.casting, ", ")) statusText := fmt.Sprintf(" [JOBS: %d/%d] [ROOT: /]", m.completed, m.total) spacer := strings.Repeat(" ", max(0, m.width-len(statusText)-20)) bar := lipgloss.JoinHorizontal(lipgloss.Top, statusStyle.Render(statusText), spacer, m.progress.View(), ) return lipgloss.JoinVertical(lipgloss.Left, header, logStyle.Width(m.width).Height(m.height-4).Render(m.logs.View()), bar, ) } // LogMsg is one line of build output streamed into the viewport. type LogMsg string // CastCompleteMsg is fired when a worker finishes a spell. type CastCompleteMsg struct{ Name string }