Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,25 @@ Automatic refresh is always enabled. It pauses when an error occurs to avoid rep

The log level uses Go's `slog` [log levels](https://pkg.go.dev/log/slog#hdr-Levels), e.g: `info`, `debug`, `warn`, `error`.

### Themes

**o7k** supports customizable YAML-based color themes (skins). Built-in skins are available under `internal/config/skins/`
and users can create their own.

Four accessibility-oriented themes are included: **High Contrast**, **Protanopia**, **Deuteranopia**, and **Tritanopia**.

To select a theme, set the `skin` property in `~/.config/o7k/config.yaml`:

```yaml
skin: dracula
```

Custom skins can be placed in `~/.config/o7k/skins/` and selected by their filename without the `.yaml` extension:

```yaml
skin: my-custom-theme
```

## Usage

**o7k** uses the standard OpenStack `clouds.yaml` configuration and automatically discovers the
Expand Down
32 changes: 24 additions & 8 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ const (
)

type Config struct {
Cache Cache `yaml:"cache"`
Log Log `yaml:"log"`
Refresh Refresh `yaml:"refresh"`
Cache Cache `yaml:"cache"`
Log Log `yaml:"log"`
Refresh Refresh `yaml:"refresh"`
Skin string `yaml:"skin"`
SkinColors Skin `yaml:"-"`
}

type Cache struct {
Expand Down Expand Up @@ -68,13 +70,15 @@ func Load() (Config, error) {
return cfg, err
}

path := filepath.Join(dir, "o7k", "config.yaml")
dir = filepath.Join(dir, "o7k")
path := filepath.Join(dir, "config.yaml")

data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return cfg, nil
}
if err != nil {
if err == nil {
if err := yaml.Unmarshal(data, &cfg); err != nil {
return cfg, err
}
} else if !os.IsNotExist(err) {
return cfg, fmt.Errorf("reading config: %w", err)
}

Expand All @@ -96,5 +100,17 @@ func Load() (Config, error) {
cfg.Refresh.Interval = defaultRefreshInterval
}

if cfg.Skin == "" {
cfg.Skin = "default"
}

if err := installSkins(dir); err != nil {
return cfg, err
}
cfg.SkinColors, err = loadSkin(dir, cfg.Skin)
if err != nil {
return cfg, err
}

return cfg, nil
}
42 changes: 42 additions & 0 deletions internal/config/skin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package config

type ColorPair struct {
Foreground string `yaml:"foreground"`
Background string `yaml:"background"`
}

type Skin struct {
Main ColorPair `yaml:"main"`
Table ColorPair `yaml:"table"`
Context ColorPair `yaml:"context"`
Command ColorPair `yaml:"command"`

ResourceTag ColorPair `yaml:"resource_tag"`
NavigationTag ColorPair `yaml:"navigation_tag"`
Status ColorPair `yaml:"status"`
Loading ColorPair `yaml:"loading"`
Info ColorPair `yaml:"info"`
StateOn ColorPair `yaml:"state_on"`
StateOff ColorPair `yaml:"state_off"`
ModalButton ColorPair `yaml:"modal_button"`
TableSelected ColorPair `yaml:"table_selected"`

CommandGhost string `yaml:"command_ghost"`
CommandPlaceholder string `yaml:"command_placeholder"`

HeaderLabel string `yaml:"header_label"`
HeaderValue string `yaml:"header_value"`
HeaderCommandKey string `yaml:"header_command_key"`
HeaderCommandText string `yaml:"header_command_text"`
Logo string `yaml:"logo"`

JSONKey string `yaml:"json_key"`
JSONString string `yaml:"json_string"`
JSONNumber string `yaml:"json_number"`
JSONBool string `yaml:"json_bool"`
JSONNull string `yaml:"json_null"`

TableTitle string `yaml:"table_title"`
TableHeader string `yaml:"table_header"`
ErrorTitle string `yaml:"error_title"`
}
82 changes: 82 additions & 0 deletions internal/config/skin_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package config

import (
"embed"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"

"gopkg.in/yaml.v3"
)

//go:embed skins/*.yaml
var embeddedSkins embed.FS

func installSkins(configDir string) error {
skinsDir := filepath.Join(configDir, "skins")

if err := os.MkdirAll(skinsDir, 0755); err != nil {
return fmt.Errorf("creating skins directory: %w", err)
}

files, err := fs.Glob(embeddedSkins, "skins/*.yaml")
if err != nil {
return fmt.Errorf("listing embedded skins: %w", err)
}

for _, file := range files {
data, err := embeddedSkins.ReadFile(file)
if err != nil {
return fmt.Errorf("reading embedded skin %s: %w", file, err)
}

path := filepath.Join(skinsDir, filepath.Base(file))

f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if os.IsExist(err) {
continue
}
if err != nil {
return fmt.Errorf("creating skin %s: %w", path, err)
}

_, writeErr := f.Write(data)
closeErr := f.Close()

if writeErr != nil {
return fmt.Errorf("writing skin %s: %w", path, writeErr)
}
if closeErr != nil {
return fmt.Errorf("closing skin %s: %w", path, closeErr)
}
}

return nil
}

func loadSkin(configDir, name string) (Skin, error) {
var skin Skin

if name == "" || name != filepath.Base(name) ||
strings.ContainsAny(name, `/\`) || name == "." || name == ".." {
return skin, fmt.Errorf("invalid skin name %q", name)
}

path := filepath.Join(configDir, "skins", name+".yaml")

data, err := os.ReadFile(path)
if err != nil {
return skin, fmt.Errorf("reading skin %q: %w", name, err)
}

decoder := yaml.NewDecoder(strings.NewReader(string(data)))
decoder.KnownFields(true)

if err := decoder.Decode(&skin); err != nil {
return skin, fmt.Errorf("parsing skin %q: %w", name, err)
}

return skin, nil
}
54 changes: 54 additions & 0 deletions internal/config/skins/default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
main:
foreground: "#FFFFFF"
background: ""
table:
foreground: "#FFFFFF"
background: ""
table_selected:
foreground: "255"
background: "#ED1944"
table_header: "#FFFFFF"
context:
foreground: "#FFFFFF"
background: ""
command:
foreground: "#87CEFA"
background: ""
resource_tag:
foreground: "255"
background: "#ED1944"
navigation_tag:
foreground: "0"
background: "255"
status:
foreground: "#000000"
background: "#FFA500"
loading:
foreground: "#000000"
background: "#ff8c00"
info:
foreground: "#000000"
background: "#00BFFF"
state_on:
foreground: "#000000"
background: "#008000"
state_off:
foreground: "#000000"
background: "#FF0000"
modal_button:
foreground: "#000000"
background: "#00BFFF"
command_ghost: "#5C6370"
command_placeholder: "#666666"
header_label: "#FFA500"
header_value: "#FFFFFF"
header_command_key: "#00BFFF"
header_command_text: "#FFFFFF"
logo: "#FFA500"
json_key: "#87CEFA"
json_string: "#98C379"
json_number: "#E5C07B"
json_bool: "#C678DD"
json_null: "#7F848E"
table_title: "#FFFFFF"
error_title: "#ED1944"
54 changes: 54 additions & 0 deletions internal/config/skins/deuteranopia.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
main:
foreground: "#F0F3F7"
background: "#151923"
table:
foreground: "#F0F3F7"
background: "#151923"
context:
foreground: "#F0F3F7"
background: "#151923"
command:
foreground: "#82BFFF"
background: "#151923"
resource_tag:
foreground: "#151923"
background: "#FFB347"
navigation_tag:
foreground: "#151923"
background: "#82BFFF"
status:
foreground: "#151923"
background: "#FFD166"
loading:
foreground: "#151923"
background: "#FFB347"
info:
foreground: "#151923"
background: "#82BFFF"
state_on:
foreground: "#151923"
background: "#82BFFF"
state_off:
foreground: "#151923"
background: "#FFB347"
modal_button:
foreground: "#151923"
background: "#82BFFF"
table_selected:
foreground: "#151923"
background: "#FFD166"
table_header: "#FFFFFF"
command_ghost: "#A0A8B8"
command_placeholder: "#A0A8B8"
header_label: "#FFD166"
header_value: "#FFFFFF"
header_command_key: "#82BFFF"
header_command_text: "#F0F3F7"
logo: "#FFD166"
json_key: "#82BFFF"
json_string: "#FFD166"
json_number: "#FFFFFF"
json_bool: "#D5A6FF"
json_null: "#A0A8B8"
table_title: "#FFFFFF"
error_title: "#FFB347"
54 changes: 54 additions & 0 deletions internal/config/skins/dracula.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
main:
foreground: "#F8F8F2"
background: "#282A36"
table:
foreground: "#F8F8F2"
background: "#282A36"
context:
foreground: "#F8F8F2"
background: "#282A36"
command:
foreground: "#8BE9FD"
background: "#282A36"
resource_tag:
foreground: "#282A36"
background: "#FF79C6"
navigation_tag:
foreground: "#282A36"
background: "#BD93F9"
status:
foreground: "#282A36"
background: "#FFB86C"
loading:
foreground: "#282A36"
background: "#F1FA8C"
info:
foreground: "#282A36"
background: "#8BE9FD"
state_on:
foreground: "#282A36"
background: "#50FA7B"
state_off:
foreground: "#282A36"
background: "#FF5555"
modal_button:
foreground: "#282A36"
background: "#BD93F9"
table_selected:
foreground: "#282A36"
background: "#BD93F9"
table_header: "#F8F8F2"
command_ghost: "#6272A4"
command_placeholder: "#6272A4"
header_label: "#FFB86C"
header_value: "#F8F8F2"
header_command_key: "#8BE9FD"
header_command_text: "#F8F8F2"
logo: "#BD93F9"
json_key: "#8BE9FD"
json_string: "#F1FA8C"
json_number: "#BD93F9"
json_bool: "#FF79C6"
json_null: "#6272A4"
table_title: "#F8F8F2"
error_title: "#FF5555"
Loading
Loading