A beautiful TUI for browsing files and reading markdown/code: - Two-pane layout with file tree and content viewer - Markdown rendering via Glamour - Syntax highlighting via Chroma - Auto-detects light/dark terminal theme - Arrow key navigation, Esc to go back - Page Up/Down, Home/End for scrolling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
197 lines
4.4 KiB
Go
197 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/muesli/termenv"
|
|
)
|
|
|
|
// Theme represents a color theme
|
|
type Theme struct {
|
|
Name string
|
|
|
|
// UI colors
|
|
Base lipgloss.Color
|
|
Surface lipgloss.Color
|
|
Overlay lipgloss.Color
|
|
Text lipgloss.Color
|
|
Subtext lipgloss.Color
|
|
Accent lipgloss.Color
|
|
Blue lipgloss.Color
|
|
Teal lipgloss.Color
|
|
Green lipgloss.Color
|
|
Yellow lipgloss.Color
|
|
Orange lipgloss.Color
|
|
Red lipgloss.Color
|
|
Purple lipgloss.Color
|
|
|
|
// For code highlighting
|
|
ChromaStyle string
|
|
GlamourStyle string
|
|
LineNumColor string
|
|
}
|
|
|
|
var (
|
|
// LightTheme for light terminal backgrounds
|
|
LightTheme = Theme{
|
|
Name: "light",
|
|
Base: lipgloss.Color("#f5f5f5"),
|
|
Surface: lipgloss.Color("#e8e8e8"),
|
|
Overlay: lipgloss.Color("#c0c0c0"),
|
|
Text: lipgloss.Color("#1a1a1a"),
|
|
Subtext: lipgloss.Color("#505050"),
|
|
Accent: lipgloss.Color("#6366f1"),
|
|
Blue: lipgloss.Color("#2563eb"),
|
|
Teal: lipgloss.Color("#0d9488"),
|
|
Green: lipgloss.Color("#16a34a"),
|
|
Yellow: lipgloss.Color("#ca8a04"),
|
|
Orange: lipgloss.Color("#ea580c"),
|
|
Red: lipgloss.Color("#dc2626"),
|
|
Purple: lipgloss.Color("#9333ea"),
|
|
|
|
ChromaStyle: "github",
|
|
GlamourStyle: "light",
|
|
LineNumColor: "#888888",
|
|
}
|
|
|
|
// DarkTheme for dark terminal backgrounds
|
|
DarkTheme = Theme{
|
|
Name: "dark",
|
|
Base: lipgloss.Color("#1e1e2e"),
|
|
Surface: lipgloss.Color("#313244"),
|
|
Overlay: lipgloss.Color("#45475a"),
|
|
Text: lipgloss.Color("#cdd6f4"),
|
|
Subtext: lipgloss.Color("#a6adc8"),
|
|
Accent: lipgloss.Color("#b4befe"),
|
|
Blue: lipgloss.Color("#89b4fa"),
|
|
Teal: lipgloss.Color("#94e2d5"),
|
|
Green: lipgloss.Color("#a6e3a1"),
|
|
Yellow: lipgloss.Color("#f9e2af"),
|
|
Orange: lipgloss.Color("#fab387"),
|
|
Red: lipgloss.Color("#f38ba8"),
|
|
Purple: lipgloss.Color("#cba6f7"),
|
|
|
|
ChromaStyle: "catppuccin-mocha",
|
|
GlamourStyle: "dark",
|
|
LineNumColor: "#6c7086",
|
|
}
|
|
|
|
// CurrentTheme is the active theme
|
|
CurrentTheme = LightTheme
|
|
)
|
|
|
|
// DetectTheme tries to detect if terminal has dark or light background
|
|
func DetectTheme() Theme {
|
|
// Check for config file override first
|
|
if theme := loadThemePreference(); theme != "" {
|
|
if theme == "dark" {
|
|
return DarkTheme
|
|
}
|
|
return LightTheme
|
|
}
|
|
|
|
// Auto-detect from terminal
|
|
if termenv.NewOutput(os.Stdout).HasDarkBackground() {
|
|
return DarkTheme
|
|
}
|
|
return LightTheme
|
|
}
|
|
|
|
// SetTheme sets the current theme and updates all styles
|
|
func SetTheme(t Theme) {
|
|
CurrentTheme = t
|
|
updateStyles()
|
|
}
|
|
|
|
// loadThemePreference loads theme from config file
|
|
func loadThemePreference() string {
|
|
configDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
configPath := filepath.Join(configDir, "reader", "theme")
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
return strings.TrimSpace(string(data))
|
|
}
|
|
|
|
// SaveThemePreference saves theme preference to config file
|
|
func SaveThemePreference(theme string) error {
|
|
configDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
readerDir := filepath.Join(configDir, "reader")
|
|
if err := os.MkdirAll(readerDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
configPath := filepath.Join(readerDir, "theme")
|
|
return os.WriteFile(configPath, []byte(theme), 0644)
|
|
}
|
|
|
|
// updateStyles updates all the lipgloss styles based on current theme
|
|
func updateStyles() {
|
|
t := CurrentTheme
|
|
|
|
activeBorderStyle = lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(t.Accent)
|
|
|
|
inactiveBorderStyle = lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(t.Overlay)
|
|
|
|
directoryStyle = lipgloss.NewStyle().
|
|
Foreground(t.Blue).
|
|
Bold(true)
|
|
|
|
fileStyle = lipgloss.NewStyle().
|
|
Foreground(t.Text)
|
|
|
|
selectedStyle = lipgloss.NewStyle().
|
|
Background(t.Surface).
|
|
Foreground(t.Accent).
|
|
Bold(true)
|
|
|
|
statusBarStyle = lipgloss.NewStyle().
|
|
Background(t.Surface).
|
|
Foreground(t.Subtext).
|
|
Padding(0, 1)
|
|
|
|
statusKeyStyle = lipgloss.NewStyle().
|
|
Background(t.Overlay).
|
|
Foreground(t.Text).
|
|
Padding(0, 1)
|
|
|
|
helpStyle = lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(t.Purple).
|
|
Padding(1, 2)
|
|
|
|
helpTitleStyle = lipgloss.NewStyle().
|
|
Foreground(t.Purple).
|
|
Bold(true).
|
|
MarginBottom(1)
|
|
|
|
helpKeyStyle = lipgloss.NewStyle().
|
|
Foreground(t.Yellow).
|
|
Width(12)
|
|
|
|
helpDescStyle = lipgloss.NewStyle().
|
|
Foreground(t.Subtext)
|
|
|
|
titleStyle = lipgloss.NewStyle().
|
|
Foreground(t.Accent).
|
|
Bold(true).
|
|
Padding(0, 1)
|
|
}
|