diff --git a/internal/tui/animations.go b/internal/tui/animations.go new file mode 100644 index 0000000..c38bfbf --- /dev/null +++ b/internal/tui/animations.go @@ -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()) +} diff --git a/internal/tui/config_tab.go b/internal/tui/config_tab.go index ef11323..44e4222 100644 --- a/internal/tui/config_tab.go +++ b/internal/tui/config_tab.go @@ -2,18 +2,11 @@ package tui import ( "fmt" - "regexp" "strings" "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 { colWidth := m.width / 2 if colWidth < 30 { @@ -22,7 +15,7 @@ func (m Model) renderConfig() string { var left, right strings.Builder - left.WriteString(renderSectionWithIcon("Profile", "๐Ÿ‘ค")) + left.WriteString(renderSectionHeader("PROFILE", "[@]")) left.WriteString("\n") if m.config != nil { fields := []struct { @@ -50,28 +43,28 @@ func (m Model) renderConfig() string { } left.WriteString("\n") - left.WriteString(renderSectionWithIcon("AI Providers", "โ—†")) + left.WriteString(renderSectionHeader("AI PROVIDERS", "[AI]")) left.WriteString("\n") if m.config != nil { for _, p := range m.config.AI.Providers { 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") if p.APIKey != "" { 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", nameStyle.Render(p.Name), - lipgloss.NewStyle().Foreground(dimColor).Render("model="+p.Model), + lipgloss.NewStyle().Foreground(dimRed).Render("model="+p.Model), keyStatus, active)) } } left.WriteString("\n") - right.WriteString(renderSectionWithIcon("Terminal", "โ–ถ")) + right.WriteString(renderSectionHeader("TERMINAL", "[$]")) right.WriteString("\n") if m.config != nil { 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(renderSectionWithIcon("BMAD Method", "โ—ˆ")) + right.WriteString(renderSectionHeader("BMAD METHOD", "[B]")) right.WriteString("\n") if m.config != nil { - installed := itemMissingStyle.Render("no") + installed := itemMissingStyle.Render("[--] no") 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 %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(renderSectionWithIcon(fmt.Sprintf("Skills (%d)", len(m.skillList)), "โšก")) + right.WriteString(renderSectionHeader(fmt.Sprintf("SKILLS (%d)", len(m.skillList)), "[!]")) right.WriteString("\n") if len(m.skillList) > 0 { for _, s := range m.skillList { @@ -105,12 +98,12 @@ func (m Model) renderConfig() string { target = "both" } right.WriteString(fmt.Sprintf(" %s %s %s\n", - lipgloss.NewStyle().Foreground(textColor).Render(s.Name), - lipgloss.NewStyle().Foreground(primaryColor).Render("["+target+"]"), - lipgloss.NewStyle().Foreground(dimColor).Render(s.Description))) + lipgloss.NewStyle().Foreground(textMain).Render(s.Name), + lipgloss.NewStyle().Foreground(cyberRed).Render("["+target+"]"), + lipgloss.NewStyle().Foreground(dimRed).Render(s.Description))) } } 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") } diff --git a/internal/tui/dashboard.go b/internal/tui/dashboard.go index ed58fab..6bb2589 100644 --- a/internal/tui/dashboard.go +++ b/internal/tui/dashboard.go @@ -15,16 +15,16 @@ func (m Model) renderDashboard() string { var left, right strings.Builder - left.WriteString(renderSectionWithIcon("System", "โ—‰")) + left.WriteString(renderSectionHeader("SYSTEM", "[*]")) left.WriteString("\n") if m.scanResult != nil { sysInfo := m.scanResult.System.String() left.WriteString(" ") - left.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(sysInfo)) + left.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(sysInfo)) } left.WriteString("\n\n") - left.WriteString(renderSectionWithIcon("Installed Tools", "โ—†")) + left.WriteString(renderSectionHeader("INSTALLED TOOLS", "[+]")) left.WriteString("\n") if m.scanResult != nil { installed := 0 @@ -33,14 +33,14 @@ func (m Model) renderDashboard() string { if t.Installed { installed++ left.WriteString(" ") - left.WriteString(itemOKStyle.Render("โœ“ ")) - left.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(t.Name)) - left.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(fmt.Sprintf(" %s", extractVersion(t.Version)))) + left.WriteString(itemOKStyle.Render("[OK] ")) + left.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(t.Name)) + left.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(fmt.Sprintf(" %s", extractVersion(t.Version)))) left.WriteString("\n") } else { left.WriteString(" ") - left.WriteString(itemMissingStyle.Render("โœ— ")) - left.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render(t.Name)) + left.WriteString(itemMissingStyle.Render("[--] ")) + left.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render(t.Name)) left.WriteString(itemPendingStyle.Render(" (missing)")) left.WriteString("\n") } @@ -51,14 +51,14 @@ func (m Model) renderDashboard() string { if total > 0 { pct = (installed * barWidth) / total } - bar := lipgloss.NewStyle().Foreground(primaryColor).Render(strings.Repeat("โ–ˆ", pct)) + - lipgloss.NewStyle().Foreground(dimColor).Render(strings.Repeat("โ–‘", barWidth-pct)) + bar := lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(strings.Repeat("โ–ˆ", pct)) + + lipgloss.NewStyle().Foreground(dimRed).Render(strings.Repeat("โ–‘", barWidth-pct)) left.WriteString(fmt.Sprintf("\n %s %d/%d\n", bar, installed, total)) } left.WriteString("\n") if m.installing { - left.WriteString(renderSectionWithIcon("Installing", "โณ")) + left.WriteString(renderSectionHeader("INSTALLING", "[~]")) left.WriteString("\n") progBar := m.progressBar.View() label := "" @@ -72,7 +72,7 @@ func (m Model) renderDashboard() string { } if len(m.installLog) > 0 { - left.WriteString(renderSectionWithIcon("Install Log", "๐Ÿ“‹")) + left.WriteString(renderSectionHeader("INSTALL LOG", "[#]")) left.WriteString("\n") for _, l := range m.installLog { left.WriteString(l + "\n") @@ -80,27 +80,27 @@ func (m Model) renderDashboard() string { left.WriteString("\n") } - right.WriteString(renderSectionWithIcon("Quick Actions", "โšก")) + right.WriteString(renderSectionHeader("QUICK ACTIONS", "[!]")) right.WriteString("\n") actions := []struct { key string desc string color lipgloss.Color }{ - {"i", "Install missing tools", primaryColor}, - {"u", "Check for updates", warmColor}, - {"s", "Rescan system", roseColor}, - {"l", "Scan LSP servers", accentColor}, - {"m", "Configure MCP servers", roseLightColor}, + {"i", "Install missing tools", cyberRed}, + {"u", "Check for updates", neonRed}, + {"s", "Rescan system", cyberPink}, + {"l", "Scan LSP servers", cyberRose}, + {"m", "Configure MCP servers", brightRed}, } for _, a := range actions { right.WriteString(fmt.Sprintf(" %s %s\n", 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(renderSectionWithIcon("Active Agents", "โ—‰")) + right.WriteString(renderSectionHeader("ACTIVE AGENTS", "[*]")) right.WriteString("\n") agents := []struct { @@ -111,24 +111,24 @@ func (m Model) renderDashboard() string { } for _, a := range agents { right.WriteString(" ") - right.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render("โ— ")) - right.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render(a.name + " ")) - right.WriteString(itemPendingStyle.Render("stopped")) + right.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(">> ")) + right.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render(a.name + " ")) + right.WriteString(itemPendingStyle.Render("[stopped]")) right.WriteString("\n") } right.WriteString("\n") if len(m.updateStatus) > 0 { - right.WriteString(renderSectionWithIcon("Updates", "โ†ป")) + right.WriteString(renderSectionHeader("UPDATES", "[^]")) right.WriteString("\n") for _, s := range m.updateStatus { if s.NeedsUpdate { right.WriteString(" ") - right.WriteString(itemWarnStyle.Render("โš  ")) - right.WriteString(fmt.Sprintf("%s: %s โ†’ %s\n", s.Tool, s.Current, s.Latest)) + right.WriteString(itemWarnStyle.Render("[!!] ")) + right.WriteString(fmt.Sprintf("%s: %s -> %s\n", s.Tool, s.Current, s.Latest)) } else if s.Error == "" { right.WriteString(" ") - right.WriteString(itemOKStyle.Render("โœ“ ")) + right.WriteString(itemOKStyle.Render("[OK] ")) 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 { - right.WriteString(renderSectionWithIcon("LSP Servers", "ยง")) + right.WriteString(renderSectionHeader("LSP SERVERS", "[L]")) right.WriteString("\n") lspInstalled := 0 for _, s := range m.lspServers { if s.Installed { lspInstalled++ right.WriteString(" ") - right.WriteString(itemOKStyle.Render("โœ“ ")) + right.WriteString(itemOKStyle.Render("[OK] ")) right.WriteString(fmt.Sprintf("%-22s (%s)\n", s.Name, s.Language)) } else { right.WriteString(" ") - right.WriteString(itemPendingStyle.Render("โ—‹ ")) + right.WriteString(itemPendingStyle.Render("[ ] ")) right.WriteString(fmt.Sprintf("%-22s (%s)\n", s.Name, s.Language)) } } @@ -155,16 +155,16 @@ func (m Model) renderDashboard() string { right.WriteString("\n") } - mcpStatus := itemPendingStyle.Render("โ—‹ not configured") + mcpStatus := itemPendingStyle.Render("[ ] not configured") if m.mcpConfigured { - mcpStatus = itemOKStyle.Render("โœ“ configured") + mcpStatus = itemOKStyle.Render("[OK] configured") } right.WriteString(fmt.Sprintf(" MCP: %s\n", mcpStatus)) if m.daemon != nil { - daemonStatus := itemPendingStyle.Render("โ—‹ stopped") + daemonStatus := itemPendingStyle.Render("[ ] stopped") if m.daemon.IsRunning() { - daemonStatus = itemOKStyle.Render("โœ“ running") + daemonStatus = itemOKStyle.Render("[OK] running") } right.WriteString(fmt.Sprintf(" Daemon: %s\n", daemonStatus)) } @@ -174,8 +174,3 @@ func (m Model) renderDashboard() string { return lipgloss.JoinHorizontal(lipgloss.Top, leftCol, rightCol) } - -func renderSectionWithIcon(title string, icon string) string { - return lipgloss.NewStyle().Foreground(primaryColor).Render(icon+" ") + - sectionStyle.Render(title) -} diff --git a/internal/tui/footer.go b/internal/tui/footer.go new file mode 100644 index 0000000..391d040 --- /dev/null +++ b/internal/tui/footer.go @@ -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) +} diff --git a/internal/tui/handlers.go b/internal/tui/handlers.go index 4810086..07b0cc4 100644 --- a/internal/tui/handlers.go +++ b/internal/tui/handlers.go @@ -142,16 +142,14 @@ func (m Model) handleTabMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } return m, nil case "enter": - m.activeTab = tab(m.tabMenuCursor) + m.switchTab(tab(m.tabMenuCursor)) m.showingTabMenu = false - m.resizeViewport() return m, nil default: for i := 0; i < int(tabCount); i++ { if msg.String() == fmt.Sprintf("%d", i+1) { - m.activeTab = tab(i) + m.switchTab(tab(i)) m.showingTabMenu = false - m.resizeViewport() return m, nil } } @@ -159,6 +157,17 @@ func (m Model) handleTabMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) { 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() +} + func (m Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "i": @@ -174,13 +183,13 @@ func (m Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } } 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()) return m, nil } needsSudo := checkNeedsSudo(m.scanResult) 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()) return m, nil } @@ -267,7 +276,7 @@ func (m Model) handleWorkflowKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "a": 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.viewport.SetContent(m.renderContent()) return m, reviewPlanCmd(m.orch, true, "") @@ -280,7 +289,7 @@ func (m Model) handleWorkflowKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } case "g": 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.viewport.SetContent(m.renderContent()) return m, generatePlanCmd(m.orch) @@ -332,7 +341,7 @@ func hasSudo() bool { func (m Model) handleChatSubmit() (tea.Model, tea.Cmd) { input := m.chatInput - m.chatLog = append(m.chatLog, userMsgStyle.Render("โŸฉ "+input)) + m.chatLog = append(m.chatLog, userMsgStyle.Render(">> "+input)) m.chatInput = "" m.chatLoading = true m.viewport.SetContent(m.renderContent()) diff --git a/internal/tui/header.go b/internal/tui/header.go new file mode 100644 index 0000000..c7b7d7e --- /dev/null +++ b/internal/tui/header.go @@ -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), + ) +} diff --git a/internal/tui/helpers.go b/internal/tui/helpers.go index f4dcba9..5dcce09 100644 --- a/internal/tui/helpers.go +++ b/internal/tui/helpers.go @@ -1,9 +1,17 @@ package tui import ( + "regexp" + "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 func parsePreviewFiles(response string) []previewFile { diff --git a/internal/tui/model.go b/internal/tui/model.go index 45edfab..4f5d60b 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -22,7 +22,6 @@ import ( "github.com/muyue/muyue/internal/proxy" "github.com/muyue/muyue/internal/scanner" "github.com/muyue/muyue/internal/skills" - "github.com/muyue/muyue/internal/version" ) 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 = 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() @@ -55,8 +54,8 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model { scanResult: scan, activeTab: tabDashboard, chatLog: []string{ - aiMsgStyle.Render(" Welcome to Studio! Chat with your AI assistant here."), - aiMsgStyle.Render(" Configure agents and workflows from the sidebar. Type /plan to start."), + aiMsgStyle.Render(" >> Welcome to Studio! Chat with your AI assistant here."), + aiMsgStyle.Render(" >> Configure agents and workflows from the sidebar. Type /plan to start."), }, orch: orch, proxyMgr: proxyMgr, @@ -77,12 +76,14 @@ func NewModel(cfg *config.MuyueConfig, scan *scanner.ScanResult) Model { studioPanel: panelChat, studioSidebarOpen: true, 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, configSection: configProfile, configField: 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 { - 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) { @@ -106,7 +113,32 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd case animTickMsg: 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() + case clockTickMsg: + m.currentTime = msg.time + return m, clockTick() case progress.FrameMsg: pm, cmd := m.progressBar.Update(msg) m.progressBar = pm.(progress.Model) @@ -120,7 +152,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case termExitMsg: 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 if m.activeTab == tabShell { m.viewport.SetContent(m.renderContent()) @@ -152,7 +184,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case aiErrMsg: m.chatLoading = false m.termAILoading = false - errText := errMsgStyle.Render(" error: " + msg.err.Error()) + errText := errMsgStyle.Render(" [ERROR] " + msg.err.Error()) if m.activeTab == tabShell && m.termAIShow { m.termAIChat = append(m.termAIChat, errText) } else { @@ -168,9 +200,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case installCompleteMsg: m.installing = false for _, r := range msg.results { - status := itemOKStyle.Render("โœ“") + status := itemOKStyle.Render("[OK]") if !r.Success { - status = itemMissingStyle.Render("โœ—") + status = itemMissingStyle.Render("[--]") } 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()) return m, nil case installProgressMsg: - status := itemOKStyle.Render("โœ“") + status := itemOKStyle.Render("[OK]") m.installLog = append(m.installLog, fmt.Sprintf(" %s %s installed", status, msg.tool)) m.installCurrent = msg.current m.installTool = "" @@ -188,9 +220,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport.SetContent(m.renderContent()) return m, nil case installBatchMsg: - status := itemOKStyle.Render("โœ“") + status := itemOKStyle.Render("[OK]") 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.installCurrent = msg.index + 1 @@ -254,7 +286,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) View() string { 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 { @@ -265,6 +297,32 @@ func (m Model) View() string { 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 b.WriteString(m.renderHeader()) b.WriteString("\n") @@ -283,45 +341,6 @@ func (m Model) View() 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 { switch m.activeTab { case tabDashboard: @@ -354,127 +373,6 @@ func (m *Model) resizeViewport() { 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) { dir := filepath.Join(os.TempDir(), "muyue-preview") 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")) } } - -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("โ–Ž"), - ) -} diff --git a/internal/tui/studio.go b/internal/tui/studio.go index 50f0102..6483161 100644 --- a/internal/tui/studio.go +++ b/internal/tui/studio.go @@ -30,7 +30,7 @@ func (m Model) renderStudio() string { func (m Model) renderStudioSidebar(width int) string { var b strings.Builder - b.WriteString(renderSectionWithIcon("Studio", "โ—ˆ")) + b.WriteString(renderSectionHeader("STUDIO", "[<>]")) b.WriteString("\n\n") panels := []struct { @@ -38,23 +38,23 @@ func (m Model) renderStudioSidebar(width int) string { panel studioPanel icon string }{ - {"Chat", panelChat, "๐Ÿ’ฌ"}, - {"Agents", panelAgents, "โ—‰"}, - {"Workflows", panelWorkflows, "โŸ"}, + {"Chat", panelChat, "[#]"}, + {"Agents", panelAgents, "[*]"}, + {"Workflows", panelWorkflows, "[~]"}, } for _, p := range panels { if m.studioPanel == p.panel { activeStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFFFFF")). - Background(primaryColor). + Background(cyberRed). Bold(true). Padding(0, 1) b.WriteString(activeStyle.Render(p.icon + " " + p.name)) b.WriteString("\n") } else { inactiveStyle := lipgloss.NewStyle(). - Foreground(textDimColor). + Foreground(textDim). Padding(0, 1) b.WriteString(inactiveStyle.Render(p.icon + " " + p.name)) b.WriteString("\n") @@ -62,7 +62,7 @@ func (m Model) renderStudioSidebar(width int) string { } 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") switch m.studioPanel { @@ -78,26 +78,26 @@ func (m Model) renderStudioSidebar(width int) string { } 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") provider := "none" if m.config != nil { 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(lipgloss.NewStyle().Foreground(mutedColor).Render("Commands")) + b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Commands")) b.WriteString("\n") cmds := []string{"/plan ", "/help"} for _, c := range cmds { - b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(" " + c)) + b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render(" " + c)) b.WriteString("\n") } if m.previewURL != "" { b.WriteString("\n") - b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Preview")) + b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Preview")) b.WriteString("\n") b.WriteString(itemOKStyle.Render(" " + m.previewURL)) b.WriteString("\n") @@ -121,43 +121,43 @@ func (m Model) renderAgentsSidebar(b *strings.Builder, width int) { var statusIcon string switch status { case proxy.StatusRunning: - statusIcon = lipgloss.NewStyle().Foreground(warmColor).Render("โ— running") + statusIcon = lipgloss.NewStyle().Foreground(neonRed).Render("[>> running]") case proxy.StatusStopped: - statusIcon = lipgloss.NewStyle().Foreground(mutedColor).Render("โ—‹ stopped") + statusIcon = lipgloss.NewStyle().Foreground(textMuted).Render("[|| stopped]") case proxy.StatusError: - statusIcon = lipgloss.NewStyle().Foreground(errorColor).Render("โœ— error") + statusIcon = lipgloss.NewStyle().Foreground(errorRed).Render("[!! error]") default: if available { - statusIcon = lipgloss.NewStyle().Foreground(successColor).Render("โœ“ available") + statusIcon = lipgloss.NewStyle().Foreground(successGreen).Render("[OK available]") } 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(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(lipgloss.NewStyle().Foreground(mutedColor).Render("Actions")) + b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Actions")) b.WriteString("\n") - b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" [c]")) - b.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(" Start Crush")) + b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" [c]")) + b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(" Start Crush")) b.WriteString("\n") - b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" [l]")) - b.WriteString(lipgloss.NewStyle().Foreground(textColor).Render(" Start Claude")) + b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" [l]")) + b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(" Start Claude")) b.WriteString("\n") } func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) { 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(lipgloss.NewStyle().Foreground(dimColor).Render("Use /plan in chat")) + b.WriteString(lipgloss.NewStyle().Foreground(dimRed).Render("Use /plan in chat")) 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") return } @@ -165,13 +165,13 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) { wf := m.orch.Workflow phaseColors := map[workflow.Phase]lipgloss.Style{ - workflow.PhaseIdle: lipgloss.NewStyle().Foreground(mutedColor), - workflow.PhaseGathering: lipgloss.NewStyle().Foreground(warningColor).Bold(true), - workflow.PhasePlanning: lipgloss.NewStyle().Foreground(roseColor).Bold(true), - workflow.PhaseReviewing: lipgloss.NewStyle().Foreground(accentColor).Bold(true), - workflow.PhaseExecuting: lipgloss.NewStyle().Foreground(primaryColor).Bold(true), - workflow.PhaseDone: lipgloss.NewStyle().Foreground(successColor).Bold(true), - workflow.PhaseError: lipgloss.NewStyle().Foreground(errorColor).Bold(true), + workflow.PhaseIdle: lipgloss.NewStyle().Foreground(textMuted), + workflow.PhaseGathering: lipgloss.NewStyle().Foreground(warnAmber).Bold(true), + workflow.PhasePlanning: lipgloss.NewStyle().Foreground(cyberPink).Bold(true), + workflow.PhaseReviewing: lipgloss.NewStyle().Foreground(cyberRose).Bold(true), + workflow.PhaseExecuting: lipgloss.NewStyle().Foreground(cyberRed).Bold(true), + workflow.PhaseDone: lipgloss.NewStyle().Foreground(successGreen).Bold(true), + workflow.PhaseError: lipgloss.NewStyle().Foreground(errorRed).Bold(true), } if style, ok := phaseColors[wf.Phase]; ok { @@ -180,9 +180,9 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) { b.WriteString("\n\n") 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(lipgloss.NewStyle().Foreground(textColor).Render(wf.Plan.Goal)) + b.WriteString(lipgloss.NewStyle().Foreground(textMain).Render(wf.Plan.Goal)) b.WriteString("\n\n") } @@ -194,7 +194,7 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) { b.WriteString("\n\n") } - b.WriteString(lipgloss.NewStyle().Foreground(mutedColor).Render("Controls")) + b.WriteString(lipgloss.NewStyle().Foreground(textMuted).Render("Controls")) b.WriteString("\n") controls := []struct { key string @@ -207,8 +207,8 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) { {"[x]", "Cancel"}, } for _, c := range controls { - b.WriteString(lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render(" " + c.key)) - b.WriteString(lipgloss.NewStyle().Foreground(textDimColor).Render(" " + c.desc)) + b.WriteString(lipgloss.NewStyle().Foreground(cyberRed).Bold(true).Render(" "+c.key)) + b.WriteString(lipgloss.NewStyle().Foreground(textDim).Render(" "+c.desc)) b.WriteString("\n") } } @@ -216,14 +216,14 @@ func (m Model) renderWorkflowSidebar(b *strings.Builder, width int) { func (m Model) renderStudioChat(width int) string { var b strings.Builder - chatHeader := renderSectionWithIcon("Chat", "๐Ÿ’ฌ") + chatHeader := renderSectionHeader("CHAT", "[#]") 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("\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("\n\n") @@ -235,6 +235,18 @@ func (m Model) renderStudioChat(width int) 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) { m.studioPanel = panel m.viewport.SetContent(m.renderContent()) diff --git a/internal/tui/styles.go b/internal/tui/styles.go index 53bd3c7..c1add3c 100644 --- a/internal/tui/styles.go +++ b/internal/tui/styles.go @@ -5,127 +5,192 @@ import ( ) var ( - primaryColor = lipgloss.Color("#E8364F") - roseColor = lipgloss.Color("#FF6B8A") - roseLightColor = lipgloss.Color("#FFB3C6") - accentColor = lipgloss.Color("#FF8FA3") - warmColor = lipgloss.Color("#FF4D6D") - successColor = lipgloss.Color("#4ADE80") - warningColor = lipgloss.Color("#FBBF24") - errorColor = lipgloss.Color("#FF4D4D") - mutedColor = lipgloss.Color("#8B7E8E") - dimColor = lipgloss.Color("#5A4F5E") - textColor = lipgloss.Color("#F0E6E8") - textDimColor = lipgloss.Color("#B8A9AD") + cyberRed = lipgloss.Color("#FF0033") + cyberRedDark = lipgloss.Color("#8B0020") + cyberRedDeep = lipgloss.Color("#5C0015") + cyberPink = lipgloss.Color("#FF1A5E") + cyberRose = lipgloss.Color("#FF4D6D") + neonRed = lipgloss.Color("#FF1744") + brightRed = lipgloss.Color("#FF5252") + dimRed = lipgloss.Color("#6B2033") + mutedRed = lipgloss.Color("#4A1525") - bgDark = lipgloss.Color("#0D0A0B") - bgPanel = lipgloss.Color("#1A1215") - bgCard = lipgloss.Color("#231A1D") - bgInput = lipgloss.Color("#2A2023") - bgHover = lipgloss.Color("#332528") + textBright = lipgloss.Color("#EAE0E2") + textMain = lipgloss.Color("#D4C4C8") + textDim = lipgloss.Color("#8A7A7E") + textMuted = lipgloss.Color("#5A4F52") - borderColor = lipgloss.Color("#3D2E32") - borderAccent = lipgloss.Color("#E8364F") + successGreen = lipgloss.Color("#00E676") + warnAmber = lipgloss.Color("#FFD740") + errorRed = lipgloss.Color("#FF1744") - tabActiveBg = lipgloss.Color("#E8364F") - tabInactiveBg = lipgloss.Color("#1A1215") + bgVoid = lipgloss.Color("#0A0A0C") + bgBase = lipgloss.Color("#0F0D10") + bgSurface = lipgloss.Color("#161218") + bgPanel = lipgloss.Color("#1C1719") + bgCard = lipgloss.Color("#221B1E") + bgInput = lipgloss.Color("#2A2225") - sectionStyle = lipgloss.NewStyle(). - Foreground(roseColor). + borderDim = lipgloss.Color("#2A1F22") + borderRed = lipgloss.Color("#FF003344") + borderRedFull = lipgloss.Color("#FF0033") +) + +var ( + baseStyle = lipgloss.NewStyle() + + titleBlockStyle = lipgloss.NewStyle(). + Foreground(cyberRed). Bold(true) - sectionIconStyle = lipgloss.NewStyle(). - Foreground(primaryColor). - Bold(true) - - itemOKStyle = lipgloss.NewStyle(). - Foreground(successColor) - - itemMissingStyle = lipgloss.NewStyle(). - Foreground(errorColor) - - 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). + sectionTitleStyle = lipgloss.NewStyle(). + Foreground(cyberRed). Bold(true) - confirmYesStyle = lipgloss.NewStyle(). - Foreground(successColor). - Bold(true) + labelStyle = lipgloss.NewStyle(). + Foreground(textDim). + Width(14) - confirmNoStyle = lipgloss.NewStyle(). - Foreground(mutedColor) + valueStyle = lipgloss.NewStyle(). + Foreground(textMain) cardStyle = lipgloss.NewStyle(). Background(bgCard). Border(lipgloss.RoundedBorder()). - BorderForeground(borderColor). + BorderForeground(borderDim). + Padding(0, 1) + + cardActiveStyle = lipgloss.NewStyle(). + Background(bgCard). + Border(lipgloss.RoundedBorder()). + BorderForeground(cyberRed). Padding(0, 1) sidebarStyle = lipgloss.NewStyle(). - Background(bgPanel). + Background(bgSurface). Border(lipgloss.Border{Right: "โ”‚"}). - BorderForeground(borderColor). + BorderForeground(borderDim). 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(). - Background(primaryColor). + Background(cyberRed). Foreground(lipgloss.Color("#FFFFFF")). Padding(0, 1). Bold(true) - labelStyle = lipgloss.NewStyle(). - Foreground(mutedColor). - Width(14) + tabBarStyle = lipgloss.NewStyle().Background(bgSurface) - valueStyle = lipgloss.NewStyle(). - Foreground(textColor) - - tabBarStyle = lipgloss.NewStyle(). - Background(bgPanel) - - pulseFrames = []string{"โฃพ", "โฃฝ", "โฃป", "โขฟ", "โกฟ", "โฃŸ", "โฃฏ", "โฃท"} + stepDoneStyle = lipgloss.NewStyle().Foreground(successGreen) + stepPendingStyle = lipgloss.NewStyle().Foreground(textMuted) + stepCurrentStyle = lipgloss.NewStyle().Foreground(cyberRed).Bold(true) + stepErrorStyle = lipgloss.NewStyle().Foreground(errorRed) ) -func getAnimFrame(frame int) string { - return pulseFrames[frame%len(pulseFrames)] +var logoLines = []string{ + "โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—", + "โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•", + "โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—", + "โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•‘", + "โ–ˆโ–ˆโ•‘ โ•šโ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘", + "โ•šโ•โ• โ•šโ•โ• โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•šโ•โ• โ•šโ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•", +} + +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 } diff --git a/internal/tui/terminal.go b/internal/tui/terminal.go index 1afd6af..e624f0b 100644 --- a/internal/tui/terminal.go +++ b/internal/tui/terminal.go @@ -54,13 +54,13 @@ func (m Model) renderShell() string { func (m Model) renderTermPanel(width int) string { var b strings.Builder - cwdStyle := lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd) - b.WriteString(renderSectionWithIcon("Terminal", "โ–ถ")) + cwdStyle := lipgloss.NewStyle().Foreground(dimRed).Render(m.termCwd) + b.WriteString(renderSectionHeader("TERMINAL", "[$]")) b.WriteString(" ") b.WriteString(cwdStyle) 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("\n") @@ -74,10 +74,10 @@ func (m Model) renderTermPanel(width int) string { func (m Model) renderAIPanel(width int) string { var b strings.Builder - b.WriteString(renderSectionWithIcon("AI Assistant", "โ—ˆ")) + b.WriteString(renderSectionHeader("AI ASSISTANT", "[?]")) 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("\n\n") @@ -87,30 +87,37 @@ func (m Model) renderAIPanel(width int) string { } 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") } - inputLabel := lipgloss.NewStyle().Foreground(roseColor).Render("โŸฉ ") + inputLabel := lipgloss.NewStyle().Foreground(cyberRed).Render(">> ") b.WriteString(inputLabel) b.WriteString(m.termAIInput) return lipgloss.NewStyle(). - Background(bgPanel). + Background(bgSurface). Border(lipgloss.Border{Left: "โ”‚"}). - BorderForeground(borderColor). + BorderForeground(borderDim). Width(width). Padding(0, 1). 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) { switch msg.String() { case "ctrl+c": if m.termCmd != nil && m.termCmd.Process != nil { m.termCmd.Process.Kill() 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.viewport.SetContent(m.renderContent()) return m, nil @@ -151,7 +158,7 @@ func (m Model) handleShellKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } 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.GotoBottom() 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 { 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 { 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() 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.GotoBottom() return m, m.runTermCommand(input) diff --git a/internal/tui/types.go b/internal/tui/types.go index b69d811..e53ca17 100644 --- a/internal/tui/types.go +++ b/internal/tui/types.go @@ -32,8 +32,8 @@ const ( tabCount ) -var tabNames = []string{"Dashboard", "Studio", "Shell", "Config"} -var tabIcons = []string{"โ—‰", "โ—ˆ", "โ–ถ", "โš™"} +var tabNames = []string{"DASH", "STUDIO", "SHELL", "CONFIG"} +var tabIcons = []string{"[โ– ]", "[<>]", "[>$]", "[//]"} type aiResponseMsg struct{ content string } type aiErrMsg struct{ err error } @@ -61,6 +61,9 @@ type spinnerTickMsg struct{ time time.Time } type termOutputMsg struct{ line string } type termExitMsg struct{} type animTickMsg struct{ time time.Time } +type clockTickMsg struct{ time time.Time } +type glitchDoneMsg struct{} +type scanDoneMsg struct{} type studioPanel int @@ -79,10 +82,20 @@ const ( configSkills ) +type transitionState int + +const ( + transitionNone transitionState = iota + transitionGlitch + transitionScan + transitionTypewriter +) + type Model struct { config *config.MuyueConfig scanResult *scanner.ScanResult activeTab tab + prevTab tab width int height int viewport viewport.Model @@ -126,7 +139,7 @@ type Model struct { termRunning bool termCwd string - studioPanel studioPanel + studioPanel studioPanel studioSidebarOpen bool termAIChat []string @@ -138,6 +151,12 @@ type Model struct { configField int animationFrame int + + transition transitionState + transitionTick int + typewriterBuf string + typewriterPos int + currentTime time.Time } type keyMap struct {