Initial commit: terminal file browser & markdown viewer

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>
This commit is contained in:
Chris Davies
2026-01-28 20:13:09 -05:00
commit 8f9c66ba46
12 changed files with 1658 additions and 0 deletions

196
theme.go Normal file
View File

@@ -0,0 +1,196 @@
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)
}