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:
Bruce12
2026-08-04 13:49:10 +07:00
committed by GitHub
co-authored by verse91 VERSE
parent b79d25a082
commit b14b26e663
10 changed files with 494 additions and 179 deletions
+36 -15
View File
@@ -144,30 +144,47 @@ func Init(cfg *Config) {
}
func AutoDetectConfigChange(onReload func(cfg *Config)) {
path, err := ConfigPath()
cfgPath, err := ConfigPath()
if err != nil {
return
}
themePath, _ := ThemePath()
go func() {
var lastMod time.Time
if info, err := os.Stat(path); err == nil {
lastMod = info.ModTime()
statMod := func(p string) time.Time {
if info, err := os.Stat(p); err == nil {
return info.ModTime()
}
return time.Time{}
}
cfgLast := statMod(cfgPath)
themeLast := statMod(themePath)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
info, err := os.Stat(path)
if err == nil {
if !lastMod.IsZero() && info.ModTime().After(lastMod) {
lastMod = info.ModTime()
if newCfg, err := Load(); err == nil {
Init(newCfg)
if onReload != nil {
onReload(newCfg)
}
changed := false
cfgMod := statMod(cfgPath)
if !cfgLast.IsZero() && cfgMod.After(cfgLast) {
cfgLast = cfgMod
changed = true
} else if cfgLast.IsZero() {
cfgLast = cfgMod
}
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)
// fallback for empty keybindings
+3 -1
View File
@@ -1,6 +1,8 @@
package config
import "time"
import (
"time"
)
func DefaultConfig() *Config {
return &Config{
+12
View File
@@ -17,6 +17,18 @@ func ConfigPath() (string, error) {
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) {
dataHome := os.Getenv("XDG_DATA_HOME")
if dataHome == "" {
+104
View File
@@ -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())
}
}
}