Skip to content

Commit 9b338c4

Browse files
authored
feat: add config fields for overriding theme colors (#203)
1 parent e7ef96a commit 9b338c4

File tree

7 files changed

+355
-1
lines changed

7 files changed

+355
-1
lines changed

docs/guide/interactive.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ Use the following command to change the theme:
2424
flow config set theme (default|light|dark|dracula|tokyo-night)
2525
```
2626
27+
**Overriding the theme's colors**
28+
29+
Additionally, you can override the theme colors by setting the `colorOverride` field in the config file. Any color not
30+
set in the `colorOverride` field will use the default color for the set theme.
31+
See the [config file reference](../types/config.md#ColorPalette) for more information.
32+
33+
```yaml
34+
2735
### Changing desktop notification settings
2836

2937
Desktop notifications can be sent when executables are completed. Use the following command to enable or disable desktop notifications:

docs/schemas/config_schema.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,55 @@
88
"currentWorkspace"
99
],
1010
"definitions": {
11+
"ColorPalette": {
12+
"description": "The color palette for the interactive UI.\nThe colors can be either an ANSI 16, ANSI 256, or TrueColor (hex) value.\nIf unset, the default color for the current theme will be used.\n",
13+
"type": "object",
14+
"properties": {
15+
"black": {
16+
"type": "string"
17+
},
18+
"body": {
19+
"type": "string"
20+
},
21+
"border": {
22+
"type": "string"
23+
},
24+
"codeStyle": {
25+
"description": "The style of the code block. For example, `monokai`, `dracula`, `github`, etc.\nSee [chroma styles](https://github.com/alecthomas/chroma/tree/master/styles) for available style names.\n",
26+
"type": "string"
27+
},
28+
"emphasis": {
29+
"type": "string"
30+
},
31+
"error": {
32+
"type": "string"
33+
},
34+
"gray": {
35+
"type": "string"
36+
},
37+
"info": {
38+
"type": "string"
39+
},
40+
"primary": {
41+
"type": "string"
42+
},
43+
"secondary": {
44+
"type": "string"
45+
},
46+
"success": {
47+
"type": "string"
48+
},
49+
"tertiary": {
50+
"type": "string"
51+
},
52+
"warning": {
53+
"type": "string"
54+
},
55+
"white": {
56+
"type": "string"
57+
}
58+
}
59+
},
1160
"Interactive": {
1261
"description": "Configurations for the interactive UI.",
1362
"type": "object",
@@ -30,6 +79,10 @@
3079
}
3180
},
3281
"properties": {
82+
"colorOverride": {
83+
"$ref": "#/definitions/ColorPalette",
84+
"description": "Override the default color palette for the interactive UI.\nThis can be used to customize the colors of the UI.\n"
85+
},
3386
"currentNamespace": {
3487
"description": "The name of the current namespace.\n\nNamespaces are used to reference executables in the CLI using the format `workspace:namespace/name`.\nIf the namespace is not set, only executables defined without a namespace will be discovered.\n",
3588
"type": "string",

docs/types/config.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Alternatively, a custom path can be set using the `FLOW_CONFIG_PATH` environment
2323

2424
| Field | Description | Type | Default | Required |
2525
| ----- | ----------- | ---- | ------- | :--------: |
26+
| `colorOverride` | Override the default color palette for the interactive UI. This can be used to customize the colors of the UI. | [ColorPalette](#ColorPalette) | <no value> | |
2627
| `currentNamespace` | The name of the current namespace. Namespaces are used to reference executables in the CLI using the format `workspace:namespace/name`. If the namespace is not set, only executables defined without a namespace will be discovered. | `string` | | |
2728
| `currentWorkspace` | The name of the current workspace. This should match a key in the `workspaces` or `remoteWorkspaces` map. | `string` | | |
2829
| `defaultLogMode` | The default log mode to use when running executables. This can either be `hidden`, `json`, `logfmt` or `text` `hidden` will not display any logs. `json` will display logs in JSON format. `logfmt` will display logs with a log level, timestamp, and message. `text` will just display the log message. | `string` | logfmt | |
@@ -36,6 +37,36 @@ Alternatively, a custom path can be set using the `FLOW_CONFIG_PATH` environment
3637

3738
## Definitions
3839

40+
### ColorPalette
41+
42+
The color palette for the interactive UI.
43+
The colors can be either an ANSI 16, ANSI 256, or TrueColor (hex) value.
44+
If unset, the default color for the current theme will be used.
45+
46+
47+
**Type:** `object`
48+
49+
50+
51+
**Properties:**
52+
53+
| Field | Description | Type | Default | Required |
54+
| ----- | ----------- | ---- | ------- | :--------: |
55+
| `black` | | `string` | <no value> | |
56+
| `body` | | `string` | <no value> | |
57+
| `border` | | `string` | <no value> | |
58+
| `codeStyle` | The style of the code block. For example, `monokai`, `dracula`, `github`, etc. See [chroma styles](https://github.com/alecthomas/chroma/tree/master/styles) for available style names. | `string` | <no value> | |
59+
| `emphasis` | | `string` | <no value> | |
60+
| `error` | | `string` | <no value> | |
61+
| `gray` | | `string` | <no value> | |
62+
| `info` | | `string` | <no value> | |
63+
| `primary` | | `string` | <no value> | |
64+
| `secondary` | | `string` | <no value> | |
65+
| `success` | | `string` | <no value> | |
66+
| `tertiary` | | `string` | <no value> | |
67+
| `warning` | | `string` | <no value> | |
68+
| `white` | | `string` | <no value> | |
69+
3970
### Interactive
4071

4172
Configurations for the interactive UI.

internal/context/context.go

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import (
55
"fmt"
66
"os"
77
"path/filepath"
8+
"runtime"
89
"strings"
910

11+
"github.com/charmbracelet/lipgloss"
1012
"github.com/jahvon/tuikit"
1113
"github.com/jahvon/tuikit/io"
14+
"github.com/jahvon/tuikit/styles"
1215
"github.com/pkg/errors"
1316

1417
"github.com/jahvon/flow/internal/cache"
@@ -89,11 +92,15 @@ func NewContext(ctx context.Context, stdIn, stdOut *os.File) *Context {
8992
tuikit.WithLoadingMsg("thinking..."),
9093
)
9194

95+
theme := flowIO.Theme(cfg.Theme.String())
96+
if cfg.ColorOverride != nil {
97+
theme = overrideThemeColor(theme, cfg.ColorOverride)
98+
}
9299
c.TUIContainer, err = tuikit.NewContainer(
93100
ctx, app,
94101
tuikit.WithInput(stdIn),
95102
tuikit.WithOutput(stdOut),
96-
tuikit.WithTheme(flowIO.Theme(cfg.Theme.String())),
103+
tuikit.WithTheme(theme),
97104
)
98105
if err != nil {
99106
panic(errors.Wrap(err, "TUI container initialization error"))
@@ -183,6 +190,14 @@ func currentWorkspace(cfg *config.Config) (*workspace.Workspace, error) {
183190
if err != nil {
184191
return nil, err
185192
}
193+
if runtime.GOOS == "darwin" {
194+
// On macOS, paths that start with /tmp (and some other system directories)
195+
// are actually symbolic links to paths under /private. The OS may return
196+
// either form of the path - e.g., both "/tmp/file" and "/private/tmp/file"
197+
// refer to the same location. We strip the "/private" prefix for consistent
198+
// path comparison, while preserving the original paths for filesystem operations.
199+
wd = strings.TrimPrefix(wd, "/private")
200+
}
186201

187202
for wsName, path := range cfg.Workspaces {
188203
rel, err := filepath.Rel(filepath.Clean(path), filepath.Clean(wd))
@@ -209,3 +224,49 @@ func currentWorkspace(cfg *config.Config) (*workspace.Workspace, error) {
209224

210225
return filesystem.LoadWorkspaceConfig(ws, wsPath)
211226
}
227+
228+
func overrideThemeColor(theme styles.Theme, palette *config.ColorPalette) styles.Theme {
229+
if palette == nil {
230+
return theme
231+
}
232+
if palette.Primary != nil {
233+
theme.PrimaryColor = lipgloss.Color(*palette.Primary)
234+
}
235+
if palette.Secondary != nil {
236+
theme.SecondaryColor = lipgloss.Color(*palette.Secondary)
237+
}
238+
if palette.Tertiary != nil {
239+
theme.TertiaryColor = lipgloss.Color(*palette.Tertiary)
240+
}
241+
if palette.Success != nil {
242+
theme.SuccessColor = lipgloss.Color(*palette.Success)
243+
}
244+
if palette.Warning != nil {
245+
theme.WarningColor = lipgloss.Color(*palette.Warning)
246+
}
247+
if palette.Error != nil {
248+
theme.ErrorColor = lipgloss.Color(*palette.Error)
249+
}
250+
if palette.Info != nil {
251+
theme.InfoColor = lipgloss.Color(*palette.Info)
252+
}
253+
if palette.Body != nil {
254+
theme.BodyColor = lipgloss.Color(*palette.Body)
255+
}
256+
if palette.Emphasis != nil {
257+
theme.EmphasisColor = lipgloss.Color(*palette.Emphasis)
258+
}
259+
if palette.White != nil {
260+
theme.White = lipgloss.Color(*palette.White)
261+
}
262+
if palette.Black != nil {
263+
theme.Black = lipgloss.Color(*palette.Black)
264+
}
265+
if palette.Gray != nil {
266+
theme.Gray = lipgloss.Color(*palette.Gray)
267+
}
268+
if palette.CodeStyle != nil {
269+
theme.ChromaCodeStyle = *palette.CodeStyle
270+
}
271+
return theme
272+
}

internal/context/context_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//nolint:testpackage
2+
package context
3+
4+
import (
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/charmbracelet/lipgloss"
10+
"github.com/jahvon/tuikit/styles"
11+
"github.com/onsi/ginkgo/v2"
12+
. "github.com/onsi/gomega"
13+
14+
"github.com/jahvon/flow/types/config"
15+
)
16+
17+
func TestContext(t *testing.T) {
18+
RegisterFailHandler(ginkgo.Fail)
19+
ginkgo.RunSpecs(t, "Context Suite")
20+
}
21+
22+
var _ = ginkgo.Describe("Context", func() {
23+
ginkgo.Describe("currentWorkspace", func() {
24+
var (
25+
cfg *config.Config
26+
tmpDir string
27+
)
28+
29+
ginkgo.BeforeEach(func() {
30+
tmpDir = ginkgo.GinkgoT().TempDir()
31+
cfg = &config.Config{
32+
Workspaces: map[string]string{
33+
"ws1": filepath.Clean(filepath.Join(tmpDir, "ws1")),
34+
"ws2": filepath.Clean(filepath.Join(tmpDir, "ws2")),
35+
},
36+
CurrentWorkspace: "ws1",
37+
WorkspaceMode: config.ConfigWorkspaceModeFixed,
38+
}
39+
})
40+
41+
ginkgo.AfterEach(func() {
42+
_ = os.RemoveAll(tmpDir)
43+
})
44+
45+
ginkgo.It("should return the current workspace in fixed mode", func() {
46+
ws, err := currentWorkspace(cfg)
47+
Expect(err).NotTo(HaveOccurred())
48+
Expect(ws.AssignedName()).To(Equal("ws1"))
49+
Expect(ws.Location()).To(Equal(filepath.Join(tmpDir, "ws1")))
50+
})
51+
52+
ginkgo.It("should return the current workspace in dynamic mode", func() {
53+
cfg.WorkspaceMode = config.ConfigWorkspaceModeDynamic
54+
Expect(os.Mkdir(filepath.Join(tmpDir, "ws2"), 0750)).To(Succeed())
55+
// os.Setenv("PWD", filepath.Join(tmpDir, "ws2"))
56+
Expect(os.Chdir(filepath.Join(tmpDir, "ws2"))).To(Succeed())
57+
58+
ws, err := currentWorkspace(cfg)
59+
Expect(err).NotTo(HaveOccurred())
60+
Expect(ws.AssignedName()).To(Equal("ws2"))
61+
Expect(ws.Location()).To(Equal(filepath.Join(tmpDir, "ws2")))
62+
})
63+
64+
ginkgo.It("should return an error if the current workspace is not found", func() {
65+
cfg.CurrentWorkspace = "ws3"
66+
_, err := currentWorkspace(cfg)
67+
Expect(err).To(HaveOccurred())
68+
})
69+
})
70+
71+
ginkgo.Describe("overrideThemeColor", func() {
72+
var theme styles.Theme
73+
var palette *config.ColorPalette
74+
75+
ginkgo.BeforeEach(func() {
76+
theme = styles.Theme{
77+
PrimaryColor: "#000000",
78+
SecondaryColor: "#FFFFFF",
79+
}
80+
palette = &config.ColorPalette{
81+
Primary: strPtr("#FF0000"),
82+
Secondary: strPtr("#00FF00"),
83+
}
84+
})
85+
86+
ginkgo.It("should override the theme colors with the palette colors", func() {
87+
newTheme := overrideThemeColor(theme, palette)
88+
Expect(newTheme.PrimaryColor).To(Equal(lipgloss.Color("#FF0000")))
89+
Expect(newTheme.SecondaryColor).To(Equal(lipgloss.Color("#00FF00")))
90+
})
91+
92+
ginkgo.It("should not change the theme if the palette is nil", func() {
93+
newTheme := overrideThemeColor(theme, nil)
94+
Expect(newTheme).To(Equal(theme))
95+
})
96+
})
97+
})
98+
99+
func strPtr(s string) *string {
100+
return &s
101+
}

types/config/config.gen.go

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)