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
+106 -93
View File
@@ -276,8 +276,6 @@ func runWrapper() {
overlay := integration.NewOverlay()
// start background update check (async)
pendingUpdate = startBackgroundUpdateCheck()
updatePrinted := false
@@ -289,10 +287,14 @@ func runWrapper() {
var isCommandActive atomic.Bool
var disableGhostText atomic.Bool
disableGhostText.Store(!config.Get().UI.GhostText)
var renderOverlayFn atomic.Value // holds func()
renderOverlayFn.Store(func() {})
config.AutoDetectConfigChange(func(cfg *config.Config) {
disableGhostText.Store(!cfg.UI.GhostText)
if renderer, ok := renderOverlayFn.Load().(func()); ok {
renderer()
}
})
renderOverlay := func() {}
isExecuting := func() bool {
if isCommandActive.Load() {
// 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
}
suggestionsEnabled := true
// Shared handler for configured navigation keys (e.g. ctrl+j / ctrl+k).
// Moves the overlay cursor when visible, otherwise opens history/spec
// list and selects the next item in the requested direction.
handleNavKey := func(dir string) {
intercepted = true
userNavigated.Store(true)
if overlay.IsVisible() {
intercepted = true
userNavigated.Store(true)
arrowDir := "down"
if dir == "up" {
arrowDir = "up"
@@ -353,7 +357,12 @@ func runWrapper() {
}
b.WriteString(overlay.Render())
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()
if activeMode == "" {
activeMode = loadMode()
@@ -443,8 +452,6 @@ func runWrapper() {
}
}()
// listen for suggestion requests from shell scripts via the ipc pipe
go func() {
defer func() {
@@ -565,14 +572,15 @@ func runWrapper() {
cursorOffset = 0
bufferMu.Unlock()
renderOverlay()
if renderer, ok := renderOverlayFn.Load().(func()); ok {
renderer()
}
}
if err := scanner.Err(); err != nil {
logger.Errorf("IPC scanner error: %v", err)
}
}()
suggestionsEnabled := true
activeModeMu.Lock()
activeMode = loadMode()
activeModeMu.Unlock()
@@ -651,7 +659,9 @@ func runWrapper() {
}
SetCurrentAISuggestion(sugg)
if overlay.InjectAISuggestion(*sugg) {
renderOverlay()
if renderer, ok := renderOverlayFn.Load().(func()); ok {
renderer()
}
}
})
}
@@ -693,7 +703,7 @@ func runWrapper() {
writeStdout([]byte(b.String()))
}
renderOverlay = func() {
renderOverlayFn.Store(func() {
renderMu.Lock()
defer renderMu.Unlock()
@@ -718,9 +728,11 @@ func runWrapper() {
renderMu.Unlock()
renderMenuNow()
})
}
})
renderOverlay()
if renderer, ok := renderOverlayFn.Load().(func()); ok {
renderer()
}
// reads from stdin and decides what to forward or intercept
// for most cases, I just handle the already have terminal shortcuts
@@ -746,30 +758,30 @@ func runWrapper() {
b := inputSlice[i]
intercepted = false
if matched, consumed := config.MatchKey(inputSlice[i:], config.Get().Keybindings.ToggleMenu); matched {
intercepted = true
suggestionsEnabled = !suggestionsEnabled
if !suggestionsEnabled {
writeStdout([]byte(overlay.ClearAndDisable()))
} else {
shouldOverlayDraw = true
if matched, consumed := config.MatchKey(inputSlice[i:], config.Get().Keybindings.ToggleMenu); matched {
intercepted = true
suggestionsEnabled = !suggestionsEnabled
if !suggestionsEnabled {
writeStdout([]byte(overlay.ClearAndDisable()))
} else {
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
i += consumed - 1
intercepted = true
activeModeMu.Lock()
if activeMode == "spec" {
activeMode = "history"
} else {
activeMode = "spec"
}
saveMode(activeMode)
activeModeMu.Unlock()
if userNavigated.Load() {
if matched, consumed := config.MatchKey(inputSlice[i:], config.Get().Keybindings.ToggleMode); matched { // ctrl+r: toggle between command specs and command history
i += consumed - 1
intercepted = true
activeModeMu.Lock()
if activeMode == "spec" {
activeMode = "history"
} else {
activeMode = "spec"
}
saveMode(activeMode)
activeModeMu.Unlock()
if userNavigated.Load() {
bufferMu.Lock()
naiveBuffer = overlay.GetTypedQuery()
cursorOffset = 0
@@ -782,61 +794,60 @@ func runWrapper() {
continue
}
var isNavUp, isNavDown bool
var navConsumed int
if isNavUp, navConsumed = config.MatchKey(inputSlice[i:], config.Get().Keybindings.NavigateUp); !isNavUp {
isNavDown, navConsumed = config.MatchKey(inputSlice[i:], config.Get().Keybindings.NavigateDown)
}
if isNavUp || isNavDown {
arrowDir := "down"
if isNavUp {
arrowDir = "up"
var isNavUp, isNavDown bool
var navConsumed int
if isNavUp, navConsumed = config.MatchKey(inputSlice[i:], config.Get().Keybindings.NavigateUp); !isNavUp {
isNavDown, navConsumed = config.MatchKey(inputSlice[i:], config.Get().Keybindings.NavigateDown)
}
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()))
renderOverlay()
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 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
continue
}
}
if b == 0x0d || b == 0x0a { // enter
intercepted = true
if b == 0x0d || b == 0x0a { // enter
intercepted = true
if b == 0x0d && i+1 < n && inputSlice[i+1] == 0x0a {
if b == 0x0d && i+1 < n && inputSlice[i+1] == 0x0a {
i++ // consume trailing \n in \r\n to prevent matching ctrl+j
}
var selectedCmd string
var shouldAutoExecute bool
if overlay.IsVisible() && (config.Get().Core.AutoExecute || userNavigated.Load()) {
@@ -925,13 +936,13 @@ func runWrapper() {
if !isNavUp {
isNavDown, navConsumed = config.MatchKey(inputSlice[i:], config.Get().Keybindings.NavigateDown)
}
if isNavUp || isNavDown {
intercepted = true
arrowDir := "down"
if isNavUp {
arrowDir = "up"
}
handleNavKey(arrowDir)
if isNavUp || isNavDown {
intercepted = true
arrowDir := "down"
if isNavUp {
arrowDir = "up"
}
handleNavKey(arrowDir)
i += navConsumed - 1
continue
}
@@ -1073,8 +1084,8 @@ func runWrapper() {
continue
}
if !intercepted {
_, _ = ptmx.Write([]byte{b})
if !intercepted {
_, _ = ptmx.Write([]byte{b})
// 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
switch b {
@@ -1210,7 +1221,9 @@ func runWrapper() {
}
}
if shouldOverlayDraw {
renderOverlay()
if renderer, ok := renderOverlayFn.Load().(func()); ok {
renderer()
}
}
}
}