ctheme 0.1.0

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.
Files changed (3) hide show
  1. package/README.md +155 -0
  2. package/bin/ctheme.js +1819 -0
  3. package/package.json +34 -0
package/bin/ctheme.js ADDED
@@ -0,0 +1,1819 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+ const readline = require("readline/promises");
7
+ const { execFileSync } = require("child_process");
8
+
9
+ const TOKENS = [
10
+ "accent",
11
+ "border",
12
+ "borderAccent",
13
+ "borderMuted",
14
+ "success",
15
+ "error",
16
+ "warning",
17
+ "muted",
18
+ "dim",
19
+ "text",
20
+ "thinkingText",
21
+ "selectedBg",
22
+ "userMessageBg",
23
+ "userMessageText",
24
+ "customMessageBg",
25
+ "customMessageText",
26
+ "customMessageLabel",
27
+ "toolPendingBg",
28
+ "toolSuccessBg",
29
+ "toolErrorBg",
30
+ "toolTitle",
31
+ "toolOutput",
32
+ "mdHeading",
33
+ "mdLink",
34
+ "mdLinkUrl",
35
+ "mdCode",
36
+ "mdCodeBlock",
37
+ "mdCodeBlockBorder",
38
+ "mdQuote",
39
+ "mdQuoteBorder",
40
+ "mdHr",
41
+ "mdListBullet",
42
+ "toolDiffAdded",
43
+ "toolDiffRemoved",
44
+ "toolDiffContext",
45
+ "syntaxComment",
46
+ "syntaxKeyword",
47
+ "syntaxFunction",
48
+ "syntaxVariable",
49
+ "syntaxString",
50
+ "syntaxNumber",
51
+ "syntaxType",
52
+ "syntaxOperator",
53
+ "syntaxPunctuation",
54
+ "thinkingOff",
55
+ "thinkingMinimal",
56
+ "thinkingLow",
57
+ "thinkingMedium",
58
+ "thinkingHigh",
59
+ "thinkingXhigh",
60
+ "bashMode"
61
+ ];
62
+
63
+ const BUILTIN_THEMES = [
64
+ "1337",
65
+ "base16-eighties-dark",
66
+ "base16-mocha-dark",
67
+ "base16-ocean-dark",
68
+ "base16-ocean-light",
69
+ "base16-256",
70
+ "catppuccin-frappe",
71
+ "catppuccin-latte",
72
+ "catppuccin-macchiato",
73
+ "catppuccin-mocha",
74
+ "coldark-cold",
75
+ "coldark-dark",
76
+ "dark-neon",
77
+ "dracula",
78
+ "github",
79
+ "gruvbox-dark",
80
+ "gruvbox-light",
81
+ "inspired-github",
82
+ "monokai-extended",
83
+ "monokai-extended-bright",
84
+ "monokai-extended-light",
85
+ "monokai-extended-origin",
86
+ "nord",
87
+ "one-half-dark",
88
+ "one-half-light",
89
+ "solarized-dark",
90
+ "solarized-light",
91
+ "sublime-snazzy",
92
+ "two-dark",
93
+ "zenburn"
94
+ ];
95
+
96
+ const FONT_CHOICES = {
97
+ ui: ["Manrope", "Space Grotesk", "Outfit", "Sora", "Plus Jakarta Sans", "Newsreader", "Fraunces"],
98
+ code: ["IBM Plex Mono", "Space Mono", "DM Mono", "Source Code Pro", "Fira Code", "JetBrains Mono"],
99
+ terminal: ["IBM Plex Mono", "Space Mono", "DM Mono", "Source Code Pro", "Fira Code", "JetBrains Mono"]
100
+ };
101
+
102
+ const PRESETS = {
103
+ noir: {
104
+ fonts: { terminal: "IBM Plex Mono", ui: "Space Grotesk", code: "IBM Plex Mono" },
105
+ accent: "#79c0ff",
106
+ bg: "#0d1117",
107
+ surface: "#161b22",
108
+ surfaceAlt: "#1f2630",
109
+ fg: "#e6edf3",
110
+ muted: "#8b949e",
111
+ dim: "#6e7681",
112
+ green: "#3fb950",
113
+ red: "#ff7b72",
114
+ yellow: "#d29922",
115
+ orange: "#ffb86c",
116
+ purple: "#bc8cff",
117
+ cyan: "#39c5cf"
118
+ },
119
+ paper: {
120
+ fonts: { terminal: "IBM Plex Mono", ui: "Newsreader", code: "IBM Plex Mono" },
121
+ accent: "#005cc5",
122
+ bg: "#fff8e8",
123
+ surface: "#fffdf7",
124
+ surfaceAlt: "#f4ecdb",
125
+ fg: "#2d261d",
126
+ muted: "#6f6558",
127
+ dim: "#8b7f71",
128
+ green: "#1f883d",
129
+ red: "#cf222e",
130
+ yellow: "#9a6700",
131
+ orange: "#bc4c00",
132
+ purple: "#8250df",
133
+ cyan: "#0a7ea4"
134
+ },
135
+ ember: {
136
+ fonts: { terminal: "JetBrains Mono", ui: "Sora", code: "JetBrains Mono" },
137
+ accent: "#ff9e64",
138
+ bg: "#16120f",
139
+ surface: "#221a15",
140
+ surfaceAlt: "#2d221c",
141
+ fg: "#f7efe8",
142
+ muted: "#b29a88",
143
+ dim: "#8d7767",
144
+ green: "#9ece6a",
145
+ red: "#f7768e",
146
+ yellow: "#e0af68",
147
+ orange: "#ff7a59",
148
+ purple: "#bb9af7",
149
+ cyan: "#7dcfff"
150
+ },
151
+ mint: {
152
+ fonts: { terminal: "JetBrains Mono", ui: "Plus Jakarta Sans", code: "JetBrains Mono" },
153
+ accent: "#2bb673",
154
+ bg: "#081411",
155
+ surface: "#10201b",
156
+ surfaceAlt: "#163029",
157
+ fg: "#e7fff7",
158
+ muted: "#91b7ab",
159
+ dim: "#6f9186",
160
+ green: "#52d273",
161
+ red: "#ff7a90",
162
+ yellow: "#d5c94f",
163
+ orange: "#ff9d5c",
164
+ purple: "#b69cff",
165
+ cyan: "#5ad1e6"
166
+ },
167
+ obsidian: {
168
+ fonts: { terminal: "IBM Plex Mono", ui: "Outfit", code: "IBM Plex Mono" },
169
+ accent: "#58a6ff",
170
+ bg: "#05070a",
171
+ surface: "#0e131a",
172
+ surfaceAlt: "#171d26",
173
+ fg: "#edf3ff",
174
+ muted: "#93a1b5",
175
+ dim: "#677387",
176
+ green: "#4fd1a5",
177
+ red: "#ff7a90",
178
+ yellow: "#f6c177",
179
+ orange: "#ff9e64",
180
+ purple: "#c4a7e7",
181
+ cyan: "#7dd3fc"
182
+ },
183
+ glacier: {
184
+ fonts: { terminal: "Space Mono", ui: "Space Grotesk", code: "Space Mono" },
185
+ accent: "#7aa2f7",
186
+ bg: "#0a1220",
187
+ surface: "#121c2f",
188
+ surfaceAlt: "#1a2840",
189
+ fg: "#eaf2ff",
190
+ muted: "#9eb3cc",
191
+ dim: "#6f86a2",
192
+ green: "#8bd5ca",
193
+ red: "#f7768e",
194
+ yellow: "#e0af68",
195
+ orange: "#ff9e64",
196
+ purple: "#bb9af7",
197
+ cyan: "#7dcfff"
198
+ },
199
+ cobalt: {
200
+ fonts: { terminal: "Space Mono", ui: "Sora", code: "Space Mono" },
201
+ accent: "#4da3ff",
202
+ bg: "#09111f",
203
+ surface: "#10203a",
204
+ surfaceAlt: "#163056",
205
+ fg: "#edf5ff",
206
+ muted: "#90a9ca",
207
+ dim: "#5f7491",
208
+ green: "#4de2a8",
209
+ red: "#ff6688",
210
+ yellow: "#ffd166",
211
+ orange: "#ff9f43",
212
+ purple: "#8f7cff",
213
+ cyan: "#56ccf2"
214
+ },
215
+ rose: {
216
+ fonts: { terminal: "DM Mono", ui: "Fraunces", code: "DM Mono" },
217
+ accent: "#ea9ab2",
218
+ bg: "#171217",
219
+ surface: "#231b23",
220
+ surfaceAlt: "#302430",
221
+ fg: "#fff1f6",
222
+ muted: "#c8a7b6",
223
+ dim: "#927787",
224
+ green: "#9ccf9b",
225
+ red: "#ff7d97",
226
+ yellow: "#f3c66f",
227
+ orange: "#ffab73",
228
+ purple: "#d4a5ff",
229
+ cyan: "#8bd3dd"
230
+ },
231
+ lotus: {
232
+ fonts: { terminal: "IBM Plex Mono", ui: "Newsreader", code: "IBM Plex Mono" },
233
+ accent: "#d16d9e",
234
+ bg: "#fff7f3",
235
+ surface: "#fffdfb",
236
+ surfaceAlt: "#f7e8e2",
237
+ fg: "#3a2432",
238
+ muted: "#866576",
239
+ dim: "#ab90a1",
240
+ green: "#5a8f62",
241
+ red: "#c7576b",
242
+ yellow: "#b98932",
243
+ orange: "#c86b3c",
244
+ purple: "#8f63b8",
245
+ cyan: "#4d8ca1"
246
+ },
247
+ dune: {
248
+ fonts: { terminal: "JetBrains Mono", ui: "Sora", code: "JetBrains Mono" },
249
+ accent: "#d97706",
250
+ bg: "#16110c",
251
+ surface: "#231912",
252
+ surfaceAlt: "#32231a",
253
+ fg: "#faefe1",
254
+ muted: "#c1a58a",
255
+ dim: "#8d735f",
256
+ green: "#8fbf7a",
257
+ red: "#f97373",
258
+ yellow: "#eab308",
259
+ orange: "#fb923c",
260
+ purple: "#c084fc",
261
+ cyan: "#67e8f9"
262
+ },
263
+ forest: {
264
+ fonts: { terminal: "IBM Plex Mono", ui: "Newsreader", code: "IBM Plex Mono" },
265
+ accent: "#7ccf7a",
266
+ bg: "#08100b",
267
+ surface: "#112016",
268
+ surfaceAlt: "#183020",
269
+ fg: "#eefbef",
270
+ muted: "#98b79d",
271
+ dim: "#6b8b73",
272
+ green: "#4ade80",
273
+ red: "#fb7185",
274
+ yellow: "#facc15",
275
+ orange: "#fb923c",
276
+ purple: "#c084fc",
277
+ cyan: "#6ee7b7"
278
+ },
279
+ aurora: {
280
+ fonts: { terminal: "Space Mono", ui: "Outfit", code: "Space Mono" },
281
+ accent: "#7c5cff",
282
+ bg: "#0f0a1f",
283
+ surface: "#181131",
284
+ surfaceAlt: "#23194a",
285
+ fg: "#f4f0ff",
286
+ muted: "#b2a7d9",
287
+ dim: "#7d72aa",
288
+ green: "#74e3b4",
289
+ red: "#ff7d9c",
290
+ yellow: "#ffd166",
291
+ orange: "#ff9f6e",
292
+ purple: "#b388ff",
293
+ cyan: "#7bdff2"
294
+ },
295
+ espresso: {
296
+ fonts: { terminal: "DM Mono", ui: "Fraunces", code: "DM Mono" },
297
+ accent: "#c67c4e",
298
+ bg: "#120d0b",
299
+ surface: "#1d1512",
300
+ surfaceAlt: "#2c201a",
301
+ fg: "#f7eee8",
302
+ muted: "#b89b8c",
303
+ dim: "#826b60",
304
+ green: "#8bcf9b",
305
+ red: "#f38ba8",
306
+ yellow: "#f9e2af",
307
+ orange: "#fab387",
308
+ purple: "#cba6f7",
309
+ cyan: "#89dceb"
310
+ },
311
+ mono: {
312
+ fonts: { terminal: "IBM Plex Mono", ui: "IBM Plex Sans", code: "IBM Plex Mono" },
313
+ accent: "#c9d1d9",
314
+ bg: "#0d0d0d",
315
+ surface: "#151515",
316
+ surfaceAlt: "#212121",
317
+ fg: "#f5f5f5",
318
+ muted: "#b0b0b0",
319
+ dim: "#767676",
320
+ green: "#b7e4c7",
321
+ red: "#ffadad",
322
+ yellow: "#f4e285",
323
+ orange: "#ffd6a5",
324
+ purple: "#d8bbff",
325
+ cyan: "#bde0fe"
326
+ },
327
+ neon: {
328
+ fonts: { terminal: "Space Mono", ui: "Space Grotesk", code: "Space Mono" },
329
+ accent: "#00f5d4",
330
+ bg: "#090414",
331
+ surface: "#130a24",
332
+ surfaceAlt: "#1c1033",
333
+ fg: "#f6f7ff",
334
+ muted: "#b8add8",
335
+ dim: "#7f76a8",
336
+ green: "#72f1b8",
337
+ red: "#ff5c8a",
338
+ yellow: "#ffe66d",
339
+ orange: "#ff9f5c",
340
+ purple: "#b388ff",
341
+ cyan: "#00bbf9"
342
+ },
343
+ harbor: {
344
+ fonts: { terminal: "IBM Plex Mono", ui: "Plus Jakarta Sans", code: "IBM Plex Mono" },
345
+ accent: "#3ba4c9",
346
+ bg: "#f4f8fb",
347
+ surface: "#ffffff",
348
+ surfaceAlt: "#e6eef4",
349
+ fg: "#18303a",
350
+ muted: "#5f7c89",
351
+ dim: "#8ba1ab",
352
+ green: "#2f855a",
353
+ red: "#d1495b",
354
+ yellow: "#b7791f",
355
+ orange: "#dd6b20",
356
+ purple: "#6b46c1",
357
+ cyan: "#0f766e"
358
+ },
359
+ solarized: {
360
+ fonts: { terminal: "Source Code Pro", ui: "Source Sans 3", code: "Source Code Pro" },
361
+ accent: "#268bd2",
362
+ bg: "#fdf6e3",
363
+ surface: "#fffdf7",
364
+ surfaceAlt: "#eee8d5",
365
+ fg: "#586e75",
366
+ muted: "#657b83",
367
+ dim: "#93a1a1",
368
+ green: "#859900",
369
+ red: "#dc322f",
370
+ yellow: "#b58900",
371
+ orange: "#cb4b16",
372
+ purple: "#6c71c4",
373
+ cyan: "#2aa198"
374
+ },
375
+ "solarized-dark": {
376
+ fonts: { terminal: "Source Code Pro", ui: "Source Sans 3", code: "Source Code Pro" },
377
+ accent: "#268bd2",
378
+ bg: "#002b36",
379
+ surface: "#073642",
380
+ surfaceAlt: "#0a3b48",
381
+ fg: "#839496",
382
+ muted: "#93a1a1",
383
+ dim: "#657b83",
384
+ green: "#859900",
385
+ red: "#dc322f",
386
+ yellow: "#b58900",
387
+ orange: "#cb4b16",
388
+ purple: "#6c71c4",
389
+ cyan: "#2aa198"
390
+ },
391
+ velvet: {
392
+ fonts: { terminal: "DM Mono", ui: "Fraunces", code: "DM Mono" },
393
+ accent: "#d4a5ff",
394
+ bg: "#140d18",
395
+ surface: "#211328",
396
+ surfaceAlt: "#2d1b37",
397
+ fg: "#faf2ff",
398
+ muted: "#b79dc7",
399
+ dim: "#7e6891",
400
+ green: "#8de1c0",
401
+ red: "#ff8fab",
402
+ yellow: "#f6d365",
403
+ orange: "#ffb86c",
404
+ purple: "#c77dff",
405
+ cyan: "#7bdff2"
406
+ }
407
+ };
408
+
409
+ const APP_PRESETS = {
410
+ evergreen: {
411
+ codeThemeId: "everforest",
412
+ theme: {
413
+ accent: "#93b259",
414
+ contrast: 45,
415
+ fonts: { code: "IBM Plex Mono", ui: "Manrope" },
416
+ ink: "#5c6a72",
417
+ opaqueWindows: true,
418
+ semanticColors: {
419
+ diffAdded: "#8da101",
420
+ diffRemoved: "#f85552",
421
+ skill: "#df69ba"
422
+ },
423
+ surface: "#fdf6e3"
424
+ },
425
+ variant: "light"
426
+ },
427
+ "evergreen-dark": {
428
+ codeThemeId: "everforest",
429
+ theme: {
430
+ accent: "#a7c080",
431
+ contrast: 60,
432
+ fonts: { code: "IBM Plex Mono", ui: "Manrope" },
433
+ ink: "#d3c6aa",
434
+ opaqueWindows: true,
435
+ semanticColors: {
436
+ diffAdded: "#a7c080",
437
+ diffRemoved: "#e67e80",
438
+ skill: "#d699b6"
439
+ },
440
+ surface: "#2d353b"
441
+ },
442
+ variant: "dark"
443
+ },
444
+ solarized: {
445
+ codeThemeId: "solarized",
446
+ theme: {
447
+ accent: "#268bd2",
448
+ contrast: 45,
449
+ fonts: { code: "Source Code Pro", ui: "Manrope" },
450
+ ink: "#586e75",
451
+ opaqueWindows: true,
452
+ semanticColors: {
453
+ diffAdded: "#859900",
454
+ diffRemoved: "#dc322f",
455
+ skill: "#d33682"
456
+ },
457
+ surface: "#fdf6e3"
458
+ },
459
+ variant: "light"
460
+ },
461
+ "solarized-dark": {
462
+ codeThemeId: "solarized",
463
+ theme: {
464
+ accent: "#268bd2",
465
+ contrast: 60,
466
+ fonts: { code: "Source Code Pro", ui: "Manrope" },
467
+ ink: "#839496",
468
+ opaqueWindows: false,
469
+ semanticColors: {
470
+ diffAdded: "#859900",
471
+ diffRemoved: "#dc322f",
472
+ skill: "#d33682"
473
+ },
474
+ surface: "#002b36"
475
+ },
476
+ variant: "dark"
477
+ },
478
+ nord: {
479
+ codeThemeId: "nord",
480
+ theme: {
481
+ accent: "#5e81ac",
482
+ contrast: 48,
483
+ fonts: { code: "Space Mono", ui: "Manrope" },
484
+ ink: "#4c566a",
485
+ opaqueWindows: true,
486
+ semanticColors: {
487
+ diffAdded: "#a3be8c",
488
+ diffRemoved: "#bf616a",
489
+ skill: "#b48ead"
490
+ },
491
+ surface: "#eceff4"
492
+ },
493
+ variant: "light"
494
+ },
495
+ "nord-dark": {
496
+ codeThemeId: "nord",
497
+ theme: {
498
+ accent: "#88c0d0",
499
+ contrast: 60,
500
+ fonts: { code: "Space Mono", ui: "Manrope" },
501
+ ink: "#e5e9f0",
502
+ opaqueWindows: true,
503
+ semanticColors: {
504
+ diffAdded: "#a3be8c",
505
+ diffRemoved: "#bf616a",
506
+ skill: "#b48ead"
507
+ },
508
+ surface: "#2e3440"
509
+ },
510
+ variant: "dark"
511
+ },
512
+ rosepine: {
513
+ codeThemeId: "rose-pine-dawn",
514
+ theme: {
515
+ accent: "#56949f",
516
+ contrast: 44,
517
+ fonts: { code: "DM Mono", ui: "Manrope" },
518
+ ink: "#575279",
519
+ opaqueWindows: true,
520
+ semanticColors: {
521
+ diffAdded: "#286983",
522
+ diffRemoved: "#b4637a",
523
+ skill: "#907aa9"
524
+ },
525
+ surface: "#faf4ed"
526
+ },
527
+ variant: "light"
528
+ },
529
+ "rosepine-dark": {
530
+ codeThemeId: "rose-pine",
531
+ theme: {
532
+ accent: "#9ccfd8",
533
+ contrast: 58,
534
+ fonts: { code: "DM Mono", ui: "Manrope" },
535
+ ink: "#e0def4",
536
+ opaqueWindows: true,
537
+ semanticColors: {
538
+ diffAdded: "#31748f",
539
+ diffRemoved: "#eb6f92",
540
+ skill: "#c4a7e7"
541
+ },
542
+ surface: "#191724"
543
+ },
544
+ variant: "dark"
545
+ },
546
+ graphite: {
547
+ codeThemeId: "github",
548
+ theme: {
549
+ accent: "#586069",
550
+ contrast: 42,
551
+ fonts: { code: "IBM Plex Mono", ui: "Manrope" },
552
+ ink: "#24292f",
553
+ opaqueWindows: true,
554
+ semanticColors: {
555
+ diffAdded: "#1a7f37",
556
+ diffRemoved: "#cf222e",
557
+ skill: "#8250df"
558
+ },
559
+ surface: "#f6f8fa"
560
+ },
561
+ variant: "light"
562
+ },
563
+ "graphite-dark": {
564
+ codeThemeId: "github",
565
+ theme: {
566
+ accent: "#7d8590",
567
+ contrast: 58,
568
+ fonts: { code: "IBM Plex Mono", ui: "Manrope" },
569
+ ink: "#c9d1d9",
570
+ opaqueWindows: true,
571
+ semanticColors: {
572
+ diffAdded: "#3fb950",
573
+ diffRemoved: "#f85149",
574
+ skill: "#d2a8ff"
575
+ },
576
+ surface: "#0d1117"
577
+ },
578
+ variant: "dark"
579
+ }
580
+ };
581
+
582
+ async function main() {
583
+ try {
584
+ const args = process.argv.slice(2);
585
+ const command = args[0];
586
+
587
+ if (!command || command === "--help" || command === "-h" || command === "help") {
588
+ printHelp();
589
+ return;
590
+ }
591
+
592
+ switch (command) {
593
+ case "list":
594
+ runList();
595
+ return;
596
+ case "status":
597
+ runStatus();
598
+ return;
599
+ case "reset":
600
+ runReset(parseFlags(args.slice(1)));
601
+ return;
602
+ case "use":
603
+ runUse(args[1], parseFlags(args.slice(2)));
604
+ return;
605
+ case "preview":
606
+ runPreview(args[1]);
607
+ return;
608
+ case "make":
609
+ await runMake(args[1], parseFlags(args.slice(2)));
610
+ return;
611
+ case "wizard":
612
+ await runMakeWizard(args[1], parseFlags(args.slice(2)));
613
+ return;
614
+ case "init":
615
+ case "new":
616
+ runInit(args[1], parseFlags(args.slice(2)));
617
+ return;
618
+ case "live":
619
+ case "apply":
620
+ runLiveTheme(args[1], parseFlags(args.slice(2)));
621
+ return;
622
+ case "font":
623
+ case "fonts":
624
+ runFontCommand(args.slice(1));
625
+ return;
626
+ case "terminal":
627
+ case "term":
628
+ runTerminal(args[1], parseFlags(args.slice(2)));
629
+ return;
630
+ default:
631
+ fail(`Unknown command: ${command}`);
632
+ }
633
+ } catch (error) {
634
+ fail(error instanceof Error ? error.message : String(error));
635
+ }
636
+ }
637
+
638
+ function printHelp() {
639
+ console.log(`ctheme
640
+
641
+ Short commands for making your terminal look better.
642
+
643
+ Commands:
644
+ list List bundled and saved themes
645
+ status Show ctheme paths and saved terminal restore state
646
+ reset [--target <name>] Restore the terminal UI back to its original state
647
+ use <name> [flags] Apply a theme to the actual terminal immediately
648
+ preview <name> Preview a theme palette in the terminal
649
+ make [name] [flags] Create a custom theme or launch the theme wizard
650
+ wizard [name] [flags] Launch the interactive theme wizard directly
651
+ init <name> [flags] Create an editable starter JSON file
652
+ live <name> [flags] Apply a theme to the current terminal immediately
653
+ font <subcommand> Install and inspect local/Google fonts
654
+ term <target> [flags] Generate or apply terminal font/color themes
655
+
656
+ Examples:
657
+ ctheme reset
658
+ ctheme use solarized
659
+ ctheme use harbor --default
660
+ ctheme make
661
+ ctheme wizard
662
+ ctheme make arjun --from velvet --accent "#ff4d6d"
663
+ ctheme init custom-light --preset harbor
664
+ ctheme live neon
665
+ ctheme live solarized --font "Space Mono"
666
+ ctheme font install "Manrope"
667
+ ctheme font list
668
+ ctheme term apple-terminal --theme noir --font "SF Mono"
669
+ ctheme term ghostty --font "JetBrains Mono" --font-size 15
670
+
671
+ Flags for make:
672
+ --preset <name> Base preset name
673
+ --from <name> Base off any bundled or installed theme
674
+ --accent <hex>
675
+ --bg <hex>
676
+ --surface <hex>
677
+ --surface-alt <hex>
678
+ --fg <hex>
679
+ --muted <hex>
680
+ --dim <hex>
681
+ --font <family>
682
+ --ui-font <family>
683
+ --code-font <family>
684
+ --terminal-font <family>
685
+ --wizard Force the interactive theme wizard
686
+
687
+ Flags for init:
688
+ --preset <name> Starter preset name
689
+ --from <name> Starter from existing theme
690
+ --force Overwrite if the file exists
691
+
692
+ Font subcommands:
693
+ list List installed local font families
694
+ install <family> Download a Google Font family into ~/Library/Fonts/pretty-code
695
+ path Show the managed font install directory
696
+
697
+ Flags for term:
698
+ Targets: apple-terminal, current, ghostty, kitty, wezterm
699
+ --font <family>
700
+ --font-size <number>
701
+ --theme <name>
702
+ --opacity <number>
703
+ --write Write snippet into your terminal config location when supported
704
+ --live Apply immediately when supported
705
+ --default Apple Terminal: set as default and startup profile
706
+
707
+ Flags for use:
708
+ --font <family>
709
+ --font-size <number>
710
+ --default Apple Terminal: set as default and startup profile
711
+
712
+ Flags for reset:
713
+ --target <name> current, apple-terminal, ghostty, kitty, wezterm
714
+ `);
715
+ }
716
+
717
+ function runList() {
718
+ const paths = getThemePaths();
719
+ const custom = new Set(Object.keys(PRESETS));
720
+
721
+ if (fs.existsSync(paths.paletteDir)) {
722
+ for (const entry of fs.readdirSync(paths.paletteDir)) {
723
+ if (entry.endsWith(".json")) {
724
+ custom.add(path.basename(entry, ".json"));
725
+ }
726
+ }
727
+ }
728
+
729
+ for (const name of [...custom].sort()) {
730
+ const source = Object.hasOwn(PRESETS, name) ? "bundled-custom" : "custom";
731
+ console.log(`${name} (${source})`);
732
+ }
733
+ }
734
+
735
+ function runStatus() {
736
+ const paths = getThemePaths();
737
+ const terminalState = readTerminalState(paths);
738
+
739
+ console.log(`home: ${paths.themeHome}`);
740
+ console.log(`theme dir: ${paths.paletteDir}`);
741
+ console.log(`snippet dir: ${paths.snippetDir}`);
742
+ console.log(`terminal state: ${paths.terminalStateFile}`);
743
+ console.log(`saved terminal targets: ${Object.keys(terminalState.targets || {}).join(", ") || "(none)"}`);
744
+ }
745
+
746
+ function runUse(themeName, flags) {
747
+ if (!themeName) {
748
+ fail("Missing theme name. Usage: ctheme use <name>");
749
+ }
750
+
751
+ runLiveTheme(themeName, flags || {});
752
+ }
753
+
754
+ function runReset(flags) {
755
+ const target = normalizeTerminalTarget(flags.target || "current");
756
+ const paths = getThemePaths();
757
+ const state = readTerminalState(paths);
758
+
759
+ if (target === "apple-terminal") {
760
+ restoreAppleTerminalState(state.targets["apple-terminal"] || null);
761
+ console.log("Restored Apple Terminal UI.");
762
+ return;
763
+ }
764
+
765
+ if (target === "ghostty") {
766
+ resetManagedFile(path.join(os.homedir(), ".config", "ghostty", "config"), "config-file = ");
767
+ console.log("Removed managed Ghostty theme include.");
768
+ return;
769
+ }
770
+
771
+ if (target === "kitty") {
772
+ resetManagedFile(path.join(os.homedir(), ".config", "kitty", "kitty.conf"), "include ");
773
+ console.log("Removed managed Kitty theme include.");
774
+ return;
775
+ }
776
+
777
+ if (target === "wezterm") {
778
+ const weztermPath = path.join(os.homedir(), ".wezterm.lua");
779
+ if (fs.existsSync(weztermPath) && fs.readFileSync(weztermPath, "utf8").includes("pretty-code")) {
780
+ fs.unlinkSync(weztermPath);
781
+ console.log(`Removed ${weztermPath}`);
782
+ } else {
783
+ console.log("No managed WezTerm file to remove.");
784
+ }
785
+ return;
786
+ }
787
+
788
+ fail(`Unsupported reset target: ${target}`);
789
+ }
790
+
791
+ function runPreview(themeName) {
792
+ assertThemeName(themeName);
793
+ const theme = buildTheme(themeName, getBasePalette(themeName));
794
+
795
+ const palette = [
796
+ ["accent", theme.colors.accent],
797
+ ["text", theme.colors.text || theme.vars.fg || "#ffffff"],
798
+ ["muted", theme.colors.muted],
799
+ ["success", theme.colors.success],
800
+ ["warning", theme.colors.warning],
801
+ ["error", theme.colors.error],
802
+ ["code", theme.colors.mdCode]
803
+ ];
804
+
805
+ console.log(`Preview: ${theme.name}`);
806
+ for (const [label, value] of palette) {
807
+ const sample = colorBlock(resolveColor(theme, value), 20);
808
+ console.log(`${label.padEnd(8)} ${sample} ${resolveColor(theme, value)}`);
809
+ }
810
+ }
811
+
812
+ async function runMake(name, flags) {
813
+ if (!name || !Object.keys(flags).length || flags.wizard) {
814
+ await runMakeWizard(name, flags);
815
+ return;
816
+ }
817
+
818
+ const preset = getBasePalette(flags.from || flags.preset || "noir");
819
+
820
+ const customized = {
821
+ ...preset,
822
+ accent: flags.accent || preset.accent,
823
+ bg: flags.bg || preset.bg,
824
+ surface: flags.surface || preset.surface,
825
+ surfaceAlt: flags["surface-alt"] || preset.surfaceAlt || preset.surface,
826
+ fg: flags.fg || preset.fg,
827
+ muted: flags.muted || preset.muted,
828
+ dim: flags.dim || preset.dim,
829
+ fonts: mergeThemeFonts(preset, flags)
830
+ };
831
+
832
+ const theme = buildTheme(name, customized);
833
+ const paths = getThemePaths();
834
+ writeThemeAssets(paths, name, customized, theme);
835
+ console.log(`Wrote ${path.join(paths.paletteDir, `${name}.json`)}`);
836
+ }
837
+
838
+ function runInit(name, flags) {
839
+ if (!name) {
840
+ fail("Missing theme name. Usage: ctheme init <name> --preset noir");
841
+ }
842
+
843
+ const paths = getThemePaths();
844
+ ensureDir(paths.paletteDir);
845
+ const filePath = path.join(paths.paletteDir, `${name}.json`);
846
+
847
+ if (fs.existsSync(filePath) && !flags.force) {
848
+ fail(`Theme already exists: ${filePath}. Re-run with --force to overwrite.`);
849
+ }
850
+
851
+ const palette = getBasePalette(flags.from || flags.preset || "noir");
852
+ writeThemeAssets(paths, name, palette);
853
+ console.log(`Created starter theme at ${filePath}`);
854
+ console.log("Edit the JSON, then run: ctheme make " + name + " --from " + name);
855
+ }
856
+
857
+ async function runMakeWizard(initialName, flags) {
858
+ const rl = readline.createInterface({
859
+ input: process.stdin,
860
+ output: process.stdout
861
+ });
862
+
863
+ try {
864
+ const availablePresets = Object.keys(PRESETS).sort();
865
+ console.log(`Theme wizard`);
866
+ console.log(`Presets: ${availablePresets.join(", ")}`);
867
+ console.log(`UI font ideas: ${FONT_CHOICES.ui.join(", ")}`);
868
+ console.log(`Code/terminal font ideas: ${FONT_CHOICES.code.join(", ")}`);
869
+
870
+ const basePresetName = await promptWithDefault(
871
+ rl,
872
+ "Base preset",
873
+ flags.from || flags.preset || "noir"
874
+ );
875
+ const preset = getBasePalette(basePresetName);
876
+ const defaultName = initialName || `${basePresetName}-custom`;
877
+ const themeName = await promptWithDefault(rl, "Theme name", defaultName);
878
+
879
+ const accent = await promptWithDefault(rl, "Accent color", flags.accent || preset.accent);
880
+ const bg = await promptWithDefault(rl, "Background color", flags.bg || preset.bg);
881
+ const surface = await promptWithDefault(rl, "Surface color", flags.surface || preset.surface);
882
+ const surfaceAlt = await promptWithDefault(
883
+ rl,
884
+ "Surface alt color",
885
+ flags["surface-alt"] || preset.surfaceAlt || preset.surface
886
+ );
887
+ const fg = await promptWithDefault(rl, "Text color", flags.fg || preset.fg);
888
+ const muted = await promptWithDefault(rl, "Muted color", flags.muted || preset.muted);
889
+ const dim = await promptWithDefault(rl, "Dim color", flags.dim || preset.dim);
890
+
891
+ const fonts = {
892
+ terminal: await promptWithDefault(
893
+ rl,
894
+ "Terminal font",
895
+ flags["terminal-font"] || flags.font || getPaletteFonts(preset).terminal
896
+ ),
897
+ ui: await promptWithDefault(
898
+ rl,
899
+ "UI font",
900
+ flags["ui-font"] || getPaletteFonts(preset).ui
901
+ ),
902
+ code: await promptWithDefault(
903
+ rl,
904
+ "Code font",
905
+ flags["code-font"] || flags.font || getPaletteFonts(preset).code
906
+ )
907
+ };
908
+
909
+ const autoInstallFonts = await promptYesNo(rl, "Install any missing Google fonts automatically?", true);
910
+ if (autoInstallFonts) {
911
+ installFontsBestEffort([fonts.terminal, fonts.ui, fonts.code]);
912
+ }
913
+
914
+ const customized = {
915
+ ...preset,
916
+ accent,
917
+ bg,
918
+ surface,
919
+ surfaceAlt,
920
+ fg,
921
+ muted,
922
+ dim,
923
+ fonts
924
+ };
925
+
926
+ const theme = buildTheme(themeName, customized);
927
+ const paths = getThemePaths();
928
+ writeThemeAssets(paths, themeName, customized, theme);
929
+ console.log(`Wrote ${path.join(paths.paletteDir, `${themeName}.json`)}`);
930
+
931
+ const applyNow = await promptYesNo(rl, "Apply this theme to the current terminal now?", true);
932
+ if (applyNow) {
933
+ runLiveTheme(themeName, { font: fonts.terminal, default: Boolean(flags.default) });
934
+ } else {
935
+ console.log(`Apply later with: ctheme use ${themeName}`);
936
+ }
937
+ } finally {
938
+ rl.close();
939
+ }
940
+ }
941
+
942
+ function runFontCommand(args) {
943
+ const subcommand = args[0];
944
+ if (!subcommand || subcommand === "help") {
945
+ console.log(`Font commands:
946
+ ctheme font list
947
+ ctheme font search "mono"
948
+ ctheme font path
949
+ ctheme font install "Manrope"
950
+ ctheme font install "IBM Plex Mono"`);
951
+ return;
952
+ }
953
+
954
+ if (subcommand === "list") {
955
+ listInstalledFonts();
956
+ return;
957
+ }
958
+
959
+ if (subcommand === "path") {
960
+ console.log(getThemePaths().managedFontDir);
961
+ return;
962
+ }
963
+
964
+ if (subcommand === "search") {
965
+ const query = args.slice(1).join(" ").trim();
966
+ if (!query) {
967
+ fail('Missing search text. Usage: ctheme font search "mono"');
968
+ }
969
+ searchGoogleFonts(query);
970
+ return;
971
+ }
972
+
973
+ if (subcommand === "install") {
974
+ const family = args.slice(1).join(" ").trim();
975
+ if (!family) {
976
+ fail('Missing font family. Usage: ctheme font install "Manrope"');
977
+ }
978
+ installGoogleFont(family);
979
+ return;
980
+ }
981
+
982
+ fail(`Unknown font subcommand: ${subcommand}`);
983
+ }
984
+
985
+ function runLiveTheme(themeName, flags) {
986
+ if (!themeName) {
987
+ fail("Missing theme name. Usage: ctheme live <name>");
988
+ }
989
+
990
+ const target = normalizeTerminalTarget(flags.target || "current");
991
+ runTerminal(target, { ...flags, theme: themeName, write: true, live: true });
992
+ }
993
+
994
+ function runTerminal(target, flags) {
995
+ if (!target) {
996
+ fail("Missing terminal target. Use apple-terminal, ghostty, wezterm, or kitty.");
997
+ }
998
+
999
+ const normalizedTarget = normalizeTerminalTarget(target);
1000
+ const themeName = flags.theme || "noir";
1001
+ assertThemeName(themeName);
1002
+ const theme = buildTheme(themeName, getBasePalette(themeName));
1003
+ const presetFonts = getThemeFonts(themeName);
1004
+ const font = flags.font || presetFonts.terminal || "JetBrains Mono";
1005
+ const fontSize = Number(flags["font-size"] || 15);
1006
+ const opacity = flags.opacity ? Number(flags.opacity) : null;
1007
+
1008
+ const paths = getThemePaths();
1009
+ ensureDir(paths.snippetDir);
1010
+
1011
+ let snippetPath;
1012
+ let snippet;
1013
+ if (normalizedTarget === "apple-terminal") {
1014
+ snippetPath = path.join(paths.snippetDir, `${theme.name}.applescript`);
1015
+ snippet = renderAppleTerminalProfile(theme, font, fontSize, theme.name);
1016
+ } else if (normalizedTarget === "ghostty") {
1017
+ snippetPath = path.join(paths.snippetDir, "ghostty.conf");
1018
+ snippet = renderGhosttySnippet(theme, font, fontSize, opacity);
1019
+ } else if (normalizedTarget === "wezterm") {
1020
+ snippetPath = path.join(paths.snippetDir, "wezterm.lua");
1021
+ snippet = renderWeztermSnippet(theme, font, fontSize, opacity);
1022
+ } else if (normalizedTarget === "kitty") {
1023
+ snippetPath = path.join(paths.snippetDir, "kitty.conf");
1024
+ snippet = renderKittySnippet(theme, font, fontSize, opacity);
1025
+ } else {
1026
+ fail(`Unsupported terminal target: ${target}`);
1027
+ }
1028
+
1029
+ fs.writeFileSync(snippetPath, snippet, "utf8");
1030
+ console.log(`Snippet written to ${snippetPath}`);
1031
+
1032
+ const shouldWrite = Boolean(flags.write) || Boolean(flags.live) || normalizedTarget === "apple-terminal";
1033
+ if (shouldWrite) {
1034
+ writeTerminalConfig(normalizedTarget, snippet, snippetPath, {
1035
+ profileName: theme.name,
1036
+ setDefault: Boolean(flags.default)
1037
+ });
1038
+ if (normalizedTarget === "apple-terminal") {
1039
+ console.log(`Applied ${theme.name} to current Apple Terminal windows immediately.`);
1040
+ } else if (flags.live) {
1041
+ console.log(`Updated ${normalizedTarget} config for ${theme.name}. Reload behavior depends on that terminal's own live-config support.`);
1042
+ }
1043
+ } else {
1044
+ console.log("Add this snippet manually or rerun with --write.");
1045
+ }
1046
+ }
1047
+
1048
+ function listInstalledFonts() {
1049
+ let output = "";
1050
+ try {
1051
+ output = execFileSync("osascript", [
1052
+ "-l",
1053
+ "JavaScript",
1054
+ "-e",
1055
+ 'ObjC.import("AppKit"); var fm=$.NSFontManager.sharedFontManager; var raw=ObjC.deepUnwrap(fm.availableFontFamilies); var names=(Array.isArray(raw)?raw:[String(raw)]).map(function(name){ return String(name); }).filter(Boolean).sort(function(a,b){ return a.localeCompare(b); }); console.log(names.join("\\n"));'
1056
+ ], { encoding: "utf8" });
1057
+ } catch (error) {
1058
+ fail(error instanceof Error ? error.message : String(error));
1059
+ }
1060
+ process.stdout.write(output);
1061
+ }
1062
+
1063
+ function installGoogleFont(family) {
1064
+ const paths = getThemePaths();
1065
+ ensureDir(paths.managedFontDir);
1066
+ const installed = downloadGoogleFontFiles(family, paths.managedFontDir);
1067
+ if (!installed.length) {
1068
+ fail(`No installable font files found for ${family}`);
1069
+ }
1070
+
1071
+ console.log(`Installed ${family} into ${paths.managedFontDir}`);
1072
+ for (const file of installed) {
1073
+ console.log(`- ${file}`);
1074
+ }
1075
+ if (!looksMonospaceFamily(family)) {
1076
+ console.log("Warning: this family looks proportional. It may render poorly in terminal grids.");
1077
+ }
1078
+ }
1079
+
1080
+ function searchGoogleFonts(query) {
1081
+ const raw = execFileSync("curl", [
1082
+ "-s",
1083
+ "-L",
1084
+ "-H",
1085
+ "User-Agent: pretty-code",
1086
+ "https://fonts.google.com/metadata/fonts"
1087
+ ], { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
1088
+
1089
+ const cleaned = raw.replace(/^\)\]\}'\n/, "");
1090
+ const parsed = JSON.parse(cleaned);
1091
+ const fonts = Array.isArray(parsed.familyMetadataList) ? parsed.familyMetadataList : [];
1092
+ const lowered = query.toLowerCase();
1093
+
1094
+ const matches = fonts
1095
+ .filter((font) => typeof font.family === "string" && font.family.toLowerCase().includes(lowered))
1096
+ .slice(0, 50);
1097
+
1098
+ if (!matches.length) {
1099
+ fail(`No Google Fonts matches for: ${query}`);
1100
+ }
1101
+
1102
+ for (const font of matches) {
1103
+ const mono = looksMonospaceFamily(font.family) ? "mono-ish" : "proportional";
1104
+ console.log(`${font.family} (${mono})`);
1105
+ }
1106
+ }
1107
+
1108
+ function downloadGoogleFontFiles(family, targetDir) {
1109
+ const slug = family.toLowerCase().replace(/[^a-z0-9]+/g, "");
1110
+ const apiBase = "https://api.github.com/repos/google/fonts/contents";
1111
+ const candidateDirs = [`ofl/${slug}`, `apache/${slug}`, `ufl/${slug}`];
1112
+ let entries = null;
1113
+
1114
+ for (const dir of candidateDirs) {
1115
+ try {
1116
+ const json = execFileSync("curl", [
1117
+ "-s",
1118
+ "-L",
1119
+ "-H",
1120
+ "User-Agent: pretty-code",
1121
+ `${apiBase}/${dir}`
1122
+ ], { encoding: "utf8" });
1123
+ const parsed = JSON.parse(json);
1124
+ if (Array.isArray(parsed)) {
1125
+ entries = parsed;
1126
+ break;
1127
+ }
1128
+ } catch (_error) {
1129
+ continue;
1130
+ }
1131
+ }
1132
+
1133
+ if (!entries) {
1134
+ fail(`Google Fonts family not found in google/fonts: ${family}`);
1135
+ }
1136
+
1137
+ const fontFiles = entries.filter((entry) => entry && typeof entry.name === "string" && /\.(ttf|otf|ttc)$/i.test(entry.name));
1138
+ const installed = [];
1139
+
1140
+ for (const entry of fontFiles) {
1141
+ const destPath = path.join(targetDir, entry.name);
1142
+ execFileSync("curl", [
1143
+ "-L",
1144
+ "--fail",
1145
+ "-H",
1146
+ "User-Agent: pretty-code",
1147
+ "--output",
1148
+ destPath,
1149
+ entry.download_url
1150
+ ], { stdio: "inherit" });
1151
+ installed.push(destPath);
1152
+ }
1153
+
1154
+ return installed;
1155
+ }
1156
+
1157
+ function installFontFiles(sourceDir, targetDir) {
1158
+ const installed = [];
1159
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
1160
+ for (const entry of entries) {
1161
+ const sourcePath = path.join(sourceDir, entry.name);
1162
+ if (entry.isDirectory()) {
1163
+ installed.push(...installFontFiles(sourcePath, targetDir));
1164
+ continue;
1165
+ }
1166
+ if (!/\.(ttf|otf|ttc)$/i.test(entry.name)) continue;
1167
+ const destPath = path.join(targetDir, entry.name);
1168
+ fs.copyFileSync(sourcePath, destPath);
1169
+ installed.push(destPath);
1170
+ }
1171
+ return installed;
1172
+ }
1173
+
1174
+ function looksMonospaceFamily(name) {
1175
+ return /(mono|code|terminal|console|courier|menlo|plex mono|fira mono|jetbrains mono|space mono|ibm plex mono)/i.test(name);
1176
+ }
1177
+
1178
+ function buildTheme(name, palette) {
1179
+ const mode = inferMode(palette);
1180
+ const readable = {
1181
+ accent: ensureReadableColor(palette.accent, palette.bg, mode === "light" ? 4.5 : 4.5),
1182
+ fg: ensureReadableColor(palette.fg, palette.bg, mode === "light" ? 9 : 7),
1183
+ muted: ensureReadableColor(palette.muted, palette.bg, mode === "light" ? 5.4 : 4.8),
1184
+ dim: ensureReadableColor(palette.dim, palette.bg, mode === "light" ? 4 : 3.4),
1185
+ green: ensureReadableColor(palette.green, palette.bg, mode === "light" ? 4.8 : 4.5),
1186
+ red: ensureReadableColor(palette.red, palette.bg, mode === "light" ? 4.8 : 4.5),
1187
+ yellow: ensureReadableColor(palette.yellow, palette.bg, mode === "light" ? 5.2 : 4.5),
1188
+ orange: ensureReadableColor(palette.orange, palette.bg, mode === "light" ? 4.8 : 4.5),
1189
+ purple: ensureReadableColor(palette.purple, palette.bg, mode === "light" ? 4.8 : 4.5),
1190
+ cyan: ensureReadableColor(palette.cyan, palette.bg, mode === "light" ? 4.8 : 4.5)
1191
+ };
1192
+
1193
+ const vars = {
1194
+ accent: readable.accent,
1195
+ bg: palette.bg,
1196
+ surface: palette.surface,
1197
+ surfaceAlt: palette.surfaceAlt || palette.surface,
1198
+ fg: readable.fg,
1199
+ muted: readable.muted,
1200
+ dim: readable.dim,
1201
+ green: readable.green,
1202
+ red: readable.red,
1203
+ yellow: readable.yellow,
1204
+ orange: readable.orange,
1205
+ purple: readable.purple,
1206
+ cyan: readable.cyan
1207
+ };
1208
+
1209
+ const colors = {
1210
+ accent: "accent",
1211
+ border: "muted",
1212
+ borderAccent: "accent",
1213
+ borderMuted: "dim",
1214
+ success: "green",
1215
+ error: "red",
1216
+ warning: "yellow",
1217
+ muted: "muted",
1218
+ dim: "dim",
1219
+ text: "fg",
1220
+ thinkingText: "muted",
1221
+ selectedBg: "surfaceAlt",
1222
+ userMessageBg: "surface",
1223
+ userMessageText: "fg",
1224
+ customMessageBg: "surfaceAlt",
1225
+ customMessageText: "fg",
1226
+ customMessageLabel: "accent",
1227
+ toolPendingBg: "surface",
1228
+ toolSuccessBg: "surface",
1229
+ toolErrorBg: "surface",
1230
+ toolTitle: "accent",
1231
+ toolOutput: "fg",
1232
+ mdHeading: "orange",
1233
+ mdLink: "accent",
1234
+ mdLinkUrl: "cyan",
1235
+ mdCode: "cyan",
1236
+ mdCodeBlock: "fg",
1237
+ mdCodeBlockBorder: "muted",
1238
+ mdQuote: "muted",
1239
+ mdQuoteBorder: "muted",
1240
+ mdHr: "dim",
1241
+ mdListBullet: "accent",
1242
+ toolDiffAdded: "green",
1243
+ toolDiffRemoved: "red",
1244
+ toolDiffContext: "muted",
1245
+ syntaxComment: "muted",
1246
+ syntaxKeyword: "purple",
1247
+ syntaxFunction: "accent",
1248
+ syntaxVariable: "orange",
1249
+ syntaxString: "green",
1250
+ syntaxNumber: "yellow",
1251
+ syntaxType: "cyan",
1252
+ syntaxOperator: "accent",
1253
+ syntaxPunctuation: "muted",
1254
+ thinkingOff: "dim",
1255
+ thinkingMinimal: "accent",
1256
+ thinkingLow: "cyan",
1257
+ thinkingMedium: "green",
1258
+ thinkingHigh: "orange",
1259
+ thinkingXhigh: "red",
1260
+ bashMode: "orange"
1261
+ };
1262
+
1263
+ const missing = TOKENS.filter((token) => !Object.hasOwn(colors, token));
1264
+ if (missing.length) {
1265
+ throw new Error(`Theme is missing required tokens: ${missing.join(", ")}`);
1266
+ }
1267
+
1268
+ return {
1269
+ $schema:
1270
+ "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
1271
+ name,
1272
+ vars,
1273
+ colors
1274
+ };
1275
+ }
1276
+
1277
+ function getBasePalette(name) {
1278
+ if (PRESETS[name]) return PRESETS[name];
1279
+
1280
+ const paths = getThemePaths();
1281
+ const palettePath = path.join(paths.paletteDir, `${name}.json`);
1282
+ if (fs.existsSync(palettePath)) {
1283
+ return JSON.parse(fs.readFileSync(palettePath, "utf8"));
1284
+ }
1285
+
1286
+ fail(`Theme not found or not remixable: ${name}`);
1287
+ }
1288
+
1289
+ function normalizeTerminalTarget(target) {
1290
+ if (!target || target === "current") {
1291
+ const detected = detectCurrentTerminalTarget();
1292
+ if (detected) return detected;
1293
+ if (process.platform === "darwin") return "apple-terminal";
1294
+ fail("Could not detect the current terminal. Pass a target like apple-terminal, ghostty, kitty, or wezterm.");
1295
+ }
1296
+
1297
+ if (target === "apple" || target === "terminal" || target === "terminal.app") {
1298
+ return "apple-terminal";
1299
+ }
1300
+
1301
+ return target;
1302
+ }
1303
+
1304
+ function detectCurrentTerminalTarget() {
1305
+ const termProgram = process.env.TERM_PROGRAM || "";
1306
+ if (termProgram === "Apple_Terminal") return "apple-terminal";
1307
+ if (termProgram === "ghostty") return "ghostty";
1308
+ if (termProgram === "WezTerm") return "wezterm";
1309
+ if (termProgram.toLowerCase() === "kitty") return "kitty";
1310
+ return "";
1311
+ }
1312
+
1313
+ function getThemeFonts(name) {
1314
+ if (PRESETS[name] && PRESETS[name].fonts) {
1315
+ return PRESETS[name].fonts;
1316
+ }
1317
+
1318
+ const paths = getThemePaths();
1319
+ const palettePath = path.join(paths.paletteDir, `${name}.json`);
1320
+ if (fs.existsSync(palettePath)) {
1321
+ try {
1322
+ const palette = JSON.parse(fs.readFileSync(palettePath, "utf8"));
1323
+ return getPaletteFonts(palette);
1324
+ } catch (_error) {
1325
+ return { terminal: "JetBrains Mono", ui: "Manrope", code: "JetBrains Mono" };
1326
+ }
1327
+ }
1328
+ return { terminal: "JetBrains Mono", ui: "Manrope", code: "JetBrains Mono" };
1329
+ }
1330
+
1331
+ function getPaletteFonts(palette) {
1332
+ const baseFonts = palette && palette.fonts ? palette.fonts : {};
1333
+ const terminal = baseFonts.terminal || baseFonts.code || "JetBrains Mono";
1334
+ const code = baseFonts.code || baseFonts.terminal || terminal;
1335
+ const ui = baseFonts.ui || "Manrope";
1336
+ return { terminal, ui, code };
1337
+ }
1338
+
1339
+ function mergeThemeFonts(preset, flags) {
1340
+ const baseFonts = getPaletteFonts(preset);
1341
+ const terminal = flags["terminal-font"] || flags.font || baseFonts.terminal;
1342
+ const code = flags["code-font"] || flags.font || baseFonts.code || terminal;
1343
+ const ui = flags["ui-font"] || baseFonts.ui;
1344
+ return { terminal, ui, code };
1345
+ }
1346
+
1347
+ async function promptWithDefault(rl, label, fallback) {
1348
+ const value = await rl.question(`${label}${fallback ? ` [${fallback}]` : ""}: `);
1349
+ const trimmed = value.trim();
1350
+ return trimmed || fallback;
1351
+ }
1352
+
1353
+ async function promptYesNo(rl, label, defaultValue) {
1354
+ const suffix = defaultValue ? " [Y/n]" : " [y/N]";
1355
+ const value = (await rl.question(`${label}${suffix}: `)).trim().toLowerCase();
1356
+ if (!value) return defaultValue;
1357
+ return value === "y" || value === "yes";
1358
+ }
1359
+
1360
+ function installFontsBestEffort(fonts) {
1361
+ for (const family of [...new Set(fonts.filter(Boolean))]) {
1362
+ try {
1363
+ installGoogleFont(family);
1364
+ } catch (error) {
1365
+ console.log(`Skipped Google Fonts install for ${family}: ${error instanceof Error ? error.message : String(error)}`);
1366
+ }
1367
+ }
1368
+ }
1369
+
1370
+ function writeThemeAssets(paths, name, palette) {
1371
+ ensureDir(paths.paletteDir);
1372
+ fs.writeFileSync(path.join(paths.paletteDir, `${name}.json`), `${JSON.stringify(palette, null, 2)}\n`, "utf8");
1373
+ }
1374
+
1375
+ function assertThemeName(themeName) {
1376
+ if (!themeName) {
1377
+ fail("Missing theme name.");
1378
+ }
1379
+ }
1380
+
1381
+ function getThemePaths() {
1382
+ const themeHome = process.env.CTHEME_HOME || path.join(os.homedir(), ".ctheme");
1383
+ return {
1384
+ themeHome,
1385
+ paletteDir: process.env.CTHEME_THEME_DIR || path.join(themeHome, "themes"),
1386
+ snippetDir: process.env.CTHEME_SNIPPET_DIR || path.join(themeHome, "snippets"),
1387
+ managedFontDir: path.join(os.homedir(), "Library", "Fonts", "pretty-code"),
1388
+ terminalStateFile: path.join(themeHome, "snippets", ".ctheme-state.json")
1389
+ };
1390
+ }
1391
+
1392
+ function ensureDir(dirPath) {
1393
+ fs.mkdirSync(dirPath, { recursive: true });
1394
+ }
1395
+
1396
+ function parseFlags(parts) {
1397
+ const flags = {};
1398
+ for (let i = 0; i < parts.length; i += 1) {
1399
+ const part = parts[i];
1400
+ if (!part.startsWith("--")) continue;
1401
+ const key = part.slice(2);
1402
+ const next = parts[i + 1];
1403
+ if (!next || next.startsWith("--")) {
1404
+ flags[key] = true;
1405
+ continue;
1406
+ }
1407
+ flags[key] = next;
1408
+ i += 1;
1409
+ }
1410
+ return flags;
1411
+ }
1412
+
1413
+ function escapeRegExp(value) {
1414
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1415
+ }
1416
+
1417
+ function resolveColor(theme, value) {
1418
+ if (typeof value === "string" && value.startsWith("#")) return value;
1419
+ if (typeof value === "string" && theme.vars && theme.vars[value]) return theme.vars[value];
1420
+ if (value === "") return theme.vars.fg || "#ffffff";
1421
+ return typeof value === "string" ? value : "#ffffff";
1422
+ }
1423
+
1424
+ function renderTmTheme(name, theme) {
1425
+ const globalBg = resolveColor(theme, theme.vars.bg || theme.colors.userMessageBg);
1426
+ const globalFg = resolveColor(theme, theme.vars.fg || theme.colors.text);
1427
+ const globalSelection = resolveColor(theme, theme.colors.selectedBg);
1428
+ const caret = resolveColor(theme, theme.colors.accent);
1429
+ const invisibles = resolveColor(theme, theme.colors.dim);
1430
+ const lineHighlight = resolveColor(theme, theme.vars.surfaceAlt || theme.colors.selectedBg);
1431
+
1432
+ const rules = [
1433
+ { name: "Comment", scope: "comment", foreground: resolveColor(theme, theme.colors.syntaxComment), fontStyle: "italic" },
1434
+ { name: "Keyword", scope: "keyword, storage", foreground: resolveColor(theme, theme.colors.syntaxKeyword) },
1435
+ { name: "String", scope: "string", foreground: resolveColor(theme, theme.colors.syntaxString) },
1436
+ { name: "Number", scope: "constant.numeric", foreground: resolveColor(theme, theme.colors.syntaxNumber) },
1437
+ { name: "Function", scope: "entity.name.function, support.function", foreground: resolveColor(theme, theme.colors.syntaxFunction) },
1438
+ { name: "Type", scope: "entity.name.type, support.type, storage.type", foreground: resolveColor(theme, theme.colors.syntaxType) },
1439
+ { name: "Variable", scope: "variable, entity.name.variable", foreground: resolveColor(theme, theme.colors.syntaxVariable) },
1440
+ { name: "Operator", scope: "keyword.operator", foreground: resolveColor(theme, theme.colors.syntaxOperator) },
1441
+ { name: "Punctuation", scope: "punctuation", foreground: resolveColor(theme, theme.colors.syntaxPunctuation) },
1442
+ { name: "Invalid", scope: "invalid", foreground: resolveColor(theme, theme.colors.error) },
1443
+ { name: "Markup Heading", scope: "markup.heading", foreground: resolveColor(theme, theme.colors.mdHeading) },
1444
+ { name: "Markup Link", scope: "markup.underline.link", foreground: resolveColor(theme, theme.colors.mdLink), fontStyle: "underline" },
1445
+ { name: "Markup Quote", scope: "markup.quote", foreground: resolveColor(theme, theme.colors.mdQuote) },
1446
+ { name: "Markup Raw", scope: "markup.raw, markup.inline.raw", foreground: resolveColor(theme, theme.colors.mdCode) }
1447
+ ];
1448
+
1449
+ const xml = [];
1450
+ xml.push('<?xml version="1.0" encoding="UTF-8"?>');
1451
+ xml.push('<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">');
1452
+ xml.push('<plist version="1.0">');
1453
+ xml.push("<dict>");
1454
+ xml.push(" <key>name</key>");
1455
+ xml.push(` <string>${escapeXml(name)}</string>`);
1456
+ xml.push(" <key>settings</key>");
1457
+ xml.push(" <array>");
1458
+ xml.push(" <dict>");
1459
+ xml.push(" <key>settings</key>");
1460
+ xml.push(" <dict>");
1461
+ xml.push(` <key>background</key><string>${escapeXml(globalBg)}</string>`);
1462
+ xml.push(` <key>foreground</key><string>${escapeXml(globalFg)}</string>`);
1463
+ xml.push(` <key>caret</key><string>${escapeXml(caret)}</string>`);
1464
+ xml.push(` <key>selection</key><string>${escapeXml(globalSelection)}</string>`);
1465
+ xml.push(` <key>invisibles</key><string>${escapeXml(invisibles)}</string>`);
1466
+ xml.push(` <key>lineHighlight</key><string>${escapeXml(lineHighlight)}</string>`);
1467
+ xml.push(" </dict>");
1468
+ xml.push(" </dict>");
1469
+
1470
+ for (const rule of rules) {
1471
+ xml.push(" <dict>");
1472
+ xml.push(` <key>name</key><string>${escapeXml(rule.name)}</string>`);
1473
+ xml.push(` <key>scope</key><string>${escapeXml(rule.scope)}</string>`);
1474
+ xml.push(" <key>settings</key>");
1475
+ xml.push(" <dict>");
1476
+ xml.push(` <key>foreground</key><string>${escapeXml(rule.foreground)}</string>`);
1477
+ if (rule.fontStyle) {
1478
+ xml.push(` <key>fontStyle</key><string>${escapeXml(rule.fontStyle)}</string>`);
1479
+ }
1480
+ xml.push(" </dict>");
1481
+ xml.push(" </dict>");
1482
+ }
1483
+
1484
+ xml.push(" </array>");
1485
+ xml.push("</dict>");
1486
+ xml.push("</plist>");
1487
+ xml.push("");
1488
+ return xml.join("\n");
1489
+ }
1490
+
1491
+ function escapeXml(value) {
1492
+ return String(value)
1493
+ .replace(/&/g, "&amp;")
1494
+ .replace(/</g, "&lt;")
1495
+ .replace(/>/g, "&gt;")
1496
+ .replace(/"/g, "&quot;");
1497
+ }
1498
+
1499
+ function colorBlock(hex, width) {
1500
+ const rgb = hexToRgb(hex);
1501
+ return `\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}m${" ".repeat(width)}\x1b[0m`;
1502
+ }
1503
+
1504
+ function hexToRgb(hex) {
1505
+ const normalized = hex.replace("#", "");
1506
+ const value = normalized.length === 3
1507
+ ? normalized.split("").map((c) => c + c).join("")
1508
+ : normalized;
1509
+ return {
1510
+ r: parseInt(value.slice(0, 2), 16),
1511
+ g: parseInt(value.slice(2, 4), 16),
1512
+ b: parseInt(value.slice(4, 6), 16)
1513
+ };
1514
+ }
1515
+
1516
+ function relativeLuminance(hex) {
1517
+ const { r, g, b } = hexToRgb(hex);
1518
+ const channel = (value) => {
1519
+ const normalized = value / 255;
1520
+ return normalized <= 0.03928
1521
+ ? normalized / 12.92
1522
+ : ((normalized + 0.055) / 1.055) ** 2.4;
1523
+ };
1524
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
1525
+ }
1526
+
1527
+ function contrastRatio(foreground, background) {
1528
+ const l1 = relativeLuminance(foreground);
1529
+ const l2 = relativeLuminance(background);
1530
+ const lighter = Math.max(l1, l2);
1531
+ const darker = Math.min(l1, l2);
1532
+ return (lighter + 0.05) / (darker + 0.05);
1533
+ }
1534
+
1535
+ function mixHex(from, to, weight) {
1536
+ const a = hexToRgb(from);
1537
+ const b = hexToRgb(to);
1538
+ const blend = (start, end) => Math.round(start + (end - start) * weight);
1539
+ return rgbToHex(blend(a.r, b.r), blend(a.g, b.g), blend(a.b, b.b));
1540
+ }
1541
+
1542
+ function ensureReadableColor(color, background, minimumRatio) {
1543
+ const initial = normalizeHex(color);
1544
+ const bg = normalizeHex(background);
1545
+
1546
+ if (contrastRatio(initial, bg) >= minimumRatio) {
1547
+ return initial;
1548
+ }
1549
+
1550
+ const bgLum = relativeLuminance(bg);
1551
+ const anchor = bgLum > 0.6 ? "#000000" : "#ffffff";
1552
+ let best = initial;
1553
+
1554
+ for (let step = 1; step <= 24; step += 1) {
1555
+ const candidate = mixHex(initial, anchor, step / 24);
1556
+ best = candidate;
1557
+ if (contrastRatio(candidate, bg) >= minimumRatio) {
1558
+ return candidate;
1559
+ }
1560
+ }
1561
+
1562
+ return best;
1563
+ }
1564
+
1565
+ function normalizeHex(hex) {
1566
+ if (typeof hex !== "string") return "#000000";
1567
+ const trimmed = hex.trim();
1568
+ if (/^#[0-9a-f]{3}$/i.test(trimmed)) {
1569
+ return `#${trimmed.slice(1).split("").map((char) => char + char).join("").toLowerCase()}`;
1570
+ }
1571
+ if (/^#[0-9a-f]{6}$/i.test(trimmed)) {
1572
+ return trimmed.toLowerCase();
1573
+ }
1574
+ return trimmed;
1575
+ }
1576
+
1577
+ function inferMode(palette) {
1578
+ const { r, g, b } = hexToRgb(palette.bg);
1579
+ const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
1580
+ return luminance > 0.6 ? "light" : "dark";
1581
+ }
1582
+
1583
+ function renderGhosttySnippet(theme, font, fontSize, opacity) {
1584
+ const bg = resolveColor(theme, theme.vars.bg);
1585
+ const fg = resolveColor(theme, theme.vars.fg);
1586
+ return [
1587
+ `theme = ${theme.name}`,
1588
+ `font-family = ${font}`,
1589
+ `font-size = ${fontSize}`,
1590
+ `background = ${bg}`,
1591
+ `foreground = ${fg}`,
1592
+ opacity ? `background-opacity = ${opacity}` : null
1593
+ ]
1594
+ .filter(Boolean)
1595
+ .join("\n")
1596
+ .concat("\n");
1597
+ }
1598
+
1599
+ function renderKittySnippet(theme, font, fontSize, opacity) {
1600
+ const bg = resolveColor(theme, theme.vars.bg);
1601
+ const fg = resolveColor(theme, theme.vars.fg);
1602
+ return [
1603
+ `font_family ${font}`,
1604
+ `font_size ${fontSize}`,
1605
+ `background ${bg}`,
1606
+ `foreground ${fg}`,
1607
+ opacity ? `background_opacity ${opacity}` : null,
1608
+ `selection_background ${resolveColor(theme, theme.colors.selectedBg)}`,
1609
+ `selection_foreground ${fg}`
1610
+ ]
1611
+ .filter(Boolean)
1612
+ .join("\n")
1613
+ .concat("\n");
1614
+ }
1615
+
1616
+ function renderWeztermSnippet(theme, font, fontSize, opacity) {
1617
+ const bg = resolveColor(theme, theme.vars.bg);
1618
+ const fg = resolveColor(theme, theme.vars.fg);
1619
+ return `local wezterm = require("wezterm")
1620
+
1621
+ return {
1622
+ font = wezterm.font("${font}"),
1623
+ font_size = ${fontSize},
1624
+ window_background_opacity = ${opacity || 1},
1625
+ colors = {
1626
+ background = "${bg}",
1627
+ foreground = "${fg}",
1628
+ selection_bg = "${resolveColor(theme, theme.colors.selectedBg)}",
1629
+ selection_fg = "${fg}",
1630
+ ansi = { "${bg}", "${resolveColor(theme, theme.colors.error)}", "${resolveColor(theme, theme.colors.success)}", "${resolveColor(theme, theme.colors.warning)}", "${resolveColor(theme, theme.colors.accent)}", "${resolveColor(theme, theme.colors.syntaxKeyword)}", "${resolveColor(theme, theme.colors.mdCode)}", "${fg}" },
1631
+ brights = { "${resolveColor(theme, theme.colors.dim)}", "${resolveColor(theme, theme.colors.error)}", "${resolveColor(theme, theme.colors.success)}", "${resolveColor(theme, theme.colors.warning)}", "${resolveColor(theme, theme.colors.accent)}", "${resolveColor(theme, theme.colors.syntaxKeyword)}", "${resolveColor(theme, theme.colors.mdCode)}", "${fg}" }
1632
+ }
1633
+ }
1634
+ `;
1635
+ }
1636
+
1637
+ function renderAppleTerminalProfile(theme, font, fontSize, profileName) {
1638
+ const background = appleScriptColor(resolveColor(theme, theme.vars.bg));
1639
+ const foreground = appleScriptColor(resolveColor(theme, theme.vars.fg));
1640
+ const cursor = appleScriptColor(resolveColor(theme, theme.colors.accent));
1641
+ const bold = appleScriptColor(brightenHex(resolveColor(theme, theme.vars.fg), 0.08));
1642
+
1643
+ return [
1644
+ 'tell application "Terminal"',
1645
+ ` if not (exists settings set "${escapeAppleScript(profileName)}") then`,
1646
+ ` make new settings set with properties {name:"${escapeAppleScript(profileName)}"}`,
1647
+ " end if",
1648
+ ` set font of settings set "${escapeAppleScript(profileName)}" to "${escapeAppleScript(font)}"`,
1649
+ ` set number of rows of settings set "${escapeAppleScript(profileName)}" to 30`,
1650
+ ` set number of columns of settings set "${escapeAppleScript(profileName)}" to 120`,
1651
+ ` set font antialiasing of settings set "${escapeAppleScript(profileName)}" to true`,
1652
+ ` set background color of settings set "${escapeAppleScript(profileName)}" to ${background}`,
1653
+ ` set normal text color of settings set "${escapeAppleScript(profileName)}" to ${foreground}`,
1654
+ ` set bold text color of settings set "${escapeAppleScript(profileName)}" to ${bold}`,
1655
+ ` set cursor color of settings set "${escapeAppleScript(profileName)}" to ${cursor}`,
1656
+ ` set current settings of tabs of windows to settings set "${escapeAppleScript(profileName)}"`,
1657
+ "end tell",
1658
+ ""
1659
+ ].join("\n");
1660
+ }
1661
+
1662
+ function writeTerminalConfig(target, snippet, snippetPath, options = {}) {
1663
+ const home = os.homedir();
1664
+ if (target === "apple-terminal" || target === "apple" || target === "terminal" || target === "terminal.app") {
1665
+ applyAppleTerminalProfile(snippetPath, options);
1666
+ return;
1667
+ }
1668
+ if (target === "ghostty") {
1669
+ const targetPath = path.join(home, ".config", "ghostty", "config");
1670
+ appendInclude(targetPath, `config-file = ${snippetPath}`);
1671
+ console.log(`Updated ${targetPath}`);
1672
+ return;
1673
+ }
1674
+ if (target === "kitty") {
1675
+ const targetPath = path.join(home, ".config", "kitty", "kitty.conf");
1676
+ appendInclude(targetPath, `include ${snippetPath}`);
1677
+ console.log(`Updated ${targetPath}`);
1678
+ return;
1679
+ }
1680
+ if (target === "wezterm") {
1681
+ const targetPath = path.join(home, ".wezterm.lua");
1682
+ if (fs.existsSync(targetPath)) {
1683
+ console.log(`Existing ${targetPath} left untouched. Merge ${snippetPath} manually.`);
1684
+ return;
1685
+ }
1686
+ fs.writeFileSync(targetPath, snippet, "utf8");
1687
+ console.log(`Created ${targetPath}`);
1688
+ }
1689
+ }
1690
+
1691
+ function applyAppleTerminalProfile(profilePath, options = {}) {
1692
+ const paths = getThemePaths();
1693
+ snapshotAppleTerminalState(paths);
1694
+ execFileSync("osascript", [profilePath], { stdio: "inherit" });
1695
+ if (options.setDefault && options.profileName) {
1696
+ execFileSync(
1697
+ "osascript",
1698
+ [
1699
+ "-e",
1700
+ `tell application "Terminal" to set default settings to settings set "${escapeAppleScript(options.profileName)}"`,
1701
+ "-e",
1702
+ `tell application "Terminal" to set startup settings to settings set "${escapeAppleScript(options.profileName)}"`
1703
+ ],
1704
+ { stdio: "inherit" }
1705
+ );
1706
+ }
1707
+ }
1708
+
1709
+ function readTerminalState(paths = getThemePaths()) {
1710
+ if (!fs.existsSync(paths.terminalStateFile)) {
1711
+ return { targets: {} };
1712
+ }
1713
+ try {
1714
+ return JSON.parse(fs.readFileSync(paths.terminalStateFile, "utf8"));
1715
+ } catch (_error) {
1716
+ return { targets: {} };
1717
+ }
1718
+ }
1719
+
1720
+ function writeTerminalState(state, paths = getThemePaths()) {
1721
+ ensureDir(path.dirname(paths.terminalStateFile));
1722
+ fs.writeFileSync(paths.terminalStateFile, `${JSON.stringify(state, null, 2)}\n`, "utf8");
1723
+ }
1724
+
1725
+ function snapshotAppleTerminalState(paths = getThemePaths()) {
1726
+ const state = readTerminalState(paths);
1727
+ if (state.targets["apple-terminal"]) {
1728
+ return;
1729
+ }
1730
+
1731
+ const currentProfile = readAppleTerminalValue('tell application "Terminal" to get name of current settings of selected tab of front window');
1732
+ const defaultProfile = readDefault("com.apple.Terminal", "Default Window Settings", "Basic");
1733
+ const startupProfile = readDefault("com.apple.Terminal", "Startup Window Settings", defaultProfile);
1734
+
1735
+ state.targets["apple-terminal"] = {
1736
+ currentProfile: currentProfile || defaultProfile || "Basic",
1737
+ defaultProfile: defaultProfile || "Basic",
1738
+ startupProfile: startupProfile || defaultProfile || "Basic"
1739
+ };
1740
+ writeTerminalState(state, paths);
1741
+ }
1742
+
1743
+ function restoreAppleTerminalState(savedState) {
1744
+ const fallbackProfile = "Basic";
1745
+ const currentProfile = savedState && savedState.currentProfile ? savedState.currentProfile : fallbackProfile;
1746
+ const defaultProfile = savedState && savedState.defaultProfile ? savedState.defaultProfile : fallbackProfile;
1747
+ const startupProfile = savedState && savedState.startupProfile ? savedState.startupProfile : defaultProfile;
1748
+
1749
+ execFileSync("osascript", [
1750
+ "-e",
1751
+ `tell application "Terminal" to set current settings of tabs of windows to settings set "${escapeAppleScript(currentProfile)}"`,
1752
+ "-e",
1753
+ `tell application "Terminal" to set default settings to settings set "${escapeAppleScript(defaultProfile)}"`,
1754
+ "-e",
1755
+ `tell application "Terminal" to set startup settings to settings set "${escapeAppleScript(startupProfile)}"`
1756
+ ], { stdio: "inherit" });
1757
+ }
1758
+
1759
+ function readAppleTerminalValue(script) {
1760
+ try {
1761
+ return execFileSync("osascript", ["-e", script], { encoding: "utf8" }).trim();
1762
+ } catch (_error) {
1763
+ return "";
1764
+ }
1765
+ }
1766
+
1767
+ function readDefault(domain, key, fallback) {
1768
+ try {
1769
+ return execFileSync("defaults", ["read", domain, key], { encoding: "utf8" }).trim();
1770
+ } catch (_error) {
1771
+ return fallback;
1772
+ }
1773
+ }
1774
+
1775
+ function resetManagedFile(targetPath, prefix) {
1776
+ if (!fs.existsSync(targetPath)) {
1777
+ return;
1778
+ }
1779
+ const existing = fs.readFileSync(targetPath, "utf8");
1780
+ const lines = existing
1781
+ .split("\n")
1782
+ .filter((line) => !line.includes("pretty-code") && !line.startsWith(prefix + path.join(os.homedir(), ".ctheme", "snippets")));
1783
+ fs.writeFileSync(targetPath, `${lines.join("\n").replace(/\n*$/, "\n")}`, "utf8");
1784
+ }
1785
+
1786
+ function appendInclude(targetPath, line) {
1787
+ ensureDir(path.dirname(targetPath));
1788
+ const existing = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : "";
1789
+ if (existing.includes(line)) return;
1790
+ const updated = `${existing.trimEnd()}${existing.trimEnd() ? "\n" : ""}${line}\n`;
1791
+ fs.writeFileSync(targetPath, updated, "utf8");
1792
+ }
1793
+
1794
+ function brightenHex(hex, amount) {
1795
+ const { r, g, b } = hexToRgb(hex);
1796
+ const brighten = (channel) => Math.max(0, Math.min(255, Math.round(channel + (255 - channel) * amount)));
1797
+ return rgbToHex(brighten(r), brighten(g), brighten(b));
1798
+ }
1799
+
1800
+ function rgbToHex(r, g, b) {
1801
+ return `#${[r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("")}`;
1802
+ }
1803
+
1804
+ function appleScriptColor(hex) {
1805
+ const { r, g, b } = hexToRgb(hex);
1806
+ const scale = (value) => Math.round((value / 255) * 65535);
1807
+ return `{${scale(r)}, ${scale(g)}, ${scale(b)}}`;
1808
+ }
1809
+
1810
+ function escapeAppleScript(value) {
1811
+ return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1812
+ }
1813
+
1814
+ function fail(message) {
1815
+ console.error(`Error: ${message}`);
1816
+ process.exit(1);
1817
+ }
1818
+
1819
+ main();