updates for themes
ober
9843e071cee2fe3d50b4b6f1be7c5a9bac44c5fc
--- a/docs/cli.md +++ b/docs/cli.md @@ -107,6 +107,13 @@ Available in the REPL and TUI. | `/checkpoints` | List recent shadow-git checkpoints. | | `/quit`, `/exit` | Exit. | +### TUI-only + +| Command | Effect | +|---|---| +| `/themes` | List TUI color themes. | +| `/theme [name]` | Cycle the TUI theme, or switch to a named theme. | + ### Tools, skills & extensions | Command | Effect | --- a/docs/tui.md +++ b/docs/tui.md @@ -65,10 +65,36 @@ rendering, live diffs, themes, and a stats sidebar. ## Themes -Three built-in themes — **dark** (default), **light**, and **gruvbox** — cycled -with `Ctrl-T`. A theme is a map from face names (`user-label`, `tool-name`, …) to -foreground/background colors and bold/italic/underline flags. Drop a JSON theme -into `~/.jcode/themes/` to add your own. +The default theme is **opencode-dark**, a port of OpenCode's warm neutral +palette. Cycle themes with `Ctrl-T`, list them with `/themes`, or switch +directly with `/theme <name>`. + +Built-ins include: + +``` +opencode-dark opencode-light tokyonight-dark +github-dark github-light nord-dark nord-light +catppuccin-dark catppuccin-light +dark light gruvbox +``` + +Set a startup theme in config: + +```json +{ + "tui": { + "theme": "github-dark" + } +} +``` + +A theme is a map from face names (`user-label`, `tool-name`, …) to +foreground/background colors and bold/italic/underline flags. `jcode` also +accepts OpenCode-style JSON themes in `~/.jcode/themes/` and +`./.jcode/themes/`; each file is registered from its filename as +`<name>-dark` and `<name>-light`. The mapper fills panel/dialog/status +backgrounds and repairs low-contrast foreground/background pairs where it can, +so custom themes are less likely to produce unreadable text. ## Rendering features --- a/src/jcode/ui/tui-input.ss +++ b/src/jcode/ui/tui-input.ss @@ -50,6 +50,7 @@ ("/search" . "Search session history") ("/compact" . "Compact conversation") ("/quit" . "Exit jcode") + ("/themes" . "List color themes") ("/theme" . "Switch color theme") ("/sidebar" . "Toggle sidebar") ("/ask-claude" . "Second opinion from claude CLI (sandboxed)") --- a/src/jcode/ui/tui-theme.ss +++ b/src/jcode/ui/tui-theme.ss @@ -3,7 +3,7 @@ (export make-face face? face-fg face-bg face-bold? face-italic? face-underline? - current-theme set-theme! + current-theme current-theme-name set-theme! set-theme-by-name! face-ref face-fg-attr face-bg-attr theme-dark theme-light theme-gruvbox ;; Theme registry @@ -41,10 +41,21 @@ ;; ---- Theme: table of face names → face structs ---- (def *current-theme* (make-parameter #f)) +(def *current-theme-name* (make-parameter #f)) (def (current-theme) (*current-theme*)) +(def (current-theme-name) (*current-theme-name*)) (def (set-theme! theme) - (*current-theme* theme)) + (*current-theme* theme) + (*current-theme-name* #f)) + +(def (set-theme-by-name! name) + (let ((theme (hash-get *theme-registry* name))) + (and theme + (begin + (*current-theme* theme) + (*current-theme-name* name) + #t)))) (def (face-ref name) "Look up a face by symbol name in current theme. Returns default face if missing." @@ -359,30 +370,27 @@ (hash-put! *theme-registry* name theme-table)) (def (get-registered-themes) - (hash-keys *theme-registry*)) - -(register-theme! "dark" theme-dark) -(register-theme! "light" theme-light) -(register-theme! "gruvbox" theme-gruvbox) + (sort string<? (hash-keys *theme-registry*))) (def (cycle-theme-by-name!) - (let* ((names (sort (get-registered-themes) string<?)) + (let* ((names (get-registered-themes)) (current (current-theme)) - (cur-name (let loop ((ns names)) - (cond ((null? ns) (car names)) - ((eq? (hash-get *theme-registry* (car ns)) current) (car ns)) - (else (loop (cdr ns)))))) + (cur-name (or (current-theme-name) + (let loop ((ns names)) + (cond ((null? ns) (car names)) + ((eq? (hash-get *theme-registry* (car ns)) current) (car ns)) + (else (loop (cdr ns))))))) (idx (let loop ((ns names) (i 0)) (cond ((null? ns) 0) ((equal? (car ns) cur-name) i) (else (loop (cdr ns) (+ i 1)))))) (next-idx (modulo (+ idx 1) (length names))) (next-name (list-ref names next-idx))) - (set-theme! (hash-get *theme-registry* next-name)))) + (set-theme-by-name! next-name))) ;; ---- opencode JSON theme loader ---- -(def (parse-hex-color s) +(def (parse-hex-color/maybe s) (cond ((and (string? s) (>= (string-length s) 7) (char=? (string-ref s 0) #\#)) (rgb (string->number (substring s 1 3) 16) @@ -393,96 +401,434 @@ (g (string->number (substring s 2 3) 16)) (b (string->number (substring s 3 4) 16))) (rgb (+ (* r 17) r) (+ (* g 17) g) (+ (* b 17) b)))) - (else TB_DEFAULT))) - -(def *opencode-slot-map* - '((primary . (user-label input-prompt)) - (secondary . (status-mode-plan)) - (accent . (spinner tool-name heading status-mode-build)) - (error . (error)) - (warning . (toast-warning-border)) - (success . (status-mode-build toast-success-border)) - (info . (tool-name toast-info-border)) - (text . (default assistant-text user-text)) - (textMuted . (dim tool-result status-dim)) - (background . ()) - (backgroundPanel . (sidebar-bg completion-item toast-bg)) - (backgroundElement . (completion-selected code-inline)) - (border . (tool-border divider sidebar-divider msg-border-user msg-border-assistant msg-border-expert msg-border-tool msg-border-error)) - (borderActive . (dialog-border sidebar-selected)) - (borderSubtle . (horizontal-rule blockquote-border thinking-border)) - (diffAdded . (diff-added)) - (diffRemoved . (diff-removed)) - (diffContext . (diff-context)) - (diffHunkHeader . (diff-hunk)) - (markdownHeading . (heading)) - (markdownLink . (link)) - (markdownCode . (code-block code-inline)) - (markdownBlockQuote . (blockquote)) - (markdownEmph . (italic)) - (markdownStrong . (bold)) - (markdownHorizontalRule . (horizontal-rule)) - (markdownListItem . (list-bullet)) - (syntaxComment . (syntax-comment)) - (syntaxKeyword . (syntax-keyword)) - (syntaxString . (syntax-string)) - (syntaxNumber . (syntax-number)) - (syntaxFunction . (syntax-function)) - (syntaxVariable . (syntax-variable)) - (syntaxType . (syntax-type)) - (syntaxOperator . (syntax-operator)) - (syntaxPunctuation . (syntax-punctuation)))) + (else #f))) + +(def (parse-hex-color s) + (or (parse-hex-color/maybe s) TB_DEFAULT)) + +(def (ansi-color->tb n) + (case n + ((0) TB_BLACK) + ((1) TB_RED) + ((2) TB_GREEN) + ((3) TB_YELLOW) + ((4) TB_BLUE) + ((5) TB_MAGENTA) + ((6) TB_CYAN) + ((7) TB_WHITE) + ((8) (bitwise-ior TB_BLACK TB_BOLD)) + ((9) (bitwise-ior TB_RED TB_BOLD)) + ((10) (bitwise-ior TB_GREEN TB_BOLD)) + ((11) (bitwise-ior TB_YELLOW TB_BOLD)) + ((12) (bitwise-ior TB_BLUE TB_BOLD)) + ((13) (bitwise-ior TB_MAGENTA TB_BOLD)) + ((14) (bitwise-ior TB_CYAN TB_BOLD)) + ((15) (bitwise-ior TB_WHITE TB_BOLD)) + (else #f))) + +(def *opencode-color-keys* + '((primary . "primary") + (secondary . "secondary") + (accent . "accent") + (error . "error") + (warning . "warning") + (success . "success") + (info . "info") + (text . "text") + (textMuted . "textMuted") + (background . "background") + (backgroundPanel . "backgroundPanel") + (backgroundElement . "backgroundElement") + (border . "border") + (borderActive . "borderActive") + (borderSubtle . "borderSubtle") + (diffAdded . "diffAdded") + (diffRemoved . "diffRemoved") + (diffContext . "diffContext") + (diffHunkHeader . "diffHunkHeader") + (diffAddedBg . "diffAddedBg") + (diffRemovedBg . "diffRemovedBg") + (diffContextBg . "diffContextBg") + (markdownText . "markdownText") + (markdownHeading . "markdownHeading") + (markdownLink . "markdownLink") + (markdownCode . "markdownCode") + (markdownBlockQuote . "markdownBlockQuote") + (markdownEmph . "markdownEmph") + (markdownStrong . "markdownStrong") + (markdownHorizontalRule . "markdownHorizontalRule") + (markdownListItem . "markdownListItem") + (markdownCodeBlock . "markdownCodeBlock") + (syntaxComment . "syntaxComment") + (syntaxKeyword . "syntaxKeyword") + (syntaxString . "syntaxString") + (syntaxNumber . "syntaxNumber") + (syntaxFunction . "syntaxFunction") + (syntaxVariable . "syntaxVariable") + (syntaxType . "syntaxType") + (syntaxOperator . "syntaxOperator") + (syntaxPunctuation . "syntaxPunctuation"))) + +(def (make-color-table pairs) + (let ((ht (make-hash-table))) + (for-each (lambda (p) (hash-put! ht (car p) (cdr p))) pairs) + ht)) (def (resolve-defs defs val) - (if (and (string? val) (string-prefix? "{" val)) - (let ((obj (try (string->json-object val) (catch (e) #f)))) - (and (hash-table? obj) (let ((ref (hash-get obj "$ref"))) - (and ref (hash-get defs ref))))) - val)) + (cond + ((and (string? val) (hash-table? defs) (hash-key? defs val)) + (hash-get defs val)) + ((and (string? val) (string-prefix? "{" val)) + (let ((obj (try (string->json-object val) (catch (e) #f)))) + (and (hash-table? obj) (let ((ref (hash-get obj "$ref"))) + (and ref (hash-get defs ref)))))) + (else val))) -(def (json-color->face val defs variant) +(def (json-color->color val defs variant) (cond ((hash-table? val) (let ((c (or (hash-get val variant) (hash-get val "dark") (hash-get val "light")))) - (if c - (json-color->face c defs variant) - (make-face TB_DEFAULT TB_DEFAULT #f #f #f)))) + (and c (json-color->color c defs variant)))) + ((number? val) + (ansi-color->tb val)) ((string? val) - (let ((resolved (resolve-defs defs val))) - (if (string? resolved) - (let ((fg (parse-hex-color resolved))) - (make-face fg TB_DEFAULT #f #f #f)) - (if (hash-table? resolved) - (json-color->face resolved defs variant) - (make-face TB_DEFAULT TB_DEFAULT #f #f #f))))) - (else (make-face TB_DEFAULT TB_DEFAULT #f #f #f)))) + (cond + ((equal? val "none") #f) + (else + (let ((hex (parse-hex-color/maybe val))) + (if hex + hex + (let ((resolved (resolve-defs defs val))) + (if (equal? resolved val) + #f + (json-color->color resolved defs variant)))))))) + (else #f))) + +(def (json-theme->colors theme-obj defs variant) + (let ((colors (make-hash-table))) + (for-each + (lambda (entry) + (let* ((sym (car entry)) + (key (cdr entry)) + (val (hash-get theme-obj key))) + (when val + (let ((c (json-color->color val defs variant))) + (when c (hash-put! colors sym c)))))) + *opencode-color-keys*) + colors)) + +(def (color-ref colors key fallback) + (let ((v (hash-get colors key))) + (if (number? v) v fallback))) + +(def (color-or-default c) + (if (number? c) c TB_DEFAULT)) + +(def (color-channel c shift) + (bitwise-and (bitwise-arithmetic-shift-right c shift) #xff)) + +(def (color-brightness c) + (/ (+ (* 299 (color-channel c 16)) + (* 587 (color-channel c 8)) + (* 114 (color-channel c 0))) + 1000.0)) + +(def (readable-color? fg bg) + (or (not (number? fg)) + (not (number? bg)) + (>= (abs (- (color-brightness fg) (color-brightness bg))) 50.0))) + +(def (auto-contrast-color bg) + (if (and (number? bg) (> (color-brightness bg) 140.0)) + (rgb #x1a #x1a #x1a) + (rgb #xee #xee #xee))) + +(def (ensure-readable fg bg fallback-fg) + (cond + ((readable-color? fg bg) fg) + ((readable-color? fallback-fg bg) fallback-fg) + (else (auto-contrast-color bg)))) + +(def (theme-face colors fg bg bold? italic? underline?) + (let ((text (color-ref colors 'text (rgb #xd4 #xd4 #xd4)))) + (make-face (color-or-default (ensure-readable fg bg text)) + (color-or-default bg) + bold? italic? underline?))) + +(def (make-opencode-style-theme colors) + (let* ((bg (color-ref colors 'background (rgb #x1e #x1e #x1e))) + (panel (color-ref colors 'backgroundPanel (rgb #x25 #x25 #x25))) + (element (color-ref colors 'backgroundElement (rgb #x2d #x2d #x2d))) + (text (color-ref colors 'text (rgb #xd4 #xd4 #xd4))) + (muted (color-ref colors 'textMuted (rgb #x80 #x80 #x80))) + (primary (color-ref colors 'primary (rgb #x56 #x9c #xd6))) + (secondary (color-ref colors 'secondary primary)) + (accent (color-ref colors 'accent (rgb #x4e #xc9 #xb0))) + (error (color-ref colors 'error (rgb #xf4 #x47 #x47))) + (warning (color-ref colors 'warning (rgb #xff #xcc #x00))) + (success (color-ref colors 'success (rgb #x4e #xc9 #xb0))) + (info (color-ref colors 'info primary)) + (border (color-ref colors 'border (rgb #x3a #x3a #x3a))) + (border-active (color-ref colors 'borderActive primary)) + (border-subtle (color-ref colors 'borderSubtle border)) + (diff-added (color-ref colors 'diffAdded success)) + (diff-removed (color-ref colors 'diffRemoved error)) + (diff-context (color-ref colors 'diffContext muted)) + (diff-hunk (color-ref colors 'diffHunkHeader primary)) + (diff-added-bg (color-ref colors 'diffAddedBg panel)) + (diff-removed-bg (color-ref colors 'diffRemovedBg panel)) + (diff-context-bg (color-ref colors 'diffContextBg bg)) + (md-text (color-ref colors 'markdownText text)) + (md-heading (color-ref colors 'markdownHeading secondary)) + (md-link (color-ref colors 'markdownLink primary)) + (md-code (color-ref colors 'markdownCode success)) + (md-quote (color-ref colors 'markdownBlockQuote muted)) + (md-emph (color-ref colors 'markdownEmph warning)) + (md-strong (color-ref colors 'markdownStrong accent)) + (md-rule (color-ref colors 'markdownHorizontalRule border-subtle)) + (md-list (color-ref colors 'markdownListItem primary)) + (md-code-block (color-ref colors 'markdownCodeBlock text)) + (syn-comment (color-ref colors 'syntaxComment muted)) + (syn-keyword (color-ref colors 'syntaxKeyword secondary)) + (syn-string (color-ref colors 'syntaxString success)) + (syn-number (color-ref colors 'syntaxNumber warning)) + (syn-function (color-ref colors 'syntaxFunction primary)) + (syn-variable (color-ref colors 'syntaxVariable accent)) + (syn-type (color-ref colors 'syntaxType info)) + (syn-operator (color-ref colors 'syntaxOperator text)) + (syn-punctuation (color-ref colors 'syntaxPunctuation muted)) + (f (lambda (fg bg bold? italic? underline?) + (theme-face colors fg bg bold? italic? underline?)))) + (make-theme-table + `((default . ,(f text bg #f #f #f)) + (user-label . ,(f primary bg #t #f #f)) + (user-text . ,(f text bg #f #f #f)) + (assistant-text . ,(f md-text bg #f #f #f)) + (tool-name . ,(f accent bg #t #f #f)) + (tool-border . ,(f border-subtle bg #f #f #f)) + (tool-result . ,(f muted bg #f #f #f)) + (code-block . ,(f md-code-block element #f #f #f)) + (code-inline . ,(f md-code element #f #f #f)) + (heading . ,(f md-heading bg #t #f #f)) + (bold . ,(f md-strong bg #t #f #f)) + (italic . ,(f md-emph bg #f #t #f)) + (link . ,(f md-link bg #f #f #t)) + (list-bullet . ,(f md-list bg #f #f #f)) + (blockquote . ,(f md-quote bg #f #t #f)) + (blockquote-border . ,(f border-subtle bg #f #f #f)) + (horizontal-rule . ,(f md-rule bg #f #f #f)) + (diff-added . ,(f diff-added diff-added-bg #f #f #f)) + (diff-removed . ,(f diff-removed diff-removed-bg #f #f #f)) + (diff-context . ,(f diff-context diff-context-bg #f #f #f)) + (diff-header . ,(f text element #t #f #f)) + (diff-hunk . ,(f diff-hunk diff-context-bg #f #f #f)) + (status-bar . ,(f text panel #f #f #f)) + (status-provider . ,(f primary panel #t #f #f)) + (status-model . ,(f text panel #f #f #f)) + (status-tokens . ,(f muted panel #f #f #f)) + (status-cost . ,(f warning panel #f #f #f)) + (status-dim . ,(f muted panel #f #f #f)) + (status-mode-build . ,(f bg success #t #f #f)) + (status-mode-plan . ,(f bg warning #t #f #f)) + (sidebar-title . ,(f text panel #t #f #f)) + (sidebar-item . ,(f muted panel #f #f #f)) + (sidebar-selected . ,(f text element #f #f #f)) + (sidebar-bg . ,(f muted panel #f #f #f)) + (sidebar-divider . ,(f border-subtle bg #f #f #f)) + (input-prompt . ,(f primary bg #t #f #f)) + (input-text . ,(f text bg #f #f #f)) + (completion-item . ,(f text panel #f #f #f)) + (completion-selected . ,(f text element #f #f #f)) + (completion-match . ,(f primary panel #t #f #f)) + (dialog-border . ,(f border-active panel #f #f #f)) + (dialog-title . ,(f text panel #t #f #f)) + (dialog-text . ,(f text panel #f #f #f)) + (dialog-action . ,(f success panel #t #f #f)) + (error . ,(f error bg #t #f #f)) + (dim . ,(f muted bg #f #f #f)) + (spinner . ,(f accent bg #f #f #f)) + (divider . ,(f border-subtle bg #f #f #f)) + (toast-error-border . ,(f error bg #f #f #f)) + (toast-warning-border . ,(f warning bg #f #f #f)) + (toast-success-border . ,(f success bg #f #f #f)) + (toast-info-border . ,(f info bg #f #f #f)) + (toast-bg . ,(f text panel #f #f #f)) + (toast-title . ,(f text panel #t #f #f)) + (toast-message . ,(f text panel #f #f #f)) + (thinking-text . ,(f muted bg #f #t #f)) + (thinking-border . ,(f border-subtle bg #f #f #f)) + (thinking-header . ,(f muted bg #f #t #f)) + (syntax-comment . ,(f syn-comment bg #f #f #f)) + (syntax-keyword . ,(f syn-keyword bg #t #f #f)) + (syntax-function . ,(f syn-function bg #f #f #f)) + (syntax-variable . ,(f syn-variable bg #f #f #f)) + (syntax-string . ,(f syn-string bg #f #f #f)) + (syntax-number . ,(f syn-number bg #f #f #f)) + (syntax-type . ,(f syn-type bg #f #t #f)) + (syntax-operator . ,(f syn-operator bg #f #f #f)) + (syntax-punctuation . ,(f syn-punctuation bg #f #f #f)) + (selection-bg . ,(f text element #f #f #f)) + (leader-indicator . ,(f warning bg #t #f #f)) + (leader-dim . ,(f muted bg #f #f #f)) + (palette-match . ,(f warning panel #t #f #f)) + (palette-keybind . ,(f muted panel #f #f #f)) + (palette-category . ,(f accent panel #t #f #f)) + (home-logo-fg . ,(f primary bg #t #f #f)) + (home-logo-shadow . ,(f border-subtle bg #f #f #f)) + (home-tip-text . ,(f muted bg #f #f #f)) + (home-tip-icon . ,(f warning bg #f #f #f)) + (msg-border-user . ,(f primary bg #f #f #f)) + (msg-border-assistant . ,(f accent bg #f #f #f)) + (msg-border-expert . ,(f secondary bg #f #f #f)) + (msg-border-tool . ,(f border bg #f #f #f)) + (msg-border-error . ,(f error bg #f #f #f)))))) + +(def (register-opencode-style-theme! name pairs) + (register-theme! name (make-opencode-style-theme (make-color-table pairs)))) + +(def (register-core-themes!) + (register-theme! "dark" theme-dark) + (register-theme! "light" theme-light) + (register-theme! "gruvbox" theme-gruvbox) + (register-opencode-style-theme! "opencode-dark" + `((background . ,(rgb #x0a #x0a #x0a)) (backgroundPanel . ,(rgb #x14 #x14 #x14)) + (backgroundElement . ,(rgb #x1e #x1e #x1e)) (text . ,(rgb #xee #xee #xee)) + (textMuted . ,(rgb #x80 #x80 #x80)) (primary . ,(rgb #xfa #xb2 #x83)) + (secondary . ,(rgb #x5c #x9c #xf5)) (accent . ,(rgb #x9d #x7c #xd8)) + (error . ,(rgb #xe0 #x6c #x75)) (warning . ,(rgb #xf5 #xa7 #x42)) + (success . ,(rgb #x7f #xd8 #x8f)) (info . ,(rgb #x56 #xb6 #xc2)) + (border . ,(rgb #x48 #x48 #x48)) (borderActive . ,(rgb #x60 #x60 #x60)) + (borderSubtle . ,(rgb #x3c #x3c #x3c)) + (diffAddedBg . ,(rgb #x20 #x30 #x3b)) (diffRemovedBg . ,(rgb #x37 #x22 #x2c)) + (syntaxComment . ,(rgb #x80 #x80 #x80)) (syntaxKeyword . ,(rgb #x9d #x7c #xd8)) + (syntaxFunction . ,(rgb #xfa #xb2 #x83)) (syntaxVariable . ,(rgb #xe0 #x6c #x75)) + (syntaxString . ,(rgb #x7f #xd8 #x8f)) (syntaxNumber . ,(rgb #xf5 #xa7 #x42)) + (syntaxType . ,(rgb #xe5 #xc0 #x7b)) (syntaxOperator . ,(rgb #x56 #xb6 #xc2)))) + (register-opencode-style-theme! "opencode-light" + `((background . ,(rgb #xff #xff #xff)) (backgroundPanel . ,(rgb #xfa #xfa #xfa)) + (backgroundElement . ,(rgb #xf5 #xf5 #xf5)) (text . ,(rgb #x1a #x1a #x1a)) + (textMuted . ,(rgb #x8a #x8a #x8a)) (primary . ,(rgb #x3b #x7d #xd8)) + (secondary . ,(rgb #x7b #x5b #xb6)) (accent . ,(rgb #xd6 #x8c #x27)) + (error . ,(rgb #xd1 #x38 #x3d)) (warning . ,(rgb #xd6 #x8c #x27)) + (success . ,(rgb #x3d #x9a #x57)) (info . ,(rgb #x31 #x87 #x95)) + (border . ,(rgb #xb8 #xb8 #xb8)) (borderActive . ,(rgb #xa0 #xa0 #xa0)) + (borderSubtle . ,(rgb #xd4 #xd4 #xd4)) + (diffAddedBg . ,(rgb #xd5 #xe5 #xd5)) (diffRemovedBg . ,(rgb #xf7 #xd8 #xdb)) + (syntaxComment . ,(rgb #x8a #x8a #x8a)) (syntaxKeyword . ,(rgb #x7b #x5b #xb6)) + (syntaxFunction . ,(rgb #x3b #x7d #xd8)) (syntaxVariable . ,(rgb #xd1 #x38 #x3d)) + (syntaxString . ,(rgb #x3d #x9a #x57)) (syntaxNumber . ,(rgb #xd6 #x8c #x27)) + (syntaxType . ,(rgb #xb0 #x85 #x1f)) (syntaxOperator . ,(rgb #x31 #x87 #x95)))) + (register-opencode-style-theme! "tokyonight-dark" + `((background . ,(rgb #x1a #x1b #x26)) (backgroundPanel . ,(rgb #x1e #x20 #x30)) + (backgroundElement . ,(rgb #x22 #x24 #x36)) (text . ,(rgb #xc8 #xd3 #xf5)) + (textMuted . ,(rgb #x82 #x8b #xb8)) (primary . ,(rgb #x82 #xaa #xff)) + (secondary . ,(rgb #xc0 #x99 #xff)) (accent . ,(rgb #xff #x96 #x6c)) + (error . ,(rgb #xff #x75 #x7f)) (warning . ,(rgb #xff #xc7 #x77)) + (success . ,(rgb #xc3 #xe8 #x8d)) (info . ,(rgb #x86 #xe1 #xfc)) + (border . ,(rgb #x73 #x7a #xa2)) (borderActive . ,(rgb #x90 #x99 #xb2)) + (borderSubtle . ,(rgb #x54 #x5c #x7e)) + (diffAddedBg . ,(rgb #x20 #x30 #x3b)) (diffRemovedBg . ,(rgb #x37 #x22 #x2c)) + (syntaxComment . ,(rgb #x82 #x8b #xb8)) (syntaxKeyword . ,(rgb #xc0 #x99 #xff)) + (syntaxFunction . ,(rgb #x82 #xaa #xff)) (syntaxVariable . ,(rgb #xff #x75 #x7f)) + (syntaxString . ,(rgb #xc3 #xe8 #x8d)) (syntaxNumber . ,(rgb #xff #x96 #x6c)) + (syntaxType . ,(rgb #xff #xc7 #x77)) (syntaxOperator . ,(rgb #x86 #xe1 #xfc)))) + (register-opencode-style-theme! "github-dark" + `((background . ,(rgb #x0d #x11 #x17)) (backgroundPanel . ,(rgb #x01 #x04 #x09)) + (backgroundElement . ,(rgb #x16 #x1b #x22)) (text . ,(rgb #xc9 #xd1 #xd9)) + (textMuted . ,(rgb #x8b #x94 #x9e)) (primary . ,(rgb #x58 #xa6 #xff)) + (secondary . ,(rgb #xbc #x8c #xff)) (accent . ,(rgb #x39 #xc5 #xcf)) + (error . ,(rgb #xf8 #x51 #x49)) (warning . ,(rgb #xe3 #xb3 #x41)) + (success . ,(rgb #x3f #xb9 #x50)) (info . ,(rgb #xd2 #x99 #x22)) + (border . ,(rgb #x30 #x36 #x3d)) (borderActive . ,(rgb #x58 #xa6 #xff)) + (borderSubtle . ,(rgb #x21 #x26 #x2d)) + (diffAddedBg . ,(rgb #x03 #x3a #x16)) (diffRemovedBg . ,(rgb #x67 #x06 #x0c)) + (syntaxComment . ,(rgb #x8b #x94 #x9e)) (syntaxKeyword . ,(rgb #xff #x7b #x72)) + (syntaxFunction . ,(rgb #xbc #x8c #xff)) (syntaxVariable . ,(rgb #xd2 #x99 #x22)) + (syntaxString . ,(rgb #x39 #xc5 #xcf)) (syntaxNumber . ,(rgb #x58 #xa6 #xff)) + (syntaxType . ,(rgb #xd2 #x99 #x22)) (syntaxOperator . ,(rgb #xff #x7b #x72)))) + (register-opencode-style-theme! "github-light" + `((background . ,(rgb #xff #xff #xff)) (backgroundPanel . ,(rgb #xf6 #xf8 #xfa)) + (backgroundElement . ,(rgb #xf0 #xf3 #xf6)) (text . ,(rgb #x24 #x29 #x2f)) + (textMuted . ,(rgb #x57 #x60 #x6a)) (primary . ,(rgb #x09 #x69 #xda)) + (secondary . ,(rgb #x82 #x50 #xdf)) (accent . ,(rgb #x1b #x7c #x83)) + (error . ,(rgb #xcf #x22 #x2e)) (warning . ,(rgb #x9a #x67 #x00)) + (success . ,(rgb #x1a #x7f #x37)) (info . ,(rgb #xbc #x4c #x00)) + (border . ,(rgb #xd0 #xd7 #xde)) (borderActive . ,(rgb #x09 #x69 #xda)) + (borderSubtle . ,(rgb #xd8 #xde #xe4)) + (diffAddedBg . ,(rgb #xda #xfb #xe1)) (diffRemovedBg . ,(rgb #xff #xeb #xe9)) + (syntaxComment . ,(rgb #x57 #x60 #x6a)) (syntaxKeyword . ,(rgb #xcf #x22 #x2e)) + (syntaxFunction . ,(rgb #x82 #x50 #xdf)) (syntaxVariable . ,(rgb #xbc #x4c #x00)) + (syntaxString . ,(rgb #x09 #x69 #xda)) (syntaxNumber . ,(rgb #x1b #x7c #x83)) + (syntaxType . ,(rgb #xbc #x4c #x00)) (syntaxOperator . ,(rgb #xcf #x22 #x2e)))) + (register-opencode-style-theme! "nord-dark" + `((background . ,(rgb #x2e #x34 #x40)) (backgroundPanel . ,(rgb #x3b #x42 #x52)) + (backgroundElement . ,(rgb #x43 #x4c #x5e)) (text . ,(rgb #xec #xef #xf4)) + (textMuted . ,(rgb #x8b #x95 #xa7)) (primary . ,(rgb #x88 #xc0 #xd0)) + (secondary . ,(rgb #x81 #xa1 #xc1)) (accent . ,(rgb #x8f #xbc #xbb)) + (error . ,(rgb #xbf #x61 #x6a)) (warning . ,(rgb #xd0 #x87 #x70)) + (success . ,(rgb #xa3 #xbe #x8c)) (info . ,(rgb #x88 #xc0 #xd0)) + (border . ,(rgb #x43 #x4c #x5e)) (borderActive . ,(rgb #x4c #x56 #x6a)) + (borderSubtle . ,(rgb #x43 #x4c #x5e)) + (syntaxComment . ,(rgb #x8b #x95 #xa7)) (syntaxKeyword . ,(rgb #x81 #xa1 #xc1)) + (syntaxFunction . ,(rgb #x88 #xc0 #xd0)) (syntaxVariable . ,(rgb #x8f #xbc #xbb)) + (syntaxString . ,(rgb #xa3 #xbe #x8c)) (syntaxNumber . ,(rgb #xb4 #x8e #xad)) + (syntaxType . ,(rgb #x8f #xbc #xbb)) (syntaxOperator . ,(rgb #x81 #xa1 #xc1)))) + (register-opencode-style-theme! "nord-light" + `((background . ,(rgb #xec #xef #xf4)) (backgroundPanel . ,(rgb #xe5 #xe9 #xf0)) + (backgroundElement . ,(rgb #xd8 #xde #xe9)) (text . ,(rgb #x2e #x34 #x40)) + (textMuted . ,(rgb #x3b #x42 #x52)) (primary . ,(rgb #x5e #x81 #xac)) + (secondary . ,(rgb #x81 #xa1 #xc1)) (accent . ,(rgb #x8f #xbc #xbb)) + (error . ,(rgb #xbf #x61 #x6a)) (warning . ,(rgb #xd0 #x87 #x70)) + (success . ,(rgb #xa3 #xbe #x8c)) (info . ,(rgb #x5e #x81 #xac)) + (border . ,(rgb #x4c #x56 #x6a)) (borderActive . ,(rgb #x43 #x4c #x5e)) + (borderSubtle . ,(rgb #x4c #x56 #x6a)) + (syntaxComment . ,(rgb #x4c #x56 #x6a)) (syntaxKeyword . ,(rgb #x81 #xa1 #xc1)) + (syntaxFunction . ,(rgb #x88 #xc0 #xd0)) (syntaxVariable . ,(rgb #x8f #xbc #xbb)) + (syntaxString . ,(rgb #xa3 #xbe #x8c)) (syntaxNumber . ,(rgb #xb4 #x8e #xad)) + (syntaxType . ,(rgb #x8f #xbc #xbb)) (syntaxOperator . ,(rgb #x81 #xa1 #xc1)))) + (register-opencode-style-theme! "catppuccin-dark" + `((background . ,(rgb #x1e #x1e #x2e)) (backgroundPanel . ,(rgb #x18 #x18 #x25)) + (backgroundElement . ,(rgb #x11 #x11 #x1b)) (text . ,(rgb #xcd #xd6 #xf4)) + (textMuted . ,(rgb #x93 #x99 #xb2)) (primary . ,(rgb #x89 #xb4 #xfa)) + (secondary . ,(rgb #xcb #xa6 #xf7)) (accent . ,(rgb #xf5 #xc2 #xe7)) + (error . ,(rgb #xf3 #x8b #xa8)) (warning . ,(rgb #xf9 #xe2 #xaf)) + (success . ,(rgb #xa6 #xe3 #xa1)) (info . ,(rgb #x94 #xe2 #xd5)) + (border . ,(rgb #x31 #x32 #x44)) (borderActive . ,(rgb #x45 #x47 #x5a)) + (borderSubtle . ,(rgb #x58 #x5b #x70)) + (diffAddedBg . ,(rgb #x24 #x31 #x2b)) (diffRemovedBg . ,(rgb #x3c #x2a #x32)) + (syntaxComment . ,(rgb #x93 #x99 #xb2)) (syntaxKeyword . ,(rgb #xcb #xa6 #xf7)) + (syntaxFunction . ,(rgb #x89 #xb4 #xfa)) (syntaxVariable . ,(rgb #xf3 #x8b #xa8)) + (syntaxString . ,(rgb #xa6 #xe3 #xa1)) (syntaxNumber . ,(rgb #xfa #xb3 #x87)) + (syntaxType . ,(rgb #xf9 #xe2 #xaf)) (syntaxOperator . ,(rgb #x89 #xdc #xeb)))) + (register-opencode-style-theme! "catppuccin-light" + `((background . ,(rgb #xef #xf1 #xf5)) (backgroundPanel . ,(rgb #xe6 #xe9 #xef)) + (backgroundElement . ,(rgb #xdc #xe0 #xe8)) (text . ,(rgb #x4c #x4f #x69)) + (textMuted . ,(rgb #x7c #x7f #x93)) (primary . ,(rgb #x1e #x66 #xf5)) + (secondary . ,(rgb #x88 #x39 #xef)) (accent . ,(rgb #xea #x76 #xcb)) + (error . ,(rgb #xd2 #x0f #x39)) (warning . ,(rgb #xdf #x8e #x1d)) + (success . ,(rgb #x40 #xa0 #x2b)) (info . ,(rgb #x17 #x92 #x99)) + (border . ,(rgb #xcc #xd0 #xda)) (borderActive . ,(rgb #xbc #xc0 #xcc)) + (borderSubtle . ,(rgb #xac #xb0 #xbe)) + (diffAddedBg . ,(rgb #xd6 #xf0 #xd9)) (diffRemovedBg . ,(rgb #xf6 #xdf #xe2)) + (syntaxComment . ,(rgb #x7c #x7f #x93)) (syntaxKeyword . ,(rgb #x88 #x39 #xef)) + (syntaxFunction . ,(rgb #x1e #x66 #xf5)) (syntaxVariable . ,(rgb #xd2 #x0f #x39)) + (syntaxString . ,(rgb #x40 #xa0 #x2b)) (syntaxNumber . ,(rgb #xfe #x64 #x0b)) + (syntaxType . ,(rgb #xdf #x8e #x1d)) (syntaxOperator . ,(rgb #x04 #xa5 #xe5))))) + +(register-core-themes!) (def (load-theme-json! path) (try (let* ((obj (call-with-input-file path read-json)) (defs (or (hash-get obj "defs") (make-hash-table))) (theme-obj (or (hash-get obj "theme") obj)) - (name (or (hash-get obj "name") "custom"))) + (name (or (hash-get obj "name") + (path-strip-extension (path-strip-directory path))))) (for-each (lambda (variant) - (let ((theme-ht (make-hash-table)) - (theme-name (string-append name "-" variant))) - (hash-for-each - (lambda (slot-key val) - (let ((mapped (assoc slot-key *opencode-slot-map*))) - (when mapped - (let ((face (json-color->face val defs variant))) - (for-each - (lambda (jcode-slot) - (hash-put! theme-ht jcode-slot face)) - (cdr mapped)))))) - theme-obj) - (unless (hash-get theme-ht 'default) - (hash-put! theme-ht 'default - (make-face (rgb #xd4 #xd4 #xd4) - (if (equal? variant "light") (rgb #xff #xff #xff) (rgb #x1e #x1e #x1e)) - #f #f #f))) - (register-theme! theme-name theme-ht))) + (let* ((colors (json-theme->colors theme-obj defs variant)) + (theme-name (string-append name "-" variant))) + (register-theme! theme-name (make-opencode-style-theme colors)))) '("dark" "light"))) (catch (e) #f))) --- a/src/jcode/ui/tui.ss +++ b/src/jcode/ui/tui.ss @@ -229,9 +229,11 @@ (tui-log "tui-main: setting input-mode=~a output-mode=~a" in-mode TB_OUTPUT_TRUECOLOR) (tb-set-input-mode! in-mode) (tb-set-output-mode! TB_OUTPUT_TRUECOLOR)) - (set-theme! theme-dark) (load-themes-from-dir! (path-join (jcode-home) "themes")) (load-themes-from-dir! (path-join (current-directory) ".jcode" "themes")) + (let ((theme-name (or (config-ref "tui" "theme") "opencode-dark"))) + (unless (set-theme-by-name! theme-name) + (set-theme-by-name! "opencode-dark"))) (let* ((w (tb-width)) (h (tb-height)) (state (make-fresh-state w h)) @@ -541,7 +543,8 @@ " /jcode Switch back to the main jcode tab" " /close-tab Close the current external tab" " /expert <q> Force this prompt to the configured expert model" - " /theme Cycle theme (Ctrl-T)" + " /themes List available themes" + " /theme [name] Cycle theme, or switch to a named theme" " /sidebar Toggle sidebar (Ctrl-B)" " /quit Exit" "" @@ -587,9 +590,29 @@ (switch-provider! state p))) ((equal? cmd "provider") (handle-provider-popup! state)) + ((equal? cmd "themes") + (add-message! state + (msg-block-system + (string-append + "Current theme: " (or (current-theme-name) "(custom)") "\n" + "Available themes: " (string-join (get-registered-themes) ", "))))) ((equal? cmd "theme") (cycle-theme!) - (add-message! state (msg-block-system "Theme cycled."))) + (add-message! state + (msg-block-system + (string-append "Theme: " (or (current-theme-name) "(custom)"))))) + ((string-prefix? "theme " cmd) + (let ((name (string-trim (substring cmd 6 (string-length cmd))))) + (cond + ((set-theme-by-name! name) + (add-message! state + (msg-block-system (string-append "Theme: " name)))) + (else + (add-message! state + (msg-block-error + (string-append + "Unknown theme: " name "\n" + "Available themes: " (string-join (get-registered-themes) ", ")))))))) ((equal? cmd "mcp") (let* ((mcp-tools (filter (lambda (n) (string-prefix? "mcp_" n)) (list-tools))) (hide? (not *mcp-disabled*)))