86f5ca4aef
Add visible activity state for running turns. Move model metadata below the input area. Render assistant Markdown with theme-aware styling.
213 lines
5.1 KiB
Go
213 lines
5.1 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"
|
|
)
|
|
|
|
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
|
|
Border string
|
|
Text string
|
|
Muted string
|
|
|
|
Title string
|
|
User string
|
|
Assistant string
|
|
Tool string
|
|
Error string
|
|
System string
|
|
}
|
|
|
|
type styles struct {
|
|
App lipgloss.Style
|
|
Header lipgloss.Style
|
|
Title lipgloss.Style
|
|
Muted lipgloss.Style
|
|
Activity lipgloss.Style
|
|
ModelMeta lipgloss.Style
|
|
Footer lipgloss.Style
|
|
Viewport lipgloss.Style
|
|
InputBox lipgloss.Style
|
|
CommandHint lipgloss.Style
|
|
UserLabel lipgloss.Style
|
|
AssistantLabel 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",
|
|
Surface: "#FFFFFF",
|
|
SurfaceAlt: "#EEF2F7",
|
|
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",
|
|
Surface: "#1F2937",
|
|
SurfaceAlt: "#0F172A",
|
|
Border: "#475569",
|
|
Text: "#E5E7EB",
|
|
Muted: "#94A3B8",
|
|
Title: "#5EEAD4",
|
|
User: "#93C5FD",
|
|
Assistant: "#5EEAD4",
|
|
Tool: "#FBBF24",
|
|
Error: "#FCA5A5",
|
|
System: "#94A3B8",
|
|
}
|
|
}
|
|
|
|
func (t theme) styles() styles {
|
|
return styles{
|
|
App: lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(t.Text)).
|
|
Background(lipgloss.Color(t.Background)),
|
|
|
|
Header: lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(t.Text)).
|
|
Background(lipgloss.Color(t.SurfaceAlt)).
|
|
Padding(0, 1),
|
|
|
|
Title: lipgloss.NewStyle().
|
|
Bold(true).
|
|
Foreground(lipgloss.Color(t.Title)),
|
|
|
|
Muted: lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(t.Muted)),
|
|
|
|
Activity: lipgloss.NewStyle().
|
|
Bold(true).
|
|
Foreground(lipgloss.Color(t.Background)).
|
|
Background(lipgloss.Color(t.Title)).
|
|
Padding(0, 1),
|
|
|
|
ModelMeta: lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(t.Text)).
|
|
Background(lipgloss.Color(t.SurfaceAlt)).
|
|
Padding(0, 1),
|
|
|
|
Footer: lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(t.Muted)).
|
|
Background(lipgloss.Color(t.Background)).
|
|
Padding(0, 1),
|
|
|
|
Viewport: lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(t.Text)).
|
|
Background(lipgloss.Color(t.Background)),
|
|
|
|
InputBox: lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(t.Text)).
|
|
Background(lipgloss.Color(t.Surface)).
|
|
Border(lipgloss.NormalBorder()).
|
|
BorderForeground(lipgloss.Color(t.Border)).
|
|
Padding(0, 1),
|
|
|
|
CommandHint: lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(t.Muted)).
|
|
Background(lipgloss.Color(t.SurfaceAlt)).
|
|
Padding(0, 1),
|
|
|
|
UserLabel: labelStyle(t.User),
|
|
AssistantLabel: labelStyle(t.Assistant),
|
|
ToolLabel: labelStyle(t.Tool),
|
|
ErrorLabel: labelStyle(t.Error),
|
|
SystemLabel: labelStyle(t.System),
|
|
|
|
UserMessage: messageStyle(t.Text, t.User),
|
|
AssistantMsg: messageStyle(t.Text, t.Assistant),
|
|
ToolMessage: messageStyle(t.Text, t.Tool),
|
|
ErrorMessage: messageStyle(t.Error, t.Error),
|
|
SystemMessage: lipgloss.NewStyle().Foreground(lipgloss.Color(t.System)).PaddingLeft(1),
|
|
}
|
|
}
|
|
|
|
func labelStyle(color string) lipgloss.Style {
|
|
return lipgloss.NewStyle().
|
|
Bold(true).
|
|
Foreground(lipgloss.Color(color))
|
|
}
|
|
|
|
func messageStyle(textColor, accentColor string) lipgloss.Style {
|
|
return lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(textColor)).
|
|
Border(lipgloss.ThickBorder(), false, false, false, true).
|
|
BorderForeground(lipgloss.Color(accentColor)).
|
|
PaddingLeft(1)
|
|
}
|
|
|
|
func applyTextareaTheme(input *textarea.Model, t theme) {
|
|
focused := textarea.Style{
|
|
Base: lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color(t.Text)).
|
|
Background(lipgloss.Color(t.Surface)),
|
|
CursorLine: lipgloss.NewStyle().
|
|
Background(lipgloss.Color(t.Surface)),
|
|
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.Surface))
|
|
|
|
input.FocusedStyle = focused
|
|
input.BlurredStyle = blurred
|
|
}
|