Skip to content

Commit ce42f3b

Browse files
Merge pull request #28 from aaravmaloo/main
Add themes to chibi
2 parents e14547b + 529e1fd commit ce42f3b

11 files changed

Lines changed: 465 additions & 109 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,22 @@ $env:CHIBI_DATA_PATH = "D:\apps\chibi-data"
8787
chibi login
8888
```
8989

90+
### Themes
91+
Chibi supports built-in themes for CLI colors.
92+
93+
- Show active theme and available themes:
94+
```bash
95+
chibi theme
96+
```
97+
- Persist a theme:
98+
```bash
99+
chibi theme nord
100+
```
101+
- Temporarily override via environment variable:
102+
```bash
103+
CHIBI_THEME=sunset chibi profile
104+
```
105+
90106
## Documentation
91107
You can check the docs [here](https://chibi-cli.pages.dev/).
92108

cmd/cmd_root.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/CosmicPredator/chibi/internal/theme"
78
"github.com/CosmicPredator/chibi/internal/ui"
89
"github.com/charmbracelet/fang"
910
"github.com/spf13/cobra"
@@ -15,6 +16,12 @@ var rootCmd = &cobra.Command{
1516
}
1617

1718
func Execute(version string) {
19+
if err := theme.Load(); err != nil {
20+
fmt.Println(ui.ErrorText(err))
21+
return
22+
}
23+
applyThemeToMessageTemplates()
24+
1825
rootCmd.CompletionOptions.DisableDefaultCmd = true
1926
rootCmd.AddCommand(
2027
loginCmd,
@@ -25,12 +32,13 @@ func Execute(version string) {
2532
mediaUpdateCmd,
2633
mediaAddCmd,
2734
mediaInfoCmd,
35+
themeCmd,
2836
)
2937
if err := fang.Execute(
30-
context.TODO(),
31-
rootCmd,
38+
context.TODO(),
39+
rootCmd,
3240
fang.WithVersion(version),
33-
); err != nil {
41+
); err != nil {
3442
fmt.Println(ui.ErrorText(err))
3543
}
3644
}

cmd/cmd_theme.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/CosmicPredator/chibi/internal/theme"
8+
"github.com/CosmicPredator/chibi/internal/ui"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func handleTheme(_ *cobra.Command, args []string) {
13+
if len(args) == 0 {
14+
fmt.Printf("Current theme: %s\n", theme.CurrentName())
15+
fmt.Printf("Available themes: %s\n", strings.Join(theme.Available(), ", "))
16+
fmt.Println("Set a theme with: chibi theme <name>")
17+
return
18+
}
19+
20+
if err := theme.Save(args[0]); err != nil {
21+
fmt.Println(ui.ErrorText(err))
22+
return
23+
}
24+
25+
applyThemeToMessageTemplates()
26+
fmt.Println(ui.SuccessText(fmt.Sprintf("Theme set to %s", theme.CurrentName())))
27+
}
28+
29+
var themeCmd = &cobra.Command{
30+
Use: "theme [name]",
31+
Short: "Show or set the active color theme",
32+
Args: cobra.MaximumNArgs(1),
33+
Run: handleTheme,
34+
}

cmd/constants.go

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,44 @@
11
package cmd
22

3-
import "github.com/charmbracelet/lipgloss"
4-
5-
var ERROR_MESSAGE_TEMPLATE = lipgloss.
6-
NewStyle().
7-
BorderStyle(lipgloss.RoundedBorder()).
8-
BorderForeground(lipgloss.Color("#FF0000")).
9-
PaddingLeft(1).
10-
PaddingRight(1).
11-
Foreground(lipgloss.Color("#FF0000"))
12-
13-
var SUCCESS_MESSAGE_TEMPLATE = lipgloss.
14-
NewStyle().
15-
BorderStyle(lipgloss.RoundedBorder()).
16-
BorderForeground(lipgloss.Color("#00FF00")).
17-
Foreground(lipgloss.Color("#00FF00")).
18-
PaddingLeft(1).
19-
PaddingRight(1)
20-
21-
var OTHER_MESSAGE_TEMPLATE = lipgloss.
22-
NewStyle().
23-
BorderStyle(lipgloss.RoundedBorder()).
24-
BorderForeground(lipgloss.Color("#00FFFF")).
25-
Foreground(lipgloss.Color("#00FFFF")).
26-
PaddingLeft(1).
27-
PaddingRight(1)
3+
import (
4+
"github.com/CosmicPredator/chibi/internal/theme"
5+
"github.com/charmbracelet/lipgloss"
6+
)
7+
8+
var ERROR_MESSAGE_TEMPLATE lipgloss.Style
9+
10+
var SUCCESS_MESSAGE_TEMPLATE lipgloss.Style
11+
12+
var OTHER_MESSAGE_TEMPLATE lipgloss.Style
13+
14+
func applyThemeToMessageTemplates() {
15+
palette := theme.Current()
16+
17+
ERROR_MESSAGE_TEMPLATE = lipgloss.
18+
NewStyle().
19+
BorderStyle(lipgloss.RoundedBorder()).
20+
BorderForeground(lipgloss.Color(palette.MessageError)).
21+
PaddingLeft(1).
22+
PaddingRight(1).
23+
Foreground(lipgloss.Color(palette.MessageError))
24+
25+
SUCCESS_MESSAGE_TEMPLATE = lipgloss.
26+
NewStyle().
27+
BorderStyle(lipgloss.RoundedBorder()).
28+
BorderForeground(lipgloss.Color(palette.MessageSuccess)).
29+
Foreground(lipgloss.Color(palette.MessageSuccess)).
30+
PaddingLeft(1).
31+
PaddingRight(1)
32+
33+
OTHER_MESSAGE_TEMPLATE = lipgloss.
34+
NewStyle().
35+
BorderStyle(lipgloss.RoundedBorder()).
36+
BorderForeground(lipgloss.Color(palette.MessageOther)).
37+
Foreground(lipgloss.Color(palette.MessageOther)).
38+
PaddingLeft(1).
39+
PaddingRight(1)
40+
}
41+
42+
func init() {
43+
applyThemeToMessageTemplates()
44+
}

internal/theme/theme.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package theme
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"sort"
10+
"strings"
11+
"sync"
12+
13+
"github.com/CosmicPredator/chibi/internal/kvdb"
14+
)
15+
16+
const (
17+
ThemeEnvName = "CHIBI_THEME"
18+
themeKey = "theme"
19+
)
20+
21+
type Palette struct {
22+
SuccessText string
23+
ErrorText string
24+
HighlightText string
25+
26+
Spinner string
27+
PromptTitle string
28+
PromptDefault string
29+
30+
KeyText string
31+
ValueText string
32+
33+
TableHeader string
34+
TableID string
35+
TableFormat string
36+
TableMetric string
37+
TableRepeating string
38+
39+
MessageError string
40+
MessageSuccess string
41+
MessageOther string
42+
}
43+
44+
var palettes = map[string]Palette{
45+
"default": {
46+
SuccessText: "#00FF00",
47+
ErrorText: "#CC0000",
48+
HighlightText: "#00FFFF",
49+
Spinner: "5",
50+
PromptTitle: "5",
51+
PromptDefault: "1",
52+
KeyText: "#FF79C6",
53+
ValueText: "#8BE9FD",
54+
TableHeader: "5",
55+
TableID: "6",
56+
TableFormat: "2",
57+
TableMetric: "3",
58+
TableRepeating: "4",
59+
MessageError: "#FF0000",
60+
MessageSuccess: "#00FF00",
61+
MessageOther: "#00FFFF",
62+
},
63+
"nord": {
64+
SuccessText: "#A3BE8C",
65+
ErrorText: "#BF616A",
66+
HighlightText: "#88C0D0",
67+
Spinner: "#81A1C1",
68+
PromptTitle: "#81A1C1",
69+
PromptDefault: "#D08770",
70+
KeyText: "#B48EAD",
71+
ValueText: "#8FBCBB",
72+
TableHeader: "#81A1C1",
73+
TableID: "#88C0D0",
74+
TableFormat: "#A3BE8C",
75+
TableMetric: "#EBCB8B",
76+
TableRepeating: "#5E81AC",
77+
MessageError: "#BF616A",
78+
MessageSuccess: "#A3BE8C",
79+
MessageOther: "#88C0D0",
80+
},
81+
"sunset": {
82+
SuccessText: "#43A047",
83+
ErrorText: "#E53935",
84+
HighlightText: "#00ACC1",
85+
Spinner: "#FB8C00",
86+
PromptTitle: "#FB8C00",
87+
PromptDefault: "#F4511E",
88+
KeyText: "#8E24AA",
89+
ValueText: "#039BE5",
90+
TableHeader: "#FB8C00",
91+
TableID: "#1E88E5",
92+
TableFormat: "#43A047",
93+
TableMetric: "#FDD835",
94+
TableRepeating: "#3949AB",
95+
MessageError: "#E53935",
96+
MessageSuccess: "#43A047",
97+
MessageOther: "#00ACC1",
98+
},
99+
}
100+
101+
var (
102+
mu sync.RWMutex
103+
current = "default"
104+
)
105+
106+
func normalizeThemeName(name string) string {
107+
return strings.TrimSpace(strings.ToLower(name))
108+
}
109+
110+
func Current() Palette {
111+
mu.RLock()
112+
defer mu.RUnlock()
113+
return palettes[current]
114+
}
115+
116+
func CurrentName() string {
117+
mu.RLock()
118+
defer mu.RUnlock()
119+
return current
120+
}
121+
122+
func Available() []string {
123+
out := make([]string, 0, len(palettes))
124+
for name := range palettes {
125+
out = append(out, name)
126+
}
127+
sort.Strings(out)
128+
return out
129+
}
130+
131+
func SetCurrent(name string) error {
132+
normalized := normalizeThemeName(name)
133+
if normalized == "" {
134+
return errors.New("theme name cannot be empty")
135+
}
136+
if _, ok := palettes[normalized]; !ok {
137+
return fmt.Errorf(
138+
"unknown theme %q (available: %s)",
139+
name,
140+
strings.Join(Available(), ", "),
141+
)
142+
}
143+
144+
mu.Lock()
145+
current = normalized
146+
mu.Unlock()
147+
return nil
148+
}
149+
150+
func Save(name string) error {
151+
if err := SetCurrent(name); err != nil {
152+
return err
153+
}
154+
155+
db, err := kvdb.Open()
156+
if err != nil {
157+
return err
158+
}
159+
defer db.Close()
160+
161+
return db.Set(context.TODO(), themeKey, []byte(CurrentName()))
162+
}
163+
164+
func Load() error {
165+
if envTheme := strings.TrimSpace(os.Getenv(ThemeEnvName)); envTheme != "" {
166+
return SetCurrent(envTheme)
167+
}
168+
169+
db, err := kvdb.Open()
170+
if err != nil {
171+
return nil
172+
}
173+
defer db.Close()
174+
175+
rawTheme, err := db.Get(context.TODO(), themeKey)
176+
if err != nil {
177+
if errors.Is(err, sql.ErrNoRows) {
178+
return nil
179+
}
180+
return nil
181+
}
182+
183+
name := strings.TrimSpace(string(rawTheme))
184+
if name == "" {
185+
return nil
186+
}
187+
188+
// Ignore stale values from old/invalid config and fallback to default.
189+
if err := SetCurrent(name); err != nil {
190+
return nil
191+
}
192+
return nil
193+
}

0 commit comments

Comments
 (0)