Merge branch 'worktree-agent-adaf01c0' into feat/phase1-foundation

This commit is contained in:
Vantz Stockwell 2026-03-17 06:21:25 -04:00
commit 3f8035ac72
3 changed files with 239 additions and 0 deletions

View File

@ -0,0 +1,94 @@
package theme
type Theme struct {
ID int64 `json:"id"`
Name string `json:"name"`
Foreground string `json:"foreground"`
Background string `json:"background"`
Cursor string `json:"cursor"`
Black string `json:"black"`
Red string `json:"red"`
Green string `json:"green"`
Yellow string `json:"yellow"`
Blue string `json:"blue"`
Magenta string `json:"magenta"`
Cyan string `json:"cyan"`
White string `json:"white"`
BrightBlack string `json:"brightBlack"`
BrightRed string `json:"brightRed"`
BrightGreen string `json:"brightGreen"`
BrightYellow string `json:"brightYellow"`
BrightBlue string `json:"brightBlue"`
BrightMagenta string `json:"brightMagenta"`
BrightCyan string `json:"brightCyan"`
BrightWhite string `json:"brightWhite"`
SelectionBg string `json:"selectionBg,omitempty"`
SelectionFg string `json:"selectionFg,omitempty"`
IsBuiltin bool `json:"isBuiltin"`
}
var BuiltinThemes = []Theme{
{
Name: "Dracula", IsBuiltin: true,
Foreground: "#f8f8f2", Background: "#282a36", Cursor: "#f8f8f2",
Black: "#21222c", Red: "#ff5555", Green: "#50fa7b", Yellow: "#f1fa8c",
Blue: "#bd93f9", Magenta: "#ff79c6", Cyan: "#8be9fd", White: "#f8f8f2",
BrightBlack: "#6272a4", BrightRed: "#ff6e6e", BrightGreen: "#69ff94",
BrightYellow: "#ffffa5", BrightBlue: "#d6acff", BrightMagenta: "#ff92df",
BrightCyan: "#a4ffff", BrightWhite: "#ffffff",
},
{
Name: "Nord", IsBuiltin: true,
Foreground: "#d8dee9", Background: "#2e3440", Cursor: "#d8dee9",
Black: "#3b4252", Red: "#bf616a", Green: "#a3be8c", Yellow: "#ebcb8b",
Blue: "#81a1c1", Magenta: "#b48ead", Cyan: "#88c0d0", White: "#e5e9f0",
BrightBlack: "#4c566a", BrightRed: "#bf616a", BrightGreen: "#a3be8c",
BrightYellow: "#ebcb8b", BrightBlue: "#81a1c1", BrightMagenta: "#b48ead",
BrightCyan: "#8fbcbb", BrightWhite: "#eceff4",
},
{
Name: "Monokai", IsBuiltin: true,
Foreground: "#f8f8f2", Background: "#272822", Cursor: "#f8f8f0",
Black: "#272822", Red: "#f92672", Green: "#a6e22e", Yellow: "#f4bf75",
Blue: "#66d9ef", Magenta: "#ae81ff", Cyan: "#a1efe4", White: "#f8f8f2",
BrightBlack: "#75715e", BrightRed: "#f92672", BrightGreen: "#a6e22e",
BrightYellow: "#f4bf75", BrightBlue: "#66d9ef", BrightMagenta: "#ae81ff",
BrightCyan: "#a1efe4", BrightWhite: "#f9f8f5",
},
{
Name: "One Dark", IsBuiltin: true,
Foreground: "#abb2bf", Background: "#282c34", Cursor: "#528bff",
Black: "#282c34", Red: "#e06c75", Green: "#98c379", Yellow: "#e5c07b",
Blue: "#61afef", Magenta: "#c678dd", Cyan: "#56b6c2", White: "#abb2bf",
BrightBlack: "#545862", BrightRed: "#e06c75", BrightGreen: "#98c379",
BrightYellow: "#e5c07b", BrightBlue: "#61afef", BrightMagenta: "#c678dd",
BrightCyan: "#56b6c2", BrightWhite: "#c8ccd4",
},
{
Name: "Solarized Dark", IsBuiltin: true,
Foreground: "#839496", Background: "#002b36", Cursor: "#839496",
Black: "#073642", Red: "#dc322f", Green: "#859900", Yellow: "#b58900",
Blue: "#268bd2", Magenta: "#d33682", Cyan: "#2aa198", White: "#eee8d5",
BrightBlack: "#002b36", BrightRed: "#cb4b16", BrightGreen: "#586e75",
BrightYellow: "#657b83", BrightBlue: "#839496", BrightMagenta: "#6c71c4",
BrightCyan: "#93a1a1", BrightWhite: "#fdf6e3",
},
{
Name: "Gruvbox Dark", IsBuiltin: true,
Foreground: "#ebdbb2", Background: "#282828", Cursor: "#ebdbb2",
Black: "#282828", Red: "#cc241d", Green: "#98971a", Yellow: "#d79921",
Blue: "#458588", Magenta: "#b16286", Cyan: "#689d6a", White: "#a89984",
BrightBlack: "#928374", BrightRed: "#fb4934", BrightGreen: "#b8bb26",
BrightYellow: "#fabd2f", BrightBlue: "#83a598", BrightMagenta: "#d3869b",
BrightCyan: "#8ec07c", BrightWhite: "#ebdbb2",
},
{
Name: "MobaXTerm Classic", IsBuiltin: true,
Foreground: "#ececec", Background: "#242424", Cursor: "#b4b4c0",
Black: "#000000", Red: "#aa4244", Green: "#7e8d53", Yellow: "#e4b46d",
Blue: "#6e9aba", Magenta: "#9e5085", Cyan: "#80d5cf", White: "#cccccc",
BrightBlack: "#808080", BrightRed: "#cc7b7d", BrightGreen: "#a5b17c",
BrightYellow: "#ecc995", BrightBlue: "#96b6cd", BrightMagenta: "#c083ac",
BrightCyan: "#a9e2de", BrightWhite: "#cccccc",
},
}

85
internal/theme/service.go Normal file
View File

@ -0,0 +1,85 @@
package theme
import (
"database/sql"
"fmt"
)
type ThemeService struct {
db *sql.DB
}
func NewThemeService(db *sql.DB) *ThemeService {
return &ThemeService{db: db}
}
func (s *ThemeService) SeedBuiltins() error {
for _, t := range BuiltinThemes {
_, err := s.db.Exec(
`INSERT OR IGNORE INTO themes (name, foreground, background, cursor,
black, red, green, yellow, blue, magenta, cyan, white,
bright_black, bright_red, bright_green, bright_yellow, bright_blue,
bright_magenta, bright_cyan, bright_white, selection_bg, selection_fg, is_builtin)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1)`,
t.Name, t.Foreground, t.Background, t.Cursor,
t.Black, t.Red, t.Green, t.Yellow, t.Blue, t.Magenta, t.Cyan, t.White,
t.BrightBlack, t.BrightRed, t.BrightGreen, t.BrightYellow, t.BrightBlue,
t.BrightMagenta, t.BrightCyan, t.BrightWhite, t.SelectionBg, t.SelectionFg,
)
if err != nil {
return fmt.Errorf("seed theme %s: %w", t.Name, err)
}
}
return nil
}
func (s *ThemeService) List() ([]Theme, error) {
rows, err := s.db.Query(
`SELECT id, name, foreground, background, cursor,
black, red, green, yellow, blue, magenta, cyan, white,
bright_black, bright_red, bright_green, bright_yellow, bright_blue,
bright_magenta, bright_cyan, bright_white,
COALESCE(selection_bg,''), COALESCE(selection_fg,''), is_builtin
FROM themes ORDER BY is_builtin DESC, name`)
if err != nil {
return nil, err
}
defer rows.Close()
var themes []Theme
for rows.Next() {
var t Theme
if err := rows.Scan(&t.ID, &t.Name, &t.Foreground, &t.Background, &t.Cursor,
&t.Black, &t.Red, &t.Green, &t.Yellow, &t.Blue, &t.Magenta, &t.Cyan, &t.White,
&t.BrightBlack, &t.BrightRed, &t.BrightGreen, &t.BrightYellow, &t.BrightBlue,
&t.BrightMagenta, &t.BrightCyan, &t.BrightWhite,
&t.SelectionBg, &t.SelectionFg, &t.IsBuiltin); err != nil {
return nil, err
}
themes = append(themes, t)
}
if err := rows.Err(); err != nil {
return nil, err
}
return themes, nil
}
func (s *ThemeService) GetByName(name string) (*Theme, error) {
var t Theme
err := s.db.QueryRow(
`SELECT id, name, foreground, background, cursor,
black, red, green, yellow, blue, magenta, cyan, white,
bright_black, bright_red, bright_green, bright_yellow, bright_blue,
bright_magenta, bright_cyan, bright_white,
COALESCE(selection_bg,''), COALESCE(selection_fg,''), is_builtin
FROM themes WHERE name = ?`, name,
).Scan(&t.ID, &t.Name, &t.Foreground, &t.Background, &t.Cursor,
&t.Black, &t.Red, &t.Green, &t.Yellow, &t.Blue, &t.Magenta, &t.Cyan, &t.White,
&t.BrightBlack, &t.BrightRed, &t.BrightGreen, &t.BrightYellow, &t.BrightBlue,
&t.BrightMagenta, &t.BrightCyan, &t.BrightWhite,
&t.SelectionBg, &t.SelectionFg, &t.IsBuiltin)
if err != nil {
return nil, fmt.Errorf("get theme %s: %w", name, err)
}
return &t, nil
}

View File

@ -0,0 +1,60 @@
package theme
import (
"path/filepath"
"testing"
"github.com/vstockwell/wraith/internal/db"
)
func setupTestDB(t *testing.T) *ThemeService {
t.Helper()
d, err := db.Open(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatal(err)
}
if err := db.Migrate(d); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { d.Close() })
return NewThemeService(d)
}
func TestSeedBuiltins(t *testing.T) {
svc := setupTestDB(t)
if err := svc.SeedBuiltins(); err != nil {
t.Fatalf("SeedBuiltins() error: %v", err)
}
themes, err := svc.List()
if err != nil {
t.Fatalf("List() error: %v", err)
}
if len(themes) != len(BuiltinThemes) {
t.Errorf("len(themes) = %d, want %d", len(themes), len(BuiltinThemes))
}
}
func TestSeedBuiltinsIdempotent(t *testing.T) {
svc := setupTestDB(t)
svc.SeedBuiltins()
svc.SeedBuiltins()
themes, _ := svc.List()
if len(themes) != len(BuiltinThemes) {
t.Errorf("len(themes) = %d after double seed, want %d", len(themes), len(BuiltinThemes))
}
}
func TestGetByName(t *testing.T) {
svc := setupTestDB(t)
svc.SeedBuiltins()
theme, err := svc.GetByName("Dracula")
if err != nil {
t.Fatalf("GetByName() error: %v", err)
}
if theme.Background != "#282a36" {
t.Errorf("Background = %q, want %q", theme.Background, "#282a36")
}
}