Files
loveuer c8d8329dbb feat(tui): codex-style input, history, bang commands, single-line status bar
- Arrow-key input history (up/down with multiline awareness)
- Bang shell commands via ! prefix (requires --yolo)
- Codex-style input bar with mode indicator (R/Y/⌘)
- Command palette virtual scrolling
- Multiline input viewport fix
- Merged model pills + footer into single status line
- Removed shift+enter (wait for TUI library support)
- Removed default footer hints
2026-06-25 20:40:50 -07:00

296 lines
7.0 KiB
Go

package tui
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/lipgloss"
)
type ThemeMode string
const (
ThemeLight ThemeMode = "light"
ThemeDark ThemeMode = "dark"
)
const (
iconReady = "✓"
iconWorking = "◆"
iconTool = "▣"
iconError = "✕"
iconCommand = "⌘"
iconSession = "◌"
iconContext = "◔"
iconModel = "◉"
)
func ParseThemeMode(value string) (ThemeMode, error) {
switch ThemeMode(strings.ToLower(strings.TrimSpace(value))) {
case "", ThemeLight:
return ThemeLight, nil
case ThemeDark:
return ThemeDark, nil
default:
return "", fmt.Errorf("unsupported theme %q; expected light or dark", value)
}
}
type theme struct {
Mode ThemeMode
Background string
Surface string
SurfaceAlt string
InputBg string
Border string
Text string
Muted string
Title string
User string
Assistant string
Tool string
Error string
System string
}
type styles struct {
App lipgloss.Style
Muted lipgloss.Style
Activity lipgloss.Style
Header lipgloss.Style
Footer lipgloss.Style
Viewport lipgloss.Style
InputBox lipgloss.Style
CommandHint lipgloss.Style
InputModeReadonly lipgloss.Style
InputModeYolo lipgloss.Style
InputModeCommand lipgloss.Style
StatusReady lipgloss.Style
StatusWarn lipgloss.Style
StatusError lipgloss.Style
StatusInfo lipgloss.Style
Pill lipgloss.Style
PillAccent lipgloss.Style
Palette lipgloss.Style
PaletteMatch lipgloss.Style
ToolName lipgloss.Style
ToolKey lipgloss.Style
ToolValue lipgloss.Style
UserLabel lipgloss.Style
ToolLabel lipgloss.Style
ErrorLabel lipgloss.Style
SystemLabel lipgloss.Style
UserMessage lipgloss.Style
AssistantMsg lipgloss.Style
ToolMessage lipgloss.Style
ErrorMessage lipgloss.Style
SystemMessage lipgloss.Style
}
func themeForMode(mode ThemeMode) theme {
if mode == ThemeDark {
return darkTheme()
}
return lightTheme()
}
func lightTheme() theme {
return theme{
Mode: ThemeLight,
Background: "#F8FAFC",
SurfaceAlt: "#EEF2F7",
InputBg: "#E4E4E4",
Border: "#CBD5E1",
Text: "#111827",
Muted: "#64748B",
Title: "#0F766E",
User: "#2563EB",
Assistant: "#0F766E",
Tool: "#B45309",
Error: "#DC2626",
System: "#64748B",
}
}
func darkTheme() theme {
return theme{
Mode: ThemeDark,
Background: "#111827",
SurfaceAlt: "#0F172A",
InputBg: "#383838",
Border: "#475569",
Text: "#E5E7EB",
Muted: "#94A3B8",
Title: "#5EEAD4",
User: "#93C5FD",
Assistant: "#5EEAD4",
Tool: "#FBBF24",
Error: "#FCA5A5",
System: "#94A3B8",
}
}
func appStyle(t theme) lipgloss.Style {
style := lipgloss.NewStyle().Foreground(lipgloss.Color(t.Text))
if t.Mode == ThemeDark {
style = style.Background(lipgloss.Color(t.Background))
}
return style
}
func (t theme) styles() styles {
return styles{
App: appStyle(t),
Muted: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)),
Activity: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)).
Padding(0, 1),
Header: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(t.Title)),
Footer: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)).
Padding(0, 1),
Viewport: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)),
InputBox: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)).
Background(lipgloss.Color(t.InputBg)).
Padding(1, 2, 1, 1),
CommandHint: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)).
Padding(0, 1),
InputModeReadonly: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(t.Muted)).
Background(lipgloss.Color(t.InputBg)).
Padding(0, 1),
InputModeYolo: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(t.Error)).
Background(lipgloss.Color(t.InputBg)).
Padding(0, 1),
InputModeCommand: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(t.User)).
Background(lipgloss.Color(t.InputBg)).
Padding(0, 1),
StatusReady: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Assistant)),
StatusWarn: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Tool)),
StatusError: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Error)),
StatusInfo: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.User)),
Pill: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)).
Background(lipgloss.Color(t.SurfaceAlt)).
Padding(0, 1),
PillAccent: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(t.Title)).
Background(lipgloss.Color(t.SurfaceAlt)).
Padding(0, 1),
Palette: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)).
Border(lipgloss.ThickBorder()).
BorderForeground(lipgloss.Color(t.Border)).
Padding(0, 2),
PaletteMatch: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(t.User)),
ToolName: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(t.Tool)),
ToolKey: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)),
ToolValue: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)),
UserLabel: labelStyle(t.User),
ToolLabel: labelStyle(t.Tool),
ErrorLabel: labelStyle(t.Error),
SystemLabel: labelStyle(t.System),
UserMessage: bubbleMessageStyle(t.Text, t.SurfaceAlt),
AssistantMsg: assistantMessageStyle(t.Text),
ToolMessage: messageStyle(t.Text),
ErrorMessage: messageStyle(t.Error),
SystemMessage: lipgloss.NewStyle().Foreground(lipgloss.Color(t.System)).Padding(0, 1),
}
}
func labelStyle(color string) lipgloss.Style {
return lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(color))
}
func messageStyle(textColor string) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(lipgloss.Color(textColor)).
Padding(0, 1)
}
func assistantMessageStyle(textColor string) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(lipgloss.Color(textColor)).
Padding(1, 2)
}
func bubbleMessageStyle(textColor, backgroundColor string) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(lipgloss.Color(textColor)).
Background(lipgloss.Color(backgroundColor)).
Padding(1, 2)
}
func applyTextareaTheme(input *textarea.Model, t theme) {
focused := textarea.Style{
Base: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)).
Background(lipgloss.Color(t.InputBg)),
CursorLine: lipgloss.NewStyle().
Background(lipgloss.Color(t.InputBg)),
Placeholder: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)),
Prompt: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)),
Text: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)),
EndOfBuffer: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Surface)),
}
blurred := focused
blurred.CursorLine = lipgloss.NewStyle().Background(lipgloss.Color(t.InputBg))
input.FocusedStyle = focused
input.BlurredStyle = blurred
}