free-coding-models 0.3.17 → 0.3.19

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/src/theme.js CHANGED
@@ -1,67 +1,318 @@
1
1
  /**
2
2
  * @file theme.js
3
- * @description Dynamic light/dark theme detector and semantic colour mappings.
3
+ * @description Semantic light/dark palette, auto theme detection, and shared TUI colour helpers.
4
+ *
5
+ * @functions
6
+ * → `detectActiveTheme` — resolve the live theme from user preference or terminal/OS signals
7
+ * → `getTheme` — return the currently active resolved theme (`dark` or `light`)
8
+ * → `cycleThemeSetting` — rotate persisted theme preference (`auto` → `dark` → `light`)
9
+ * → `getThemeStatusLabel` — format the settings/help label for the current theme mode
10
+ * → `getProviderRgb` — return the provider accent colour for the active theme
11
+ * → `getTierRgb` — return the tier accent colour for the active theme
12
+ *
13
+ * @exports { THEME_OPTIONS, detectActiveTheme, getTheme, cycleThemeSetting, getThemeStatusLabel, getReadableTextRgb, getProviderRgb, getTierRgb, themeColors }
14
+ *
15
+ * @see src/render-table.js
16
+ * @see src/overlays.js
17
+ * @see src/tier-colors.js
4
18
  */
5
19
 
6
20
  import chalk from 'chalk'
7
21
  import { execSync } from 'child_process'
8
22
 
23
+ export const THEME_OPTIONS = ['auto', 'dark', 'light']
24
+
9
25
  let activeTheme = 'dark'
10
26
 
11
- export function detectActiveTheme(configTheme) {
27
+ const PALETTES = {
28
+ dark: {
29
+ text: [234, 239, 248],
30
+ textStrong: [255, 255, 255],
31
+ muted: [149, 160, 182],
32
+ soft: [178, 190, 210],
33
+ accent: [110, 214, 255],
34
+ accentStrong: [72, 198, 255],
35
+ info: [129, 210, 255],
36
+ success: [112, 231, 181],
37
+ successStrong: [139, 239, 176],
38
+ warning: [255, 208, 102],
39
+ warningStrong: [255, 221, 140],
40
+ danger: [255, 129, 129],
41
+ dangerStrong: [255, 166, 166],
42
+ hotkey: [255, 214, 102],
43
+ link: [149, 205, 255],
44
+ border: [111, 125, 149],
45
+ footerLove: [255, 168, 209],
46
+ footerCoffee: [255, 209, 134],
47
+ footerDiscord: [207, 179, 255],
48
+ overlayFg: [234, 239, 248],
49
+ overlayBg: {
50
+ settings: [7, 13, 24],
51
+ help: [9, 18, 31],
52
+ recommend: [8, 21, 20],
53
+ feedback: [31, 13, 20],
54
+ changelog: [12, 24, 44],
55
+ commandPalette: [14, 20, 36],
56
+ },
57
+ cursor: {
58
+ defaultBg: [39, 55, 90],
59
+ defaultFg: [255, 255, 255],
60
+ installBg: [22, 60, 69],
61
+ installFg: [255, 255, 255],
62
+ settingsBg: [26, 54, 34],
63
+ settingsFg: [255, 255, 255],
64
+ legacyBg: [67, 31, 69],
65
+ legacyFg: [255, 255, 255],
66
+ modelBg: [62, 73, 115],
67
+ modelFg: [255, 255, 255],
68
+ recommendedBg: [20, 51, 33],
69
+ recommendedFg: [234, 239, 248],
70
+ favoriteBg: [76, 55, 17],
71
+ favoriteFg: [255, 244, 220],
72
+ },
73
+ },
74
+ light: {
75
+ text: [28, 36, 51],
76
+ textStrong: [8, 12, 20],
77
+ muted: [95, 109, 129],
78
+ soft: [76, 89, 109],
79
+ accent: [0, 120, 186],
80
+ accentStrong: [0, 99, 163],
81
+ info: [0, 109, 168],
82
+ success: [0, 118, 68],
83
+ successStrong: [0, 96, 56],
84
+ warning: [146, 90, 0],
85
+ warningStrong: [171, 102, 0],
86
+ danger: [177, 53, 53],
87
+ dangerStrong: [147, 35, 48],
88
+ hotkey: [171, 98, 0],
89
+ link: [0, 94, 170],
90
+ border: [151, 166, 188],
91
+ footerLove: [176, 79, 128],
92
+ footerCoffee: [170, 102, 0],
93
+ footerDiscord: [104, 83, 190],
94
+ overlayFg: [28, 36, 51],
95
+ overlayBg: {
96
+ settings: [248, 250, 255],
97
+ help: [246, 250, 255],
98
+ recommend: [246, 252, 248],
99
+ feedback: [255, 247, 248],
100
+ changelog: [244, 248, 255],
101
+ commandPalette: [242, 247, 255],
102
+ },
103
+ cursor: {
104
+ defaultBg: [217, 231, 255],
105
+ defaultFg: [9, 18, 35],
106
+ installBg: [218, 242, 236],
107
+ installFg: [12, 33, 26],
108
+ settingsBg: [225, 244, 229],
109
+ settingsFg: [14, 43, 27],
110
+ legacyBg: [248, 228, 244],
111
+ legacyFg: [76, 28, 73],
112
+ modelBg: [209, 223, 255],
113
+ modelFg: [9, 18, 35],
114
+ recommendedBg: [221, 245, 229],
115
+ recommendedFg: [17, 47, 28],
116
+ favoriteBg: [255, 241, 208],
117
+ favoriteFg: [79, 53, 0],
118
+ },
119
+ },
120
+ }
121
+
122
+ const PROVIDER_PALETTES = {
123
+ dark: {
124
+ nvidia: [132, 235, 168],
125
+ groq: [255, 191, 144],
126
+ cerebras: [153, 215, 255],
127
+ sambanova: [255, 215, 142],
128
+ openrouter: [228, 191, 239],
129
+ huggingface: [255, 235, 122],
130
+ replicate: [166, 212, 255],
131
+ deepinfra: [146, 222, 213],
132
+ fireworks: [255, 184, 194],
133
+ codestral: [245, 175, 212],
134
+ hyperbolic: [255, 160, 127],
135
+ scaleway: [115, 209, 255],
136
+ googleai: [166, 210, 255],
137
+ siliconflow: [145, 232, 243],
138
+ together: [255, 232, 98],
139
+ cloudflare: [255, 191, 118],
140
+ perplexity: [243, 157, 195],
141
+ qwen: [255, 213, 128],
142
+ zai: [150, 208, 255],
143
+ iflow: [211, 229, 101],
144
+ },
145
+ light: {
146
+ nvidia: [0, 126, 73],
147
+ groq: [171, 86, 22],
148
+ cerebras: [0, 102, 177],
149
+ sambanova: [165, 94, 0],
150
+ openrouter: [122, 65, 156],
151
+ huggingface: [135, 104, 0],
152
+ replicate: [0, 94, 163],
153
+ deepinfra: [0, 122, 117],
154
+ fireworks: [183, 55, 72],
155
+ codestral: [157, 61, 110],
156
+ hyperbolic: [178, 68, 27],
157
+ scaleway: [0, 113, 189],
158
+ googleai: [0, 111, 168],
159
+ siliconflow: [0, 115, 138],
160
+ together: [122, 101, 0],
161
+ cloudflare: [176, 92, 0],
162
+ perplexity: [171, 62, 121],
163
+ qwen: [132, 89, 0],
164
+ zai: [0, 104, 171],
165
+ iflow: [107, 130, 0],
166
+ },
167
+ }
168
+
169
+ const TIER_PALETTES = {
170
+ dark: {
171
+ 'S+': [111, 255, 164],
172
+ 'S': [147, 241, 101],
173
+ 'A+': [201, 233, 104],
174
+ 'A': [255, 211, 101],
175
+ 'A-': [255, 178, 100],
176
+ 'B+': [255, 145, 112],
177
+ 'B': [255, 113, 113],
178
+ 'C': [255, 139, 164],
179
+ },
180
+ light: {
181
+ 'S+': [0, 122, 58],
182
+ 'S': [54, 122, 0],
183
+ 'A+': [95, 113, 0],
184
+ 'A': [128, 92, 0],
185
+ 'A-': [156, 80, 0],
186
+ 'B+': [171, 69, 0],
187
+ 'B': [168, 44, 44],
188
+ 'C': [123, 35, 75],
189
+ },
190
+ }
191
+
192
+ function currentPalette() {
193
+ return PALETTES[activeTheme] ?? PALETTES.dark
194
+ }
195
+
196
+ function themeLabel(theme) {
197
+ return theme.charAt(0).toUpperCase() + theme.slice(1)
198
+ }
199
+
200
+ function buildStyle({ fgRgb = null, bgRgb = null, bold = false, italic = false } = {}) {
201
+ let style = chalk
202
+ if (bgRgb) style = style.bgRgb(...bgRgb)
203
+ if (fgRgb) style = style.rgb(...fgRgb)
204
+ if (bold) style = style.bold
205
+ if (italic) style = style.italic
206
+ return style
207
+ }
208
+
209
+ export function getReadableTextRgb(bgRgb) {
210
+ const [r, g, b] = bgRgb
211
+ const yiq = (r * 299 + g * 587 + b * 114) / 1000
212
+ return yiq >= 150 ? [10, 16, 28] : [248, 251, 255]
213
+ }
214
+
215
+ export function detectActiveTheme(configTheme = 'auto') {
12
216
  if (configTheme === 'dark' || configTheme === 'light') {
13
- activeTheme = configTheme;
14
- return activeTheme;
217
+ activeTheme = configTheme
218
+ return activeTheme
15
219
  }
16
-
17
- // Auto detect
18
- const fgbg = process.env.COLORFGBG || '';
19
- if (fgbg.includes(';15') || fgbg.includes(';base03')) {
20
- activeTheme = 'light';
21
- return activeTheme;
22
- } else if (fgbg) {
23
- activeTheme = 'dark';
24
- return activeTheme;
220
+
221
+ const fgbg = process.env.COLORFGBG || ''
222
+ if (fgbg.includes(';15') || fgbg.includes(';7') || fgbg.includes(';base03')) {
223
+ activeTheme = 'light'
224
+ return activeTheme
25
225
  }
26
-
226
+ if (fgbg) {
227
+ activeTheme = 'dark'
228
+ return activeTheme
229
+ }
230
+
27
231
  if (process.platform === 'darwin') {
28
232
  try {
29
- const style = execSync('defaults read -g AppleInterfaceStyle 2>/dev/null', { timeout: 100 }).toString().trim();
30
- activeTheme = style === 'Dark' ? 'dark' : 'light';
233
+ const style = execSync('defaults read -g AppleInterfaceStyle 2>/dev/null', { timeout: 100 }).toString().trim()
234
+ activeTheme = style === 'Dark' ? 'dark' : 'light'
31
235
  } catch {
32
- activeTheme = 'light';
236
+ activeTheme = 'light'
33
237
  }
34
- } else {
35
- activeTheme = 'dark';
238
+ return activeTheme
36
239
  }
37
-
38
- return activeTheme;
240
+
241
+ activeTheme = 'dark'
242
+ return activeTheme
39
243
  }
40
244
 
41
245
  export function getTheme() {
42
- return activeTheme;
246
+ return activeTheme
247
+ }
248
+
249
+ export function cycleThemeSetting(currentTheme = 'auto') {
250
+ const currentIdx = THEME_OPTIONS.indexOf(currentTheme)
251
+ const nextIdx = currentIdx === -1 ? 0 : (currentIdx + 1) % THEME_OPTIONS.length
252
+ return THEME_OPTIONS[nextIdx]
253
+ }
254
+
255
+ export function getThemeStatusLabel(setting = 'auto') {
256
+ if (setting === 'auto') return `Auto → ${themeLabel(activeTheme)}`
257
+ return themeLabel(setting)
258
+ }
259
+
260
+ export function getProviderRgb(providerKey) {
261
+ const palette = PROVIDER_PALETTES[activeTheme] ?? PROVIDER_PALETTES.dark
262
+ return palette[providerKey] ?? currentPalette().accent
263
+ }
264
+
265
+ export function getTierRgb(tier) {
266
+ const palette = TIER_PALETTES[activeTheme] ?? TIER_PALETTES.dark
267
+ return palette[tier] ?? currentPalette().textStrong
268
+ }
269
+
270
+ function paintRgb(rgb, text, options = {}) {
271
+ return buildStyle({ fgRgb: rgb, ...options })(text)
272
+ }
273
+
274
+ function paintBg(bgRgb, text, fgRgb = null, options = {}) {
275
+ return buildStyle({ bgRgb, fgRgb: fgRgb ?? getReadableTextRgb(bgRgb), ...options })(text)
43
276
  }
44
277
 
45
- // Semantic colors
46
278
  export const themeColors = {
47
- text: (str) => activeTheme === 'light' ? chalk.black(str) : chalk.white(str),
48
- textBold: (str) => activeTheme === 'light' ? chalk.black.bold(str) : chalk.white.bold(str),
49
- dim: (str) => activeTheme === 'light' ? chalk.gray(str) : chalk.dim(str),
50
- dimYellow: (str) => activeTheme === 'light' ? chalk.rgb(180, 150, 0)(str) : chalk.dim.yellow(str),
51
- bgCursor: (str) => activeTheme === 'light' ? chalk.bgRgb(220, 220, 240).black(str) : chalk.bgRgb(30, 30, 60)(str),
52
- bgCursorInstall: (str) => activeTheme === 'light' ? chalk.bgRgb(220, 220, 240).black(str) : chalk.bgRgb(24, 44, 62)(str),
53
- bgCursorSettingsList: (str) => activeTheme === 'light' ? chalk.bgRgb(220, 240, 220).black(str) : chalk.bgRgb(30, 45, 30)(str),
54
- bgCursorLegacy: (str) => activeTheme === 'light' ? chalk.bgRgb(240, 220, 240).black(str) : chalk.bgRgb(55, 25, 55)(str),
55
-
56
- bgModelCursor: (str) => activeTheme === 'light' ? chalk.bgRgb(230, 210, 230).black(str) : chalk.bgRgb(155, 55, 135)(str),
57
- bgModelRecommended: (str) => activeTheme === 'light' ? chalk.bgRgb(200, 240, 200).black(str) : chalk.bgRgb(15, 40, 15)(str),
58
- bgModelFavorite: (str) => activeTheme === 'light' ? chalk.bgRgb(250, 230, 190).black(str) : chalk.bgRgb(88, 64, 10)(str),
59
-
60
- overlayBgSettings: (str) => activeTheme === 'light' ? chalk.bgRgb(245, 245, 250).black(str) : chalk.bgRgb(0, 0, 0).white(str),
61
- overlayBgHelp: (str) => activeTheme === 'light' ? chalk.bgRgb(250, 250, 250).black(str) : chalk.bgRgb(0, 0, 0).white(str),
62
- overlayBgRecommend: (str) => activeTheme === 'light' ? chalk.bgRgb(240, 250, 245).black(str) : chalk.bgRgb(0, 0, 0).white(str),
63
- overlayBgFeedback: (str) => activeTheme === 'light' ? chalk.bgRgb(255, 245, 245).black(str) : chalk.bgRgb(46, 20, 20).white(str),
64
-
65
- // Header badges text color override
66
- badgeText: (str) => activeTheme === 'light' ? chalk.rgb(255, 255, 255)(str) : chalk.rgb(0, 0, 0)(str),
279
+ text: (text) => paintRgb(currentPalette().text, text),
280
+ textBold: (text) => paintRgb(currentPalette().textStrong, text, { bold: true }),
281
+ dim: (text) => paintRgb(currentPalette().muted, text),
282
+ soft: (text) => paintRgb(currentPalette().soft, text),
283
+ accent: (text) => paintRgb(currentPalette().accent, text),
284
+ accentBold: (text) => paintRgb(currentPalette().accentStrong, text, { bold: true }),
285
+ info: (text) => paintRgb(currentPalette().info, text),
286
+ success: (text) => paintRgb(currentPalette().success, text),
287
+ successBold: (text) => paintRgb(currentPalette().successStrong, text, { bold: true }),
288
+ warning: (text) => paintRgb(currentPalette().warning, text),
289
+ warningBold: (text) => paintRgb(currentPalette().warningStrong, text, { bold: true }),
290
+ error: (text) => paintRgb(currentPalette().danger, text),
291
+ errorBold: (text) => paintRgb(currentPalette().dangerStrong, text, { bold: true }),
292
+ hotkey: (text) => paintRgb(currentPalette().hotkey, text, { bold: true }),
293
+ link: (text) => paintRgb(currentPalette().link, text),
294
+ border: (text) => paintRgb(currentPalette().border, text),
295
+ footerLove: (text) => paintRgb(currentPalette().footerLove, text),
296
+ footerCoffee: (text) => paintRgb(currentPalette().footerCoffee, text),
297
+ footerDiscord: (text) => paintRgb(currentPalette().footerDiscord, text),
298
+ metricGood: (text) => paintRgb(currentPalette().successStrong, text),
299
+ metricOk: (text) => paintRgb(currentPalette().info, text),
300
+ metricWarn: (text) => paintRgb(currentPalette().warning, text),
301
+ metricBad: (text) => paintRgb(currentPalette().danger, text),
302
+ provider: (providerKey, text, { bold = false } = {}) => paintRgb(getProviderRgb(providerKey), text, { bold }),
303
+ tier: (tier, text, { bold = true } = {}) => paintRgb(getTierRgb(tier), text, { bold }),
304
+ badge: (text, bgRgb, fgRgb = null) => paintBg(bgRgb, ` ${text} `, fgRgb, { bold: true }),
305
+ bgCursor: (text) => paintBg(currentPalette().cursor.defaultBg, text, currentPalette().cursor.defaultFg),
306
+ bgCursorInstall: (text) => paintBg(currentPalette().cursor.installBg, text, currentPalette().cursor.installFg),
307
+ bgCursorSettingsList: (text) => paintBg(currentPalette().cursor.settingsBg, text, currentPalette().cursor.settingsFg),
308
+ bgCursorLegacy: (text) => paintBg(currentPalette().cursor.legacyBg, text, currentPalette().cursor.legacyFg),
309
+ bgModelCursor: (text) => paintBg(currentPalette().cursor.modelBg, text, currentPalette().cursor.modelFg),
310
+ bgModelRecommended: (text) => paintBg(currentPalette().cursor.recommendedBg, text, currentPalette().cursor.recommendedFg),
311
+ bgModelFavorite: (text) => paintBg(currentPalette().cursor.favoriteBg, text, currentPalette().cursor.favoriteFg),
312
+ overlayBgSettings: (text) => paintBg(currentPalette().overlayBg.settings, text, currentPalette().overlayFg),
313
+ overlayBgHelp: (text) => paintBg(currentPalette().overlayBg.help, text, currentPalette().overlayFg),
314
+ overlayBgRecommend: (text) => paintBg(currentPalette().overlayBg.recommend, text, currentPalette().overlayFg),
315
+ overlayBgFeedback: (text) => paintBg(currentPalette().overlayBg.feedback, text, currentPalette().overlayFg),
316
+ overlayBgChangelog: (text) => paintBg(currentPalette().overlayBg.changelog, text, currentPalette().overlayFg),
317
+ overlayBgCommandPalette: (text) => paintBg(currentPalette().overlayBg.commandPalette, text, currentPalette().overlayFg),
67
318
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file tier-colors.js
3
- * @description Chalk colour functions for each tier level, extracted from bin/free-coding-models.js.
3
+ * @description Theme-aware Chalk colour functions for each tier level.
4
4
  *
5
5
  * @details
6
6
  * The tier system maps model quality tiers (S+, S, A+, A, A-, B+, B, C) to a
@@ -9,9 +9,10 @@
9
9
  * single, consistent visual language without depending on the whole TUI entry point.
10
10
  *
11
11
  * The gradient is deliberately designed so that the higher the tier the more
12
- * "neon" and attention-grabbing the colour, while lower tiers fade toward dark red.
13
- * `chalk.rgb()` is used for fine-grained control terminal 256-colour and truecolour
14
- * modes both support this; on terminals that don't, chalk gracefully degrades.
12
+ * The previous palette used very dark reds and bright yellows directly, which
13
+ * became muddy on dark terminals and nearly invisible on light ones. This
14
+ * module now delegates to the semantic theme palette so tier colours stay
15
+ * readable in both modes while keeping the same best→worst ordering.
15
16
  *
16
17
  * @exports
17
18
  * TIER_COLOR — object mapping tier string → chalk colouring function
@@ -21,17 +22,14 @@
21
22
  */
22
23
 
23
24
  import chalk from 'chalk'
25
+ import { getTierRgb } from './theme.js'
24
26
 
25
- // 📖 Tier colors: green gradient (best) yellow orange red (worst).
26
- // 📖 Uses chalk.rgb() for fine-grained color control across 8 tier levels.
27
- // 📖 Each entry is a function (t) => styled string so it can be applied to any text.
28
- export const TIER_COLOR = {
29
- 'S+': t => chalk.bold.rgb(0, 255, 80)(t), // 🟢 bright neon green — elite
30
- 'S': t => chalk.bold.rgb(80, 220, 0)(t), // 🟢 green — excellent
31
- 'A+': t => chalk.bold.rgb(170, 210, 0)(t), // 🟡 yellow-green — great
32
- 'A': t => chalk.bold.rgb(240, 190, 0)(t), // 🟡 yellow — good
33
- 'A-': t => chalk.bold.rgb(255, 130, 0)(t), // 🟠 amber — decent
34
- 'B+': t => chalk.bold.rgb(255, 70, 0)(t), // 🟠 orange-red — average
35
- 'B': t => chalk.bold.rgb(210, 20, 0)(t), // 🔴 red — below avg
36
- 'C': t => chalk.bold.rgb(140, 0, 0)(t), // 🔴 dark red — lightweight
37
- }
27
+ // 📖 TIER_COLOR remains object-like for existing call sites, but every access is
28
+ // 📖 resolved lazily from the live theme so `G`/Settings theme switches repaint
29
+ // 📖 the whole TUI without rebuilding import-time constants.
30
+ export const TIER_COLOR = new Proxy({}, {
31
+ get(_target, tier) {
32
+ if (typeof tier !== 'string') return undefined
33
+ return (text) => chalk.bold.rgb(...getTierRgb(tier))(text)
34
+ },
35
+ })