From 2a3566a75140a1441fed827e7a590871aca5185e Mon Sep 17 00:00:00 2001 From: Kyriakos Akriotis Date: Sat, 17 Oct 2026 13:21:02 +0200 Subject: [PATCH 1/6] configurable and default embedded sking #162 --- internal/config/config.go | 32 +++- internal/config/skin.go | 42 +++++ internal/config/skin_helpers.go | 82 ++++++++++ internal/config/skins/colorblind.yaml | 48 ++++++ internal/config/skins/default.yaml | 48 ++++++ internal/config/skins/high-contrast.yaml | 48 ++++++ internal/config/skins/norton-commander.yaml | 50 ++++++ internal/ui/app.go | 14 +- internal/ui/render.go | 4 +- internal/ui/styles.go | 170 +++++++++++--------- 10 files changed, 451 insertions(+), 87 deletions(-) create mode 100644 internal/config/skin.go create mode 100644 internal/config/skin_helpers.go create mode 100644 internal/config/skins/colorblind.yaml create mode 100644 internal/config/skins/default.yaml create mode 100644 internal/config/skins/high-contrast.yaml create mode 100644 internal/config/skins/norton-commander.yaml diff --git a/internal/config/config.go b/internal/config/config.go index 667e6fd..eff4dd8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 { @@ -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) } @@ -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 } diff --git a/internal/config/skin.go b/internal/config/skin.go new file mode 100644 index 0000000..07dbb83 --- /dev/null +++ b/internal/config/skin.go @@ -0,0 +1,42 @@ +package config + +type ColorPair struct { + Foreground string `yaml:"foreground"` + Background string `yaml:"background"` +} + +type Skin struct { + 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"` + + Command string `yaml:"command"` + 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"` + TableBorder string `yaml:"table_border"` + DetailBorder string `yaml:"detail_border"` + + ErrorModal ColorPair `yaml:"error_modal"` + ErrorBorder string `yaml:"error_border"` + ErrorTitle string `yaml:"error_title"` +} diff --git a/internal/config/skin_helpers.go b/internal/config/skin_helpers.go new file mode 100644 index 0000000..9ce8828 --- /dev/null +++ b/internal/config/skin_helpers.go @@ -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 +} diff --git a/internal/config/skins/colorblind.yaml b/internal/config/skins/colorblind.yaml new file mode 100644 index 0000000..e3796f8 --- /dev/null +++ b/internal/config/skins/colorblind.yaml @@ -0,0 +1,48 @@ +resource_tag: + foreground: "#000000" + background: "#E69F00" +navigation_tag: + foreground: "#000000" + background: "#FFFFFF" +status: + foreground: "#000000" + background: "#F0E442" +loading: + foreground: "#000000" + background: "#E69F00" +info: + foreground: "#000000" + background: "#56B4E9" +state_on: + foreground: "#000000" + background: "#009E73" +state_off: + foreground: "#FFFFFF" + background: "#D55E00" +modal_button: + foreground: "#000000" + background: "#56B4E9" +table_selected: + foreground: "#000000" + background: "#F0E442" +command: "#56B4E9" +command_ghost: "#999999" +command_placeholder: "#999999" +header_label: "#E69F00" +header_value: "#FFFFFF" +header_command_key: "#56B4E9" +header_command_text: "#FFFFFF" +logo: "#E69F00" +json_key: "#56B4E9" +json_string: "#009E73" +json_number: "#F0E442" +json_bool: "#CC79A7" +json_null: "#999999" +table_title: "#FFFFFF" +table_border: "#0072B2" +detail_border: "#0072B2" +error_modal: + foreground: "#FFFFFF" + background: "" +error_border: "#D55E00" +error_title: "#D55E00" diff --git a/internal/config/skins/default.yaml b/internal/config/skins/default.yaml new file mode 100644 index 0000000..2e43c93 --- /dev/null +++ b/internal/config/skins/default.yaml @@ -0,0 +1,48 @@ +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" +table_selected: + foreground: "255" + background: "#ED1944" +command: "#87CEFA" +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" +table_border: "#00BFFF" +detail_border: "#00BFFF" +error_modal: + foreground: "#FFFFFF" + background: "" +error_border: "#ED1944" +error_title: "#ED1944" diff --git a/internal/config/skins/high-contrast.yaml b/internal/config/skins/high-contrast.yaml new file mode 100644 index 0000000..5d369cc --- /dev/null +++ b/internal/config/skins/high-contrast.yaml @@ -0,0 +1,48 @@ +resource_tag: + foreground: "#FFFFFF" + background: "#9B0000" +navigation_tag: + foreground: "#000000" + background: "#FFFFFF" +status: + foreground: "#000000" + background: "#FFFF00" +loading: + foreground: "#000000" + background: "#FFFF00" +info: + foreground: "#000000" + background: "#00FFFF" +state_on: + foreground: "#000000" + background: "#00FF00" +state_off: + foreground: "#FFFFFF" + background: "#B00000" +modal_button: + foreground: "#000000" + background: "#FFFFFF" +table_selected: + foreground: "#000000" + background: "#FFFF00" +command: "#00FFFF" +command_ghost: "#B3B3B3" +command_placeholder: "#B3B3B3" +header_label: "#FFFF00" +header_value: "#FFFFFF" +header_command_key: "#00FFFF" +header_command_text: "#FFFFFF" +logo: "#FFFF00" +json_key: "#00FFFF" +json_string: "#00FF00" +json_number: "#FFFF00" +json_bool: "#FF80FF" +json_null: "#B3B3B3" +table_title: "#FFFFFF" +table_border: "#FFFFFF" +detail_border: "#FFFFFF" +error_modal: + foreground: "#FFFFFF" + background: "" +error_border: "#FF5555" +error_title: "#FF5555" diff --git a/internal/config/skins/norton-commander.yaml b/internal/config/skins/norton-commander.yaml new file mode 100644 index 0000000..89925e4 --- /dev/null +++ b/internal/config/skins/norton-commander.yaml @@ -0,0 +1,50 @@ +# Norton Commander 5.5-inspired palette based on the supplied screenshot. +# For full DOS-blue panels, set your terminal background to #0000AA. +resource_tag: + foreground: "#FFFF55" + background: "#0000AA" +navigation_tag: + foreground: "#000000" + background: "#00AAAA" +status: + foreground: "#000000" + background: "#00AAAA" +loading: + foreground: "#000000" + background: "#FFFF55" +info: + foreground: "#000000" + background: "#00AAAA" +state_on: + foreground: "#000000" + background: "#55FF55" +state_off: + foreground: "#FFFFFF" + background: "#AA0000" +modal_button: + foreground: "#000000" + background: "#00AAAA" +table_selected: + foreground: "#000000" + background: "#00AAAA" +command: "#55FFFF" +command_ghost: "#00AAAA" +command_placeholder: "#00AAAA" +header_label: "#FFFF55" +header_value: "#55FFFF" +header_command_key: "#FFFF55" +header_command_text: "#55FFFF" +logo: "#FFFF55" +json_key: "#55FFFF" +json_string: "#55FFFF" +json_number: "#FFFF55" +json_bool: "#FFFF55" +json_null: "#00AAAA" +table_title: "#FFFF55" +table_border: "#55FFFF" +detail_border: "#55FFFF" +error_modal: + foreground: "#FFFFFF" + background: "#AA0000" +error_border: "#FF5555" +error_title: "#FFFF55" diff --git a/internal/ui/app.go b/internal/ui/app.go index b8f0016..30d438d 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -27,6 +27,7 @@ const refreshInterval = 30 * time.Second type Model struct { version string + skin config.Skin registry *resource.Registry resource resource.Resource @@ -81,6 +82,8 @@ type Model struct { } func New(registry *resource.Registry, openstackContext *openstack.Context, pluginHost *plugins.Host, version string, cfg config.Config) Model { + initStyles(cfg.SkinColors) + r, ok := registry.Get("contexts") if !ok { return Model{ @@ -107,17 +110,17 @@ func New(registry *resource.Registry, openstackContext *openstack.Context, plugi table.WithHeight(10), ) - t.SetStyles(tableStyles()) + t.SetStyles(tableStyles(cfg.SkinColors)) command := textinput.New() command.Prompt = ":" command.CharLimit = 64 command.ShowSuggestions = true - command.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#87CEFA")) - command.TextStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#87CEFA")) - command.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#87CEFA")) - command.PlaceholderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")) + command.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.SkinColors.Command)) + command.TextStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.SkinColors.Command)) + command.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.SkinColors.Command)) + command.PlaceholderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.SkinColors.CommandPlaceholder)) detailViewport := viewport.New(1, 1) detailViewport.SetHorizontalStep(4) @@ -137,6 +140,7 @@ func New(registry *resource.Registry, openstackContext *openstack.Context, plugi m := Model{ version: version, + skin: cfg.SkinColors, registry: registry, resource: r, diff --git a/internal/ui/render.go b/internal/ui/render.go index 8a7c0c4..fdcfc75 100644 --- a/internal/ui/render.go +++ b/internal/ui/render.go @@ -238,7 +238,7 @@ func (m Model) renderTable() string { right := remaining - left borderStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#00BFFF")) + Foreground(lipgloss.Color(m.skin.TableBorder)) top := borderStyle.Render( "┌" + @@ -326,7 +326,7 @@ func (m Model) renderDetails() string { right := remaining - left borderStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#00BFFF")) + Foreground(lipgloss.Color(m.skin.DetailBorder)) top := borderStyle.Render( "┌" + diff --git a/internal/ui/styles.go b/internal/ui/styles.go index c9f7305..c645262 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -1,6 +1,7 @@ package ui import ( + "github.com/akyriako/o7k/internal/config" "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/lipgloss" ) @@ -8,135 +9,160 @@ import ( //Header and AppView var ( + resourceTagStyle lipgloss.Style + navigationTagStyle lipgloss.Style + commandStyle lipgloss.Style + commandGhostStyle lipgloss.Style + statusStyle lipgloss.Style + headerLabelStyle lipgloss.Style + headerValueStyle lipgloss.Style + headerCommandKeyStyle lipgloss.Style + headerCommandTextStyle lipgloss.Style + logoStyle lipgloss.Style + loadingStyle lipgloss.Style + infoStyle lipgloss.Style + autoRefreshStateOnStyle lipgloss.Style + autoRefreshStateOffStyle lipgloss.Style + modalButtonStyle lipgloss.Style + jsonKeyStyle lipgloss.Style + jsonStringStyle lipgloss.Style + jsonNumberStyle lipgloss.Style + jsonBoolStyle lipgloss.Style + jsonNullStyle lipgloss.Style + detailScrollbarTrackStyle lipgloss.Style + detailScrollbarThumbStyle lipgloss.Style + errorModalStyle lipgloss.Style + errorTitleStyle lipgloss.Style + tableTitleStyle lipgloss.Style + errorScrollbarTrackStyle lipgloss.Style + errorScrollbarThumbStyle lipgloss.Style +) + +func initStyles(skin config.Skin) { resourceTagStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("255")). - Background(lipgloss.Color("#ED1944")). - Bold(true) + Foreground(lipgloss.Color(skin.ResourceTag.Foreground)). + Background(lipgloss.Color(skin.ResourceTag.Background)). + Bold(true) navigationTagStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("0")). - Background(lipgloss.Color("255")). - Bold(true) + Foreground(lipgloss.Color(skin.NavigationTag.Foreground)). + Background(lipgloss.Color(skin.NavigationTag.Background)) commandStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#87CEFA")) + Foreground(lipgloss.Color(skin.Command)) commandGhostStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#5C6370")). - Faint(true) + Foreground(lipgloss.Color(skin.CommandGhost)). + Faint(true) statusStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#000000")). - Background(lipgloss.Color("#FFA500")). - Align(lipgloss.Center) + Foreground(lipgloss.Color(skin.Status.Foreground)). + Background(lipgloss.Color(skin.Status.Background)). + Align(lipgloss.Center) headerLabelStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFA500")). - Bold(true) + Foreground(lipgloss.Color(skin.HeaderLabel)). + Bold(true) headerValueStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFFFFF")) + Foreground(lipgloss.Color(skin.HeaderValue)) headerCommandKeyStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#00BFFF")). - Bold(true) + Foreground(lipgloss.Color(skin.HeaderCommandKey)). + Bold(true) headerCommandTextStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFFFFF")) + Foreground(lipgloss.Color(skin.HeaderCommandText)) logoStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFA500")). - Bold(true) + Foreground(lipgloss.Color(skin.Logo)). + Bold(true) loadingStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#000000")). - Background(lipgloss.Color("#ff8c00")). - Bold(true) + Foreground(lipgloss.Color(skin.Loading.Foreground)). + Background(lipgloss.Color(skin.Loading.Background)). + Bold(true) infoStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#000000")). - Background(lipgloss.Color("#00BFFF")). - Bold(true) + Foreground(lipgloss.Color(skin.Info.Foreground)). + Background(lipgloss.Color(skin.Info.Background)). + Bold(true) autoRefreshStateOnStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#000000")). - Background(lipgloss.Color("#008000")). - Bold(true) + Foreground(lipgloss.Color(skin.StateOn.Foreground)). + Background(lipgloss.Color(skin.StateOn.Background)). + Bold(true) autoRefreshStateOffStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#000000")). - Background(lipgloss.Color("#FF0000")). - Bold(true) + Foreground(lipgloss.Color(skin.StateOff.Foreground)). + Background(lipgloss.Color(skin.StateOff.Background)). + Bold(true) modalButtonStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#000000")). - Background(lipgloss.Color("#00BFFF")). - Bold(true) -) + Foreground(lipgloss.Color(skin.ModalButton.Foreground)). + Background(lipgloss.Color(skin.ModalButton.Background)). + Bold(true) -//DetailsView + //DetailsView -var ( jsonKeyStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#87CEFA")) + Foreground(lipgloss.Color(skin.JSONKey)) jsonStringStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#98C379")) + Foreground(lipgloss.Color(skin.JSONString)) jsonNumberStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#E5C07B")) + Foreground(lipgloss.Color(skin.JSONNumber)) jsonBoolStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#C678DD")) + Foreground(lipgloss.Color(skin.JSONBool)) jsonNullStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#7F848E")) + Foreground(lipgloss.Color(skin.JSONNull)) detailScrollbarTrackStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#00BFFF")) + Foreground(lipgloss.Color(skin.DetailBorder)) detailScrollbarThumbStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#00BFFF")). - Bold(true) -) + Foreground(lipgloss.Color(skin.DetailBorder)). + Bold(true) -//ErrorView + //ErrorView -var ( errorModalStyle = lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#ED1944")). - Foreground(lipgloss.Color("#FFFFFF")). - Padding(1, 1). - Width(60) + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color(skin.ErrorBorder)). + Foreground(lipgloss.Color(skin.ErrorModal.Foreground)). + Padding(1, 1). + Width(60) errorTitleStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#ED1944")). - Bold(true) -) + Foreground(lipgloss.Color(skin.ErrorTitle)). + Bold(true) -//TableView + //TableView -var ( tableTitleStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFFFFF")).Bold(true) -) + Foreground(lipgloss.Color(skin.TableTitle)).Bold(true) + + errorScrollbarTrackStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(skin.ErrorBorder)) + + errorScrollbarThumbStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(skin.ErrorBorder)) -func tableStyles() table.Styles { + if skin.ErrorModal.Background != "" { + errorModalStyle = errorModalStyle.Background(lipgloss.Color(skin.ErrorModal.Background)) + } +} + +func tableStyles(skin config.Skin) table.Styles { styles := table.DefaultStyles() styles.Selected = styles.Selected. - Foreground(lipgloss.Color("255")). - Background(lipgloss.Color("#ED1944")). + Foreground(lipgloss.Color(skin.TableSelected.Foreground)). + Background(lipgloss.Color(skin.TableSelected.Background)). Bold(true) return styles } - -var ( - errorScrollbarTrackStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#ED1944")) - - errorScrollbarThumbStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#ED1944")) -) From a28b04556b3956c8e599f329b871adf33d3639a4 Mon Sep 17 00:00:00 2001 From: Kyriakos Akriotis Date: Tue, 20 Oct 2026 07:50:35 +0200 Subject: [PATCH 2/6] added background color for the whole terminal view #162 --- internal/config/skin.go | 16 ++--- internal/config/skins/default.yaml | 20 +++--- internal/config/skins/norton-commander.yaml | 38 ++++++------ internal/ui/app.go | 30 ++++++--- internal/ui/render.go | 68 ++++++++++++++------- internal/ui/styles.go | 62 +++++++++++-------- 6 files changed, 144 insertions(+), 90 deletions(-) diff --git a/internal/config/skin.go b/internal/config/skin.go index 07dbb83..0cc007d 100644 --- a/internal/config/skin.go +++ b/internal/config/skin.go @@ -6,6 +6,11 @@ type ColorPair struct { } 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"` @@ -16,7 +21,6 @@ type Skin struct { ModalButton ColorPair `yaml:"modal_button"` TableSelected ColorPair `yaml:"table_selected"` - Command string `yaml:"command"` CommandGhost string `yaml:"command_ghost"` CommandPlaceholder string `yaml:"command_placeholder"` @@ -32,11 +36,7 @@ type Skin struct { JSONBool string `yaml:"json_bool"` JSONNull string `yaml:"json_null"` - TableTitle string `yaml:"table_title"` - TableBorder string `yaml:"table_border"` - DetailBorder string `yaml:"detail_border"` - - ErrorModal ColorPair `yaml:"error_modal"` - ErrorBorder string `yaml:"error_border"` - ErrorTitle string `yaml:"error_title"` + TableTitle string `yaml:"table_title"` + TableHeader string `yaml:"table_header"` + ErrorTitle string `yaml:"error_title"` } diff --git a/internal/config/skins/default.yaml b/internal/config/skins/default.yaml index 2e43c93..6a2cb1c 100644 --- a/internal/config/skins/default.yaml +++ b/internal/config/skins/default.yaml @@ -1,3 +1,15 @@ +main: + foreground: "#FFFFFF" + background: "" +table: + foreground: "#FFFFFF" + background: "" +context: + foreground: "#FFFFFF" + background: "" +command: + foreground: "#87CEFA" + background: "" resource_tag: foreground: "255" background: "#ED1944" @@ -25,7 +37,7 @@ modal_button: table_selected: foreground: "255" background: "#ED1944" -command: "#87CEFA" +table_header: "#FFFFFF" command_ghost: "#5C6370" command_placeholder: "#666666" header_label: "#FFA500" @@ -39,10 +51,4 @@ json_number: "#E5C07B" json_bool: "#C678DD" json_null: "#7F848E" table_title: "#FFFFFF" -table_border: "#00BFFF" -detail_border: "#00BFFF" -error_modal: - foreground: "#FFFFFF" - background: "" -error_border: "#ED1944" error_title: "#ED1944" diff --git a/internal/config/skins/norton-commander.yaml b/internal/config/skins/norton-commander.yaml index 89925e4..c46314d 100644 --- a/internal/config/skins/norton-commander.yaml +++ b/internal/config/skins/norton-commander.yaml @@ -1,14 +1,29 @@ -# Norton Commander 5.5-inspired palette based on the supplied screenshot. -# For full DOS-blue panels, set your terminal background to #0000AA. -resource_tag: - foreground: "#FFFF55" +main: + foreground: "#55FFFF" background: "#0000AA" -navigation_tag: +table: + foreground: "#55FFFF" + background: "#0000AA" +table_title: "#FFFF55" +table_header: "#FFFF55" +table_selected: + foreground: "#000000" + background: "#FFFF55" +context: + foreground: "#55FFFF" + background: "#0000AA" +command: + foreground: "#55FFFF" + background: "#0000AA" +resource_tag: foreground: "#000000" + background: "#55FF55" +navigation_tag: + foreground: "#FFFFFF" background: "#00AAAA" status: foreground: "#000000" - background: "#00AAAA" + background: "#FFA500" loading: foreground: "#000000" background: "#FFFF55" @@ -24,10 +39,6 @@ state_off: modal_button: foreground: "#000000" background: "#00AAAA" -table_selected: - foreground: "#000000" - background: "#00AAAA" -command: "#55FFFF" command_ghost: "#00AAAA" command_placeholder: "#00AAAA" header_label: "#FFFF55" @@ -40,11 +51,4 @@ json_string: "#55FFFF" json_number: "#FFFF55" json_bool: "#FFFF55" json_null: "#00AAAA" -table_title: "#FFFF55" -table_border: "#55FFFF" -detail_border: "#55FFFF" -error_modal: - foreground: "#FFFFFF" - background: "#AA0000" -error_border: "#FF5555" error_title: "#FFFF55" diff --git a/internal/ui/app.go b/internal/ui/app.go index 30d438d..c01dc20 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -117,10 +117,10 @@ func New(registry *resource.Registry, openstackContext *openstack.Context, plugi command.CharLimit = 64 command.ShowSuggestions = true - command.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.SkinColors.Command)) - command.TextStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.SkinColors.Command)) - command.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.SkinColors.Command)) - command.PlaceholderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.SkinColors.CommandPlaceholder)) + command.PromptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.SkinColors.Command.Foreground)).Background(lipgloss.Color(cfg.SkinColors.Command.Background)) + command.TextStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.SkinColors.Command.Foreground)).Background(lipgloss.Color(cfg.SkinColors.Command.Background)) + command.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.SkinColors.Command.Foreground)).Background(lipgloss.Color(cfg.SkinColors.Command.Background)) + command.PlaceholderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(cfg.SkinColors.CommandPlaceholder)).Background(lipgloss.Color(cfg.SkinColors.Command.Background)) detailViewport := viewport.New(1, 1) detailViewport.SetHorizontalStep(4) @@ -714,19 +714,19 @@ func (m Model) View() string { if m.width > compactWidth { for _, entry := range m.navigation { resourceTags.WriteString( - navigationTagStyle.Render(" <"+entry.resource+"> ") + " ", + navigationTagStyle.Render(" <"+entry.resource+"> ") + mainStyle.Render(" "), ) } } activeResourceStyle := resourceTagStyle if color := plugins.ResourceColor(m.resource); color != "" { - activeResourceStyle = activeResourceStyle.Background(lipgloss.Color(color)) + activeResourceStyle = activeResourceStyle.Background(lipgloss.Color(color)).Foreground(lipgloss.Color("#FFFFFF")) } if m.detailMode { resourceTags.WriteString( - navigationTagStyle.Render(" <"+m.resource.Kind()+"> ") + " ", + navigationTagStyle.Render(" <"+m.resource.Kind()+"> ") + mainStyle.Render(" "), ) resourceTags.WriteString( activeResourceStyle.Render(" <" + strings.ToLower(m.detailLabel) + "> "), @@ -776,6 +776,7 @@ func (m Model) View() string { lipgloss.Top, lipgloss.NewStyle(). Width(max(m.width-lipgloss.Width(rightTag), 1)). + Background(lipgloss.Color(m.skin.Main.Background)). Render(resourceTags.String()), rightTag, ) @@ -793,6 +794,11 @@ func (m Model) View() string { commandLine = m.command.View() } + commandLine = lipgloss.NewStyle(). + Width(m.width). + Background(lipgloss.Color(m.skin.Main.Background)). + Render(commandLine) + view := header + "\n\n" + contentView + @@ -801,10 +807,14 @@ func (m Model) View() string { "\n" + commandLine + view = lipgloss.NewStyle(). + Width(m.width). + Height(m.height). + Background(lipgloss.Color(m.skin.Main.Background)). + Render(view) + if m.err != nil { - if m.err != nil { - return overlayCenter(view, m.renderErrorModal()) - } + return overlayCenter(view, m.renderErrorModal()) } return view diff --git a/internal/ui/render.go b/internal/ui/render.go index fdcfc75..ec393bd 100644 --- a/internal/ui/render.go +++ b/internal/ui/render.go @@ -51,18 +51,19 @@ func (m Model) renderHeader() string { left := lipgloss.NewStyle(). Width(leftWidth). + Background(lipgloss.Color(m.skin.Main.Background)). Render(context) center := m.renderHeaderCommands(centerWidth) - // Fill all available space before the logo, so the logo - // remains at the terminal's right edge. center = lipgloss.NewStyle(). Width(centerWidth). + Background(lipgloss.Color(m.skin.Main.Background)). Render(center) right := lipgloss.NewStyle(). Width(rightWidth). Align(lipgloss.Right). + Background(lipgloss.Color(m.skin.Main.Background)). Render(renderedLogo) if !showContext { @@ -201,11 +202,16 @@ func (m Model) renderHeaderCommands(availableWidth int) string { } text = ansi.Truncate(text, max(width-keyWidth-1, 0), "") - lines = append(lines, key+text+" ") + line := key + text + lineWidth := lipgloss.Width(line) + if lineWidth < width { + line += mainStyle.Render(strings.Repeat(" ", width-lineWidth)) + } + lines = append(lines, line) } for len(lines) < commandsPerColumn { - lines = append(lines, "") + lines = append(lines, mainStyle.Render(strings.Repeat(" ", width))) } columns = append(columns, strings.Join(lines, "\n")) @@ -238,7 +244,8 @@ func (m Model) renderTable() string { right := remaining - left borderStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color(m.skin.TableBorder)) + Foreground(lipgloss.Color(m.skin.Main.Foreground)). + Background(lipgloss.Color(m.skin.Main.Background)) top := borderStyle.Render( "┌" + @@ -280,23 +287,38 @@ func (m Model) renderTable() string { emptyMessage, ) + emptyLines := strings.Split(emptyContent, "\n") + for i, line := range emptyLines { + emptyLines[i] = tableBodyStyle.Width(innerWidth).Align(lipgloss.Center).Render(line) + } + emptyContent = strings.Join(emptyLines, "\n") + body = header + "\n" + emptyContent } } - bodyLines := strings.Split(body, "\n") + selectedStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color(m.skin.TableSelected.Foreground)). + Background(lipgloss.Color(m.skin.TableSelected.Background)) + paddingStyle := lipgloss.NewStyle(). + Background(lipgloss.Color(m.skin.Table.Background)) + + bodyLines := strings.Split(body, "\n") for i, line := range bodyLines { - lineWidth := lipgloss.Width(line) + selected := i > 0 && i-1 == m.table.Cursor() + if selected { + line = selectedStyle.Render(ansi.Strip(line)) + } else if i > 0 && strings.TrimSpace(ansi.Strip(line)) == "" { + line = paddingStyle.Render(strings.Repeat(" ", innerWidth)) + } + lineWidth := lipgloss.Width(line) if lineWidth < innerWidth { - line += strings.Repeat(" ", innerWidth-lineWidth) + line += paddingStyle.Render(strings.Repeat(" ", innerWidth-lineWidth)) } - bodyLines[i] = - borderStyle.Render("│") + - line + - borderStyle.Render("│") + bodyLines[i] = borderStyle.Render("│") + line + borderStyle.Render("│") } return top + @@ -326,7 +348,8 @@ func (m Model) renderDetails() string { right := remaining - left borderStyle := lipgloss.NewStyle(). - Foreground(lipgloss.Color(m.skin.DetailBorder)) + Foreground(lipgloss.Color(m.skin.Main.Foreground)). + Background(lipgloss.Color(m.skin.Main.Background)) top := borderStyle.Render( "┌" + @@ -367,7 +390,7 @@ func (m Model) renderDetails() string { bodyLines[i] = borderStyle.Render("│") + - line + + detailBodyStyle.Render(line) + rightBorder } @@ -442,17 +465,16 @@ func overlayCenter(background, foreground string) string { } func (m Model) renderErrorModal() string { - hint := lipgloss.NewStyle(). - Faint(true). - Render("Press ") + + hintStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color(m.skin.Main.Foreground)). + Background(lipgloss.Color(m.skin.Main.Background)). + Faint(true) + + hint := hintStyle.Render("Press ") + modalButtonStyle.Render(" Esc ") + - lipgloss.NewStyle(). - Faint(true). - Render(" or ") + + hintStyle.Render(" or ") + modalButtonStyle.Render(" Enter ") + - lipgloss.NewStyle(). - Faint(true). - Render(" to dismiss") + hintStyle.Render(" to dismiss") content := errorTitleStyle.Render("Error") + "\n\n" + diff --git a/internal/ui/styles.go b/internal/ui/styles.go index c645262..8c8c8e2 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -9,10 +9,11 @@ import ( //Header and AppView var ( + mainStyle lipgloss.Style + tableBodyStyle lipgloss.Style + detailBodyStyle lipgloss.Style resourceTagStyle lipgloss.Style navigationTagStyle lipgloss.Style - commandStyle lipgloss.Style - commandGhostStyle lipgloss.Style statusStyle lipgloss.Style headerLabelStyle lipgloss.Style headerValueStyle lipgloss.Style @@ -39,6 +40,13 @@ var ( ) func initStyles(skin config.Skin) { + mainStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(skin.Main.Foreground)). + Background(lipgloss.Color(skin.Main.Background)) + + tableBodyStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(skin.Table.Foreground)).Background(lipgloss.Color(skin.Table.Background)) + detailBodyStyle = mainStyle + resourceTagStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(skin.ResourceTag.Foreground)). Background(lipgloss.Color(skin.ResourceTag.Background)). @@ -48,13 +56,6 @@ func initStyles(skin config.Skin) { Foreground(lipgloss.Color(skin.NavigationTag.Foreground)). Background(lipgloss.Color(skin.NavigationTag.Background)) - commandStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.Command)) - - commandGhostStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.CommandGhost)). - Faint(true) - statusStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(skin.Status.Foreground)). Background(lipgloss.Color(skin.Status.Background)). @@ -62,20 +63,25 @@ func initStyles(skin config.Skin) { headerLabelStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(skin.HeaderLabel)). + Background(lipgloss.Color(skin.Context.Background)). Bold(true) headerValueStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.HeaderValue)) + Foreground(lipgloss.Color(skin.HeaderValue)). + Background(lipgloss.Color(skin.Context.Background)) headerCommandKeyStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(skin.HeaderCommandKey)). + Background(lipgloss.Color(skin.Main.Background)). Bold(true) headerCommandTextStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.HeaderCommandText)) + Foreground(lipgloss.Color(skin.HeaderCommandText)). + Background(lipgloss.Color(skin.Main.Background)) logoStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(skin.Logo)). + Background(lipgloss.Color(skin.Main.Background)). Bold(true) loadingStyle = lipgloss.NewStyle(). @@ -121,18 +127,20 @@ func initStyles(skin config.Skin) { Foreground(lipgloss.Color(skin.JSONNull)) detailScrollbarTrackStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.DetailBorder)) + Foreground(lipgloss.Color(skin.Main.Foreground)) detailScrollbarThumbStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.DetailBorder)). + Foreground(lipgloss.Color(skin.Main.Foreground)). Bold(true) //ErrorView errorModalStyle = lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color(skin.ErrorBorder)). - Foreground(lipgloss.Color(skin.ErrorModal.Foreground)). + BorderForeground(lipgloss.Color(skin.Main.Foreground)). + BorderBackground(lipgloss.Color(skin.Main.Background)). + Foreground(lipgloss.Color(skin.Main.Foreground)). + Background(lipgloss.Color(skin.Main.Background)). Padding(1, 1). Width(60) @@ -140,25 +148,29 @@ func initStyles(skin config.Skin) { Foreground(lipgloss.Color(skin.ErrorTitle)). Bold(true) - //TableView - - tableTitleStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.TableTitle)).Bold(true) - errorScrollbarTrackStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.ErrorBorder)) + Foreground(lipgloss.Color(skin.Main.Foreground)) errorScrollbarThumbStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.ErrorBorder)) + Foreground(lipgloss.Color(skin.Main.Foreground)) + + //TableView - if skin.ErrorModal.Background != "" { - errorModalStyle = errorModalStyle.Background(lipgloss.Color(skin.ErrorModal.Background)) - } + tableTitleStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color(skin.TableTitle)).Bold(true) } func tableStyles(skin config.Skin) table.Styles { styles := table.DefaultStyles() + styles.Cell = styles.Cell. + Foreground(lipgloss.Color(skin.Table.Foreground)). + Background(lipgloss.Color(skin.Table.Background)) + + styles.Header = styles.Header. + Foreground(lipgloss.Color(skin.TableHeader)). + Background(lipgloss.Color(skin.Table.Background)) + styles.Selected = styles.Selected. Foreground(lipgloss.Color(skin.TableSelected.Foreground)). Background(lipgloss.Color(skin.TableSelected.Background)). From ca2b1bd04c0d4b3f35a38dd548f82780eb533066 Mon Sep 17 00:00:00 2001 From: Kyriakos Akriotis Date: Tue, 20 Oct 2026 08:03:39 +0200 Subject: [PATCH 3/6] fixed details view background color in json content #162 --- internal/ui/helpers.go | 16 ++++++++++++++++ internal/ui/render.go | 2 +- internal/ui/styles.go | 19 +++++++++++++------ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/internal/ui/helpers.go b/internal/ui/helpers.go index d9622ec..59bd884 100644 --- a/internal/ui/helpers.go +++ b/internal/ui/helpers.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "regexp" "strings" "github.com/charmbracelet/lipgloss" @@ -151,3 +152,18 @@ func (m Model) buildCacheKey(kind string) string { hash := sha256.Sum256([]byte(identity)) return fmt.Sprintf("%s:%s", hex.EncodeToString(hash[:8]), kind) } + +var ansiSGR = regexp.MustCompile("\x1b\\[[0-9;]*m") + +func preserveBackground(content string, background lipgloss.Color) string { + style := lipgloss.NewStyle().Background(background) + prefix := ansiSGR.FindString(style.Render("x")) + + if prefix == "" { + return content + } + + return prefix + ansiSGR.ReplaceAllStringFunc(content, func(sequence string) string { + return sequence + prefix + }) +} diff --git a/internal/ui/render.go b/internal/ui/render.go index ec393bd..f716224 100644 --- a/internal/ui/render.go +++ b/internal/ui/render.go @@ -390,7 +390,7 @@ func (m Model) renderDetails() string { bodyLines[i] = borderStyle.Render("│") + - detailBodyStyle.Render(line) + + preserveBackground(detailBodyStyle.Render(line), lipgloss.Color(m.skin.Main.Background)) + rightBorder } diff --git a/internal/ui/styles.go b/internal/ui/styles.go index 8c8c8e2..3c5d5c1 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -112,25 +112,32 @@ func initStyles(skin config.Skin) { //DetailsView jsonKeyStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.JSONKey)) + Foreground(lipgloss.Color(skin.JSONKey)). + Background(lipgloss.Color(skin.Main.Background)) jsonStringStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.JSONString)) + Foreground(lipgloss.Color(skin.JSONString)). + Background(lipgloss.Color(skin.Main.Background)) jsonNumberStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.JSONNumber)) + Foreground(lipgloss.Color(skin.JSONNumber)). + Background(lipgloss.Color(skin.Main.Background)) jsonBoolStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.JSONBool)) + Foreground(lipgloss.Color(skin.JSONBool)). + Background(lipgloss.Color(skin.Main.Background)) jsonNullStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.JSONNull)) + Foreground(lipgloss.Color(skin.JSONNull)). + Background(lipgloss.Color(skin.Main.Background)) detailScrollbarTrackStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color(skin.Main.Foreground)) + Foreground(lipgloss.Color(skin.Main.Foreground)). + Background(lipgloss.Color(skin.Main.Background)) detailScrollbarThumbStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color(skin.Main.Foreground)). + Background(lipgloss.Color(skin.Main.Background)). Bold(true) //ErrorView From 3aad505f84b11dafca922c8f2533aeb911c00d9e Mon Sep 17 00:00:00 2001 From: Kyriakos Akriotis Date: Tue, 20 Oct 2026 08:19:59 +0200 Subject: [PATCH 4/6] added more skins #162 --- internal/config/skins/colorblind.yaml | 48 -------------------- internal/config/skins/default.yaml | 8 ++-- internal/config/skins/deuteranopia.yaml | 54 +++++++++++++++++++++++ internal/config/skins/dracula.yaml | 54 +++++++++++++++++++++++ internal/config/skins/greece.yaml | 54 +++++++++++++++++++++++ internal/config/skins/gruvbox-dark.yaml | 54 +++++++++++++++++++++++ internal/config/skins/high-contrast.yaml | 22 +++++---- internal/config/skins/mocha.yaml | 54 +++++++++++++++++++++++ internal/config/skins/nord.yaml | 54 +++++++++++++++++++++++ internal/config/skins/protanopia.yaml | 54 +++++++++++++++++++++++ internal/config/skins/solarized-dark.yaml | 54 +++++++++++++++++++++++ internal/config/skins/tokyo-night.yaml | 54 +++++++++++++++++++++++ internal/config/skins/tritanopia.yaml | 54 +++++++++++++++++++++++ 13 files changed, 558 insertions(+), 60 deletions(-) delete mode 100644 internal/config/skins/colorblind.yaml create mode 100644 internal/config/skins/deuteranopia.yaml create mode 100644 internal/config/skins/dracula.yaml create mode 100644 internal/config/skins/greece.yaml create mode 100644 internal/config/skins/gruvbox-dark.yaml create mode 100644 internal/config/skins/mocha.yaml create mode 100644 internal/config/skins/nord.yaml create mode 100644 internal/config/skins/protanopia.yaml create mode 100644 internal/config/skins/solarized-dark.yaml create mode 100644 internal/config/skins/tokyo-night.yaml create mode 100644 internal/config/skins/tritanopia.yaml diff --git a/internal/config/skins/colorblind.yaml b/internal/config/skins/colorblind.yaml deleted file mode 100644 index e3796f8..0000000 --- a/internal/config/skins/colorblind.yaml +++ /dev/null @@ -1,48 +0,0 @@ -resource_tag: - foreground: "#000000" - background: "#E69F00" -navigation_tag: - foreground: "#000000" - background: "#FFFFFF" -status: - foreground: "#000000" - background: "#F0E442" -loading: - foreground: "#000000" - background: "#E69F00" -info: - foreground: "#000000" - background: "#56B4E9" -state_on: - foreground: "#000000" - background: "#009E73" -state_off: - foreground: "#FFFFFF" - background: "#D55E00" -modal_button: - foreground: "#000000" - background: "#56B4E9" -table_selected: - foreground: "#000000" - background: "#F0E442" -command: "#56B4E9" -command_ghost: "#999999" -command_placeholder: "#999999" -header_label: "#E69F00" -header_value: "#FFFFFF" -header_command_key: "#56B4E9" -header_command_text: "#FFFFFF" -logo: "#E69F00" -json_key: "#56B4E9" -json_string: "#009E73" -json_number: "#F0E442" -json_bool: "#CC79A7" -json_null: "#999999" -table_title: "#FFFFFF" -table_border: "#0072B2" -detail_border: "#0072B2" -error_modal: - foreground: "#FFFFFF" - background: "" -error_border: "#D55E00" -error_title: "#D55E00" diff --git a/internal/config/skins/default.yaml b/internal/config/skins/default.yaml index 6a2cb1c..7df0ec7 100644 --- a/internal/config/skins/default.yaml +++ b/internal/config/skins/default.yaml @@ -4,6 +4,10 @@ main: table: foreground: "#FFFFFF" background: "" +table_selected: + foreground: "255" + background: "#ED1944" +table_header: "#FFFFFF" context: foreground: "#FFFFFF" background: "" @@ -34,10 +38,6 @@ state_off: modal_button: foreground: "#000000" background: "#00BFFF" -table_selected: - foreground: "255" - background: "#ED1944" -table_header: "#FFFFFF" command_ghost: "#5C6370" command_placeholder: "#666666" header_label: "#FFA500" diff --git a/internal/config/skins/deuteranopia.yaml b/internal/config/skins/deuteranopia.yaml new file mode 100644 index 0000000..2663b15 --- /dev/null +++ b/internal/config/skins/deuteranopia.yaml @@ -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" \ No newline at end of file diff --git a/internal/config/skins/dracula.yaml b/internal/config/skins/dracula.yaml new file mode 100644 index 0000000..b54f78a --- /dev/null +++ b/internal/config/skins/dracula.yaml @@ -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" \ No newline at end of file diff --git a/internal/config/skins/greece.yaml b/internal/config/skins/greece.yaml new file mode 100644 index 0000000..5c18ba4 --- /dev/null +++ b/internal/config/skins/greece.yaml @@ -0,0 +1,54 @@ +main: + foreground: "#FFFFFF" + background: "#0D5EAF" +table: + foreground: "#FFFFFF" + background: "#0D5EAF" +context: + foreground: "#FFFFFF" + background: "#0D5EAF" +command: + foreground: "#66CCFF" + background: "#0D5EAF" +resource_tag: + foreground: "#0D5EAF" + background: "#FFFFFF" +navigation_tag: + foreground: "#FFFFFF" + background: "#1B365D" +status: + foreground: "#0D5EAF" + background: "#FFD166" +loading: + foreground: "#0D5EAF" + background: "#FFB347" +info: + foreground: "#0D5EAF" + background: "#66CCFF" +state_on: + foreground: "#FFFFFF" + background: "#176B45" +state_off: + foreground: "#FFFFFF" + background: "#B42332" +modal_button: + foreground: "#0D5EAF" + background: "#FFFFFF" +table_selected: + foreground: "#0D5EAF" + background: "#FFFFFF" +table_header: "#FFFFFF" +command_ghost: "#A9C9E8" +command_placeholder: "#B8D8F0" +header_label: "#FFD166" +header_value: "#FFFFFF" +header_command_key: "#66CCFF" +header_command_text: "#FFFFFF" +logo: "#FFFFFF" +json_key: "#A9E6FF" +json_string: "#FFE29A" +json_number: "#FFFFFF" +json_bool: "#FFCC80" +json_null: "#B8D8F0" +table_title: "#FFFFFF" +error_title: "#FFD166" \ No newline at end of file diff --git a/internal/config/skins/gruvbox-dark.yaml b/internal/config/skins/gruvbox-dark.yaml new file mode 100644 index 0000000..e273269 --- /dev/null +++ b/internal/config/skins/gruvbox-dark.yaml @@ -0,0 +1,54 @@ +main: + foreground: "#EBDBB2" + background: "#282828" +table: + foreground: "#EBDBB2" + background: "#282828" +context: + foreground: "#EBDBB2" + background: "#282828" +command: + foreground: "#83A598" + background: "#282828" +resource_tag: + foreground: "#282828" + background: "#FB4934" +navigation_tag: + foreground: "#282828" + background: "#FABD2F" +status: + foreground: "#282828" + background: "#FABD2F" +loading: + foreground: "#282828" + background: "#FE8019" +info: + foreground: "#282828" + background: "#83A598" +state_on: + foreground: "#282828" + background: "#B8BB26" +state_off: + foreground: "#282828" + background: "#FB4934" +modal_button: + foreground: "#282828" + background: "#FABD2F" +table_selected: + foreground: "#282828" + background: "#FABD2F" +table_header: "#EBDBB2" +command_ghost: "#928374" +command_placeholder: "#928374" +header_label: "#FABD2F" +header_value: "#EBDBB2" +header_command_key: "#83A598" +header_command_text: "#EBDBB2" +logo: "#FABD2F" +json_key: "#83A598" +json_string: "#B8BB26" +json_number: "#FE8019" +json_bool: "#D3869B" +json_null: "#928374" +table_title: "#EBDBB2" +error_title: "#FB4934" \ No newline at end of file diff --git a/internal/config/skins/high-contrast.yaml b/internal/config/skins/high-contrast.yaml index 5d369cc..b00b2c4 100644 --- a/internal/config/skins/high-contrast.yaml +++ b/internal/config/skins/high-contrast.yaml @@ -1,3 +1,15 @@ +main: + foreground: "#FFFFFF" + background: "#000000" +table: + foreground: "#FFFFFF" + background: "#000000" +context: + foreground: "#FFFFFF" + background: "#000000" +command: + foreground: "#00FFFF" + background: "#000000" resource_tag: foreground: "#FFFFFF" background: "#9B0000" @@ -25,7 +37,7 @@ modal_button: table_selected: foreground: "#000000" background: "#FFFF00" -command: "#00FFFF" +table_header: "#FFFFFF" command_ghost: "#B3B3B3" command_placeholder: "#B3B3B3" header_label: "#FFFF00" @@ -39,10 +51,4 @@ json_number: "#FFFF00" json_bool: "#FF80FF" json_null: "#B3B3B3" table_title: "#FFFFFF" -table_border: "#FFFFFF" -detail_border: "#FFFFFF" -error_modal: - foreground: "#FFFFFF" - background: "" -error_border: "#FF5555" -error_title: "#FF5555" +error_title: "#FF5555" \ No newline at end of file diff --git a/internal/config/skins/mocha.yaml b/internal/config/skins/mocha.yaml new file mode 100644 index 0000000..fe71f71 --- /dev/null +++ b/internal/config/skins/mocha.yaml @@ -0,0 +1,54 @@ +main: + foreground: "#CDD6F4" + background: "#1E1E2E" +table: + foreground: "#CDD6F4" + background: "#1E1E2E" +context: + foreground: "#CDD6F4" + background: "#1E1E2E" +command: + foreground: "#89B4FA" + background: "#1E1E2E" +resource_tag: + foreground: "#1E1E2E" + background: "#F38BA8" +navigation_tag: + foreground: "#1E1E2E" + background: "#CBA6F7" +status: + foreground: "#1E1E2E" + background: "#F9E2AF" +loading: + foreground: "#1E1E2E" + background: "#FAB387" +info: + foreground: "#1E1E2E" + background: "#89DCEB" +state_on: + foreground: "#1E1E2E" + background: "#A6E3A1" +state_off: + foreground: "#1E1E2E" + background: "#F38BA8" +modal_button: + foreground: "#1E1E2E" + background: "#CBA6F7" +table_selected: + foreground: "#1E1E2E" + background: "#CBA6F7" +table_header: "#CDD6F4" +command_ghost: "#6C7086" +command_placeholder: "#7F849C" +header_label: "#F9E2AF" +header_value: "#CDD6F4" +header_command_key: "#89B4FA" +header_command_text: "#CDD6F4" +logo: "#CBA6F7" +json_key: "#89B4FA" +json_string: "#A6E3A1" +json_number: "#FAB387" +json_bool: "#CBA6F7" +json_null: "#9399B2" +table_title: "#CDD6F4" +error_title: "#F38BA8" \ No newline at end of file diff --git a/internal/config/skins/nord.yaml b/internal/config/skins/nord.yaml new file mode 100644 index 0000000..8f4bf6d --- /dev/null +++ b/internal/config/skins/nord.yaml @@ -0,0 +1,54 @@ +main: + foreground: "#ECEFF4" + background: "#2E3440" +table: + foreground: "#ECEFF4" + background: "#2E3440" +context: + foreground: "#ECEFF4" + background: "#2E3440" +command: + foreground: "#88C0D0" + background: "#2E3440" +resource_tag: + foreground: "#2E3440" + background: "#BF616A" +navigation_tag: + foreground: "#2E3440" + background: "#88C0D0" +status: + foreground: "#2E3440" + background: "#EBCB8B" +loading: + foreground: "#2E3440" + background: "#D08770" +info: + foreground: "#2E3440" + background: "#8FBCBB" +state_on: + foreground: "#2E3440" + background: "#A3BE8C" +state_off: + foreground: "#ECEFF4" + background: "#BF616A" +modal_button: + foreground: "#2E3440" + background: "#88C0D0" +table_selected: + foreground: "#2E3440" + background: "#88C0D0" +table_header: "#ECEFF4" +command_ghost: "#7B88A1" +command_placeholder: "#7B88A1" +header_label: "#EBCB8B" +header_value: "#ECEFF4" +header_command_key: "#88C0D0" +header_command_text: "#ECEFF4" +logo: "#88C0D0" +json_key: "#81A1C1" +json_string: "#A3BE8C" +json_number: "#EBCB8B" +json_bool: "#B48EAD" +json_null: "#D8DEE9" +table_title: "#ECEFF4" +error_title: "#BF616A" \ No newline at end of file diff --git a/internal/config/skins/protanopia.yaml b/internal/config/skins/protanopia.yaml new file mode 100644 index 0000000..f6dcecb --- /dev/null +++ b/internal/config/skins/protanopia.yaml @@ -0,0 +1,54 @@ +main: + foreground: "#E6EDF3" + background: "#101820" +table: + foreground: "#E6EDF3" + background: "#101820" +context: + foreground: "#E6EDF3" + background: "#101820" +command: + foreground: "#70CFFF" + background: "#101820" +resource_tag: + foreground: "#101820" + background: "#FFCC66" +navigation_tag: + foreground: "#101820" + background: "#70CFFF" +status: + foreground: "#101820" + background: "#FFCC66" +loading: + foreground: "#101820" + background: "#F4B860" +info: + foreground: "#101820" + background: "#70CFFF" +state_on: + foreground: "#101820" + background: "#70CFFF" +state_off: + foreground: "#101820" + background: "#FFCC66" +modal_button: + foreground: "#101820" + background: "#70CFFF" +table_selected: + foreground: "#101820" + background: "#FFCC66" +table_header: "#FFFFFF" +command_ghost: "#9AA8B5" +command_placeholder: "#9AA8B5" +header_label: "#FFCC66" +header_value: "#FFFFFF" +header_command_key: "#70CFFF" +header_command_text: "#E6EDF3" +logo: "#FFCC66" +json_key: "#70CFFF" +json_string: "#FFD580" +json_number: "#FFFFFF" +json_bool: "#B5A1FF" +json_null: "#AAB5C0" +table_title: "#FFFFFF" +error_title: "#FFCC66" \ No newline at end of file diff --git a/internal/config/skins/solarized-dark.yaml b/internal/config/skins/solarized-dark.yaml new file mode 100644 index 0000000..2058c94 --- /dev/null +++ b/internal/config/skins/solarized-dark.yaml @@ -0,0 +1,54 @@ +main: + foreground: "#93A1A1" + background: "#002B36" +table: + foreground: "#93A1A1" + background: "#002B36" +context: + foreground: "#93A1A1" + background: "#002B36" +command: + foreground: "#268BD2" + background: "#002B36" +resource_tag: + foreground: "#FDF6E3" + background: "#DC322F" +navigation_tag: + foreground: "#002B36" + background: "#2AA198" +status: + foreground: "#002B36" + background: "#B58900" +loading: + foreground: "#002B36" + background: "#CB4B16" +info: + foreground: "#002B36" + background: "#2AA198" +state_on: + foreground: "#002B36" + background: "#859900" +state_off: + foreground: "#FDF6E3" + background: "#DC322F" +modal_button: + foreground: "#002B36" + background: "#2AA198" +table_selected: + foreground: "#002B36" + background: "#2AA198" +table_header: "#EEE8D5" +command_ghost: "#657B83" +command_placeholder: "#657B83" +header_label: "#B58900" +header_value: "#EEE8D5" +header_command_key: "#2AA198" +header_command_text: "#93A1A1" +logo: "#B58900" +json_key: "#268BD2" +json_string: "#859900" +json_number: "#D33682" +json_bool: "#CB4B16" +json_null: "#657B83" +table_title: "#EEE8D5" +error_title: "#DC322F" \ No newline at end of file diff --git a/internal/config/skins/tokyo-night.yaml b/internal/config/skins/tokyo-night.yaml new file mode 100644 index 0000000..eed8f6a --- /dev/null +++ b/internal/config/skins/tokyo-night.yaml @@ -0,0 +1,54 @@ +main: + foreground: "#C0CAF5" + background: "#1A1B26" +table: + foreground: "#C0CAF5" + background: "#1A1B26" +context: + foreground: "#C0CAF5" + background: "#1A1B26" +command: + foreground: "#7AA2F7" + background: "#1A1B26" +resource_tag: + foreground: "#1A1B26" + background: "#F7768E" +navigation_tag: + foreground: "#1A1B26" + background: "#BB9AF7" +status: + foreground: "#1A1B26" + background: "#E0AF68" +loading: + foreground: "#1A1B26" + background: "#FF9E64" +info: + foreground: "#1A1B26" + background: "#7DCFFF" +state_on: + foreground: "#1A1B26" + background: "#9ECE6A" +state_off: + foreground: "#1A1B26" + background: "#F7768E" +modal_button: + foreground: "#1A1B26" + background: "#7AA2F7" +table_selected: + foreground: "#1A1B26" + background: "#7AA2F7" +table_header: "#C0CAF5" +command_ghost: "#565F89" +command_placeholder: "#565F89" +header_label: "#E0AF68" +header_value: "#C0CAF5" +header_command_key: "#7DCFFF" +header_command_text: "#C0CAF5" +logo: "#7AA2F7" +json_key: "#7DCFFF" +json_string: "#9ECE6A" +json_number: "#FF9E64" +json_bool: "#BB9AF7" +json_null: "#565F89" +table_title: "#C0CAF5" +error_title: "#F7768E" \ No newline at end of file diff --git a/internal/config/skins/tritanopia.yaml b/internal/config/skins/tritanopia.yaml new file mode 100644 index 0000000..5cfe532 --- /dev/null +++ b/internal/config/skins/tritanopia.yaml @@ -0,0 +1,54 @@ +main: + foreground: "#F5F5F5" + background: "#19191D" +table: + foreground: "#F5F5F5" + background: "#19191D" +context: + foreground: "#F5F5F5" + background: "#19191D" +command: + foreground: "#FF9EB5" + background: "#19191D" +resource_tag: + foreground: "#19191D" + background: "#FF9EB5" +navigation_tag: + foreground: "#19191D" + background: "#A8E6CF" +status: + foreground: "#19191D" + background: "#FFB4A2" +loading: + foreground: "#19191D" + background: "#FF9EB5" +info: + foreground: "#19191D" + background: "#A8E6CF" +state_on: + foreground: "#19191D" + background: "#A8E6CF" +state_off: + foreground: "#19191D" + background: "#FF9EB5" +modal_button: + foreground: "#19191D" + background: "#A8E6CF" +table_selected: + foreground: "#19191D" + background: "#FFB4A2" +table_header: "#FFFFFF" +command_ghost: "#A8A8B0" +command_placeholder: "#A8A8B0" +header_label: "#FFB4A2" +header_value: "#FFFFFF" +header_command_key: "#FF9EB5" +header_command_text: "#F5F5F5" +logo: "#FFB4A2" +json_key: "#FF9EB5" +json_string: "#A8E6CF" +json_number: "#FFFFFF" +json_bool: "#FFB4A2" +json_null: "#A8A8B0" +table_title: "#FFFFFF" +error_title: "#FF9EB5" \ No newline at end of file From e05ffc21519f53dc2d14fc8d189103837f9dd4b1 Mon Sep 17 00:00:00 2001 From: Kyriakos Akriotis Date: Tue, 20 Oct 2026 08:24:19 +0200 Subject: [PATCH 5/6] fixed commandline background in compact mode#162 --- internal/ui/app.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/ui/app.go b/internal/ui/app.go index c01dc20..b70ca36 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -799,6 +799,10 @@ func (m Model) View() string { Background(lipgloss.Color(m.skin.Main.Background)). Render(commandLine) + if commandLine == "" { + commandLine = mainStyle.Render(strings.Repeat(" ", m.width)) + } + view := header + "\n\n" + contentView + From 42c99967c7f07bccd1497abedd60c49ea3973fc0 Mon Sep 17 00:00:00 2001 From: Kyriakos Akriotis Date: Tue, 20 Oct 2026 08:28:09 +0200 Subject: [PATCH 6/6] amended readme #162 --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 66f4984..2234be5 100644 --- a/README.md +++ b/README.md @@ -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