110 lines
4.0 KiB
Go
Executable File
110 lines
4.0 KiB
Go
Executable File
// 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 }
|