free-coding-models 0.3.17 → 0.3.18

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,315 @@
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
+ },
56
+ cursor: {
57
+ defaultBg: [39, 55, 90],
58
+ defaultFg: [255, 255, 255],
59
+ installBg: [22, 60, 69],
60
+ installFg: [255, 255, 255],
61
+ settingsBg: [26, 54, 34],
62
+ settingsFg: [255, 255, 255],
63
+ legacyBg: [67, 31, 69],
64
+ legacyFg: [255, 255, 255],
65
+ modelBg: [62, 73, 115],
66
+ modelFg: [255, 255, 255],
67
+ recommendedBg: [20, 51, 33],
68
+ recommendedFg: [234, 239, 248],
69
+ favoriteBg: [76, 55, 17],
70
+ favoriteFg: [255, 244, 220],
71
+ },
72
+ },
73
+ light: {
74
+ text: [28, 36, 51],
75
+ textStrong: [8, 12, 20],
76
+ muted: [95, 109, 129],
77
+ soft: [76, 89, 109],
78
+ accent: [0, 120, 186],
79
+ accentStrong: [0, 99, 163],
80
+ info: [0, 109, 168],
81
+ success: [0, 118, 68],
82
+ successStrong: [0, 96, 56],
83
+ warning: [146, 90, 0],
84
+ warningStrong: [171, 102, 0],
85
+ danger: [177, 53, 53],
86
+ dangerStrong: [147, 35, 48],
87
+ hotkey: [171, 98, 0],
88
+ link: [0, 94, 170],
89
+ border: [151, 166, 188],
90
+ footerLove: [176, 79, 128],
91
+ footerCoffee: [170, 102, 0],
92
+ footerDiscord: [104, 83, 190],
93
+ overlayFg: [28, 36, 51],
94
+ overlayBg: {
95
+ settings: [248, 250, 255],
96
+ help: [246, 250, 255],
97
+ recommend: [246, 252, 248],
98
+ feedback: [255, 247, 248],
99
+ changelog: [244, 248, 255],
100
+ },
101
+ cursor: {
102
+ defaultBg: [217, 231, 255],
103
+ defaultFg: [9, 18, 35],
104
+ installBg: [218, 242, 236],
105
+ installFg: [12, 33, 26],
106
+ settingsBg: [225, 244, 229],
107
+ settingsFg: [14, 43, 27],
108
+ legacyBg: [248, 228, 244],
109
+ legacyFg: [76, 28, 73],
110
+ modelBg: [209, 223, 255],
111
+ modelFg: [9, 18, 35],
112
+ recommendedBg: [221, 245, 229],
113
+ recommendedFg: [17, 47, 28],
114
+ favoriteBg: [255, 241, 208],
115
+ favoriteFg: [79, 53, 0],
116
+ },
117
+ },
118
+ }
119
+
120
+ const PROVIDER_PALETTES = {
121
+ dark: {
122
+ nvidia: [132, 235, 168],
123
+ groq: [255, 191, 144],
124
+ cerebras: [153, 215, 255],
125
+ sambanova: [255, 215, 142],
126
+ openrouter: [228, 191, 239],
127
+ huggingface: [255, 235, 122],
128
+ replicate: [166, 212, 255],
129
+ deepinfra: [146, 222, 213],
130
+ fireworks: [255, 184, 194],
131
+ codestral: [245, 175, 212],
132
+ hyperbolic: [255, 160, 127],
133
+ scaleway: [115, 209, 255],
134
+ googleai: [166, 210, 255],
135
+ siliconflow: [145, 232, 243],
136
+ together: [255, 232, 98],
137
+ cloudflare: [255, 191, 118],
138
+ perplexity: [243, 157, 195],
139
+ qwen: [255, 213, 128],
140
+ zai: [150, 208, 255],
141
+ iflow: [211, 229, 101],
142
+ },
143
+ light: {
144
+ nvidia: [0, 126, 73],
145
+ groq: [171, 86, 22],
146
+ cerebras: [0, 102, 177],
147
+ sambanova: [165, 94, 0],
148
+ openrouter: [122, 65, 156],
149
+ huggingface: [135, 104, 0],
150
+ replicate: [0, 94, 163],
151
+ deepinfra: [0, 122, 117],
152
+ fireworks: [183, 55, 72],
153
+ codestral: [157, 61, 110],
154
+ hyperbolic: [178, 68, 27],
155
+ scaleway: [0, 113, 189],
156
+ googleai: [0, 111, 168],
157
+ siliconflow: [0, 115, 138],
158
+ together: [122, 101, 0],
159
+ cloudflare: [176, 92, 0],
160
+ perplexity: [171, 62, 121],
161
+ qwen: [132, 89, 0],
162
+ zai: [0, 104, 171],
163
+ iflow: [107, 130, 0],
164
+ },
165
+ }
166
+
167
+ const TIER_PALETTES = {
168
+ dark: {
169
+ 'S+': [111, 255, 164],
170
+ 'S': [147, 241, 101],
171
+ 'A+': [201, 233, 104],
172
+ 'A': [255, 211, 101],
173
+ 'A-': [255, 178, 100],
174
+ 'B+': [255, 145, 112],
175
+ 'B': [255, 113, 113],
176
+ 'C': [255, 139, 164],
177
+ },
178
+ light: {
179
+ 'S+': [0, 122, 58],
180
+ 'S': [54, 122, 0],
181
+ 'A+': [95, 113, 0],
182
+ 'A': [128, 92, 0],
183
+ 'A-': [156, 80, 0],
184
+ 'B+': [171, 69, 0],
185
+ 'B': [168, 44, 44],
186
+ 'C': [123, 35, 75],
187
+ },
188
+ }
189
+
190
+ function currentPalette() {
191
+ return PALETTES[activeTheme] ?? PALETTES.dark
192
+ }
193
+
194
+ function themeLabel(theme) {
195
+ return theme.charAt(0).toUpperCase() + theme.slice(1)
196
+ }
197
+
198
+ function buildStyle({ fgRgb = null, bgRgb = null, bold = false, italic = false } = {}) {
199
+ let style = chalk
200
+ if (bgRgb) style = style.bgRgb(...bgRgb)
201
+ if (fgRgb) style = style.rgb(...fgRgb)
202
+ if (bold) style = style.bold
203
+ if (italic) style = style.italic
204
+ return style
205
+ }
206
+
207
+ export function getReadableTextRgb(bgRgb) {
208
+ const [r, g, b] = bgRgb
209
+ const yiq = (r * 299 + g * 587 + b * 114) / 1000
210
+ return yiq >= 150 ? [10, 16, 28] : [248, 251, 255]
211
+ }
212
+
213
+ export function detectActiveTheme(configTheme = 'auto') {
12
214
  if (configTheme === 'dark' || configTheme === 'light') {
13
- activeTheme = configTheme;
14
- return activeTheme;
215
+ activeTheme = configTheme
216
+ return activeTheme
15
217
  }
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;
218
+
219
+ const fgbg = process.env.COLORFGBG || ''
220
+ if (fgbg.includes(';15') || fgbg.includes(';7') || fgbg.includes(';base03')) {
221
+ activeTheme = 'light'
222
+ return activeTheme
25
223
  }
26
-
224
+ if (fgbg) {
225
+ activeTheme = 'dark'
226
+ return activeTheme
227
+ }
228
+
27
229
  if (process.platform === 'darwin') {
28
230
  try {
29
- const style = execSync('defaults read -g AppleInterfaceStyle 2>/dev/null', { timeout: 100 }).toString().trim();
30
- activeTheme = style === 'Dark' ? 'dark' : 'light';
231
+ const style = execSync('defaults read -g AppleInterfaceStyle 2>/dev/null', { timeout: 100 }).toString().trim()
232
+ activeTheme = style === 'Dark' ? 'dark' : 'light'
31
233
  } catch {
32
- activeTheme = 'light';
234
+ activeTheme = 'light'
33
235
  }
34
- } else {
35
- activeTheme = 'dark';
236
+ return activeTheme
36
237
  }
37
-
38
- return activeTheme;
238
+
239
+ activeTheme = 'dark'
240
+ return activeTheme
39
241
  }
40
242
 
41
243
  export function getTheme() {
42
- return activeTheme;
244
+ return activeTheme
245
+ }
246
+
247
+ export function cycleThemeSetting(currentTheme = 'auto') {
248
+ const currentIdx = THEME_OPTIONS.indexOf(currentTheme)
249
+ const nextIdx = currentIdx === -1 ? 0 : (currentIdx + 1) % THEME_OPTIONS.length
250
+ return THEME_OPTIONS[nextIdx]
251
+ }
252
+
253
+ export function getThemeStatusLabel(setting = 'auto') {
254
+ if (setting === 'auto') return `Auto → ${themeLabel(activeTheme)}`
255
+ return themeLabel(setting)
256
+ }
257
+
258
+ export function getProviderRgb(providerKey) {
259
+ const palette = PROVIDER_PALETTES[activeTheme] ?? PROVIDER_PALETTES.dark
260
+ return palette[providerKey] ?? currentPalette().accent
261
+ }
262
+
263
+ export function getTierRgb(tier) {
264
+ const palette = TIER_PALETTES[activeTheme] ?? TIER_PALETTES.dark
265
+ return palette[tier] ?? currentPalette().textStrong
266
+ }
267
+
268
+ function paintRgb(rgb, text, options = {}) {
269
+ return buildStyle({ fgRgb: rgb, ...options })(text)
270
+ }
271
+
272
+ function paintBg(bgRgb, text, fgRgb = null, options = {}) {
273
+ return buildStyle({ bgRgb, fgRgb: fgRgb ?? getReadableTextRgb(bgRgb), ...options })(text)
43
274
  }
44
275
 
45
- // Semantic colors
46
276
  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),
277
+ text: (text) => paintRgb(currentPalette().text, text),
278
+ textBold: (text) => paintRgb(currentPalette().textStrong, text, { bold: true }),
279
+ dim: (text) => paintRgb(currentPalette().muted, text),
280
+ soft: (text) => paintRgb(currentPalette().soft, text),
281
+ accent: (text) => paintRgb(currentPalette().accent, text),
282
+ accentBold: (text) => paintRgb(currentPalette().accentStrong, text, { bold: true }),
283
+ info: (text) => paintRgb(currentPalette().info, text),
284
+ success: (text) => paintRgb(currentPalette().success, text),
285
+ successBold: (text) => paintRgb(currentPalette().successStrong, text, { bold: true }),
286
+ warning: (text) => paintRgb(currentPalette().warning, text),
287
+ warningBold: (text) => paintRgb(currentPalette().warningStrong, text, { bold: true }),
288
+ error: (text) => paintRgb(currentPalette().danger, text),
289
+ errorBold: (text) => paintRgb(currentPalette().dangerStrong, text, { bold: true }),
290
+ hotkey: (text) => paintRgb(currentPalette().hotkey, text, { bold: true }),
291
+ link: (text) => paintRgb(currentPalette().link, text),
292
+ border: (text) => paintRgb(currentPalette().border, text),
293
+ footerLove: (text) => paintRgb(currentPalette().footerLove, text),
294
+ footerCoffee: (text) => paintRgb(currentPalette().footerCoffee, text),
295
+ footerDiscord: (text) => paintRgb(currentPalette().footerDiscord, text),
296
+ metricGood: (text) => paintRgb(currentPalette().successStrong, text),
297
+ metricOk: (text) => paintRgb(currentPalette().info, text),
298
+ metricWarn: (text) => paintRgb(currentPalette().warning, text),
299
+ metricBad: (text) => paintRgb(currentPalette().danger, text),
300
+ provider: (providerKey, text, { bold = false } = {}) => paintRgb(getProviderRgb(providerKey), text, { bold }),
301
+ tier: (tier, text, { bold = true } = {}) => paintRgb(getTierRgb(tier), text, { bold }),
302
+ badge: (text, bgRgb, fgRgb = null) => paintBg(bgRgb, ` ${text} `, fgRgb, { bold: true }),
303
+ bgCursor: (text) => paintBg(currentPalette().cursor.defaultBg, text, currentPalette().cursor.defaultFg),
304
+ bgCursorInstall: (text) => paintBg(currentPalette().cursor.installBg, text, currentPalette().cursor.installFg),
305
+ bgCursorSettingsList: (text) => paintBg(currentPalette().cursor.settingsBg, text, currentPalette().cursor.settingsFg),
306
+ bgCursorLegacy: (text) => paintBg(currentPalette().cursor.legacyBg, text, currentPalette().cursor.legacyFg),
307
+ bgModelCursor: (text) => paintBg(currentPalette().cursor.modelBg, text, currentPalette().cursor.modelFg),
308
+ bgModelRecommended: (text) => paintBg(currentPalette().cursor.recommendedBg, text, currentPalette().cursor.recommendedFg),
309
+ bgModelFavorite: (text) => paintBg(currentPalette().cursor.favoriteBg, text, currentPalette().cursor.favoriteFg),
310
+ overlayBgSettings: (text) => paintBg(currentPalette().overlayBg.settings, text, currentPalette().overlayFg),
311
+ overlayBgHelp: (text) => paintBg(currentPalette().overlayBg.help, text, currentPalette().overlayFg),
312
+ overlayBgRecommend: (text) => paintBg(currentPalette().overlayBg.recommend, text, currentPalette().overlayFg),
313
+ overlayBgFeedback: (text) => paintBg(currentPalette().overlayBg.feedback, text, currentPalette().overlayFg),
314
+ overlayBgChangelog: (text) => paintBg(currentPalette().overlayBg.changelog, text, currentPalette().overlayFg),
67
315
  }
@@ -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
+ })