feat(config): add theme customization (#81)
Added theme customization via config.toml #17 ``` [theme] border = "#665C54" accent = "#FABD2F" muted = "#928374" text = "#EBDBB2" text_sel = "#FBF1C7" match = "#FE8019" desc = "#A89984" desc_sel = "#EBDBB2" sel_bg = "#504945" scroll_info = "#83A598" ghost_text = "#7C6F64" hist= "#fe8019" hist_sel = "#d65d0e" sys = "#fb4934" sys_sel = "#cc241d" alias = "#b8bb26" alias_sel = "#98971a" ``` --------- Co-authored-by: verse91 <versedev.store@proton.me> Co-authored-by: VERSE <sunnyone0901@gmail.com>
This commit is contained in:
+50
-70
@@ -2,7 +2,6 @@ package integration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -129,38 +128,6 @@ func ComputeCursorCol(data []byte) int {
|
|||||||
return col
|
return col
|
||||||
}
|
}
|
||||||
|
|
||||||
type Theme struct {
|
|
||||||
Border color.Color
|
|
||||||
Accent color.Color
|
|
||||||
Muted color.Color
|
|
||||||
Text color.Color
|
|
||||||
TextSel color.Color
|
|
||||||
Match color.Color
|
|
||||||
Desc color.Color
|
|
||||||
DescSel color.Color
|
|
||||||
SelBg color.Color
|
|
||||||
ScrollInfo color.Color
|
|
||||||
GhostText color.Color
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentTheme = Theme{
|
|
||||||
Border: lipgloss.Color("#a277ff"),
|
|
||||||
Accent: lipgloss.Color("#61ffca"),
|
|
||||||
Muted: lipgloss.Color("#6d6a7f"),
|
|
||||||
Text: lipgloss.Color("#edecee"),
|
|
||||||
TextSel: lipgloss.Color("#ffffff"),
|
|
||||||
Match: lipgloss.Color("#61ffca"),
|
|
||||||
Desc: lipgloss.Color("#9692a8"),
|
|
||||||
DescSel: lipgloss.Color("#edecee"),
|
|
||||||
SelBg: lipgloss.Color("#3d375e"),
|
|
||||||
ScrollInfo: lipgloss.Color("#a277ff"),
|
|
||||||
GhostText: lipgloss.Color("#4B4A4C"),
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetTheme(t Theme) {
|
|
||||||
currentTheme = t
|
|
||||||
}
|
|
||||||
|
|
||||||
type Overlay struct {
|
type Overlay struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
Visible bool
|
Visible bool
|
||||||
@@ -477,7 +444,11 @@ func (o *Overlay) RenderGhostText(buffer string, userNavigated bool, cursorAtEnd
|
|||||||
|
|
||||||
if ghostText != "" {
|
if ghostText != "" {
|
||||||
width := termWidth()
|
width := termWidth()
|
||||||
cursorCol := o.PromptLen + lipgloss.Width(buffer)
|
totalCol := o.PromptLen + lipgloss.Width(buffer)
|
||||||
|
cursorCol := totalCol
|
||||||
|
if width > 0 {
|
||||||
|
cursorCol = totalCol % width
|
||||||
|
}
|
||||||
availableCols := width - cursorCol
|
availableCols := width - cursorCol
|
||||||
if availableCols <= 0 {
|
if availableCols <= 0 {
|
||||||
ghostText = ""
|
ghostText = ""
|
||||||
@@ -498,7 +469,7 @@ func (o *Overlay) RenderGhostText(buffer string, userNavigated bool, cursorAtEnd
|
|||||||
|
|
||||||
s.WriteString(ansi.SaveCursor)
|
s.WriteString(ansi.SaveCursor)
|
||||||
if ghostText != "" {
|
if ghostText != "" {
|
||||||
styled := lipgloss.NewStyle().Foreground(currentTheme.GhostText).Render(ghostText)
|
styled := lipgloss.NewStyle().Foreground(lipgloss.Color(config.Theme().GhostText)).Render(ghostText)
|
||||||
s.WriteString(styled)
|
s.WriteString(styled)
|
||||||
}
|
}
|
||||||
if padLen > 0 {
|
if padLen > 0 {
|
||||||
@@ -520,28 +491,18 @@ func termWidth() int {
|
|||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourceTag renders a pill-style source label (" alias ") with inverted
|
|
||||||
// colors when selected.
|
|
||||||
func sourceTag(label, bgHex, fgHex string, selected bool) string {
|
|
||||||
style := lipgloss.NewStyle().Background(lipgloss.Color(bgHex)).Foreground(lipgloss.Color(fgHex))
|
|
||||||
if selected {
|
|
||||||
style = lipgloss.NewStyle().Background(lipgloss.Color(fgHex)).Foreground(lipgloss.Color("#110f18")).Bold(true)
|
|
||||||
}
|
|
||||||
return style.Render(" " + label + " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderMatchedTitle(title, typed string, selected bool, w int) string {
|
func renderMatchedTitle(title, typed string, selected bool, w int) string {
|
||||||
t := currentTheme
|
t := config.Theme()
|
||||||
textColor := t.Text
|
textColor := t.Text
|
||||||
if selected {
|
if selected {
|
||||||
textColor = t.TextSel
|
textColor = t.TextSel
|
||||||
}
|
}
|
||||||
|
|
||||||
base := lipgloss.NewStyle().Foreground(textColor)
|
base := lipgloss.NewStyle().Foreground(lipgloss.Color(textColor))
|
||||||
match := lipgloss.NewStyle().Foreground(t.Match).Bold(true)
|
match := lipgloss.NewStyle().Foreground(lipgloss.Color(t.Match)).Bold(true)
|
||||||
if selected {
|
if selected {
|
||||||
base = base.Background(t.SelBg)
|
base = base.Background(lipgloss.Color(t.SelBg))
|
||||||
match = match.Background(t.SelBg)
|
match = match.Background(lipgloss.Color(t.SelBg))
|
||||||
}
|
}
|
||||||
|
|
||||||
display := fixedWidth(title, w)
|
display := fixedWidth(title, w)
|
||||||
@@ -570,9 +531,9 @@ func (o *Overlay) draw() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
t := currentTheme
|
t := config.Theme()
|
||||||
border := lipgloss.NewStyle().Foreground(t.Border)
|
border := lipgloss.NewStyle().Foreground(lipgloss.Color(t.Border))
|
||||||
scrollStyle := lipgloss.NewStyle().Foreground(t.ScrollInfo)
|
scrollStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(t.ScrollInfo))
|
||||||
|
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
s.WriteString(ansi.ResetModeAutoWrap)
|
s.WriteString(ansi.ResetModeAutoWrap)
|
||||||
@@ -580,6 +541,10 @@ func (o *Overlay) draw() string {
|
|||||||
typedLen := lipgloss.Width(o.TypedQuery)
|
typedLen := lipgloss.Width(o.TypedQuery)
|
||||||
width := termWidth()
|
width := termWidth()
|
||||||
|
|
||||||
|
// ComputeCursorCol returns the total visual width, not the column on the
|
||||||
|
// current line. When the prompt is wider than the terminal the cursor has
|
||||||
|
// wrapped, so using PromptLen directly overflows the screen and the box
|
||||||
|
// lands at the wrong horizontal position.
|
||||||
boxWidth := config.Get().UI.MaxWidth
|
boxWidth := config.Get().UI.MaxWidth
|
||||||
if boxWidth <= 0 {
|
if boxWidth <= 0 {
|
||||||
boxWidth = 76 // Default if 0
|
boxWidth = 76 // Default if 0
|
||||||
@@ -700,14 +665,14 @@ func (o *Overlay) draw() string {
|
|||||||
|
|
||||||
bg := lipgloss.NewStyle()
|
bg := lipgloss.NewStyle()
|
||||||
if selected {
|
if selected {
|
||||||
bg = bg.Background(t.SelBg)
|
bg = bg.Background(lipgloss.Color(t.SelBg))
|
||||||
}
|
}
|
||||||
|
|
||||||
marker := " "
|
marker := " "
|
||||||
markerStyle := bg.Foreground(t.Muted)
|
markerStyle := bg.Foreground(lipgloss.Color(t.Muted))
|
||||||
if selected {
|
if selected {
|
||||||
marker = "▶"
|
marker = "▶"
|
||||||
markerStyle = bg.Foreground(t.Accent).Bold(true)
|
markerStyle = bg.Foreground(lipgloss.Color(t.Accent)).Bold(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
iconGlyph := lookupIcon(it.Icon)
|
iconGlyph := lookupIcon(it.Icon)
|
||||||
@@ -715,7 +680,7 @@ func (o *Overlay) draw() string {
|
|||||||
if selected {
|
if selected {
|
||||||
iconColor = t.Accent
|
iconColor = t.Accent
|
||||||
}
|
}
|
||||||
iconStr := bg.Foreground(iconColor).Render(fixedWidth(iconGlyph, iconW))
|
iconStr := bg.Foreground(lipgloss.Color(iconColor)).Render(fixedWidth(iconGlyph, iconW))
|
||||||
|
|
||||||
title := renderMatchedTitle(it.Cmd, o.TypedQuery, selected, titleW)
|
title := renderMatchedTitle(it.Cmd, o.TypedQuery, selected, titleW)
|
||||||
|
|
||||||
@@ -727,26 +692,41 @@ func (o *Overlay) draw() string {
|
|||||||
var desc string
|
var desc string
|
||||||
if isClassic {
|
if isClassic {
|
||||||
if it.Icon == "alias" {
|
if it.Icon == "alias" {
|
||||||
desc = bg.Foreground(descColor).Render(fixedWidth("alias: "+it.Desc, descW))
|
desc = bg.Foreground(lipgloss.Color(descColor)).Render(fixedWidth("alias: "+it.Desc, descW))
|
||||||
} else {
|
} else {
|
||||||
desc = bg.Foreground(descColor).Render(fixedWidth(it.Desc, descW))
|
desc = bg.Foreground(lipgloss.Color(descColor)).Render(fixedWidth(it.Desc, descW))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch it.Icon {
|
switch it.Icon {
|
||||||
case "alias":
|
case "alias":
|
||||||
tag := sourceTag("alias", "#2a2342", "#a277ff", selected)
|
boxStyle := lipgloss.NewStyle().Background(lipgloss.Color(t.Alias)).Foreground(lipgloss.Color(t.AliasSel))
|
||||||
rem := max(descW-lipgloss.Width(tag)-1, 0)
|
if selected {
|
||||||
desc = tag + bg.Render(" ") + bg.Foreground(descColor).Render(fixedWidth(it.Desc, rem))
|
boxStyle = lipgloss.NewStyle().Background(lipgloss.Color(t.AliasSel)).Foreground(lipgloss.Color(t.SelText)).Bold(true)
|
||||||
|
}
|
||||||
|
tag := boxStyle.Render(" alias ")
|
||||||
|
tw := lipgloss.Width(tag)
|
||||||
|
rem := max(descW-tw-1, 0)
|
||||||
|
desc = tag + bg.Render(" ") + bg.Foreground(lipgloss.Color(descColor)).Render(fixedWidth(it.Desc, rem))
|
||||||
case "history":
|
case "history":
|
||||||
tag := sourceTag("history", "#1a2d36", "#61ffca", selected)
|
boxStyle := lipgloss.NewStyle().Background(lipgloss.Color(t.History)).Foreground(lipgloss.Color(t.HistorySel))
|
||||||
rem := max(descW-lipgloss.Width(tag), 0)
|
if selected {
|
||||||
|
boxStyle = lipgloss.NewStyle().Background(lipgloss.Color(t.HistorySel)).Foreground(lipgloss.Color(t.SelText)).Bold(true)
|
||||||
|
}
|
||||||
|
tag := boxStyle.Render(" history ")
|
||||||
|
tw := lipgloss.Width(tag)
|
||||||
|
rem := max(descW-tw, 0)
|
||||||
desc = tag + bg.Render(strings.Repeat(" ", rem))
|
desc = tag + bg.Render(strings.Repeat(" ", rem))
|
||||||
case "system":
|
case "system":
|
||||||
tag := sourceTag("system", "#1e1d28", "#a277ff", selected)
|
boxStyle := lipgloss.NewStyle().Background(lipgloss.Color(t.Sys)).Foreground(lipgloss.Color(t.SysSel))
|
||||||
rem := max(descW-lipgloss.Width(tag), 0)
|
if selected {
|
||||||
|
boxStyle = lipgloss.NewStyle().Background(lipgloss.Color(t.SysSel)).Foreground(lipgloss.Color(t.SelText)).Bold(true)
|
||||||
|
}
|
||||||
|
tag := boxStyle.Render(" system ")
|
||||||
|
tw := lipgloss.Width(tag)
|
||||||
|
rem := max(descW-tw, 0)
|
||||||
desc = tag + bg.Render(strings.Repeat(" ", rem))
|
desc = tag + bg.Render(strings.Repeat(" ", rem))
|
||||||
default:
|
default:
|
||||||
desc = bg.Foreground(descColor).Render(fixedWidth(it.Desc, descW))
|
desc = bg.Foreground(lipgloss.Color(descColor)).Render(fixedWidth(it.Desc, descW))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -777,11 +757,11 @@ func (o *Overlay) draw() string {
|
|||||||
|
|
||||||
footerInfo := ""
|
footerInfo := ""
|
||||||
if !isClassic {
|
if !isClassic {
|
||||||
keyStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#a277ff")).Bold(true)
|
keyStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(t.Key)).Bold(true)
|
||||||
selectKey := keyStyle.Render(config.FormatKeyName(config.Get().Keybindings.SelectSuggestion))
|
selectKey := keyStyle.Render(config.FormatKeyName(config.Get().Keybindings.SelectSuggestion))
|
||||||
ctrlRKey := keyStyle.Render(config.FormatKeyName(config.Get().Keybindings.ToggleMode))
|
ctrlRKey := keyStyle.Render(config.FormatKeyName(config.Get().Keybindings.ToggleMode))
|
||||||
acceptText := lipgloss.NewStyle().Foreground(t.ScrollInfo).Render(" Accept")
|
acceptText := lipgloss.NewStyle().Foreground(lipgloss.Color(t.ScrollInfo)).Render(" Accept")
|
||||||
modeText := lipgloss.NewStyle().Foreground(t.ScrollInfo).Render(" Mode")
|
modeText := lipgloss.NewStyle().Foreground(lipgloss.Color(t.ScrollInfo)).Render(" Mode")
|
||||||
footerInfo = fmt.Sprintf(" %s%s • %s%s ", selectKey, acceptText, ctrlRKey, modeText)
|
footerInfo = fmt.Sprintf(" %s%s • %s%s ", selectKey, acceptText, ctrlRKey, modeText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+36
-15
@@ -144,30 +144,47 @@ func Init(cfg *Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func AutoDetectConfigChange(onReload func(cfg *Config)) {
|
func AutoDetectConfigChange(onReload func(cfg *Config)) {
|
||||||
path, err := ConfigPath()
|
cfgPath, err := ConfigPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
themePath, _ := ThemePath()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
var lastMod time.Time
|
statMod := func(p string) time.Time {
|
||||||
if info, err := os.Stat(path); err == nil {
|
if info, err := os.Stat(p); err == nil {
|
||||||
lastMod = info.ModTime()
|
return info.ModTime()
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
}
|
}
|
||||||
|
cfgLast := statMod(cfgPath)
|
||||||
|
themeLast := statMod(themePath)
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
info, err := os.Stat(path)
|
changed := false
|
||||||
if err == nil {
|
cfgMod := statMod(cfgPath)
|
||||||
if !lastMod.IsZero() && info.ModTime().After(lastMod) {
|
if !cfgLast.IsZero() && cfgMod.After(cfgLast) {
|
||||||
lastMod = info.ModTime()
|
cfgLast = cfgMod
|
||||||
if newCfg, err := Load(); err == nil {
|
changed = true
|
||||||
Init(newCfg)
|
} else if cfgLast.IsZero() {
|
||||||
if onReload != nil {
|
cfgLast = cfgMod
|
||||||
onReload(newCfg)
|
}
|
||||||
}
|
if themePath != "" {
|
||||||
|
themeMod := statMod(themePath)
|
||||||
|
if !themeLast.IsZero() && (themeMod.After(themeLast) || themeMod.IsZero()) {
|
||||||
|
themeLast = themeMod
|
||||||
|
changed = true
|
||||||
|
} else if themeLast.IsZero() {
|
||||||
|
themeLast = themeMod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
if newCfg, err := Load(); err == nil {
|
||||||
|
Init(newCfg)
|
||||||
|
if onReload != nil {
|
||||||
|
onReload(newCfg)
|
||||||
}
|
}
|
||||||
} else if lastMod.IsZero() {
|
|
||||||
lastMod = info.ModTime()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,6 +207,10 @@ func Load() (*Config, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if themePath, err := ThemePath(); err == nil {
|
||||||
|
LoadTheme(themePath)
|
||||||
|
}
|
||||||
|
|
||||||
applyEnv(cfg)
|
applyEnv(cfg)
|
||||||
|
|
||||||
// fallback for empty keybindings
|
// fallback for empty keybindings
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ func ConfigPath() (string, error) {
|
|||||||
return filepath.Join(dir, "iris", "config.toml"), nil
|
return filepath.Join(dir, "iris", "config.toml"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ThemePath() (string, error) {
|
||||||
|
configHome := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
if configHome != "" {
|
||||||
|
return filepath.Join(configHome, "iris", "theme.toml"), nil
|
||||||
|
}
|
||||||
|
dir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(dir, "iris", "theme.toml"), nil
|
||||||
|
}
|
||||||
|
|
||||||
func StatePath() (string, error) {
|
func StatePath() (string, error) {
|
||||||
dataHome := os.Getenv("XDG_DATA_HOME")
|
dataHome := os.Getenv("XDG_DATA_HOME")
|
||||||
if dataHome == "" {
|
if dataHome == "" {
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultTheme = ThemeStyles{
|
||||||
|
Border: "#a277ff",
|
||||||
|
Accent: "#61ffca",
|
||||||
|
Muted: "#6d6a7f",
|
||||||
|
Text: "#edecee",
|
||||||
|
TextSel: "#ffffff",
|
||||||
|
Key: "#a277ff",
|
||||||
|
Match: "#61ffca",
|
||||||
|
Desc: "#9692a8",
|
||||||
|
DescSel: "#edecee",
|
||||||
|
SelBg: "#3d375e",
|
||||||
|
SelText: "#110f18",
|
||||||
|
ScrollInfo: "#a277ff",
|
||||||
|
GhostText: "#4B4A4C",
|
||||||
|
History: "#1a2d36",
|
||||||
|
HistorySel: "#61ffca",
|
||||||
|
Sys: "#1e1d28",
|
||||||
|
SysSel: "#a277ff",
|
||||||
|
Alias: "#2a2342",
|
||||||
|
AliasSel: "#a277ff",
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
themeMu sync.RWMutex
|
||||||
|
themeStyles = defaultTheme
|
||||||
|
)
|
||||||
|
|
||||||
|
func Theme() ThemeStyles {
|
||||||
|
themeMu.RLock()
|
||||||
|
defer themeMu.RUnlock()
|
||||||
|
return themeStyles
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeStyles struct {
|
||||||
|
Border string `toml:"border"`
|
||||||
|
Accent string `toml:"accent"`
|
||||||
|
Muted string `toml:"muted"`
|
||||||
|
Text string `toml:"text"`
|
||||||
|
TextSel string `toml:"text_sel"`
|
||||||
|
Key string `toml:"key"`
|
||||||
|
Match string `toml:"match"`
|
||||||
|
Desc string `toml:"desc"`
|
||||||
|
DescSel string `toml:"desc_sel"`
|
||||||
|
SelBg string `toml:"sel_bg"`
|
||||||
|
SelText string `toml:"sel_text"`
|
||||||
|
ScrollInfo string `toml:"scroll_info"`
|
||||||
|
GhostText string `toml:"ghost_text"`
|
||||||
|
Sys string `toml:"sys"`
|
||||||
|
SysSel string `toml:"sys_sel"`
|
||||||
|
History string `toml:"hist"`
|
||||||
|
HistorySel string `toml:"hist_sel"`
|
||||||
|
Alias string `toml:"alias"`
|
||||||
|
AliasSel string `toml:"alias_sel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadTheme reads theme.toml at filePath
|
||||||
|
// Missing file - use defaults silently
|
||||||
|
// Missing/empty fields - fall back to the default value for that field
|
||||||
|
func LoadTheme(filePath string) {
|
||||||
|
themeMu.Lock()
|
||||||
|
defer themeMu.Unlock()
|
||||||
|
|
||||||
|
themeStyles = defaultTheme
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
// missing or unreadable file: use defaults
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var t ThemeStyles
|
||||||
|
if _, err := toml.Decode(string(data), &t); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
applyThemeWithFallback(&themeStyles, t, defaultTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyThemeWithFallback copies non-empty string fields from src into dst,
|
||||||
|
// using def for any field that is empty in src
|
||||||
|
func applyThemeWithFallback(dst *ThemeStyles, src, def ThemeStyles) {
|
||||||
|
dv := reflect.ValueOf(dst).Elem()
|
||||||
|
sv := reflect.ValueOf(src)
|
||||||
|
dv2 := reflect.ValueOf(def)
|
||||||
|
|
||||||
|
for i := range dv.NumField() {
|
||||||
|
sf := sv.Field(i).String()
|
||||||
|
if sf != "" {
|
||||||
|
dv.Field(i).SetString(sf)
|
||||||
|
} else {
|
||||||
|
dv.Field(i).SetString(dv2.Field(i).String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package root
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/versenilvis/iris/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ThemeCmd = &cobra.Command{
|
||||||
|
Use: "theme",
|
||||||
|
Short: "manage iris theme",
|
||||||
|
}
|
||||||
|
|
||||||
|
var ThemeInitCmd = &cobra.Command{
|
||||||
|
Use: "init",
|
||||||
|
Short: "initialize default theme file with comments",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
path, err := config.ThemePath()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to get theme path: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, statErr := os.Stat(path); statErr == nil {
|
||||||
|
fmt.Printf("theme file already exists at %s\n", path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = os.MkdirAll(filepath.Dir(path), 0755)
|
||||||
|
|
||||||
|
defaultContent := `# ~/.config/iris/theme.toml
|
||||||
|
# iris theme file
|
||||||
|
|
||||||
|
# overlay border color
|
||||||
|
border = "#a277ff"
|
||||||
|
|
||||||
|
# accent color for selected marker and icon
|
||||||
|
accent = "#61ffca"
|
||||||
|
|
||||||
|
# muted color for unselected icons
|
||||||
|
muted = "#6d6a7f"
|
||||||
|
|
||||||
|
# default text color
|
||||||
|
text = "#edecee"
|
||||||
|
|
||||||
|
# text color when selected
|
||||||
|
text_sel = "#ffffff"
|
||||||
|
|
||||||
|
# footer shortcut key color
|
||||||
|
key = "#a277ff"
|
||||||
|
|
||||||
|
# matched text color
|
||||||
|
match = "#61ffca"
|
||||||
|
|
||||||
|
# description text color
|
||||||
|
desc = "#9692a8"
|
||||||
|
|
||||||
|
# description text color when selected
|
||||||
|
desc_sel = "#edecee"
|
||||||
|
|
||||||
|
# selected row background
|
||||||
|
sel_bg = "#3d375e"
|
||||||
|
|
||||||
|
# selected tag pill text color
|
||||||
|
sel_text = "#110f18"
|
||||||
|
|
||||||
|
# scroll counter color
|
||||||
|
scroll_info = "#a277ff"
|
||||||
|
|
||||||
|
# inline ghost text color
|
||||||
|
ghost_text = "#4B4A4C"
|
||||||
|
|
||||||
|
# "system" tag background color
|
||||||
|
sys = "#1e1d28"
|
||||||
|
|
||||||
|
# "system" tag selected background color
|
||||||
|
sys_sel = "#a277ff"
|
||||||
|
|
||||||
|
# "history" tag background color
|
||||||
|
hist = "#1a2d36"
|
||||||
|
|
||||||
|
# "history" tag selected background color
|
||||||
|
hist_sel = "#61ffca"
|
||||||
|
|
||||||
|
# "alias" tag background color
|
||||||
|
alias = "#2a2342"
|
||||||
|
|
||||||
|
# "alias" tag selected background color (and alias word color)
|
||||||
|
alias_sel = "#a277ff"
|
||||||
|
`
|
||||||
|
err = os.WriteFile(path, []byte(defaultContent), 0644)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to write theme file: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("initialized theme file at %s\n", path)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ThemeCmd.AddCommand(ThemeInitCmd)
|
||||||
|
rootCmd.AddCommand(ThemeCmd)
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package root
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/versenilvis/iris/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestThemeInitCommand(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "iris-theme-cmd-test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// UserConfigDir follows HOME on macOS and XDG_CONFIG_HOME on Unix.
|
||||||
|
// Override both so the command cannot read or write the user's real theme.
|
||||||
|
t.Setenv("HOME", tmpDir)
|
||||||
|
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
||||||
|
|
||||||
|
ThemeInitCmd.Run(ThemeInitCmd, []string{})
|
||||||
|
|
||||||
|
themePath, err := config.ThemePath()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get theme path: %v", err)
|
||||||
|
}
|
||||||
|
if _, statErr := os.Stat(themePath); statErr != nil {
|
||||||
|
t.Errorf("expected theme file to be created at %s, but it was not", themePath)
|
||||||
|
}
|
||||||
|
content, err := os.ReadFile(themePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read theme file: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(content), "sel_text = \"#110f18\"") {
|
||||||
|
t.Error("expected initialized theme to include sel_text = \"#110f18\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
+106
-93
@@ -276,8 +276,6 @@ func runWrapper() {
|
|||||||
|
|
||||||
overlay := integration.NewOverlay()
|
overlay := integration.NewOverlay()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// start background update check (async)
|
// start background update check (async)
|
||||||
pendingUpdate = startBackgroundUpdateCheck()
|
pendingUpdate = startBackgroundUpdateCheck()
|
||||||
updatePrinted := false
|
updatePrinted := false
|
||||||
@@ -289,10 +287,14 @@ func runWrapper() {
|
|||||||
var isCommandActive atomic.Bool
|
var isCommandActive atomic.Bool
|
||||||
var disableGhostText atomic.Bool
|
var disableGhostText atomic.Bool
|
||||||
disableGhostText.Store(!config.Get().UI.GhostText)
|
disableGhostText.Store(!config.Get().UI.GhostText)
|
||||||
|
var renderOverlayFn atomic.Value // holds func()
|
||||||
|
renderOverlayFn.Store(func() {})
|
||||||
config.AutoDetectConfigChange(func(cfg *config.Config) {
|
config.AutoDetectConfigChange(func(cfg *config.Config) {
|
||||||
disableGhostText.Store(!cfg.UI.GhostText)
|
disableGhostText.Store(!cfg.UI.GhostText)
|
||||||
|
if renderer, ok := renderOverlayFn.Load().(func()); ok {
|
||||||
|
renderer()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
renderOverlay := func() {}
|
|
||||||
isExecuting := func() bool {
|
isExecuting := func() bool {
|
||||||
if isCommandActive.Load() {
|
if isCommandActive.Load() {
|
||||||
// for bash: no preexec/precmd hooks, so fall back to TIOCGPGRP to detect when shell returns
|
// for bash: no preexec/precmd hooks, so fall back to TIOCGPGRP to detect when shell returns
|
||||||
@@ -312,14 +314,16 @@ func runWrapper() {
|
|||||||
return pgrp != shellPGID
|
return pgrp != shellPGID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suggestionsEnabled := true
|
||||||
|
|
||||||
// Shared handler for configured navigation keys (e.g. ctrl+j / ctrl+k).
|
// Shared handler for configured navigation keys (e.g. ctrl+j / ctrl+k).
|
||||||
// Moves the overlay cursor when visible, otherwise opens history/spec
|
// Moves the overlay cursor when visible, otherwise opens history/spec
|
||||||
// list and selects the next item in the requested direction.
|
// list and selects the next item in the requested direction.
|
||||||
handleNavKey := func(dir string) {
|
handleNavKey := func(dir string) {
|
||||||
intercepted = true
|
|
||||||
userNavigated.Store(true)
|
|
||||||
|
|
||||||
if overlay.IsVisible() {
|
if overlay.IsVisible() {
|
||||||
|
intercepted = true
|
||||||
|
userNavigated.Store(true)
|
||||||
|
|
||||||
arrowDir := "down"
|
arrowDir := "down"
|
||||||
if dir == "up" {
|
if dir == "up" {
|
||||||
arrowDir = "up"
|
arrowDir = "up"
|
||||||
@@ -353,7 +357,12 @@ func runWrapper() {
|
|||||||
}
|
}
|
||||||
b.WriteString(overlay.Render())
|
b.WriteString(overlay.Render())
|
||||||
writeStdout([]byte(b.String()))
|
writeStdout([]byte(b.String()))
|
||||||
} else {
|
} else if suggestionsEnabled {
|
||||||
|
// hidden-overlay history navigation only when suggestions are enabled;
|
||||||
|
// otherwise let the navigation keys pass through to the shell
|
||||||
|
intercepted = true
|
||||||
|
userNavigated.Store(true)
|
||||||
|
|
||||||
activeModeMu.Lock()
|
activeModeMu.Lock()
|
||||||
if activeMode == "" {
|
if activeMode == "" {
|
||||||
activeMode = loadMode()
|
activeMode = loadMode()
|
||||||
@@ -443,8 +452,6 @@ func runWrapper() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// listen for suggestion requests from shell scripts via the ipc pipe
|
// listen for suggestion requests from shell scripts via the ipc pipe
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -565,14 +572,15 @@ func runWrapper() {
|
|||||||
cursorOffset = 0
|
cursorOffset = 0
|
||||||
bufferMu.Unlock()
|
bufferMu.Unlock()
|
||||||
|
|
||||||
renderOverlay()
|
if renderer, ok := renderOverlayFn.Load().(func()); ok {
|
||||||
|
renderer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
logger.Errorf("IPC scanner error: %v", err)
|
logger.Errorf("IPC scanner error: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
suggestionsEnabled := true
|
|
||||||
activeModeMu.Lock()
|
activeModeMu.Lock()
|
||||||
activeMode = loadMode()
|
activeMode = loadMode()
|
||||||
activeModeMu.Unlock()
|
activeModeMu.Unlock()
|
||||||
@@ -651,7 +659,9 @@ func runWrapper() {
|
|||||||
}
|
}
|
||||||
SetCurrentAISuggestion(sugg)
|
SetCurrentAISuggestion(sugg)
|
||||||
if overlay.InjectAISuggestion(*sugg) {
|
if overlay.InjectAISuggestion(*sugg) {
|
||||||
renderOverlay()
|
if renderer, ok := renderOverlayFn.Load().(func()); ok {
|
||||||
|
renderer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -693,7 +703,7 @@ func runWrapper() {
|
|||||||
writeStdout([]byte(b.String()))
|
writeStdout([]byte(b.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
renderOverlay = func() {
|
renderOverlayFn.Store(func() {
|
||||||
renderMu.Lock()
|
renderMu.Lock()
|
||||||
defer renderMu.Unlock()
|
defer renderMu.Unlock()
|
||||||
|
|
||||||
@@ -718,9 +728,11 @@ func runWrapper() {
|
|||||||
renderMu.Unlock()
|
renderMu.Unlock()
|
||||||
renderMenuNow()
|
renderMenuNow()
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
|
|
||||||
renderOverlay()
|
if renderer, ok := renderOverlayFn.Load().(func()); ok {
|
||||||
|
renderer()
|
||||||
|
}
|
||||||
|
|
||||||
// reads from stdin and decides what to forward or intercept
|
// reads from stdin and decides what to forward or intercept
|
||||||
// for most cases, I just handle the already have terminal shortcuts
|
// for most cases, I just handle the already have terminal shortcuts
|
||||||
@@ -746,30 +758,30 @@ func runWrapper() {
|
|||||||
b := inputSlice[i]
|
b := inputSlice[i]
|
||||||
intercepted = false
|
intercepted = false
|
||||||
|
|
||||||
if matched, consumed := config.MatchKey(inputSlice[i:], config.Get().Keybindings.ToggleMenu); matched {
|
if matched, consumed := config.MatchKey(inputSlice[i:], config.Get().Keybindings.ToggleMenu); matched {
|
||||||
intercepted = true
|
intercepted = true
|
||||||
suggestionsEnabled = !suggestionsEnabled
|
suggestionsEnabled = !suggestionsEnabled
|
||||||
if !suggestionsEnabled {
|
if !suggestionsEnabled {
|
||||||
writeStdout([]byte(overlay.ClearAndDisable()))
|
writeStdout([]byte(overlay.ClearAndDisable()))
|
||||||
} else {
|
} else {
|
||||||
shouldOverlayDraw = true
|
shouldOverlayDraw = true
|
||||||
|
}
|
||||||
|
i += consumed - 1
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
i += consumed - 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if matched, consumed := config.MatchKey(inputSlice[i:], config.Get().Keybindings.ToggleMode); matched { // ctrl+r: toggle between command specs and command history
|
if matched, consumed := config.MatchKey(inputSlice[i:], config.Get().Keybindings.ToggleMode); matched { // ctrl+r: toggle between command specs and command history
|
||||||
i += consumed - 1
|
i += consumed - 1
|
||||||
intercepted = true
|
intercepted = true
|
||||||
activeModeMu.Lock()
|
activeModeMu.Lock()
|
||||||
if activeMode == "spec" {
|
if activeMode == "spec" {
|
||||||
activeMode = "history"
|
activeMode = "history"
|
||||||
} else {
|
} else {
|
||||||
activeMode = "spec"
|
activeMode = "spec"
|
||||||
}
|
}
|
||||||
saveMode(activeMode)
|
saveMode(activeMode)
|
||||||
activeModeMu.Unlock()
|
activeModeMu.Unlock()
|
||||||
if userNavigated.Load() {
|
if userNavigated.Load() {
|
||||||
bufferMu.Lock()
|
bufferMu.Lock()
|
||||||
naiveBuffer = overlay.GetTypedQuery()
|
naiveBuffer = overlay.GetTypedQuery()
|
||||||
cursorOffset = 0
|
cursorOffset = 0
|
||||||
@@ -782,61 +794,60 @@ func runWrapper() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var isNavUp, isNavDown bool
|
var isNavUp, isNavDown bool
|
||||||
var navConsumed int
|
var navConsumed int
|
||||||
if isNavUp, navConsumed = config.MatchKey(inputSlice[i:], config.Get().Keybindings.NavigateUp); !isNavUp {
|
if isNavUp, navConsumed = config.MatchKey(inputSlice[i:], config.Get().Keybindings.NavigateUp); !isNavUp {
|
||||||
isNavDown, navConsumed = config.MatchKey(inputSlice[i:], config.Get().Keybindings.NavigateDown)
|
isNavDown, navConsumed = config.MatchKey(inputSlice[i:], config.Get().Keybindings.NavigateDown)
|
||||||
}
|
|
||||||
|
|
||||||
if isNavUp || isNavDown {
|
|
||||||
arrowDir := "down"
|
|
||||||
if isNavUp {
|
|
||||||
arrowDir = "up"
|
|
||||||
}
|
}
|
||||||
handleNavKey(arrowDir)
|
|
||||||
i += navConsumed - 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if matched, consumed := config.MatchKey(inputSlice[i:], config.Get().Keybindings.SelectSuggestion); matched && config.Get().Keybindings.SelectSuggestion != "" {
|
if isNavUp || isNavDown {
|
||||||
if overlay.IsVisible() {
|
arrowDir := "down"
|
||||||
intercepted = true
|
if isNavUp {
|
||||||
selected := overlay.GetCurrentCmd()
|
arrowDir = "up"
|
||||||
if selected != "" {
|
|
||||||
activeModeMu.RLock()
|
|
||||||
currentMode := activeMode
|
|
||||||
activeModeMu.RUnlock()
|
|
||||||
if currentMode == "spec" {
|
|
||||||
s := strings.TrimSpace(selected)
|
|
||||||
if strings.HasSuffix(s, "/") || strings.HasSuffix(s, "\\") {
|
|
||||||
selected = s
|
|
||||||
} else {
|
|
||||||
selected = s + " "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bufferMu.Lock()
|
|
||||||
naiveBuffer = selected
|
|
||||||
cursorOffset = 0
|
|
||||||
bufferMu.Unlock()
|
|
||||||
_, _ = ptmx.Write(append([]byte{0x15}, selected...))
|
|
||||||
|
|
||||||
overlay.ClearGhostTextState()
|
|
||||||
userNavigated.Store(false)
|
|
||||||
writeStdout([]byte(overlay.Render()))
|
|
||||||
renderOverlay()
|
|
||||||
}
|
}
|
||||||
|
handleNavKey(arrowDir)
|
||||||
|
i += navConsumed - 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched, consumed := config.MatchKey(inputSlice[i:], config.Get().Keybindings.SelectSuggestion); matched && config.Get().Keybindings.SelectSuggestion != "" {
|
||||||
|
if overlay.IsVisible() {
|
||||||
|
intercepted = true
|
||||||
|
selected := overlay.GetCurrentCmd()
|
||||||
|
if selected != "" {
|
||||||
|
activeModeMu.RLock()
|
||||||
|
currentMode := activeMode
|
||||||
|
activeModeMu.RUnlock()
|
||||||
|
if currentMode == "spec" {
|
||||||
|
s := strings.TrimSpace(selected)
|
||||||
|
if strings.HasSuffix(s, "/") || strings.HasSuffix(s, "\\") {
|
||||||
|
selected = s
|
||||||
|
} else {
|
||||||
|
selected = s + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bufferMu.Lock()
|
||||||
|
naiveBuffer = selected
|
||||||
|
cursorOffset = 0
|
||||||
|
bufferMu.Unlock()
|
||||||
|
_, _ = ptmx.Write(append([]byte{0x15}, selected...))
|
||||||
|
|
||||||
|
overlay.ClearGhostTextState()
|
||||||
|
userNavigated.Store(false)
|
||||||
|
writeStdout([]byte(overlay.Render()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// always consume the full binding atomically, even when the overlay is hidden
|
||||||
i += consumed - 1
|
i += consumed - 1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
if b == 0x0d || b == 0x0a { // enter
|
||||||
|
intercepted = true
|
||||||
|
|
||||||
if b == 0x0d || b == 0x0a { // enter
|
if b == 0x0d && i+1 < n && inputSlice[i+1] == 0x0a {
|
||||||
intercepted = true
|
|
||||||
|
|
||||||
if b == 0x0d && i+1 < n && inputSlice[i+1] == 0x0a {
|
|
||||||
i++ // consume trailing \n in \r\n to prevent matching ctrl+j
|
i++ // consume trailing \n in \r\n to prevent matching ctrl+j
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedCmd string
|
var selectedCmd string
|
||||||
var shouldAutoExecute bool
|
var shouldAutoExecute bool
|
||||||
if overlay.IsVisible() && (config.Get().Core.AutoExecute || userNavigated.Load()) {
|
if overlay.IsVisible() && (config.Get().Core.AutoExecute || userNavigated.Load()) {
|
||||||
@@ -925,13 +936,13 @@ func runWrapper() {
|
|||||||
if !isNavUp {
|
if !isNavUp {
|
||||||
isNavDown, navConsumed = config.MatchKey(inputSlice[i:], config.Get().Keybindings.NavigateDown)
|
isNavDown, navConsumed = config.MatchKey(inputSlice[i:], config.Get().Keybindings.NavigateDown)
|
||||||
}
|
}
|
||||||
if isNavUp || isNavDown {
|
if isNavUp || isNavDown {
|
||||||
intercepted = true
|
intercepted = true
|
||||||
arrowDir := "down"
|
arrowDir := "down"
|
||||||
if isNavUp {
|
if isNavUp {
|
||||||
arrowDir = "up"
|
arrowDir = "up"
|
||||||
}
|
}
|
||||||
handleNavKey(arrowDir)
|
handleNavKey(arrowDir)
|
||||||
i += navConsumed - 1
|
i += navConsumed - 1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1073,8 +1084,8 @@ func runWrapper() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !intercepted {
|
if !intercepted {
|
||||||
_, _ = ptmx.Write([]byte{b})
|
_, _ = ptmx.Write([]byte{b})
|
||||||
// we handle line editing keys manually to keep naiveBuffer in sync
|
// we handle line editing keys manually to keep naiveBuffer in sync
|
||||||
// since terminal is in raw mode, we must update our state for every change
|
// since terminal is in raw mode, we must update our state for every change
|
||||||
switch b {
|
switch b {
|
||||||
@@ -1210,7 +1221,9 @@ func runWrapper() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if shouldOverlayDraw {
|
if shouldOverlayDraw {
|
||||||
renderOverlay()
|
if renderer, ok := renderOverlayFn.Load().(func()); ok {
|
||||||
|
renderer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
border = "#d79921"
|
||||||
|
accent = "#fabd2f"
|
||||||
|
muted = "#928374"
|
||||||
|
text = "#ebdbb2"
|
||||||
|
text_sel = "#fbf1c7"
|
||||||
|
key = "#d79921"
|
||||||
|
match = "#b8bb26"
|
||||||
|
desc = "#a89984"
|
||||||
|
desc_sel = "#ebdbb2"
|
||||||
|
sel_bg = "#3c3836"
|
||||||
|
sel_text = "#282828"
|
||||||
|
scroll_info = "#d79921"
|
||||||
|
ghost_text = "#665c54"
|
||||||
|
sys = "#32302f"
|
||||||
|
sys_sel = "#fb4934"
|
||||||
|
hist = "#282828"
|
||||||
|
hist_sel = "#83a598"
|
||||||
|
alias = "#32302f"
|
||||||
|
alias_sel = "#d3869b"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
border = "#7aa2f7"
|
||||||
|
accent = "#7dcfff"
|
||||||
|
muted = "#565f89"
|
||||||
|
text = "#c0caf5"
|
||||||
|
text_sel = "#ffffff"
|
||||||
|
key = "#bb9af7"
|
||||||
|
match = "#73daca"
|
||||||
|
desc = "#9aa5ce"
|
||||||
|
desc_sel = "#c0caf5"
|
||||||
|
sel_bg = "#283451"
|
||||||
|
sel_text = "#1a1b26"
|
||||||
|
scroll_info = "#7aa2f7"
|
||||||
|
ghost_text = "#414868"
|
||||||
|
sys = "#24283b"
|
||||||
|
sys_sel = "#f7768e"
|
||||||
|
hist = "#1f2335"
|
||||||
|
hist_sel = "#7dcfff"
|
||||||
|
alias = "#24283b"
|
||||||
|
alias_sel = "#bb9af7"
|
||||||
Reference in New Issue
Block a user