@brimveyn/aimux-config 0.5.3 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/actions.ts +58 -0
- package/src/defaults.ts +3 -0
- package/src/index.ts +21 -31
- package/src/resolver.ts +1 -1
- package/src/tui/index.ts +5 -0
- package/src/tui/registry.ts +95 -0
- package/src/tui/resolve.ts +156 -0
- package/src/tui/shiki.ts +149 -0
- package/src/tui/themes/aimux.json +110 -0
- package/src/tui/themes/aura.json +69 -0
- package/src/tui/themes/ayu.json +80 -0
- package/src/tui/themes/carbonfox.json +248 -0
- package/src/tui/themes/catppuccin-frappe.json +230 -0
- package/src/tui/themes/catppuccin-macchiato.json +230 -0
- package/src/tui/themes/catppuccin.json +112 -0
- package/src/tui/themes/cobalt2.json +225 -0
- package/src/tui/themes/cursor.json +249 -0
- package/src/tui/themes/dracula.json +219 -0
- package/src/tui/themes/everforest.json +241 -0
- package/src/tui/themes/flexoki.json +237 -0
- package/src/tui/themes/github.json +233 -0
- package/src/tui/themes/gruvbox.json +242 -0
- package/src/tui/themes/kanagawa.json +77 -0
- package/src/tui/themes/lucent-orng.json +234 -0
- package/src/tui/themes/material.json +235 -0
- package/src/tui/themes/matrix.json +77 -0
- package/src/tui/themes/mercury.json +252 -0
- package/src/tui/themes/monokai.json +221 -0
- package/src/tui/themes/nightowl.json +221 -0
- package/src/tui/themes/nord.json +223 -0
- package/src/tui/themes/one-dark.json +84 -0
- package/src/tui/themes/opencode.json +245 -0
- package/src/tui/themes/orng.json +249 -0
- package/src/tui/themes/osaka-jade.json +93 -0
- package/src/tui/themes/palenight.json +222 -0
- package/src/tui/themes/rosepine.json +234 -0
- package/src/tui/themes/solarized.json +223 -0
- package/src/tui/themes/synthwave84.json +226 -0
- package/src/tui/themes/tokyonight.json +243 -0
- package/src/tui/themes/vercel.json +245 -0
- package/src/tui/themes/vesper.json +218 -0
- package/src/tui/themes/zenburn.json +223 -0
- package/src/tui/tokens.ts +111 -0
- package/src/tui/types.ts +30 -0
- package/src/types.ts +9 -35
- package/src/house-themes.ts +0 -55
- package/src/neutral-scale.ts +0 -75
- package/src/oklch.ts +0 -164
- package/src/palette-to-shiki.ts +0 -134
- package/src/palette-utils.ts +0 -130
- package/src/themes/opencode.ts +0 -1460
- package/src/themes.ts +0 -30
package/package.json
CHANGED
package/src/actions.ts
CHANGED
|
@@ -547,6 +547,64 @@ export const gitDestructiveSelected: ActionFn = (ctx: ModeContext) => {
|
|
|
547
547
|
return r([{ path: file.path, type: 'git-mode-set-pending-delete' }])
|
|
548
548
|
}
|
|
549
549
|
|
|
550
|
+
export const gitToggleFoldAll: ActionFn = (ctx: ModeContext) => {
|
|
551
|
+
const file = selectedGitFile(ctx)
|
|
552
|
+
if (!file) return r([])
|
|
553
|
+
const key = gitFileKey(file.section, file.path)
|
|
554
|
+
return r([{ key, type: 'git-mode-fold-toggle-all' }])
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export const gitStageAll: ActionFn = (ctx: ModeContext) => {
|
|
558
|
+
if (ctx.state.gitMode.headOffset > 0) {
|
|
559
|
+
return r([
|
|
560
|
+
{
|
|
561
|
+
message: 'disabled while viewing HEAD~N (press 0 or [ to return)',
|
|
562
|
+
type: 'git-mode-set-message',
|
|
563
|
+
},
|
|
564
|
+
])
|
|
565
|
+
}
|
|
566
|
+
const files = ctx.state.gitPanel.files
|
|
567
|
+
const pending = clearPendingDelete(ctx)
|
|
568
|
+
if (files.length === 0) return r(pending)
|
|
569
|
+
const actions: AppAction[] = [...pending]
|
|
570
|
+
for (const f of files) {
|
|
571
|
+
if (f.section === 'staged') continue
|
|
572
|
+
actions.push({
|
|
573
|
+
fromSection: f.section,
|
|
574
|
+
path: f.path,
|
|
575
|
+
toSection: 'staged',
|
|
576
|
+
type: 'git-mode-optimistic-move',
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
return r(actions, [{ type: 'git-stage-all' }])
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export const gitUnstageAll: ActionFn = (ctx: ModeContext) => {
|
|
583
|
+
if (ctx.state.gitMode.headOffset > 0) {
|
|
584
|
+
return r([
|
|
585
|
+
{
|
|
586
|
+
message: 'disabled while viewing HEAD~N (press 0 or [ to return)',
|
|
587
|
+
type: 'git-mode-set-message',
|
|
588
|
+
},
|
|
589
|
+
])
|
|
590
|
+
}
|
|
591
|
+
const files = ctx.state.gitPanel.files
|
|
592
|
+
const pending = clearPendingDelete(ctx)
|
|
593
|
+
if (files.length === 0) return r(pending)
|
|
594
|
+
const actions: AppAction[] = [...pending]
|
|
595
|
+
for (const f of files) {
|
|
596
|
+
if (f.section !== 'staged') continue
|
|
597
|
+
const toSection = f.status === 'A' ? 'untracked' : 'unstaged'
|
|
598
|
+
actions.push({
|
|
599
|
+
fromSection: 'staged',
|
|
600
|
+
path: f.path,
|
|
601
|
+
toSection,
|
|
602
|
+
type: 'git-mode-optimistic-move',
|
|
603
|
+
})
|
|
604
|
+
}
|
|
605
|
+
return r(actions, [{ type: 'git-unstage-all' }])
|
|
606
|
+
}
|
|
607
|
+
|
|
550
608
|
export const gitCommitOpen: ActionFn = (ctx: ModeContext) => {
|
|
551
609
|
if (ctx.state.gitMode.headOffset > 0) {
|
|
552
610
|
return r([
|
package/src/defaults.ts
CHANGED
|
@@ -98,7 +98,10 @@ export function getDefaultKeymapConfig(): ResolvedKeymapConfig {
|
|
|
98
98
|
.mode('git-mode', (m) =>
|
|
99
99
|
m
|
|
100
100
|
.map('a', actions.gitStageSelected, 'Stage')
|
|
101
|
+
.map('A', actions.gitStageAll, 'Stage all')
|
|
101
102
|
.map('d', actions.gitDestructiveSelected, 'Unstage/delete')
|
|
103
|
+
.map('D', actions.gitUnstageAll, 'Unstage all')
|
|
104
|
+
.map('e', actions.gitToggleFoldAll, 'Expand/collapse all folds')
|
|
102
105
|
.map('c', actions.gitCommitOpen, 'Commit')
|
|
103
106
|
.map('p', actions.gitPush, 'Push')
|
|
104
107
|
.map('v', actions.toggleGitDiffView, 'Toggle split/stacked')
|
package/src/index.ts
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
// Public API —
|
|
1
|
+
// Public API — what users import from '@brimveyn/aimux-config'.
|
|
2
2
|
|
|
3
3
|
export { defineConfig } from './define-config'
|
|
4
4
|
export * as actions from './actions'
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export { OPENCODE_THEME_IDS, OPENCODE_THEMES } from './themes/opencode'
|
|
5
|
+
|
|
6
|
+
// TUI theme system (1:1 port of opencode TUI).
|
|
8
7
|
export {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
8
|
+
isKnownThemeId,
|
|
9
|
+
migrateThemeId,
|
|
10
|
+
type ResolvedTuiTheme,
|
|
11
|
+
resolveTuiTheme,
|
|
12
|
+
type RGBA,
|
|
13
|
+
THEME_IDS,
|
|
14
|
+
type ThemeId,
|
|
15
|
+
type ThemeMode,
|
|
16
|
+
TUI_COLOR_TOKENS,
|
|
17
|
+
TUI_THEMES,
|
|
18
|
+
type TuiColorToken,
|
|
19
|
+
type TuiColorValue,
|
|
20
|
+
type TuiShikiOptions,
|
|
21
|
+
type TuiThemeJson,
|
|
22
|
+
tuiThemeToShiki,
|
|
23
|
+
} from './tui'
|
|
24
|
+
|
|
24
25
|
export { GroupBuilder, KeymapBuilder, ModeBindingBuilder } from './keymap-builder'
|
|
25
26
|
export { getDefaultKeymapConfig } from './defaults'
|
|
26
27
|
export { resolveConfig } from './resolver'
|
|
@@ -28,15 +29,11 @@ export { isAutoCommitEnabled, setAutoCommitEnabled } from './auto-commit-runtime
|
|
|
28
29
|
export { getMultiRepoConfig, setMultiRepoConfig } from './multi-repo-runtime'
|
|
29
30
|
export { DEFAULT_MULTI_REPO_CONFIG } from './defaults'
|
|
30
31
|
|
|
31
|
-
//
|
|
32
|
+
// User-facing config types (keymap/backends/sessions/etc.).
|
|
32
33
|
export type {
|
|
33
|
-
// Action types
|
|
34
34
|
Action,
|
|
35
35
|
ActionFn,
|
|
36
|
-
AimuxPalette,
|
|
37
|
-
AimuxTheme,
|
|
38
36
|
AimuxThemeConfig,
|
|
39
|
-
// User-facing config
|
|
40
37
|
AimuxUserConfig,
|
|
41
38
|
AIUsageTool,
|
|
42
39
|
AIUsageToolConfig,
|
|
@@ -57,20 +54,16 @@ export type {
|
|
|
57
54
|
GroupBuilderApi,
|
|
58
55
|
HooksConfig,
|
|
59
56
|
KeyInput,
|
|
60
|
-
// Keymap builder API (type surface)
|
|
61
57
|
KeymapBuilderApi,
|
|
62
58
|
KeyResult,
|
|
63
|
-
// Layout
|
|
64
59
|
LayoutNode,
|
|
65
60
|
ModalState,
|
|
66
61
|
ModeBindingBuilderApi,
|
|
67
62
|
ModeContext,
|
|
68
|
-
// Mode / state
|
|
69
63
|
ModeId,
|
|
70
64
|
ModeKeymapDef,
|
|
71
65
|
MultiRepoConfig,
|
|
72
66
|
MultiRepoState,
|
|
73
|
-
// Resolved config (internal, but exported for tooling)
|
|
74
67
|
ResolvedConfig,
|
|
75
68
|
ResolvedKeymapConfig,
|
|
76
69
|
SessionRecord,
|
|
@@ -81,7 +74,4 @@ export type {
|
|
|
81
74
|
SplitDirection,
|
|
82
75
|
StatusBarConfig,
|
|
83
76
|
TabSession,
|
|
84
|
-
Theme,
|
|
85
|
-
ThemeId,
|
|
86
|
-
ThemeMode,
|
|
87
77
|
} from './types'
|
package/src/resolver.ts
CHANGED
|
@@ -51,8 +51,8 @@ export function resolveConfig(userConfig: AimuxUserConfig): ResolvedConfig {
|
|
|
51
51
|
function resolveTheme(userConfig: AimuxUserConfig['theme']): ResolvedConfig['theme'] {
|
|
52
52
|
if (!userConfig) return undefined
|
|
53
53
|
return {
|
|
54
|
+
initialId: userConfig.initialId,
|
|
54
55
|
initialMode: userConfig.initialMode ?? userConfig.mode,
|
|
55
|
-
paletteOverrides: userConfig.paletteOverrides,
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
package/src/tui/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { isKnownThemeId, migrateThemeId, THEME_IDS, type ThemeId, TUI_THEMES } from './registry'
|
|
2
|
+
export { resolveTuiTheme } from './resolve'
|
|
3
|
+
export { type TuiShikiOptions, tuiThemeToShiki } from './shiki'
|
|
4
|
+
export { TUI_COLOR_TOKENS, type TuiColorToken } from './tokens'
|
|
5
|
+
export type { ResolvedTuiTheme, RGBA, ThemeMode, TuiColorValue, TuiThemeJson } from './types'
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Static registry of every TUI theme JSON shipped with aimux. Generated by
|
|
2
|
+
// hand: re-run after `bun scripts/import-opencode-tui-themes.ts` if upstream
|
|
3
|
+
// adds/removes themes.
|
|
4
|
+
|
|
5
|
+
import type { TuiThemeJson } from './types'
|
|
6
|
+
|
|
7
|
+
import aimux from './themes/aimux.json'
|
|
8
|
+
import aura from './themes/aura.json'
|
|
9
|
+
import ayu from './themes/ayu.json'
|
|
10
|
+
import carbonfox from './themes/carbonfox.json'
|
|
11
|
+
import catppuccinFrappe from './themes/catppuccin-frappe.json'
|
|
12
|
+
import catppuccinMacchiato from './themes/catppuccin-macchiato.json'
|
|
13
|
+
import catppuccin from './themes/catppuccin.json'
|
|
14
|
+
import cobalt2 from './themes/cobalt2.json'
|
|
15
|
+
import cursor from './themes/cursor.json'
|
|
16
|
+
import dracula from './themes/dracula.json'
|
|
17
|
+
import everforest from './themes/everforest.json'
|
|
18
|
+
import flexoki from './themes/flexoki.json'
|
|
19
|
+
import github from './themes/github.json'
|
|
20
|
+
import gruvbox from './themes/gruvbox.json'
|
|
21
|
+
import kanagawa from './themes/kanagawa.json'
|
|
22
|
+
import lucentOrng from './themes/lucent-orng.json'
|
|
23
|
+
import material from './themes/material.json'
|
|
24
|
+
import matrix from './themes/matrix.json'
|
|
25
|
+
import mercury from './themes/mercury.json'
|
|
26
|
+
import monokai from './themes/monokai.json'
|
|
27
|
+
import nightowl from './themes/nightowl.json'
|
|
28
|
+
import nord from './themes/nord.json'
|
|
29
|
+
import oneDark from './themes/one-dark.json'
|
|
30
|
+
import opencode from './themes/opencode.json'
|
|
31
|
+
import orng from './themes/orng.json'
|
|
32
|
+
import osakaJade from './themes/osaka-jade.json'
|
|
33
|
+
import palenight from './themes/palenight.json'
|
|
34
|
+
import rosepine from './themes/rosepine.json'
|
|
35
|
+
import solarized from './themes/solarized.json'
|
|
36
|
+
import synthwave84 from './themes/synthwave84.json'
|
|
37
|
+
import tokyonight from './themes/tokyonight.json'
|
|
38
|
+
import vercel from './themes/vercel.json'
|
|
39
|
+
import vesper from './themes/vesper.json'
|
|
40
|
+
import zenburn from './themes/zenburn.json'
|
|
41
|
+
|
|
42
|
+
export const TUI_THEMES: Record<string, TuiThemeJson> = {
|
|
43
|
+
'aimux': aimux as TuiThemeJson,
|
|
44
|
+
'aura': aura as TuiThemeJson,
|
|
45
|
+
'ayu': ayu as TuiThemeJson,
|
|
46
|
+
'carbonfox': carbonfox as TuiThemeJson,
|
|
47
|
+
'catppuccin': catppuccin as TuiThemeJson,
|
|
48
|
+
'catppuccin-frappe': catppuccinFrappe as TuiThemeJson,
|
|
49
|
+
'catppuccin-macchiato': catppuccinMacchiato as TuiThemeJson,
|
|
50
|
+
'cobalt2': cobalt2 as TuiThemeJson,
|
|
51
|
+
'cursor': cursor as TuiThemeJson,
|
|
52
|
+
'dracula': dracula as TuiThemeJson,
|
|
53
|
+
'everforest': everforest as TuiThemeJson,
|
|
54
|
+
'flexoki': flexoki as TuiThemeJson,
|
|
55
|
+
'github': github as TuiThemeJson,
|
|
56
|
+
'gruvbox': gruvbox as TuiThemeJson,
|
|
57
|
+
'kanagawa': kanagawa as TuiThemeJson,
|
|
58
|
+
'lucent-orng': lucentOrng as TuiThemeJson,
|
|
59
|
+
'material': material as TuiThemeJson,
|
|
60
|
+
'matrix': matrix as TuiThemeJson,
|
|
61
|
+
'mercury': mercury as TuiThemeJson,
|
|
62
|
+
'monokai': monokai as TuiThemeJson,
|
|
63
|
+
'nightowl': nightowl as TuiThemeJson,
|
|
64
|
+
'nord': nord as TuiThemeJson,
|
|
65
|
+
'one-dark': oneDark as TuiThemeJson,
|
|
66
|
+
'opencode': opencode as TuiThemeJson,
|
|
67
|
+
'orng': orng as TuiThemeJson,
|
|
68
|
+
'osaka-jade': osakaJade as TuiThemeJson,
|
|
69
|
+
'palenight': palenight as TuiThemeJson,
|
|
70
|
+
'rosepine': rosepine as TuiThemeJson,
|
|
71
|
+
'solarized': solarized as TuiThemeJson,
|
|
72
|
+
'synthwave84': synthwave84 as TuiThemeJson,
|
|
73
|
+
'tokyonight': tokyonight as TuiThemeJson,
|
|
74
|
+
'vercel': vercel as TuiThemeJson,
|
|
75
|
+
'vesper': vesper as TuiThemeJson,
|
|
76
|
+
'zenburn': zenburn as TuiThemeJson,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ThemeId = keyof typeof TUI_THEMES & string
|
|
80
|
+
|
|
81
|
+
export const THEME_IDS: ThemeId[] = Object.keys(TUI_THEMES).sort() as ThemeId[]
|
|
82
|
+
|
|
83
|
+
export function isKnownThemeId(id: string): id is ThemeId {
|
|
84
|
+
return id in TUI_THEMES
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const LEGACY_ID_MAP: Record<string, ThemeId> = {
|
|
88
|
+
'aimux-dark': 'aimux',
|
|
89
|
+
'aimux-light': 'aimux',
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function migrateThemeId(id: string): ThemeId {
|
|
93
|
+
if (isKnownThemeId(id)) return id
|
|
94
|
+
return LEGACY_ID_MAP[id] ?? 'aimux'
|
|
95
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Port of opencode TUI's resolveTheme + ansiToRgba.
|
|
2
|
+
// Source: opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx:198-301
|
|
3
|
+
// (pinned reference). Behavior preserved verbatim; output uses CSS-style color
|
|
4
|
+
// strings (hex / rgba) so values flow directly into opentui without conversion.
|
|
5
|
+
|
|
6
|
+
import type { ResolvedTuiTheme, RGBA, ThemeMode, TuiColorValue, TuiThemeJson } from './types'
|
|
7
|
+
|
|
8
|
+
import { TUI_COLOR_TOKENS, type TuiColorToken } from './tokens'
|
|
9
|
+
|
|
10
|
+
const TRANSPARENT: RGBA = { a: 0, b: 0, g: 0, r: 0 }
|
|
11
|
+
|
|
12
|
+
function rgbaFromHex(hex: string): RGBA {
|
|
13
|
+
const h = hex.replace('#', '')
|
|
14
|
+
// expand short forms (#rgb, #rgba)
|
|
15
|
+
const expanded =
|
|
16
|
+
h.length === 3 || h.length === 4
|
|
17
|
+
? h
|
|
18
|
+
.split('')
|
|
19
|
+
.map((c) => c + c)
|
|
20
|
+
.join('')
|
|
21
|
+
: h
|
|
22
|
+
if (expanded.length === 6) {
|
|
23
|
+
const num = parseInt(expanded, 16)
|
|
24
|
+
return {
|
|
25
|
+
a: 255,
|
|
26
|
+
b: num & 0xff,
|
|
27
|
+
g: (num >> 8) & 0xff,
|
|
28
|
+
r: (num >> 16) & 0xff,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (expanded.length === 8) {
|
|
32
|
+
const num = parseInt(expanded, 16) >>> 0
|
|
33
|
+
return {
|
|
34
|
+
a: num & 0xff,
|
|
35
|
+
b: (num >> 8) & 0xff,
|
|
36
|
+
g: (num >> 16) & 0xff,
|
|
37
|
+
r: (num >> 24) & 0xff,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { a: 255, b: 0, g: 0, r: 0 }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ansiToRgba(code: number): RGBA {
|
|
44
|
+
if (code < 16) {
|
|
45
|
+
const ansiColors = [
|
|
46
|
+
'#000000', // Black
|
|
47
|
+
'#800000', // Red
|
|
48
|
+
'#008000', // Green
|
|
49
|
+
'#808000', // Yellow
|
|
50
|
+
'#000080', // Blue
|
|
51
|
+
'#800080', // Magenta
|
|
52
|
+
'#008080', // Cyan
|
|
53
|
+
'#c0c0c0', // White
|
|
54
|
+
'#808080', // Bright Black
|
|
55
|
+
'#ff0000', // Bright Red
|
|
56
|
+
'#00ff00', // Bright Green
|
|
57
|
+
'#ffff00', // Bright Yellow
|
|
58
|
+
'#0000ff', // Bright Blue
|
|
59
|
+
'#ff00ff', // Bright Magenta
|
|
60
|
+
'#00ffff', // Bright Cyan
|
|
61
|
+
'#ffffff', // Bright White
|
|
62
|
+
]
|
|
63
|
+
return rgbaFromHex(ansiColors[code] ?? '#000000')
|
|
64
|
+
}
|
|
65
|
+
if (code < 232) {
|
|
66
|
+
const index = code - 16
|
|
67
|
+
const b = index % 6
|
|
68
|
+
const g = Math.floor(index / 6) % 6
|
|
69
|
+
const r = Math.floor(index / 36)
|
|
70
|
+
const val = (x: number) => (x === 0 ? 0 : x * 40 + 55)
|
|
71
|
+
return { a: 255, b: val(b), g: val(g), r: val(r) }
|
|
72
|
+
}
|
|
73
|
+
if (code < 256) {
|
|
74
|
+
const gray = (code - 232) * 10 + 8
|
|
75
|
+
return { a: 255, b: gray, g: gray, r: gray }
|
|
76
|
+
}
|
|
77
|
+
return { a: 255, b: 0, g: 0, r: 0 }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Recursive color resolver. Mirrors opencode TUI `resolveColor`. */
|
|
81
|
+
function resolveColorRgba(
|
|
82
|
+
c: TuiColorValue,
|
|
83
|
+
theme: TuiThemeJson,
|
|
84
|
+
mode: ThemeMode,
|
|
85
|
+
chain: string[] = []
|
|
86
|
+
): RGBA {
|
|
87
|
+
if (typeof c === 'string') {
|
|
88
|
+
if (c === 'transparent' || c === 'none') return TRANSPARENT
|
|
89
|
+
if (c.startsWith('#')) return rgbaFromHex(c)
|
|
90
|
+
if (chain.includes(c)) {
|
|
91
|
+
throw new Error(`Circular color reference: ${[...chain, c].join(' -> ')}`)
|
|
92
|
+
}
|
|
93
|
+
const defs = theme.defs ?? {}
|
|
94
|
+
const next = defs[c] ?? (theme.theme as Record<string, TuiColorValue>)[c]
|
|
95
|
+
if (next === undefined) {
|
|
96
|
+
throw new Error(`Color reference "${c}" not found in defs or theme`)
|
|
97
|
+
}
|
|
98
|
+
return resolveColorRgba(next, theme, mode, [...chain, c])
|
|
99
|
+
}
|
|
100
|
+
if (typeof c === 'number') return ansiToRgba(c)
|
|
101
|
+
return resolveColorRgba(c[mode], theme, mode, chain)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function rgbaToCss(c: RGBA): string {
|
|
105
|
+
const r = Math.max(0, Math.min(255, Math.round(c.r)))
|
|
106
|
+
const g = Math.max(0, Math.min(255, Math.round(c.g)))
|
|
107
|
+
const b = Math.max(0, Math.min(255, Math.round(c.b)))
|
|
108
|
+
const a = Math.max(0, Math.min(255, Math.round(c.a)))
|
|
109
|
+
if (a === 255) {
|
|
110
|
+
const hex = (v: number) => v.toString(16).padStart(2, '0')
|
|
111
|
+
return `#${hex(r)}${hex(g)}${hex(b)}`
|
|
112
|
+
}
|
|
113
|
+
return `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(3)})`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a TUI theme JSON to a flat record of CSS color strings.
|
|
118
|
+
* @param mode "dark" or "light" — selects between {dark, light} branches in token values.
|
|
119
|
+
*/
|
|
120
|
+
export function resolveTuiTheme(theme: TuiThemeJson, mode: ThemeMode): ResolvedTuiTheme {
|
|
121
|
+
const out: Partial<Record<TuiColorToken, string>> = {}
|
|
122
|
+
|
|
123
|
+
// Pass 1: every token defined in the JSON, except optional/derived ones.
|
|
124
|
+
for (const [key, value] of Object.entries(theme.theme)) {
|
|
125
|
+
if (key === 'selectedListItemText' || key === 'backgroundMenu' || key === 'thinkingOpacity')
|
|
126
|
+
continue
|
|
127
|
+
out[key as TuiColorToken] = rgbaToCss(resolveColorRgba(value as TuiColorValue, theme, mode))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Pass 2: fallbacks (matches opencode behavior).
|
|
131
|
+
if (theme.theme.selectedListItemText !== undefined) {
|
|
132
|
+
out.selectedListItemText = rgbaToCss(
|
|
133
|
+
resolveColorRgba(theme.theme.selectedListItemText, theme, mode)
|
|
134
|
+
)
|
|
135
|
+
} else if (out.background !== undefined) {
|
|
136
|
+
out.selectedListItemText = out.background
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (theme.theme.backgroundMenu !== undefined) {
|
|
140
|
+
out.backgroundMenu = rgbaToCss(resolveColorRgba(theme.theme.backgroundMenu, theme, mode))
|
|
141
|
+
} else if (out.backgroundElement !== undefined) {
|
|
142
|
+
out.backgroundMenu = out.backgroundElement
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Sanity: every token should be present. Any missing token in a JSON falls
|
|
146
|
+
// back to text (foreground) so we never hand an `undefined` to opentui.
|
|
147
|
+
const fallback = out.text ?? out.background ?? '#000000'
|
|
148
|
+
for (const tok of TUI_COLOR_TOKENS) {
|
|
149
|
+
if (out[tok] === undefined) out[tok] = fallback
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
...(out as Record<TuiColorToken, string>),
|
|
154
|
+
thinkingOpacity: theme.theme.thinkingOpacity ?? 0.6,
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/tui/shiki.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Build a Shiki ThemeRegistrationRaw from a resolved TUI theme.
|
|
2
|
+
|
|
3
|
+
import type { ThemeRegistrationRaw } from 'shiki'
|
|
4
|
+
|
|
5
|
+
import type { ResolvedTuiTheme, ThemeMode } from './types'
|
|
6
|
+
|
|
7
|
+
export interface TuiShikiOptions {
|
|
8
|
+
name: string
|
|
9
|
+
mode: ThemeMode
|
|
10
|
+
theme: ResolvedTuiTheme
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function tuiThemeToShiki(opts: TuiShikiOptions): ThemeRegistrationRaw {
|
|
14
|
+
const { mode, name, theme: t } = opts
|
|
15
|
+
const bg = t.background
|
|
16
|
+
const fg = t.text
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
bg,
|
|
20
|
+
colors: { 'editor.background': bg, 'editor.foreground': fg },
|
|
21
|
+
fg,
|
|
22
|
+
name,
|
|
23
|
+
settings: [
|
|
24
|
+
{ settings: { background: bg, foreground: fg } },
|
|
25
|
+
{
|
|
26
|
+
scope: ['comment', 'punctuation.definition.comment', 'comment.documentation'],
|
|
27
|
+
settings: { fontStyle: 'italic', foreground: t.syntaxComment },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
scope: ['string', 'string.quoted', 'string.template'],
|
|
31
|
+
settings: { foreground: t.syntaxString },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
scope: ['string.regexp', 'string.escape', 'constant.character.escape'],
|
|
35
|
+
settings: { foreground: t.syntaxString },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
scope: [
|
|
39
|
+
'constant.numeric',
|
|
40
|
+
'constant.language',
|
|
41
|
+
'constant.language.boolean',
|
|
42
|
+
'constant.character',
|
|
43
|
+
'constant.other',
|
|
44
|
+
'support.constant',
|
|
45
|
+
],
|
|
46
|
+
settings: { foreground: t.syntaxNumber },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
scope: [
|
|
50
|
+
'keyword',
|
|
51
|
+
'keyword.control',
|
|
52
|
+
'keyword.operator.new',
|
|
53
|
+
'keyword.other',
|
|
54
|
+
'storage',
|
|
55
|
+
'storage.type',
|
|
56
|
+
'storage.modifier',
|
|
57
|
+
],
|
|
58
|
+
settings: { fontStyle: 'bold', foreground: t.syntaxKeyword },
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
scope: ['keyword.operator', 'meta.brace', 'punctuation.separator'],
|
|
62
|
+
settings: { foreground: t.syntaxOperator },
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
scope: ['punctuation', 'punctuation.section'],
|
|
66
|
+
settings: { foreground: t.syntaxPunctuation },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
scope: [
|
|
70
|
+
'entity.name.function',
|
|
71
|
+
'support.function',
|
|
72
|
+
'meta.function-call',
|
|
73
|
+
'entity.name.function.member',
|
|
74
|
+
],
|
|
75
|
+
settings: { foreground: t.syntaxFunction },
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
scope: [
|
|
79
|
+
'entity.name.type',
|
|
80
|
+
'support.type',
|
|
81
|
+
'support.class',
|
|
82
|
+
'entity.name.class',
|
|
83
|
+
'entity.name.interface',
|
|
84
|
+
'entity.name.namespace',
|
|
85
|
+
'entity.name.type.enum',
|
|
86
|
+
'entity.name.type.alias',
|
|
87
|
+
'entity.name.tag.jsx',
|
|
88
|
+
'support.class.component',
|
|
89
|
+
],
|
|
90
|
+
settings: { fontStyle: 'italic', foreground: t.syntaxType },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
scope: ['variable', 'meta.variable'],
|
|
94
|
+
settings: { foreground: t.syntaxVariable },
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
scope: ['variable.parameter', 'meta.function.parameters'],
|
|
98
|
+
settings: { fontStyle: 'italic', foreground: t.syntaxVariable },
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
scope: ['variable.other.property', 'variable.other.member', 'meta.object-literal.key'],
|
|
102
|
+
settings: { foreground: t.syntaxVariable },
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
scope: ['variable.language', 'variable.language.this'],
|
|
106
|
+
settings: { fontStyle: 'italic', foreground: t.syntaxNumber },
|
|
107
|
+
},
|
|
108
|
+
{ scope: ['entity.name.tag', 'meta.tag'], settings: { foreground: t.error } },
|
|
109
|
+
{
|
|
110
|
+
scope: ['entity.other.attribute-name'],
|
|
111
|
+
settings: { fontStyle: 'italic', foreground: t.warning },
|
|
112
|
+
},
|
|
113
|
+
// Markdown
|
|
114
|
+
{
|
|
115
|
+
scope: ['markup.heading'],
|
|
116
|
+
settings: { fontStyle: 'bold', foreground: t.markdownHeading },
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
scope: ['markup.bold'],
|
|
120
|
+
settings: { fontStyle: 'bold', foreground: t.markdownStrong },
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
scope: ['markup.italic'],
|
|
124
|
+
settings: { fontStyle: 'italic', foreground: t.markdownEmph },
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
scope: ['markup.underline.link', 'markup.inline.raw', 'markup.raw'],
|
|
128
|
+
settings: { foreground: t.markdownCode },
|
|
129
|
+
},
|
|
130
|
+
{ scope: ['markup.list'], settings: { foreground: t.markdownListItem } },
|
|
131
|
+
{
|
|
132
|
+
scope: ['markup.quote'],
|
|
133
|
+
settings: { fontStyle: 'italic', foreground: t.markdownBlockQuote },
|
|
134
|
+
},
|
|
135
|
+
// Diff
|
|
136
|
+
{
|
|
137
|
+
scope: ['markup.inserted', 'meta.diff.header.to-file'],
|
|
138
|
+
settings: { foreground: t.diffAdded },
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
scope: ['markup.deleted', 'meta.diff.header.from-file'],
|
|
142
|
+
settings: { foreground: t.diffRemoved },
|
|
143
|
+
},
|
|
144
|
+
{ scope: ['markup.changed'], settings: { foreground: t.warning } },
|
|
145
|
+
{ scope: ['meta.diff.range'], settings: { foreground: t.diffHunkHeader } },
|
|
146
|
+
],
|
|
147
|
+
type: mode,
|
|
148
|
+
}
|
|
149
|
+
}
|