feat: complete TUI redesign with cyberpunk theme
All checks were successful
PR Check / check (pull_request) Successful in 18s

- Dark theme with red accents (cyberpunk aesthetic)
- Epuré cyberpunk style: clean dark backgrounds, sharp red highlights
- Full cyberpunk animations: glitch effect, scan line, typewriter
- Mixed Unicode + ASCII icons
- Rounded borders (╭ ╮ ╯ ╰) on cards and panels
- ASCII art block titles (■) with red styling
- Header: MUYUE branding, status indicators, live clock
- Footer: shortcuts, version, update indicator
- Tab transitions: glitch → scan → typewriter sequence
- Extracted header.go, footer.go, animations.go as new files

Controls unchanged: ctrl+t tabs, ctrl+s sidebar, ctrl+a AI panel

file changes:
- styles.go: new color palette (cyberRed, bgVoid, dimRed), block titles
- types.go: added transition state, clock tick, glitch/scan/done messages
- animations.go: new file with glitch, scan, typewriter, hex stream effects
- header.go: new file with logo, tabs, status dots, live clock
- footer.go: new file with shortcuts, version, update indicator
- model.go: integrated transition state machine, clock updates
- dashboard.go, studio.go, terminal.go, config_tab.go: updated icons/styles

Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-20 22:14:22 +02:00
parent 22fb2823ce
commit 5f91ef2a78
12 changed files with 771 additions and 417 deletions

115
internal/tui/animations.go Normal file
View File

@@ -0,0 +1,115 @@
package tui
import (
"fmt"
"math/rand"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
)
var glitchChars = "!@#$%^&*()_+-=[]{}|;':,./<>?~`"
func init() {
rand.Seed(time.Now().UnixNano())
}
func randomGlitchChar() string {
return string(glitchChars[rand.Intn(len(glitchChars))])
}
func glitchText(text string, intensity int) string {
runes := []rune(text)
for i := 0; i < intensity; i++ {
pos := rand.Intn(len(runes))
if runes[pos] != ' ' && runes[pos] != '\n' {
runes[pos] = []rune(randomGlitchChar())[0]
}
}
return string(runes)
}
func generateScanLine(width int, frame int) string {
pos := frame % width
line := strings.Repeat(" ", pos) +
lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(strings.Repeat("━", min(20, width-pos)))
if pos+20 < width {
line += strings.Repeat(" ", width-pos-20)
}
return line[:min(width, len(line))]
}
func typewriterRender(text string, pos int) string {
if pos >= len(text) {
return text
}
if pos <= 0 {
return ""
}
shown := text[:pos]
cursor := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render("█")
return shown + cursor
}
func renderGlitchEffect(width, height, frame int) string {
var b strings.Builder
for y := 0; y < height; y++ {
line := ""
for x := 0; x < width; x++ {
if rand.Float64() < 0.15 {
c := randomGlitchChar()
style := lipgloss.NewStyle()
r := rand.Float64()
if r < 0.4 {
style = style.Foreground(cyberRed)
} else if r < 0.7 {
style = style.Foreground(cyberPink)
} else {
style = style.Foreground(textMuted)
}
line += style.Render(c)
} else {
line += " "
}
}
b.WriteString(line)
if y < height-1 {
b.WriteString("\n")
}
}
return b.String()
}
func renderScanEffect(width, height, frame int) string {
var b strings.Builder
scanY := frame % (height + 10)
for y := 0; y < height; y++ {
if y == scanY || y == scanY+1 {
b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Render(strings.Repeat("━", width)))
} else if y == scanY-1 || y == scanY+2 {
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(strings.Repeat("─", width)))
} else if y < scanY {
b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf("%*s", width, "")))
} else {
b.WriteString(strings.Repeat(" ", width))
}
if y < height-1 {
b.WriteString("\n")
}
}
return b.String()
}
func generateHexStream(width, lines int) string {
var b strings.Builder
for y := 0; y < lines; y++ {
for x := 0; x < width/3; x++ {
b.WriteString(fmt.Sprintf("%02X", rand.Intn(256)))
}
if y < lines-1 {
b.WriteString("\n")
}
}
return lipgloss.NewStyle().Foreground(dimRed).Render(b.String())
}

View File

@@ -2,18 +2,11 @@ package tui
import ( import (
"fmt" "fmt"
"regexp"
"strings" "strings"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
var versionRegex = regexp.MustCompile(`\d+\.\d+\.\d+`)
func extractVersion(s string) string {
return versionRegex.FindString(s)
}
func (m Model) renderConfig() string { func (m Model) renderConfig() string {
colWidth := m.width / 2 colWidth := m.width / 2
if colWidth < 30 { if colWidth < 30 {
@@ -22,7 +15,7 @@ func (m Model) renderConfig() string {
var left, right strings.Builder var left, right strings.Builder
left.WriteString(renderSectionWithIcon("Profile", "👤")) left.WriteString(renderSectionHeader("PROFILE", "[@]"))
left.WriteString("\n") left.WriteString("\n")
if m.config != nil { if m.config != nil {
fields := []struct { fields := []struct {
@@ -50,28 +43,28 @@ func (m Model) renderConfig() string {
} }
left.WriteString("\n") left.WriteString("\n")
left.WriteString(renderSectionWithIcon("AI Providers", "")) left.WriteString(renderSectionHeader("AI PROVIDERS", "[AI]"))
left.WriteString("\n") left.WriteString("\n")
if m.config != nil { if m.config != nil {
for _, p := range m.config.AI.Providers { for _, p := range m.config.AI.Providers {
active := "" active := ""
if p.Active { if p.Active {
active = lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" ") active = lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" >>")
} }
keyStatus := itemMissingStyle.Render("no key") keyStatus := itemMissingStyle.Render("no key")
if p.APIKey != "" { if p.APIKey != "" {
keyStatus = itemOKStyle.Render("configured") keyStatus = itemOKStyle.Render("configured")
} }
nameStyle := lipgloss.NewStyle().Foreground(textColor).Bold(true) nameStyle := lipgloss.NewStyle().Foreground(textBright).Bold(true)
left.WriteString(fmt.Sprintf(" %s %s key=%s%s\n", left.WriteString(fmt.Sprintf(" %s %s key=%s%s\n",
nameStyle.Render(p.Name), nameStyle.Render(p.Name),
lipgloss.NewStyle().Foreground(dimColor).Render("model="+p.Model), lipgloss.NewStyle().Foreground(dimRed).Render("model="+p.Model),
keyStatus, active)) keyStatus, active))
} }
} }
left.WriteString("\n") left.WriteString("\n")
right.WriteString(renderSectionWithIcon("Terminal", "")) right.WriteString(renderSectionHeader("TERMINAL", "[$]"))
right.WriteString("\n") right.WriteString("\n")
if m.config != nil { if m.config != nil {
right.WriteString(fmt.Sprintf(" %s %v\n", labelStyle.Render("Custom Prompt:"), valueStyle.Render(fmt.Sprintf("%v", m.config.Terminal.CustomPrompt)))) right.WriteString(fmt.Sprintf(" %s %v\n", labelStyle.Render("Custom Prompt:"), valueStyle.Render(fmt.Sprintf("%v", m.config.Terminal.CustomPrompt))))
@@ -81,12 +74,12 @@ func (m Model) renderConfig() string {
} }
right.WriteString("\n") right.WriteString("\n")
right.WriteString(renderSectionWithIcon("BMAD Method", "")) right.WriteString(renderSectionHeader("BMAD METHOD", "[B]"))
right.WriteString("\n") right.WriteString("\n")
if m.config != nil { if m.config != nil {
installed := itemMissingStyle.Render("no") installed := itemMissingStyle.Render("[--] no")
if m.config.BMAD.Installed { if m.config.BMAD.Installed {
installed = itemOKStyle.Render("yes") installed = itemOKStyle.Render("[OK] yes")
} }
right.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Installed:"), installed)) right.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Installed:"), installed))
right.WriteString(fmt.Sprintf(" %s %v\n", labelStyle.Render("Global:"), valueStyle.Render(fmt.Sprintf("%v", m.config.BMAD.Global)))) right.WriteString(fmt.Sprintf(" %s %v\n", labelStyle.Render("Global:"), valueStyle.Render(fmt.Sprintf("%v", m.config.BMAD.Global))))
@@ -96,7 +89,7 @@ func (m Model) renderConfig() string {
} }
right.WriteString("\n") right.WriteString("\n")
right.WriteString(renderSectionWithIcon(fmt.Sprintf("Skills (%d)", len(m.skillList)), "")) right.WriteString(renderSectionHeader(fmt.Sprintf("SKILLS (%d)", len(m.skillList)), "[!]"))
right.WriteString("\n") right.WriteString("\n")
if len(m.skillList) > 0 { if len(m.skillList) > 0 {
for _, s := range m.skillList { for _, s := range m.skillList {
@@ -105,12 +98,12 @@ func (m Model) renderConfig() string {
target = "both" target = "both"
} }
right.WriteString(fmt.Sprintf(" %s %s %s\n", right.WriteString(fmt.Sprintf(" %s %s %s\n",
lipgloss.NewStyle().Foreground(textColor).Render(s.Name), lipgloss.NewStyle().Foreground(textMain).Render(s.Name),
lipgloss.NewStyle().Foreground(primaryColor).Render("["+target+"]"), lipgloss.NewStyle().Foreground(cyberRed).Render("["+target+"]"),
lipgloss.NewStyle().Foreground(dimColor).Render(s.Description))) lipgloss.NewStyle().Foreground(dimRed).Render(s.Description)))
} }
} else { } else {
right.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render(" No skills. Run `muyue skills init`.")) right.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render(" No skills. Run `muyue skills init`."))
right.WriteString("\n") right.WriteString("\n")
} }

View File

@@ -15,16 +15,16 @@ func (m Model) renderDashboard() string {
var left, right strings.Builder var left, right strings.Builder
left.WriteString(renderSectionWithIcon("System", "")) left.WriteString(renderSectionHeader("SYSTEM", "[*]"))
left.WriteString("\n") left.WriteString("\n")
if m.scanResult != nil { if m.scanResult != nil {
sysInfo := m.scanResult.System.String() sysInfo := m.scanResult.System.String()
left.WriteString(" ") left.WriteString(" ")
left.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(sysInfo)) left.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(sysInfo))
} }
left.WriteString("\n\n") left.WriteString("\n\n")
left.WriteString(renderSectionWithIcon("Installed Tools", "")) left.WriteString(renderSectionHeader("INSTALLED TOOLS", "[+]"))
left.WriteString("\n") left.WriteString("\n")
if m.scanResult != nil { if m.scanResult != nil {
installed := 0 installed := 0
@@ -33,14 +33,14 @@ func (m Model) renderDashboard() string {
if t.Installed { if t.Installed {
installed++ installed++
left.WriteString(" ") left.WriteString(" ")
left.WriteString(itemOKStyle.Render(" ")) left.WriteString(itemOKStyle.Render("[OK] "))
left.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(t.Name)) left.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(t.Name))
left.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(fmt.Sprintf(" %s", extractVersion(t.Version)))) left.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf(" %s", extractVersion(t.Version))))
left.WriteString("\n") left.WriteString("\n")
} else { } else {
left.WriteString(" ") left.WriteString(" ")
left.WriteString(itemMissingStyle.Render(" ")) left.WriteString(itemMissingStyle.Render("[--] "))
left.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render(t.Name)) left.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render(t.Name))
left.WriteString(itemPendingStyle.Render(" (missing)")) left.WriteString(itemPendingStyle.Render(" (missing)"))
left.WriteString("\n") left.WriteString("\n")
} }
@@ -51,14 +51,14 @@ func (m Model) renderDashboard() string {
if total > 0 { if total > 0 {
pct = (installed * barWidth) / total pct = (installed * barWidth) / total
} }
bar := lipgloss.NewStyle().Foreground(primaryColor).Render(strings.Repeat("█", pct)) + bar := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(strings.Repeat("█", pct)) +
lipgloss.NewStyle().Foreground(dimColor).Render(strings.Repeat("░", barWidth-pct)) lipgloss.NewStyle().Foreground(dimRed).Render(strings.Repeat("░", barWidth-pct))
left.WriteString(fmt.Sprintf("\n %s %d/%d\n", bar, installed, total)) left.WriteString(fmt.Sprintf("\n %s %d/%d\n", bar, installed, total))
} }
left.WriteString("\n") left.WriteString("\n")
if m.installing { if m.installing {
left.WriteString(renderSectionWithIcon("Installing", "")) left.WriteString(renderSectionHeader("INSTALLING", "[~]"))
left.WriteString("\n") left.WriteString("\n")
progBar := m.progressBar.View() progBar := m.progressBar.View()
label := "" label := ""
@@ -72,7 +72,7 @@ func (m Model) renderDashboard() string {
} }
if len(m.installLog) > 0 { if len(m.installLog) > 0 {
left.WriteString(renderSectionWithIcon("Install Log", "📋")) left.WriteString(renderSectionHeader("INSTALL LOG", "[#]"))
left.WriteString("\n") left.WriteString("\n")
for _, l := range m.installLog { for _, l := range m.installLog {
left.WriteString(l + "\n") left.WriteString(l + "\n")
@@ -80,27 +80,27 @@ func (m Model) renderDashboard() string {
left.WriteString("\n") left.WriteString("\n")
} }
right.WriteString(renderSectionWithIcon("Quick Actions", "")) right.WriteString(renderSectionHeader("QUICK ACTIONS", "[!]"))
right.WriteString("\n") right.WriteString("\n")
actions := []struct { actions := []struct {
key string key string
desc string desc string
color lipgloss.Color color lipgloss.Color
}{ }{
{"i", "Install missing tools", primaryColor}, {"i", "Install missing tools", cyberRed},
{"u", "Check for updates", warmColor}, {"u", "Check for updates", neonRed},
{"s", "Rescan system", roseColor}, {"s", "Rescan system", cyberPink},
{"l", "Scan LSP servers", accentColor}, {"l", "Scan LSP servers", cyberRose},
{"m", "Configure MCP servers", roseLightColor}, {"m", "Configure MCP servers", brightRed},
} }
for _, a := range actions { for _, a := range actions {
right.WriteString(fmt.Sprintf(" %s %s\n", right.WriteString(fmt.Sprintf(" %s %s\n",
lipgloss.NewStyle().Foreground(a.color).Bold(true).Render("["+a.key+"]"), lipgloss.NewStyle().Foreground(a.color).Bold(true).Render("["+a.key+"]"),
lipgloss.NewStyle().Foreground(textColor).Render(a.desc))) lipgloss.NewStyle().Foreground(textMain).Render(a.desc)))
} }
right.WriteString("\n") right.WriteString("\n")
right.WriteString(renderSectionWithIcon("Active Agents", "")) right.WriteString(renderSectionHeader("ACTIVE AGENTS", "[*]"))
right.WriteString("\n") right.WriteString("\n")
agents := []struct { agents := []struct {
@@ -111,24 +111,24 @@ func (m Model) renderDashboard() string {
} }
for _, a := range agents { for _, a := range agents {
right.WriteString(" ") right.WriteString(" ")
right.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(" ")) right.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(">> "))
right.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render(a.name + " ")) right.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render(a.name + " "))
right.WriteString(itemPendingStyle.Render("stopped")) right.WriteString(itemPendingStyle.Render("[stopped]"))
right.WriteString("\n") right.WriteString("\n")
} }
right.WriteString("\n") right.WriteString("\n")
if len(m.updateStatus) > 0 { if len(m.updateStatus) > 0 {
right.WriteString(renderSectionWithIcon("Updates", "")) right.WriteString(renderSectionHeader("UPDATES", "[^]"))
right.WriteString("\n") right.WriteString("\n")
for _, s := range m.updateStatus { for _, s := range m.updateStatus {
if s.NeedsUpdate { if s.NeedsUpdate {
right.WriteString(" ") right.WriteString(" ")
right.WriteString(itemWarnStyle.Render(" ")) right.WriteString(itemWarnStyle.Render("[!!] "))
right.WriteString(fmt.Sprintf("%s: %s %s\n", s.Tool, s.Current, s.Latest)) right.WriteString(fmt.Sprintf("%s: %s -> %s\n", s.Tool, s.Current, s.Latest))
} else if s.Error == "" { } else if s.Error == "" {
right.WriteString(" ") right.WriteString(" ")
right.WriteString(itemOKStyle.Render(" ")) right.WriteString(itemOKStyle.Render("[OK] "))
right.WriteString(fmt.Sprintf("%s: up to date\n", s.Tool)) right.WriteString(fmt.Sprintf("%s: up to date\n", s.Tool))
} }
} }
@@ -136,18 +136,18 @@ func (m Model) renderDashboard() string {
} }
if len(m.lspServers) > 0 { if len(m.lspServers) > 0 {
right.WriteString(renderSectionWithIcon("LSP Servers", "§")) right.WriteString(renderSectionHeader("LSP SERVERS", "[L]"))
right.WriteString("\n") right.WriteString("\n")
lspInstalled := 0 lspInstalled := 0
for _, s := range m.lspServers { for _, s := range m.lspServers {
if s.Installed { if s.Installed {
lspInstalled++ lspInstalled++
right.WriteString(" ") right.WriteString(" ")
right.WriteString(itemOKStyle.Render(" ")) right.WriteString(itemOKStyle.Render("[OK] "))
right.WriteString(fmt.Sprintf("%-22s (%s)\n", s.Name, s.Language)) right.WriteString(fmt.Sprintf("%-22s (%s)\n", s.Name, s.Language))
} else { } else {
right.WriteString(" ") right.WriteString(" ")
right.WriteString(itemPendingStyle.Render(" ")) right.WriteString(itemPendingStyle.Render("[ ] "))
right.WriteString(fmt.Sprintf("%-22s (%s)\n", s.Name, s.Language)) right.WriteString(fmt.Sprintf("%-22s (%s)\n", s.Name, s.Language))
} }
} }
@@ -155,16 +155,16 @@ func (m Model) renderDashboard() string {
right.WriteString("\n") right.WriteString("\n")
} }
mcpStatus := itemPendingStyle.Render(" not configured") mcpStatus := itemPendingStyle.Render("[ ] not configured")
if m.mcpConfigured { if m.mcpConfigured {
mcpStatus = itemOKStyle.Render(" configured") mcpStatus = itemOKStyle.Render("[OK] configured")
} }
right.WriteString(fmt.Sprintf(" MCP: %s\n", mcpStatus)) right.WriteString(fmt.Sprintf(" MCP: %s\n", mcpStatus))
if m.daemon != nil { if m.daemon != nil {
daemonStatus := itemPendingStyle.Render(" stopped") daemonStatus := itemPendingStyle.Render("[ ] stopped")
if m.daemon.IsRunning() { if m.daemon.IsRunning() {
daemonStatus = itemOKStyle.Render(" running") daemonStatus = itemOKStyle.Render("[OK] running")
} }
right.WriteString(fmt.Sprintf(" Daemon: %s\n", daemonStatus)) right.WriteString(fmt.Sprintf(" Daemon: %s\n", daemonStatus))
} }
@@ -174,8 +174,3 @@ func (m Model) renderDashboard() string {
return lipgloss.JoinHorizontal(lipgloss.Top, leftCol, rightCol) return lipgloss.JoinHorizontal(lipgloss.Top, leftCol, rightCol)
} }
func renderSectionWithIcon(title string, icon string) string {
return lipgloss.NewStyle().Foreground(primaryColor).Render(icon+" ") +
sectionStyle.Render(title)
}

74
internal/tui/footer.go Normal file
View File

@@ -0,0 +1,74 @@
package tui
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/muyue/muyue/internal/version"
)
func (m Model) renderFooter() string {
profile := "unknown"
if m.config != nil && m.config.Profile.Pseudo != "" {
profile = m.config.Profile.Pseudo
}
left := fmt.Sprintf(" %s@%s",
lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(profile),
lipgloss.NewStyle().Foreground(dimRed).Render(version.Name))
leftR := statusBarStyle.Render(left)
var helpText string
switch m.activeTab {
case tabDashboard:
helpText = "[i] install [u] update [s] scan [ctrl+t] tabs"
case tabStudio:
helpText = "[enter] send [ctrl+s] sidebar [ctrl+t] tabs"
case tabShell:
helpText = "[enter] run [ctrl+a] AI panel [ctrl+c] kill"
case tabConfig:
helpText = "[up/down] sections [ctrl+t] tabs"
default:
helpText = "[ctrl+t] tabs [ctrl+c] quit"
}
rightR := statusBarStyle.Render(helpText)
updateIndicator := ""
if len(m.updateStatus) > 0 {
needsUpdate := false
for _, s := range m.updateStatus {
if s.NeedsUpdate {
needsUpdate = true
break
}
}
if needsUpdate {
updateIndicator = lipgloss.NewStyle().Foreground(warnAmber).Render(" [UPD] ")
} else {
updateIndicator = lipgloss.NewStyle().Foreground(successGreen).Render(" [OK] ")
}
}
verStr := lipgloss.NewStyle().Foreground(dimRed).Render("v" + version.Version)
midContent := lipgloss.NewStyle().Background(bgSurface).Padding(0, 1).Render(
updateIndicator + verStr,
)
gap := m.width - lipgloss.Width(leftR) - lipgloss.Width(rightR) - lipgloss.Width(midContent)
if gap < 0 {
gap = 0
}
statusLine := lipgloss.JoinHorizontal(lipgloss.Bottom,
leftR,
strings.Repeat(" ", gap),
midContent,
rightR,
)
helpLine := lipgloss.NewStyle().Background(bgSurface).Foreground(textMuted).Render(
lipgloss.NewStyle().Padding(0, 1).Render(m.helpModel.View(keys)))
return lipgloss.JoinVertical(lipgloss.Left, statusLine, helpLine)
}

View File

@@ -142,21 +142,30 @@ func (m Model) handleTabMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
return m, nil return m, nil
case "enter": case "enter":
m.activeTab = tab(m.tabMenuCursor) m.switchTab(tab(m.tabMenuCursor))
m.showingTabMenu = false m.showingTabMenu = false
m.resizeViewport()
return m, nil return m, nil
default: default:
for i := 0; i < int(tabCount); i++ { for i := 0; i < int(tabCount); i++ {
if msg.String() == fmt.Sprintf("%d", i+1) { if msg.String() == fmt.Sprintf("%d", i+1) {
m.activeTab = tab(i) m.switchTab(tab(i))
m.showingTabMenu = false m.showingTabMenu = false
return m, nil
}
}
}
return m, nil
}
func (m *Model) switchTab(t tab) {
if t == m.activeTab {
return
}
m.prevTab = m.activeTab
m.activeTab = t
m.transition = transitionGlitch
m.transitionTick = 0
m.resizeViewport() m.resizeViewport()
return m, nil
}
}
}
return m, nil
} }
func (m Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
@@ -174,13 +183,13 @@ func (m Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
} }
if len(missing) == 0 { if len(missing) == 0 {
m.installLog = append(m.installLog, itemOKStyle.Render(" All tools already installed!")) m.installLog = append(m.installLog, itemOKStyle.Render("[OK] All tools already installed!"))
m.viewport.SetContent(m.renderContent()) m.viewport.SetContent(m.renderContent())
return m, nil return m, nil
} }
needsSudo := checkNeedsSudo(m.scanResult) needsSudo := checkNeedsSudo(m.scanResult)
if needsSudo && !hasSudo() { if needsSudo && !hasSudo() {
m.installLog = append(m.installLog, errMsgStyle.Render(" Some tools require sudo. Run: sudo muyue install")) m.installLog = append(m.installLog, errMsgStyle.Render("[!!] Some tools require sudo. Run: sudo muyue install"))
m.viewport.SetContent(m.renderContent()) m.viewport.SetContent(m.renderContent())
return m, nil return m, nil
} }
@@ -267,7 +276,7 @@ func (m Model) handleWorkflowKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() { switch msg.String() {
case "a": case "a":
if wf.Phase == workflow.PhaseReviewing { if wf.Phase == workflow.PhaseReviewing {
m.chatLog = append(m.chatLog, userMsgStyle.Render(" [Plan approved]")) m.chatLog = append(m.chatLog, userMsgStyle.Render(">> [Plan approved]"))
m.chatLoading = true m.chatLoading = true
m.viewport.SetContent(m.renderContent()) m.viewport.SetContent(m.renderContent())
return m, reviewPlanCmd(m.orch, true, "") return m, reviewPlanCmd(m.orch, true, "")
@@ -280,7 +289,7 @@ func (m Model) handleWorkflowKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
case "g": case "g":
if wf.Phase == workflow.PhaseGathering && len(wf.Plan.Questions) > 0 && len(wf.Plan.Answers) >= len(wf.Plan.Questions) { if wf.Phase == workflow.PhaseGathering && len(wf.Plan.Questions) > 0 && len(wf.Plan.Answers) >= len(wf.Plan.Questions) {
m.chatLog = append(m.chatLog, userMsgStyle.Render(" [Generate plan]")) m.chatLog = append(m.chatLog, userMsgStyle.Render(">> [Generate plan]"))
m.chatLoading = true m.chatLoading = true
m.viewport.SetContent(m.renderContent()) m.viewport.SetContent(m.renderContent())
return m, generatePlanCmd(m.orch) return m, generatePlanCmd(m.orch)
@@ -332,7 +341,7 @@ func hasSudo() bool {
func (m Model) handleChatSubmit() (tea.Model, tea.Cmd) { func (m Model) handleChatSubmit() (tea.Model, tea.Cmd) {
input := m.chatInput input := m.chatInput
m.chatLog = append(m.chatLog, userMsgStyle.Render(" "+input)) m.chatLog = append(m.chatLog, userMsgStyle.Render(">> "+input))
m.chatInput = "" m.chatInput = ""
m.chatLoading = true m.chatLoading = true
m.viewport.SetContent(m.renderContent()) m.viewport.SetContent(m.renderContent())

178
internal/tui/header.go Normal file
View File

@@ -0,0 +1,178 @@
package tui
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/muyue/muyue/internal/version"
)
func (m Model) renderHeader() string {
var tabs []string
for i, name := range tabNames {
icon := tabIcons[i]
if tab(i) == m.activeTab {
tabStyle := lipgloss.NewStyle().
Background(cyberRed).
Foreground(lipgloss.Color("#FFFFFF")).
Bold(true).
Padding(0, 2)
tabs = append(tabs, tabStyle.Render(icon+" "+name))
} else {
tabStyle := lipgloss.NewStyle().
Background(bgSurface).
Foreground(textDim).
Padding(0, 2)
tabs = append(tabs, tabStyle.Render(icon+" "+name))
}
}
tabLine := tabBarStyle.Render(lipgloss.JoinHorizontal(lipgloss.Bottom, tabs...))
timeStr := ""
if !m.currentTime.IsZero() {
timeStr = m.currentTime.Format("15:04:05")
}
dateStr := ""
if !m.currentTime.IsZero() {
dateStr = m.currentTime.Format("02/01/2006")
}
rightInfo := lipgloss.NewStyle().Background(bgSurface).Padding(0, 1).Render(
lipgloss.JoinHorizontal(lipgloss.Center,
lipgloss.NewStyle().Foreground(textDim).Render(dateStr+" "),
lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(timeStr),
lipgloss.NewStyle().Foreground(textMuted).Render(" "+getAnimFrame(m.animationFrame)),
),
)
statusDots := ""
if m.config != nil {
hasAI := false
for _, p := range m.config.AI.Providers {
if p.Active && p.APIKey != "" {
hasAI = true
break
}
}
if hasAI {
statusDots += lipgloss.NewStyle().Foreground(successGreen).Render("●")
} else {
statusDots += lipgloss.NewStyle().Foreground(errorRed).Render("●")
}
} else {
statusDots += lipgloss.NewStyle().Foreground(warnAmber).Render("●")
}
statusDots += lipgloss.NewStyle().Foreground(textMuted).Render(" ")
if m.mcpConfigured {
statusDots += lipgloss.NewStyle().Foreground(successGreen).Render("●")
} else {
statusDots += lipgloss.NewStyle().Foreground(warnAmber).Render("●")
}
statusInfo := lipgloss.NewStyle().Background(bgSurface).Padding(0, 1).Render(
lipgloss.JoinHorizontal(lipgloss.Center,
lipgloss.NewStyle().Foreground(textDim).Render("SYS "),
statusDots,
),
)
badge := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render("MUYUE")
versionBadge := lipgloss.NewStyle().Foreground(dimRed).Render("v" + version.Version)
logoLine := lipgloss.NewStyle().Background(bgVoid).Padding(0, 1).Render(
lipgloss.JoinHorizontal(lipgloss.Center, badge, " ", versionBadge),
)
topLine := lipgloss.JoinHorizontal(lipgloss.Bottom,
logoLine,
strings.Repeat(" ", max(0, m.width-lipgloss.Width(logoLine)-lipgloss.Width(rightInfo)-lipgloss.Width(statusInfo))),
statusInfo,
rightInfo,
)
return lipgloss.JoinVertical(lipgloss.Left, topLine, tabLine)
}
func (m Model) renderTabMenuOverlay() string {
menuStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(cyberRed).
Background(bgCard).
Padding(1, 3)
tabItemStyle := lipgloss.NewStyle().
Foreground(textDim).
Padding(0, 2)
tabItemActiveStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")).
Background(cyberRed).
Bold(true).
Padding(0, 2)
descs := []string{
"tools, updates & system status",
"chat, agents & workflows",
"terminal + AI assistant",
"profile, API keys & settings",
}
var items []string
for i, name := range tabNames {
num := lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf(" %d.", i+1))
icon := tabIcons[i] + " "
if i == m.tabMenuCursor {
item := fmt.Sprintf("%s %s%-10s %s", num, icon, name, lipgloss.NewStyle().Foreground(cyberRose).Render(descs[i]))
items = append(items, tabItemActiveStyle.Render(">"+item))
} else {
item := fmt.Sprintf("%s %s%-10s %s", num, icon, name, lipgloss.NewStyle().Foreground(textMuted).Render(descs[i]))
items = append(items, tabItemStyle.Render(" "+item))
}
}
header := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render("SWITCH TAB")
content := header + "\n\n" +
strings.Join(items, "\n") +
"\n\n" +
lipgloss.NewStyle().Foreground(textMuted).Render("up/down navigate | enter select | esc cancel")
box := menuStyle.Render(content)
return lipgloss.Place(m.width, m.height,
0.5, 0.5,
box,
lipgloss.WithWhitespaceBackground(bgVoid),
lipgloss.WithWhitespaceForeground(textMuted),
)
}
func (m Model) renderQuitOverlay() string {
yesStyle := confirmNoStyle
noStyle := confirmYesStyle
if m.confirmCursor == 0 {
yesStyle = confirmYesStyle
noStyle = confirmNoStyle
}
frame := lipgloss.NewStyle().Foreground(cyberRed).Render(getAnimFrame(m.animationFrame))
box := fmt.Sprintf("\n\n %s Quit muyue?\n\n %s %s",
frame,
yesStyle.Render("[ Yes ]"),
noStyle.Render("[ No ]"),
)
content := confirmBoxStyle.Render(box)
return lipgloss.Place(m.width, m.height,
0.5, 0.5,
content,
lipgloss.WithWhitespaceBackground(bgVoid),
lipgloss.WithWhitespaceForeground(textMuted),
)
}

View File

@@ -1,9 +1,17 @@
package tui package tui
import ( import (
"regexp"
"github.com/muyue/muyue/internal/workflow" "github.com/muyue/muyue/internal/workflow"
) )
var versionRegex = regexp.MustCompile(`\d+\.\d+\.\d+`)
func extractVersion(s string) string {
return versionRegex.FindString(s)
}
type previewFile = workflow.PreviewFile type previewFile = workflow.PreviewFile
func parsePreviewFiles(response string) []previewFile { func parsePreviewFiles(response string) []previewFile {

View File

@@ -22,7 +22,6 @@ import (
"github.com/muyue/muyue/internal/proxy" "github.com/muyue/muyue/internal/proxy"
"github.com/muyue/muyue/internal/scanner" "github.com/muyue/muyue/internal/scanner"
"github.com/muyue/muyue/internal/skills" "github.com/muyue/muyue/internal/skills"
"github.com/muyue/muyue/internal/version"
) )
func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model { func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
@@ -44,9 +43,9 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
sp := spinner.New() sp := spinner.New()
sp.Spinner = spinner.Dot sp.Spinner = spinner.Dot
sp.Style = lipgloss.NewStyle().Foreground(primaryColor) sp.Style = lipgloss.NewStyle().Foreground(cyberRed)
prog := progress.New(progress.WithGradient("#E8364F", "#FF6B8A")) prog := progress.New(progress.WithGradient("#FF0033", "#FF1A5E"))
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
@@ -55,8 +54,8 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
scanResult: scan, scanResult: scan,
activeTab: tabDashboard, activeTab: tabDashboard,
chatLog: []string{ chatLog: []string{
aiMsgStyle.Render(" Welcome to Studio! Chat with your AI assistant here."), aiMsgStyle.Render(" >> Welcome to Studio! Chat with your AI assistant here."),
aiMsgStyle.Render(" Configure agents and workflows from the sidebar. Type /plan <goal> to start."), aiMsgStyle.Render(" >> Configure agents and workflows from the sidebar. Type /plan <goal> to start."),
}, },
orch: orch, orch: orch,
proxyMgr: proxyMgr, proxyMgr: proxyMgr,
@@ -77,12 +76,14 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model {
studioPanel: panelChat, studioPanel: panelChat,
studioSidebarOpen: true, studioSidebarOpen: true,
termAIChat: []string{ termAIChat: []string{
aiMsgStyle.Render(" I know your system inside out. Ask me anything."), aiMsgStyle.Render(" >> I know your system inside out. Ask me anything."),
}, },
termAIShow: true, termAIShow: true,
configSection: configProfile, configSection: configProfile,
configField: 0, configField: 0,
animationFrame: 0, animationFrame: 0,
currentTime: time.Now(),
transition: transitionNone,
} }
} }
@@ -92,8 +93,14 @@ func animTick() tea.Cmd {
}) })
} }
func clockTick() tea.Cmd {
return tea.Tick(1*time.Second, func(t time.Time) tea.Msg {
return clockTickMsg{time: t}
})
}
func (m Model) Init() tea.Cmd { func (m Model) Init() tea.Cmd {
return tea.Batch(spinner.Tick, animTick(), tea.EnterAltScreen) return tea.Batch(spinner.Tick, animTick(), clockTick(), tea.EnterAltScreen)
} }
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -106,7 +113,32 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd return m, cmd
case animTickMsg: case animTickMsg:
m.animationFrame++ m.animationFrame++
if m.transition == transitionGlitch {
m.transitionTick++
if m.transitionTick > 5 {
m.transition = transitionScan
m.transitionTick = 0
}
} else if m.transition == transitionScan {
m.transitionTick++
if m.transitionTick > 8 {
m.transition = transitionTypewriter
m.transitionTick = 0
m.typewriterBuf = m.renderContent()
m.typewriterPos = 0
}
} else if m.transition == transitionTypewriter {
m.typewriterPos += 3
if m.typewriterPos >= len(m.typewriterBuf) {
m.transition = transitionNone
}
}
return m, animTick() return m, animTick()
case clockTickMsg:
m.currentTime = msg.time
return m, clockTick()
case progress.FrameMsg: case progress.FrameMsg:
pm, cmd := m.progressBar.Update(msg) pm, cmd := m.progressBar.Update(msg)
m.progressBar = pm.(progress.Model) m.progressBar = pm.(progress.Model)
@@ -120,7 +152,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
case termExitMsg: case termExitMsg:
m.termRunning = false m.termRunning = false
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render("(process exited)")) m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimRed).Render("(process exited)"))
m.termCmd = nil m.termCmd = nil
if m.activeTab == tabShell { if m.activeTab == tabShell {
m.viewport.SetContent(m.renderContent()) m.viewport.SetContent(m.renderContent())
@@ -152,7 +184,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case aiErrMsg: case aiErrMsg:
m.chatLoading = false m.chatLoading = false
m.termAILoading = false m.termAILoading = false
errText := errMsgStyle.Render(" error: " + msg.err.Error()) errText := errMsgStyle.Render(" [ERROR] " + msg.err.Error())
if m.activeTab == tabShell && m.termAIShow { if m.activeTab == tabShell && m.termAIShow {
m.termAIChat = append(m.termAIChat, errText) m.termAIChat = append(m.termAIChat, errText)
} else { } else {
@@ -168,9 +200,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case installCompleteMsg: case installCompleteMsg:
m.installing = false m.installing = false
for _, r := range msg.results { for _, r := range msg.results {
status := itemOKStyle.Render("") status := itemOKStyle.Render("[OK]")
if !r.Success { if !r.Success {
status = itemMissingStyle.Render("") status = itemMissingStyle.Render("[--]")
} }
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, r.Tool, r.Message)) m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, r.Tool, r.Message))
} }
@@ -179,7 +211,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.viewport.SetContent(m.renderContent()) m.viewport.SetContent(m.renderContent())
return m, nil return m, nil
case installProgressMsg: case installProgressMsg:
status := itemOKStyle.Render("") status := itemOKStyle.Render("[OK]")
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s installed", status, msg.tool)) m.installLog = append(m.installLog, fmt.Sprintf(" %s %s installed", status, msg.tool))
m.installCurrent = msg.current m.installCurrent = msg.current
m.installTool = "" m.installTool = ""
@@ -188,9 +220,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.viewport.SetContent(m.renderContent()) m.viewport.SetContent(m.renderContent())
return m, nil return m, nil
case installBatchMsg: case installBatchMsg:
status := itemOKStyle.Render("") status := itemOKStyle.Render("[OK]")
if !msg.result.Success { if !msg.result.Success {
status = itemMissingStyle.Render("") status = itemMissingStyle.Render("[--]")
} }
m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, msg.result.Tool, msg.result.Message)) m.installLog = append(m.installLog, fmt.Sprintf(" %s %s: %s", status, msg.result.Tool, msg.result.Message))
m.installCurrent = msg.index + 1 m.installCurrent = msg.index + 1
@@ -254,7 +286,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m Model) View() string { func (m Model) View() string {
if !m.ready { if !m.ready {
return lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render("Loading muyue...") return lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render("Initializing muyue...")
} }
if m.showingQuit { if m.showingQuit {
@@ -265,6 +297,32 @@ func (m Model) View() string {
return m.renderTabMenuOverlay() return m.renderTabMenuOverlay()
} }
if m.transition == transitionGlitch {
return renderGlitchEffect(m.width, m.height, m.transitionTick)
}
if m.transition == transitionScan {
return renderScanEffect(m.width, m.height, m.transitionTick)
}
if m.transition == transitionTypewriter {
var b strings.Builder
b.WriteString(m.renderHeader())
b.WriteString("\n")
b.WriteString(typewriterRender(m.typewriterBuf, m.typewriterPos))
if m.activeTab == tabStudio {
b.WriteString("\n")
b.WriteString(m.renderStudioInput())
}
if m.activeTab == tabShell {
b.WriteString("\n")
b.WriteString(m.renderShellInput())
}
b.WriteString("\n")
b.WriteString(m.renderFooter())
return b.String()
}
var b strings.Builder var b strings.Builder
b.WriteString(m.renderHeader()) b.WriteString(m.renderHeader())
b.WriteString("\n") b.WriteString("\n")
@@ -283,45 +341,6 @@ func (m Model) View() string {
return b.String() return b.String()
} }
func (m Model) renderHeader() string {
var tabs []string
for i, name := range tabNames {
icon := tabIcons[i]
if tab(i) == m.activeTab {
tabStyle := lipgloss.NewStyle().
Background(primaryColor).
Foreground(lipgloss.Color("#FFFFFF")).
Bold(true).
Padding(0, 2)
tabs = append(tabs, tabStyle.Render(icon+" "+name))
} else {
tabStyle := lipgloss.NewStyle().
Background(bgPanel).
Foreground(textDimColor).
Padding(0, 2)
tabs = append(tabs, tabStyle.Render(icon+" "+name))
}
}
tabLine := tabBarStyle.Render(lipgloss.JoinHorizontal(lipgloss.Bottom, tabs...))
badge := lipgloss.NewStyle().
Foreground(roseColor).
Bold(true).
Render("muyue")
versionBadge := lipgloss.NewStyle().
Foreground(dimColor).
Render("v" + version.Version)
anim := lipgloss.NewStyle().Foreground(warmColor).Render(getAnimFrame(m.animationFrame))
logoLine := lipgloss.NewStyle().Background(bgDark).Padding(0, 1).Render(
lipgloss.JoinHorizontal(lipgloss.Center, badge, " ", versionBadge, " ", anim),
)
return lipgloss.JoinVertical(lipgloss.Left, logoLine, tabLine)
}
func (m Model) renderContent() string { func (m Model) renderContent() string {
switch m.activeTab { switch m.activeTab {
case tabDashboard: case tabDashboard:
@@ -354,127 +373,6 @@ func (m *Model) resizeViewport() {
m.viewport.SetContent(m.renderContent()) m.viewport.SetContent(m.renderContent())
} }
func (m Model) renderFooter() string {
profile := "unknown"
if m.config != nil && m.config.Profile.Pseudo != "" {
profile = m.config.Profile.Pseudo
}
left := fmt.Sprintf(" %s@%s",
lipgloss.NewStyle().Foreground(roseColor).Bold(true).Render(profile),
lipgloss.NewStyle().Foreground(dimColor).Render(version.Name))
leftR := statusBarStyle.Render(left)
var helpText string
switch m.activeTab {
case tabDashboard:
helpText = "[i] install [u] update [s] scan [ctrl+t] tabs"
case tabStudio:
helpText = "[enter] send [ctrl+s] sidebar [ctrl+t] tabs"
case tabShell:
helpText = "[enter] run [ctrl+a] AI panel [ctrl+c] kill"
case tabConfig:
helpText = "[↑↓] sections [ctrl+t] tabs"
default:
helpText = "[ctrl+t] tabs [ctrl+c] quit"
}
rightR := statusBarStyle.Render(helpText)
gap := m.width - lipgloss.Width(leftR) - lipgloss.Width(rightR)
if gap < 0 {
gap = 0
}
statusLine := lipgloss.JoinHorizontal(lipgloss.Bottom,
leftR,
strings.Repeat(" ", gap),
rightR,
)
return lipgloss.JoinVertical(lipgloss.Left, statusLine,
lipgloss.NewStyle().Background(bgPanel).Foreground(dimColor).Render(
lipgloss.NewStyle().Padding(0, 1).Render(m.helpModel.View(keys))))
}
func (m Model) renderQuitOverlay() string {
yesStyle := confirmNoStyle
noStyle := confirmYesStyle
if m.confirmCursor == 0 {
yesStyle = confirmYesStyle
noStyle = confirmNoStyle
}
frame := lipgloss.NewStyle().Foreground(primaryColor).Render(getAnimFrame(m.animationFrame))
box := fmt.Sprintf("\n\n %s Quit muyue?\n\n %s %s",
frame,
yesStyle.Render("[ Yes ]"),
noStyle.Render("[ No ]"),
)
content := confirmBoxStyle.Render(box)
return lipgloss.Place(m.width, m.height,
0.5, 0.5,
content,
lipgloss.WithWhitespaceBackground(bgDark),
lipgloss.WithWhitespaceForeground(dimColor),
)
}
func (m Model) renderTabMenuOverlay() string {
menuStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(primaryColor).
Background(bgCard).
Padding(1, 3)
tabItemStyle := lipgloss.NewStyle().
Foreground(textDimColor).
Padding(0, 2)
tabItemActiveStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")).
Background(primaryColor).
Bold(true).
Padding(0, 2)
descs := []string{
"tools, updates & system status",
"chat, agents & workflows",
"terminal + AI assistant",
"profile, API keys & settings",
}
var items []string
for i, name := range tabNames {
num := lipgloss.NewStyle().Foreground(dimColor).Render(fmt.Sprintf(" %d.", i+1))
icon := tabIcons[i] + " "
if i == m.tabMenuCursor {
item := fmt.Sprintf("%s %s%-10s %s", num, icon, name, lipgloss.NewStyle().Foreground(roseLightColor).Render(descs[i]))
items = append(items, tabItemActiveStyle.Render("▸"+item))
} else {
item := fmt.Sprintf("%s %s%-10s %s", num, icon, name, lipgloss.NewStyle().Foreground(dimColor).Render(descs[i]))
items = append(items, tabItemStyle.Render(" "+item))
}
}
header := lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render("Switch Tab")
content := header + "\n\n" +
strings.Join(items, "\n") +
"\n\n" +
lipgloss.NewStyle().Foreground(dimColor).Render("↑↓ navigate · enter select · esc cancel")
box := menuStyle.Render(content)
return lipgloss.Place(m.width, m.height,
0.5, 0.5,
box,
lipgloss.WithWhitespaceBackground(bgDark),
lipgloss.WithWhitespaceForeground(dimColor),
)
}
func (m *Model) handlePreview(files []previewFile) { func (m *Model) handlePreview(files []previewFile) {
dir := filepath.Join(os.TempDir(), "muyue-preview") dir := filepath.Join(os.TempDir(), "muyue-preview")
os.RemoveAll(dir) os.RemoveAll(dir)
@@ -495,22 +393,3 @@ func (m *Model) handlePreview(files []previewFile) {
m.chatLog = append(m.chatLog, itemOKStyle.Render(" Preview: http://127.0.0.1:8765")) m.chatLog = append(m.chatLog, itemOKStyle.Render(" Preview: http://127.0.0.1:8765"))
} }
} }
func (m Model) renderStudioInput() string {
if m.chatLoading {
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
inputStyle.Render("⟩ ") + m.spinner.View() + lipgloss.NewStyle().Foreground(mutedColor).Render(" thinking..."),
)
}
cursor := lipgloss.NewStyle().Foreground(primaryColor).Render("▎")
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
inputStyle.Render("⟩ ") + m.chatInput + cursor,
)
}
func (m Model) renderShellInput() string {
prompt := lipgloss.NewStyle().Foreground(successColor).Render(" ")
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
prompt + m.termInput + lipgloss.NewStyle().Foreground(primaryColor).Render("▎"),
)
}

View File

@@ -30,7 +30,7 @@ func (m Model) renderStudio() string {
func (m Model) renderStudioSidebar(width int) string { func (m Model) renderStudioSidebar(width int) string {
var b strings.Builder var b strings.Builder
b.WriteString(renderSectionWithIcon("Studio", "")) b.WriteString(renderSectionHeader("STUDIO", "[<>]"))
b.WriteString("\n\n") b.WriteString("\n\n")
panels := []struct { panels := []struct {
@@ -38,23 +38,23 @@ func (m Model) renderStudioSidebar(width int) string {
panel studioPanel panel studioPanel
icon string icon string
}{ }{
{"Chat", panelChat, "💬"}, {"Chat", panelChat, "[#]"},
{"Agents", panelAgents, ""}, {"Agents", panelAgents, "[*]"},
{"Workflows", panelWorkflows, ""}, {"Workflows", panelWorkflows, "[~]"},
} }
for _, p := range panels { for _, p := range panels {
if m.studioPanel == p.panel { if m.studioPanel == p.panel {
activeStyle := lipgloss.NewStyle(). activeStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")). Foreground(lipgloss.Color("#FFFFFF")).
Background(primaryColor). Background(cyberRed).
Bold(true). Bold(true).
Padding(0, 1) Padding(0, 1)
b.WriteString(activeStyle.Render(p.icon + " " + p.name)) b.WriteString(activeStyle.Render(p.icon + " " + p.name))
b.WriteString("\n") b.WriteString("\n")
} else { } else {
inactiveStyle := lipgloss.NewStyle(). inactiveStyle := lipgloss.NewStyle().
Foreground(textDimColor). Foreground(textDim).
Padding(0, 1) Padding(0, 1)
b.WriteString(inactiveStyle.Render(p.icon + " " + p.name)) b.WriteString(inactiveStyle.Render(p.icon + " " + p.name))
b.WriteString("\n") b.WriteString("\n")
@@ -62,7 +62,7 @@ func (m Model) renderStudioSidebar(width int) string {
} }
b.WriteString("\n") b.WriteString("\n")
b.WriteString(lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("─", width-4))) b.WriteString(lipgloss.NewStyle().Foreground(borderDim).Render(strings.Repeat("─", width-4)))
b.WriteString("\n\n") b.WriteString("\n\n")
switch m.studioPanel { switch m.studioPanel {
@@ -78,26 +78,26 @@ func (m Model) renderStudioSidebar(width int) string {
} }
func (m Model) renderChatSidebar(b *strings.Builder, width int) { func (m Model) renderChatSidebar(b *strings.Builder, width int) {
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Active Provider")) b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Active Provider"))
b.WriteString("\n") b.WriteString("\n")
provider := "none" provider := "none"
if m.config != nil { if m.config != nil {
provider = m.config.Profile.Preferences.DefaultAI provider = m.config.Profile.Preferences.DefaultAI
} }
b.WriteString(lipgloss.NewStyle().Foreground(roseColor).Bold(true).Render(" " + provider)) b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" " + provider))
b.WriteString("\n\n") b.WriteString("\n\n")
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Commands")) b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Commands"))
b.WriteString("\n") b.WriteString("\n")
cmds := []string{"/plan <goal>", "/help"} cmds := []string{"/plan <goal>", "/help"}
for _, c := range cmds { for _, c := range cmds {
b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(" " + c)) b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(" " + c))
b.WriteString("\n") b.WriteString("\n")
} }
if m.previewURL != "" { if m.previewURL != "" {
b.WriteString("\n") b.WriteString("\n")
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Preview")) b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Preview"))
b.WriteString("\n") b.WriteString("\n")
b.WriteString(itemOKStyle.Render(" " + m.previewURL)) b.WriteString(itemOKStyle.Render(" " + m.previewURL))
b.WriteString("\n") b.WriteString("\n")
@@ -121,43 +121,43 @@ func (m Model) renderAgentsSidebar(b *strings.Builder, width int) {
var statusIcon string var statusIcon string
switch status { switch status {
case proxy.StatusRunning: case proxy.StatusRunning:
statusIcon = lipgloss.NewStyle().Foreground(warmColor).Render(" running") statusIcon = lipgloss.NewStyle().Foreground(neonRed).Render("[>> running]")
case proxy.StatusStopped: case proxy.StatusStopped:
statusIcon = lipgloss.NewStyle().Foreground(mutedColor).Render(" stopped") statusIcon = lipgloss.NewStyle().Foreground(textMuted).Render("[|| stopped]")
case proxy.StatusError: case proxy.StatusError:
statusIcon = lipgloss.NewStyle().Foreground(errorColor).Render(" error") statusIcon = lipgloss.NewStyle().Foreground(errorRed).Render("[!! error]")
default: default:
if available { if available {
statusIcon = lipgloss.NewStyle().Foreground(successColor).Render(" available") statusIcon = lipgloss.NewStyle().Foreground(successGreen).Render("[OK available]")
} else { } else {
statusIcon = lipgloss.NewStyle().Foreground(dimColor).Render(" not installed") statusIcon = lipgloss.NewStyle().Foreground(textMuted).Render("[-- not installed]")
} }
} }
b.WriteString(lipgloss.NewStyle().Foreground(textColor).Bold(true).Render(a.name)) b.WriteString(lipgloss.NewStyle().Foreground(textBright).Bold(true).Render(a.name))
b.WriteString("\n") b.WriteString("\n")
b.WriteString(fmt.Sprintf(" %s\n", statusIcon)) b.WriteString(fmt.Sprintf(" %s\n", statusIcon))
b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(fmt.Sprintf(" %s\n", a.tool))) b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf(" %s\n", a.tool)))
b.WriteString("\n") b.WriteString("\n")
} }
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Actions")) b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Actions"))
b.WriteString("\n") b.WriteString("\n")
b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" [c]")) b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" [c]"))
b.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(" Start Crush")) b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(" Start Crush"))
b.WriteString("\n") b.WriteString("\n")
b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" [l]")) b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" [l]"))
b.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(" Start Claude")) b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(" Start Claude"))
b.WriteString("\n") b.WriteString("\n")
} }
func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) { func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
if m.orch == nil || m.orch.Workflow == nil { if m.orch == nil || m.orch.Workflow == nil {
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("No active workflow.")) b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("No active workflow."))
b.WriteString("\n\n") b.WriteString("\n\n")
b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render("Use /plan <goal> in chat")) b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render("Use /plan <goal> in chat"))
b.WriteString("\n") b.WriteString("\n")
b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render("to start a workflow.")) b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render("to start a workflow."))
b.WriteString("\n") b.WriteString("\n")
return return
} }
@@ -165,13 +165,13 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
wf := m.orch.Workflow wf := m.orch.Workflow
phaseColors := map[workflow.Phase]lipgloss.Style{ phaseColors := map[workflow.Phase]lipgloss.Style{
workflow.PhaseIdle: lipgloss.NewStyle().Foreground(mutedColor), workflow.PhaseIdle: lipgloss.NewStyle().Foreground(textMuted),
workflow.PhaseGathering: lipgloss.NewStyle().Foreground(warningColor).Bold(true), workflow.PhaseGathering: lipgloss.NewStyle().Foreground(warnAmber).Bold(true),
workflow.PhasePlanning: lipgloss.NewStyle().Foreground(roseColor).Bold(true), workflow.PhasePlanning: lipgloss.NewStyle().Foreground(cyberPink).Bold(true),
workflow.PhaseReviewing: lipgloss.NewStyle().Foreground(accentColor).Bold(true), workflow.PhaseReviewing: lipgloss.NewStyle().Foreground(cyberRose).Bold(true),
workflow.PhaseExecuting: lipgloss.NewStyle().Foreground(primaryColor).Bold(true), workflow.PhaseExecuting: lipgloss.NewStyle().Foreground(cyberRed).Bold(true),
workflow.PhaseDone: lipgloss.NewStyle().Foreground(successColor).Bold(true), workflow.PhaseDone: lipgloss.NewStyle().Foreground(successGreen).Bold(true),
workflow.PhaseError: lipgloss.NewStyle().Foreground(errorColor).Bold(true), workflow.PhaseError: lipgloss.NewStyle().Foreground(errorRed).Bold(true),
} }
if style, ok := phaseColors[wf.Phase]; ok { if style, ok := phaseColors[wf.Phase]; ok {
@@ -180,9 +180,9 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
b.WriteString("\n\n") b.WriteString("\n\n")
if wf.Plan.Goal != "" { if wf.Plan.Goal != "" {
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Goal")) b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Goal"))
b.WriteString("\n") b.WriteString("\n")
b.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(wf.Plan.Goal)) b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(wf.Plan.Goal))
b.WriteString("\n\n") b.WriteString("\n\n")
} }
@@ -194,7 +194,7 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
b.WriteString("\n\n") b.WriteString("\n\n")
} }
b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Controls")) b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Controls"))
b.WriteString("\n") b.WriteString("\n")
controls := []struct { controls := []struct {
key string key string
@@ -207,8 +207,8 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
{"[x]", "Cancel"}, {"[x]", "Cancel"},
} }
for _, c := range controls { for _, c := range controls {
b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" " + c.key)) b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" "+c.key))
b.WriteString(lipgloss.NewStyle().Foreground(textDimColor).Render(" " + c.desc)) b.WriteString(lipgloss.NewStyle().Foreground(textDim).Render(" "+c.desc))
b.WriteString("\n") b.WriteString("\n")
} }
} }
@@ -216,14 +216,14 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) {
func (m Model) renderStudioChat(width int) string { func (m Model) renderStudioChat(width int) string {
var b strings.Builder var b strings.Builder
chatHeader := renderSectionWithIcon("Chat", "💬") chatHeader := renderSectionHeader("CHAT", "[#]")
if m.chatLoading { if m.chatLoading {
chatHeader += " " + m.spinner.View() + lipgloss.NewStyle().Foreground(warningColor).Render("thinking...") chatHeader += " " + m.spinner.View() + lipgloss.NewStyle().Foreground(warnAmber).Render("thinking...")
} }
b.WriteString(chatHeader) b.WriteString(chatHeader)
b.WriteString("\n\n") b.WriteString("\n\n")
sep := lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("─", max(width-4, 10))) sep := lipgloss.NewStyle().Foreground(borderDim).Render(strings.Repeat("─", max(width-4, 10)))
b.WriteString(" " + sep) b.WriteString(" " + sep)
b.WriteString("\n\n") b.WriteString("\n\n")
@@ -235,6 +235,18 @@ func (m Model) renderStudioChat(width int) string {
return b.String() return b.String()
} }
func (m Model) renderStudioInput() string {
if m.chatLoading {
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
inputStyle.Render(">> ") + m.spinner.View() + lipgloss.NewStyle().Foreground(textMuted).Render(" thinking..."),
)
}
cursor := lipgloss.NewStyle().Foreground(cyberRed).Render("▎")
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
inputStyle.Render(">> ") + m.chatInput + cursor,
)
}
func (m Model) handleStudioPanelSwitch(panel studioPanel) { func (m Model) handleStudioPanelSwitch(panel studioPanel) {
m.studioPanel = panel m.studioPanel = panel
m.viewport.SetContent(m.renderContent()) m.viewport.SetContent(m.renderContent())

View File

@@ -5,127 +5,192 @@ import (
) )
var ( var (
primaryColor = lipgloss.Color("#E8364F") cyberRed = lipgloss.Color("#FF0033")
roseColor = lipgloss.Color("#FF6B8A") cyberRedDark = lipgloss.Color("#8B0020")
roseLightColor = lipgloss.Color("#FFB3C6") cyberRedDeep = lipgloss.Color("#5C0015")
accentColor = lipgloss.Color("#FF8FA3") cyberPink = lipgloss.Color("#FF1A5E")
warmColor = lipgloss.Color("#FF4D6D") cyberRose = lipgloss.Color("#FF4D6D")
successColor = lipgloss.Color("#4ADE80") neonRed = lipgloss.Color("#FF1744")
warningColor = lipgloss.Color("#FBBF24") brightRed = lipgloss.Color("#FF5252")
errorColor = lipgloss.Color("#FF4D4D") dimRed = lipgloss.Color("#6B2033")
mutedColor = lipgloss.Color("#8B7E8E") mutedRed = lipgloss.Color("#4A1525")
dimColor = lipgloss.Color("#5A4F5E")
textColor = lipgloss.Color("#F0E6E8")
textDimColor = lipgloss.Color("#B8A9AD")
bgDark = lipgloss.Color("#0D0A0B") textBright = lipgloss.Color("#EAE0E2")
bgPanel = lipgloss.Color("#1A1215") textMain = lipgloss.Color("#D4C4C8")
bgCard = lipgloss.Color("#231A1D") textDim = lipgloss.Color("#8A7A7E")
bgInput = lipgloss.Color("#2A2023") textMuted = lipgloss.Color("#5A4F52")
bgHover = lipgloss.Color("#332528")
borderColor = lipgloss.Color("#3D2E32") successGreen = lipgloss.Color("#00E676")
borderAccent = lipgloss.Color("#E8364F") warnAmber = lipgloss.Color("#FFD740")
errorRed = lipgloss.Color("#FF1744")
tabActiveBg = lipgloss.Color("#E8364F") bgVoid = lipgloss.Color("#0A0A0C")
tabInactiveBg = lipgloss.Color("#1A1215") bgBase = lipgloss.Color("#0F0D10")
bgSurface = lipgloss.Color("#161218")
bgPanel = lipgloss.Color("#1C1719")
bgCard = lipgloss.Color("#221B1E")
bgInput = lipgloss.Color("#2A2225")
sectionStyle = lipgloss.NewStyle(). borderDim = lipgloss.Color("#2A1F22")
Foreground(roseColor). borderRed = lipgloss.Color("#FF003344")
borderRedFull = lipgloss.Color("#FF0033")
)
var (
baseStyle = lipgloss.NewStyle()
titleBlockStyle = lipgloss.NewStyle().
Foreground(cyberRed).
Bold(true) Bold(true)
sectionIconStyle = lipgloss.NewStyle(). sectionTitleStyle = lipgloss.NewStyle().
Foreground(primaryColor). Foreground(cyberRed).
Bold(true) Bold(true)
itemOKStyle = lipgloss.NewStyle(). labelStyle = lipgloss.NewStyle().
Foreground(successColor) Foreground(textDim).
Width(14)
itemMissingStyle = lipgloss.NewStyle(). valueStyle = lipgloss.NewStyle().
Foreground(errorColor) Foreground(textMain)
itemWarnStyle = lipgloss.NewStyle().
Foreground(warningColor)
itemPendingStyle = lipgloss.NewStyle().
Foreground(mutedColor)
userMsgStyle = lipgloss.NewStyle().
Foreground(roseLightColor)
aiMsgStyle = lipgloss.NewStyle().
Foreground(textColor)
errMsgStyle = lipgloss.NewStyle().
Foreground(errorColor)
inputStyle = lipgloss.NewStyle().
Foreground(roseColor)
stepDoneStyle = lipgloss.NewStyle().
Foreground(successColor)
stepPendingStyle = lipgloss.NewStyle().
Foreground(mutedColor)
stepCurrentStyle = lipgloss.NewStyle().
Foreground(primaryColor).
Bold(true)
stepErrorStyle = lipgloss.NewStyle().
Foreground(errorColor)
statusBarStyle = lipgloss.NewStyle().
Background(bgPanel).
Foreground(textDimColor).
Padding(0, 1)
confirmBoxStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(primaryColor).
Background(bgCard).
Foreground(textColor).
Padding(1, 3).
Bold(true)
confirmYesStyle = lipgloss.NewStyle().
Foreground(successColor).
Bold(true)
confirmNoStyle = lipgloss.NewStyle().
Foreground(mutedColor)
cardStyle = lipgloss.NewStyle(). cardStyle = lipgloss.NewStyle().
Background(bgCard). Background(bgCard).
Border(lipgloss.RoundedBorder()). Border(lipgloss.RoundedBorder()).
BorderForeground(borderColor). BorderForeground(borderDim).
Padding(0, 1)
cardActiveStyle = lipgloss.NewStyle().
Background(bgCard).
Border(lipgloss.RoundedBorder()).
BorderForeground(cyberRed).
Padding(0, 1) Padding(0, 1)
sidebarStyle = lipgloss.NewStyle(). sidebarStyle = lipgloss.NewStyle().
Background(bgPanel). Background(bgSurface).
Border(lipgloss.Border{Right: "│"}). Border(lipgloss.Border{Right: "│"}).
BorderForeground(borderColor). BorderForeground(borderDim).
Padding(0, 1) Padding(0, 1)
statusBarStyle = lipgloss.NewStyle().
Background(bgSurface).
Foreground(textDim).
Padding(0, 1)
inputStyle = lipgloss.NewStyle().
Foreground(cyberRed)
userMsgStyle = lipgloss.NewStyle().
Foreground(cyberRose)
aiMsgStyle = lipgloss.NewStyle().
Foreground(textMain)
errMsgStyle = lipgloss.NewStyle().
Foreground(errorRed)
itemOKStyle = lipgloss.NewStyle().Foreground(successGreen)
itemMissingStyle = lipgloss.NewStyle().Foreground(errorRed)
itemWarnStyle = lipgloss.NewStyle().Foreground(warnAmber)
itemPendingStyle = lipgloss.NewStyle().Foreground(textMuted)
confirmBoxStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(cyberRed).
Background(bgCard).
Foreground(textBright).
Padding(1, 3).
Bold(true)
confirmYesStyle = lipgloss.NewStyle().Foreground(successGreen).Bold(true)
confirmNoStyle = lipgloss.NewStyle().Foreground(textMuted)
badgeStyle = lipgloss.NewStyle(). badgeStyle = lipgloss.NewStyle().
Background(primaryColor). Background(cyberRed).
Foreground(lipgloss.Color("#FFFFFF")). Foreground(lipgloss.Color("#FFFFFF")).
Padding(0, 1). Padding(0, 1).
Bold(true) Bold(true)
labelStyle = lipgloss.NewStyle(). tabBarStyle = lipgloss.NewStyle().Background(bgSurface)
Foreground(mutedColor).
Width(14)
valueStyle = lipgloss.NewStyle(). stepDoneStyle = lipgloss.NewStyle().Foreground(successGreen)
Foreground(textColor) stepPendingStyle = lipgloss.NewStyle().Foreground(textMuted)
stepCurrentStyle = lipgloss.NewStyle().Foreground(cyberRed).Bold(true)
tabBarStyle = lipgloss.NewStyle(). stepErrorStyle = lipgloss.NewStyle().Foreground(errorRed)
Background(bgPanel)
pulseFrames = []string{"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"}
) )
func getAnimFrame(frame int) string { var logoLines = []string{
return pulseFrames[frame%len(pulseFrames)] "███╗ ███╗██╗ ██╗ █████╗ ███╗ ██╗███████╗",
"████╗ ████║╚██╗ ██╔╝██╔══██╗████╗ ██║██╔════╝",
"██╔████╔██║ ╚████╔╝ ███████║██╔██╗ ██║███████╗",
"██║╚██╔╝██║ ╚██╔╝ ██╔══██║██║╚██╗██║╚════██║",
"██║ ╚═╝ ██║ ██║ ██║ ██║██║ ╚████║███████║",
"╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝",
}
var scanFrames = []string{
"─────────────────────────── ───",
" ─────────────────────────── ─── ",
"── ──────────────────────────── ",
"─ ─── ────────────────────────────",
"─── ─────────────────────────── ─",
" ──── ─────────────────────────────",
}
func getAnimFrame(frame int) string {
frames := []string{
"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
}
return frames[frame%len(frames)]
}
func getScanFrame(frame int) string {
return scanFrames[frame%len(scanFrames)]
}
func renderLogo() string {
styled := make([]string, len(logoLines))
for i, line := range logoLines {
styled[i] = lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(line)
}
return lipgloss.JoinVertical(lipgloss.Left, styled...)
}
func renderBlockTitle(text string) string {
width := len(text) + 6
top := lipgloss.NewStyle().Foreground(dimRed).Render(
"╭" + repeatStr("─", width) + "╮",
)
content := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(
"│ ■ " + text + " ■ │",
)
bottom := lipgloss.NewStyle().Foreground(dimRed).Render(
"╰" + repeatStr("─", width) + "╯",
)
return lipgloss.JoinVertical(lipgloss.Left, top, content, bottom)
}
func renderSectionHeader(title string, icon string) string {
return lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(
"■ "+icon+" "+title+" ■",
)
}
func renderProgressBar(pct float64, width int) string {
filled := int(float64(width) * pct)
empty := width - filled
bar := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(
repeatStr("█", filled),
) + lipgloss.NewStyle().Foreground(dimRed).Render(
repeatStr("░", empty),
)
return bar
}
func repeatStr(s string, n int) string {
result := ""
for i := 0; i < n; i++ {
result += s
}
return result
} }

View File

@@ -54,13 +54,13 @@ func (m Model) renderShell() string {
func (m Model) renderTermPanel(width int) string { func (m Model) renderTermPanel(width int) string {
var b strings.Builder var b strings.Builder
cwdStyle := lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd) cwdStyle := lipgloss.NewStyle().Foreground(dimRed).Render(m.termCwd)
b.WriteString(renderSectionWithIcon("Terminal", "")) b.WriteString(renderSectionHeader("TERMINAL", "[$]"))
b.WriteString(" ") b.WriteString(" ")
b.WriteString(cwdStyle) b.WriteString(cwdStyle)
b.WriteString("\n\n") b.WriteString("\n\n")
sep := lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("─", max(width-4, 10))) sep := lipgloss.NewStyle().Foreground(borderDim).Render(strings.Repeat("─", max(width-4, 10)))
b.WriteString(" " + sep) b.WriteString(" " + sep)
b.WriteString("\n") b.WriteString("\n")
@@ -74,10 +74,10 @@ func (m Model) renderTermPanel(width int) string {
func (m Model) renderAIPanel(width int) string { func (m Model) renderAIPanel(width int) string {
var b strings.Builder var b strings.Builder
b.WriteString(renderSectionWithIcon("AI Assistant", "")) b.WriteString(renderSectionHeader("AI ASSISTANT", "[?]"))
b.WriteString("\n\n") b.WriteString("\n\n")
sep := lipgloss.NewStyle().Foreground(borderColor).Render(strings.Repeat("─", max(width-4, 10))) sep := lipgloss.NewStyle().Foreground(borderDim).Render(strings.Repeat("─", max(width-4, 10)))
b.WriteString(" " + sep) b.WriteString(" " + sep)
b.WriteString("\n\n") b.WriteString("\n\n")
@@ -87,30 +87,37 @@ func (m Model) renderAIPanel(width int) string {
} }
if m.termAILoading { if m.termAILoading {
b.WriteString(lipgloss.NewStyle().Foreground(warmColor).Render(" " + getAnimFrame(m.animationFrame) + " thinking...")) b.WriteString(lipgloss.NewStyle().Foreground(neonRed).Render(" " + getAnimFrame(m.animationFrame) + " thinking..."))
b.WriteString("\n") b.WriteString("\n")
} }
inputLabel := lipgloss.NewStyle().Foreground(roseColor).Render(" ") inputLabel := lipgloss.NewStyle().Foreground(cyberRed).Render(">> ")
b.WriteString(inputLabel) b.WriteString(inputLabel)
b.WriteString(m.termAIInput) b.WriteString(m.termAIInput)
return lipgloss.NewStyle(). return lipgloss.NewStyle().
Background(bgPanel). Background(bgSurface).
Border(lipgloss.Border{Left: "│"}). Border(lipgloss.Border{Left: "│"}).
BorderForeground(borderColor). BorderForeground(borderDim).
Width(width). Width(width).
Padding(0, 1). Padding(0, 1).
Render(b.String()) Render(b.String())
} }
func (m Model) renderShellInput() string {
prompt := lipgloss.NewStyle().Foreground(successGreen).Render("> ")
return lipgloss.NewStyle().Background(bgInput).Padding(0, 1).Render(
prompt + m.termInput + lipgloss.NewStyle().Foreground(cyberRed).Render("▎"),
)
}
func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() { switch msg.String() {
case "ctrl+c": case "ctrl+c":
if m.termCmd != nil && m.termCmd.Process != nil { if m.termCmd != nil && m.termCmd.Process != nil {
m.termCmd.Process.Kill() m.termCmd.Process.Kill()
m.termRunning = false m.termRunning = false
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(errorColor).Render("^C")) m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(errorRed).Render("^C"))
m.termCmd = nil m.termCmd = nil
m.viewport.SetContent(m.renderContent()) m.viewport.SetContent(m.renderContent())
return m, nil return m, nil
@@ -151,7 +158,7 @@ func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
if isDangerousCommand(input) { if isDangerousCommand(input) {
m.termLog = append(m.termLog, errMsgStyle.Render(" blocked: potentially dangerous command")) m.termLog = append(m.termLog, errMsgStyle.Render(" [BLOCKED] potentially dangerous command"))
m.viewport.SetContent(m.renderContent()) m.viewport.SetContent(m.renderContent())
m.viewport.GotoBottom() m.viewport.GotoBottom()
return m, nil return m, nil
@@ -165,7 +172,7 @@ func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
if err := os.Chdir(dir); err == nil { if err := os.Chdir(dir); err == nil {
m.termCwd, _ = os.Getwd() m.termCwd, _ = os.Getwd()
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd+" $ "+input)) m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimRed).Render(m.termCwd+" $ ")+input)
} else { } else {
m.termLog = append(m.termLog, errMsgStyle.Render(" cd: "+err.Error())) m.termLog = append(m.termLog, errMsgStyle.Render(" cd: "+err.Error()))
} }
@@ -173,7 +180,7 @@ func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.viewport.GotoBottom() m.viewport.GotoBottom()
return m, nil return m, nil
} }
m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd+" $ ")+input) m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimRed).Render(m.termCwd+" $ ")+input)
m.viewport.SetContent(m.renderContent()) m.viewport.SetContent(m.renderContent())
m.viewport.GotoBottom() m.viewport.GotoBottom()
return m, m.runTermCommand(input) return m, m.runTermCommand(input)

View File

@@ -32,8 +32,8 @@ const (
tabCount tabCount
) )
var tabNames = []string{"Dashboard", "Studio", "Shell", "Config"} var tabNames = []string{"DASH", "STUDIO", "SHELL", "CONFIG"}
var tabIcons = []string{"", "", "", ""} var tabIcons = []string{"[■]", "[<>]", "[>$]", "[//]"}
type aiResponseMsg struct{ content string } type aiResponseMsg struct{ content string }
type aiErrMsg struct{ err error } type aiErrMsg struct{ err error }
@@ -61,6 +61,9 @@ type spinnerTickMsg struct{ time time.Time }
type termOutputMsg struct{ line string } type termOutputMsg struct{ line string }
type termExitMsg struct{} type termExitMsg struct{}
type animTickMsg struct{ time time.Time } type animTickMsg struct{ time time.Time }
type clockTickMsg struct{ time time.Time }
type glitchDoneMsg struct{}
type scanDoneMsg struct{}
type studioPanel int type studioPanel int
@@ -79,10 +82,20 @@ const (
configSkills configSkills
) )
type transitionState int
const (
transitionNone transitionState = iota
transitionGlitch
transitionScan
transitionTypewriter
)
type Model struct { type Model struct {
config *config.MuyueConfig config *config.MuyueConfig
scanResult *scanner.ScanResult scanResult *scanner.ScanResult
activeTab tab activeTab tab
prevTab tab
width int width int
height int height int
viewport viewport.Model viewport viewport.Model
@@ -138,6 +151,12 @@ type Model struct {
configField int configField int
animationFrame int animationFrame int
transition transitionState
transitionTick int
typewriterBuf string
typewriterPos int
currentTime time.Time
} }
type keyMap struct { type keyMap struct {