Skip to content

Commit

Permalink
feat: add config fields for overriding theme colors (#203)
Browse files Browse the repository at this point in the history
  • Loading branch information
jahvon authored Dec 25, 2024
1 parent e7ef96a commit 9b338c4
Show file tree
Hide file tree
Showing 7 changed files with 355 additions and 1 deletion.
8 changes: 8 additions & 0 deletions docs/guide/interactive.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ Use the following command to change the theme:
flow config set theme (default|light|dark|dracula|tokyo-night)
```
**Overriding the theme's colors**

Additionally, you can override the theme colors by setting the `colorOverride` field in the config file. Any color not
set in the `colorOverride` field will use the default color for the set theme.
See the [config file reference](../types/config.md#ColorPalette) for more information.

```yaml

### Changing desktop notification settings

Desktop notifications can be sent when executables are completed. Use the following command to enable or disable desktop notifications:
Expand Down
53 changes: 53 additions & 0 deletions docs/schemas/config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,55 @@
"currentWorkspace"
],
"definitions": {
"ColorPalette": {
"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",
"type": "object",
"properties": {
"black": {
"type": "string"
},
"body": {
"type": "string"
},
"border": {
"type": "string"
},
"codeStyle": {
"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",
"type": "string"
},
"emphasis": {
"type": "string"
},
"error": {
"type": "string"
},
"gray": {
"type": "string"
},
"info": {
"type": "string"
},
"primary": {
"type": "string"
},
"secondary": {
"type": "string"
},
"success": {
"type": "string"
},
"tertiary": {
"type": "string"
},
"warning": {
"type": "string"
},
"white": {
"type": "string"
}
}
},
"Interactive": {
"description": "Configurations for the interactive UI.",
"type": "object",
Expand All @@ -30,6 +79,10 @@
}
},
"properties": {
"colorOverride": {
"$ref": "#/definitions/ColorPalette",
"description": "Override the default color palette for the interactive UI.\nThis can be used to customize the colors of the UI.\n"
},
"currentNamespace": {
"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",
"type": "string",
Expand Down
31 changes: 31 additions & 0 deletions docs/types/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Alternatively, a custom path can be set using the `FLOW_CONFIG_PATH` environment

| Field | Description | Type | Default | Required |
| ----- | ----------- | ---- | ------- | :--------: |
| `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> | |
| `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` | | |
| `currentWorkspace` | The name of the current workspace. This should match a key in the `workspaces` or `remoteWorkspaces` map. | `string` | | |
| `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 | |
Expand All @@ -36,6 +37,36 @@ Alternatively, a custom path can be set using the `FLOW_CONFIG_PATH` environment

## Definitions

### ColorPalette

The color palette for the interactive UI.
The colors can be either an ANSI 16, ANSI 256, or TrueColor (hex) value.
If unset, the default color for the current theme will be used.


**Type:** `object`



**Properties:**

| Field | Description | Type | Default | Required |
| ----- | ----------- | ---- | ------- | :--------: |
| `black` | | `string` | <no value> | |
| `body` | | `string` | <no value> | |
| `border` | | `string` | <no value> | |
| `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> | |
| `emphasis` | | `string` | <no value> | |
| `error` | | `string` | <no value> | |
| `gray` | | `string` | <no value> | |
| `info` | | `string` | <no value> | |
| `primary` | | `string` | <no value> | |
| `secondary` | | `string` | <no value> | |
| `success` | | `string` | <no value> | |
| `tertiary` | | `string` | <no value> | |
| `warning` | | `string` | <no value> | |
| `white` | | `string` | <no value> | |

### Interactive

Configurations for the interactive UI.
Expand Down
63 changes: 62 additions & 1 deletion internal/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/charmbracelet/lipgloss"
"github.com/jahvon/tuikit"
"github.com/jahvon/tuikit/io"
"github.com/jahvon/tuikit/styles"
"github.com/pkg/errors"

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

theme := flowIO.Theme(cfg.Theme.String())
if cfg.ColorOverride != nil {
theme = overrideThemeColor(theme, cfg.ColorOverride)
}
c.TUIContainer, err = tuikit.NewContainer(
ctx, app,
tuikit.WithInput(stdIn),
tuikit.WithOutput(stdOut),
tuikit.WithTheme(flowIO.Theme(cfg.Theme.String())),
tuikit.WithTheme(theme),
)
if err != nil {
panic(errors.Wrap(err, "TUI container initialization error"))
Expand Down Expand Up @@ -183,6 +190,14 @@ func currentWorkspace(cfg *config.Config) (*workspace.Workspace, error) {
if err != nil {
return nil, err
}
if runtime.GOOS == "darwin" {
// On macOS, paths that start with /tmp (and some other system directories)
// are actually symbolic links to paths under /private. The OS may return
// either form of the path - e.g., both "/tmp/file" and "/private/tmp/file"
// refer to the same location. We strip the "/private" prefix for consistent
// path comparison, while preserving the original paths for filesystem operations.
wd = strings.TrimPrefix(wd, "/private")
}

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

return filesystem.LoadWorkspaceConfig(ws, wsPath)
}

func overrideThemeColor(theme styles.Theme, palette *config.ColorPalette) styles.Theme {
if palette == nil {
return theme
}
if palette.Primary != nil {
theme.PrimaryColor = lipgloss.Color(*palette.Primary)
}
if palette.Secondary != nil {
theme.SecondaryColor = lipgloss.Color(*palette.Secondary)
}
if palette.Tertiary != nil {
theme.TertiaryColor = lipgloss.Color(*palette.Tertiary)
}
if palette.Success != nil {
theme.SuccessColor = lipgloss.Color(*palette.Success)
}
if palette.Warning != nil {
theme.WarningColor = lipgloss.Color(*palette.Warning)
}
if palette.Error != nil {
theme.ErrorColor = lipgloss.Color(*palette.Error)
}
if palette.Info != nil {
theme.InfoColor = lipgloss.Color(*palette.Info)
}
if palette.Body != nil {
theme.BodyColor = lipgloss.Color(*palette.Body)
}
if palette.Emphasis != nil {
theme.EmphasisColor = lipgloss.Color(*palette.Emphasis)
}
if palette.White != nil {
theme.White = lipgloss.Color(*palette.White)
}
if palette.Black != nil {
theme.Black = lipgloss.Color(*palette.Black)
}
if palette.Gray != nil {
theme.Gray = lipgloss.Color(*palette.Gray)
}
if palette.CodeStyle != nil {
theme.ChromaCodeStyle = *palette.CodeStyle
}
return theme
}
101 changes: 101 additions & 0 deletions internal/context/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//nolint:testpackage
package context

import (
"os"
"path/filepath"
"testing"

"github.com/charmbracelet/lipgloss"
"github.com/jahvon/tuikit/styles"
"github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/jahvon/flow/types/config"
)

func TestContext(t *testing.T) {
RegisterFailHandler(ginkgo.Fail)
ginkgo.RunSpecs(t, "Context Suite")
}

var _ = ginkgo.Describe("Context", func() {
ginkgo.Describe("currentWorkspace", func() {
var (
cfg *config.Config
tmpDir string
)

ginkgo.BeforeEach(func() {
tmpDir = ginkgo.GinkgoT().TempDir()
cfg = &config.Config{
Workspaces: map[string]string{
"ws1": filepath.Clean(filepath.Join(tmpDir, "ws1")),
"ws2": filepath.Clean(filepath.Join(tmpDir, "ws2")),
},
CurrentWorkspace: "ws1",
WorkspaceMode: config.ConfigWorkspaceModeFixed,
}
})

ginkgo.AfterEach(func() {
_ = os.RemoveAll(tmpDir)
})

ginkgo.It("should return the current workspace in fixed mode", func() {
ws, err := currentWorkspace(cfg)
Expect(err).NotTo(HaveOccurred())
Expect(ws.AssignedName()).To(Equal("ws1"))
Expect(ws.Location()).To(Equal(filepath.Join(tmpDir, "ws1")))
})

ginkgo.It("should return the current workspace in dynamic mode", func() {
cfg.WorkspaceMode = config.ConfigWorkspaceModeDynamic
Expect(os.Mkdir(filepath.Join(tmpDir, "ws2"), 0750)).To(Succeed())
// os.Setenv("PWD", filepath.Join(tmpDir, "ws2"))
Expect(os.Chdir(filepath.Join(tmpDir, "ws2"))).To(Succeed())

ws, err := currentWorkspace(cfg)
Expect(err).NotTo(HaveOccurred())
Expect(ws.AssignedName()).To(Equal("ws2"))
Expect(ws.Location()).To(Equal(filepath.Join(tmpDir, "ws2")))
})

ginkgo.It("should return an error if the current workspace is not found", func() {
cfg.CurrentWorkspace = "ws3"
_, err := currentWorkspace(cfg)
Expect(err).To(HaveOccurred())
})
})

ginkgo.Describe("overrideThemeColor", func() {
var theme styles.Theme
var palette *config.ColorPalette

ginkgo.BeforeEach(func() {
theme = styles.Theme{
PrimaryColor: "#000000",
SecondaryColor: "#FFFFFF",
}
palette = &config.ColorPalette{
Primary: strPtr("#FF0000"),
Secondary: strPtr("#00FF00"),
}
})

ginkgo.It("should override the theme colors with the palette colors", func() {
newTheme := overrideThemeColor(theme, palette)
Expect(newTheme.PrimaryColor).To(Equal(lipgloss.Color("#FF0000")))
Expect(newTheme.SecondaryColor).To(Equal(lipgloss.Color("#00FF00")))
})

ginkgo.It("should not change the theme if the palette is nil", func() {
newTheme := overrideThemeColor(theme, nil)
Expect(newTheme).To(Equal(theme))
})
})
})

func strPtr(s string) *string {
return &s
}
56 changes: 56 additions & 0 deletions types/config/config.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 9b338c4

Please sign in to comment.