codex-terminal-themes 1.0.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.
- package/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +23 -0
- package/LICENSE +21 -0
- package/LICENSE.txt +21 -0
- package/README.md +169 -0
- package/SECURITY.md +5 -0
- package/SUPPORT.md +5 -0
- package/bin/codex-terminal-themes.mjs +12 -0
- package/docs/app.js +1393 -0
- package/docs/assets/theme-gallery-header.png +0 -0
- package/docs/favicon.svg +10 -0
- package/docs/index.html +240 -0
- package/docs/preview.svg +24 -0
- package/docs/site-data.json +51078 -0
- package/docs/styles.css +780 -0
- package/metadata/README.md +71 -0
- package/metadata/themes.json +16185 -0
- package/metadata/themes.schema.json +218 -0
- package/package.json +109 -0
- package/src/cli.mjs +1763 -0
- package/stylelint.config.mjs +12 -0
- package/themes/80s.tmTheme +229 -0
- package/themes/Active4D.tmTheme +407 -0
- package/themes/All Hallow's Eve Custom.tmTheme +275 -0
- package/themes/All Hallow's Eve.tmTheme +277 -0
- package/themes/All Hallows Eve.tmTheme +277 -0
- package/themes/Amy.tmTheme +559 -0
- package/themes/Artic Fall.tmTheme +275 -0
- package/themes/BBEdit.tmTheme +439 -0
- package/themes/Bespin.tmTheme +516 -0
- package/themes/Black Pearl II.tmTheme +496 -0
- package/themes/Black Pearl.tmTheme +400 -0
- package/themes/Blackboard 2.tmTheme +348 -0
- package/themes/Blackboard Black.tmTheme +350 -0
- package/themes/Blackboard.tmTheme +348 -0
- package/themes/Blueberry Jelly.tmTheme +242 -0
- package/themes/Bluesy.tmTheme +242 -0
- package/themes/Blurb 2.tmTheme +229 -0
- package/themes/Blurb.tmTheme +229 -0
- package/themes/Bongzilla 2.tmTheme +223 -0
- package/themes/Bongzilla.tmTheme +223 -0
- package/themes/Brightly Dark.tmTheme +242 -0
- package/themes/Brilliance Black.tmTheme +2619 -0
- package/themes/Brilliance Dull.tmTheme +2243 -0
- package/themes/Bromozoid.tmTheme +227 -0
- package/themes/CSSEdit.tmTheme +203 -0
- package/themes/Classic Modified.tmTheme +469 -0
- package/themes/Clouds Midnight.tmTheme +361 -0
- package/themes/Clouds of Ruby.tmTheme +649 -0
- package/themes/Clouds.tmTheme +348 -0
- package/themes/Cloudy Fields.tmTheme +240 -0
- package/themes/Coal Graal.tmTheme +282 -0
- package/themes/Cobalt.tmTheme +561 -0
- package/themes/Coda.tmTheme +317 -0
- package/themes/Colorful.tmTheme +307 -0
- package/themes/Cool Glow.tmTheme +234 -0
- package/themes/Corona.tmTheme +290 -0
- package/themes/Cowabunga.tmTheme +242 -0
- package/themes/DanBurst.tmTheme +665 -0
- package/themes/Daniel Fischer.tmTheme +627 -0
- package/themes/Dark Ocean.tmTheme +242 -0
- package/themes/DarkNeon.tmTheme +818 -0
- package/themes/Dawn.tmTheme +437 -0
- package/themes/DawnCustom.tmTheme +443 -0
- package/themes/Django (Smoothy).tmTheme +453 -0
- package/themes/Django Blues.tmTheme +182 -0
- package/themes/Django Extended.tmTheme +495 -0
- package/themes/Django.tmTheme +436 -0
- package/themes/Dobdark.tmTheme +615 -0
- package/themes/Dominion Day.tmTheme +562 -0
- package/themes/Doo-Daa.tmTheme +242 -0
- package/themes/Drankin Purp.tmTheme +227 -0
- package/themes/Dreamweaver_Blackbam_Aptana303.tmTheme +980 -0
- package/themes/Easy on my Eyes There Buddy.tmTheme +227 -0
- package/themes/Eiffel.tmTheme +435 -0
- package/themes/Emacs Strict.tmTheme +241 -0
- package/themes/Emacs.tmTheme +241 -0
- package/themes/Epic Blue.tmTheme +320 -0
- package/themes/Erebus.tmTheme +467 -0
- package/themes/Espresso Libre.tmTheme +402 -0
- package/themes/Espresso Tutti.tmTheme +392 -0
- package/themes/Espresso.tmTheme +329 -0
- package/themes/Fade to Grey.tmTheme +308 -0
- package/themes/Fluidvision.tmTheme +443 -0
- package/themes/ForLaTeX.tmTheme +214 -0
- package/themes/Freckle.tmTheme +279 -0
- package/themes/Friendship Bracelet.tmTheme +303 -0
- package/themes/GaGaGaGroovy.tmTheme +227 -0
- package/themes/Gangrene.tmTheme +242 -0
- package/themes/GitHub.tmTheme +653 -0
- package/themes/GlitterBomb.tmTheme +387 -0
- package/themes/Halloween Night.tmTheme +303 -0
- package/themes/Happy happy joy joy.tmTheme +841 -0
- package/themes/Happydeluxe.tmTheme +184 -0
- package/themes/HelvectorLight.tmTheme +557 -0
- package/themes/Humane.tmTheme +220 -0
- package/themes/IDLE.tmTheme +235 -0
- package/themes/IR_Black.tmTheme +810 -0
- package/themes/IR_White.tmTheme +792 -0
- package/themes/Jane Fonda, Baby Redux.tmTheme +264 -0
- package/themes/Johnny.tmTheme +798 -0
- package/themes/Juicy.tmTheme +250 -0
- package/themes/Kuroir Theme.tmTheme +707 -0
- package/themes/LAZY.tmTheme +291 -0
- package/themes/Lowlight.tmTheme +605 -0
- package/themes/Mac Classic.tmTheme +476 -0
- package/themes/Made of Code.tmTheme +695 -0
- package/themes/MagicWB (Amiga).tmTheme +376 -0
- package/themes/Malibu Nights.tmTheme +257 -0
- package/themes/Menage A Trois.tmTheme +881 -0
- package/themes/Merbivore Soft.tmTheme +285 -0
- package/themes/Merbivore.tmTheme +285 -0
- package/themes/Midnight.tmTheme +321 -0
- package/themes/Mmm Sandy.tmTheme +227 -0
- package/themes/Monokai.tmTheme +289 -0
- package/themes/MultiMarkdown.tmTheme +183 -0
- package/themes/Mustang.tmTheme +339 -0
- package/themes/Neopro Inverted.tmTheme +328 -0
- package/themes/Neopro.tmTheme +330 -0
- package/themes/Nice One.tmTheme +222 -0
- package/themes/No Way.tmTheme +255 -0
- package/themes/Overcast.tmTheme +659 -0
- package/themes/Pastels on Dark.tmTheme +703 -0
- package/themes/Pastie.tmTheme +321 -0
- package/themes/Peridinkle.tmTheme +240 -0
- package/themes/Play!.tmTheme +736 -0
- package/themes/Puss.tmTheme +227 -0
- package/themes/Putty.tmTheme +275 -0
- package/themes/Quail.tmTheme +257 -0
- package/themes/RDark.tmTheme +235 -0
- package/themes/Rails Envy.tmTheme +299 -0
- package/themes/Railscasts 2.tmTheme +368 -0
- package/themes/Railscasts.tmTheme +278 -0
- package/themes/Resesif.tmTheme +298 -0
- package/themes/Ringo.tmTheme +240 -0
- package/themes/Ruby Blue.tmTheme +366 -0
- package/themes/RubyRobot.tmTheme +250 -0
- package/themes/Ryan Light.tmTheme +232 -0
- package/themes/Seafoam.tmTheme +242 -0
- package/themes/Sidewalk Chalk.tmTheme +276 -0
- package/themes/Sin City (that yellow bastard).tmTheme +585 -0
- package/themes/Slate.tmTheme +436 -0
- package/themes/Slush & Poppies.tmTheme +336 -0
- package/themes/Slush and Poppies.tmTheme +336 -0
- package/themes/Smokey Morning.tmTheme +229 -0
- package/themes/Smoothy original.tmTheme +623 -0
- package/themes/Smoothy.tmTheme +623 -0
- package/themes/Solarized (dark).tmTheme +2051 -0
- package/themes/Solarized-dark.tmTheme +312 -0
- package/themes/Solarized-light.tmTheme +305 -0
- package/themes/Sometheme.tmTheme +240 -0
- package/themes/SoylentTheme.tmTheme +353 -0
- package/themes/SpaceCadet.tmTheme +212 -0
- package/themes/Spectacular.tmTheme +436 -0
- package/themes/Starlight.tmTheme +857 -0
- package/themes/Stoneship Bright.tmTheme +348 -0
- package/themes/Stoneship.tmTheme +361 -0
- package/themes/Summer Camp Mod.tmTheme +229 -0
- package/themes/Summer Camp.tmTheme +229 -0
- package/themes/Summery Drink.tmTheme +242 -0
- package/themes/Sunburst.tmTheme +665 -0
- package/themes/Swyphs II.tmTheme +306 -0
- package/themes/Tango Bright.tmTheme +1 -0
- package/themes/Tango.tmTheme +450 -0
- package/themes/Teenage Dream.tmTheme +242 -0
- package/themes/Texari.tmTheme +727 -0
- package/themes/Text Ex Machina.tmTheme +295 -0
- package/themes/Tomorrow-Night-Blue.tmTheme +175 -0
- package/themes/Tomorrow-Night-Eighties.tmTheme +175 -0
- package/themes/Tomorrow-Night.tmTheme +175 -0
- package/themes/Tomorrow.tmTheme +349 -0
- package/themes/ToyChest.tmTheme +503 -0
- package/themes/TravisJeffery.tmTheme +1261 -0
- package/themes/Tubster.tmTheme +280 -0
- package/themes/Twilight BG FG.tmTheme +1000 -0
- package/themes/Twilight.tmTheme +516 -0
- package/themes/TwilightMod.tmTheme +529 -0
- package/themes/Two Days Ago.tmTheme +242 -0
- package/themes/Vibrant Fin.tmTheme +447 -0
- package/themes/Vibrant Ink.tmTheme +447 -0
- package/themes/Vibrant Scala.tmTheme +292 -0
- package/themes/Vibrant Tango.tmTheme +438 -0
- package/themes/Wandering.tmTheme +681 -0
- package/themes/Whimsy in Blue.tmTheme +240 -0
- package/themes/Why/342/200/231s Poignant.tmTheme" +191 -0
- package/themes/Windows XP.tmTheme +350 -0
- package/themes/Witch.tmTheme +282 -0
- package/themes/Yurple.tmTheme +227 -0
- package/themes/ZZZ.tmTheme +131 -0
- package/themes/Zachstronaut.tmTheme +381 -0
- package/themes/Zenburnesque.tmTheme +343 -0
- package/themes/[ Argonaut ].tmTheme +387 -0
- package/themes/barf.tmTheme +254 -0
- package/themes/converted-vscode-AmoledShinyBlack.tmTheme +528 -0
- package/themes/converted-vscode-AmoledShinyBlack2.tmTheme +609 -0
- package/themes/converted-vscode-AmoledShinyBlack3.tmTheme +683 -0
- package/themes/converted-vscode-AmoledShinyBlack4.tmTheme +896 -0
- package/themes/converted-vscode-AmoledShinyBlack5.tmTheme +1023 -0
- package/themes/converted-vscode-AmoledShinyBlack6.tmTheme +2092 -0
- package/themes/eclips3.media (ECLM).tmTheme +294 -0
- package/themes/evin.tmTheme +253 -0
- package/themes/fake.tmTheme +669 -0
- package/themes/fapfap.tmTheme +508 -0
- package/themes/helloKitty.tmTheme +293 -0
- package/themes/iLife 05.tmTheme +619 -0
- package/themes/iPlastic.tmTheme +397 -0
- package/themes/idleFingers.tmTheme +380 -0
- package/themes/krTheme.tmTheme +551 -0
- package/themes/mark.james.name.tmTheme +1117 -0
- package/themes/minimal Theme.tmTheme +551 -0
- package/themes/mint.tmTheme +617 -0
- package/themes/mintBlue Dark.tmTheme +653 -0
- package/themes/mintBlue.tmTheme +655 -0
- package/themes/modifiedPastels.tmTheme +745 -0
- package/themes/monoindustrial.tmTheme +451 -0
- package/themes/my-theme-blackboard.tmTheme +363 -0
- package/themes/my-theme-classic.tmTheme +465 -0
- package/themes/nppGLua.tmTheme +293 -0
- package/themes/reST testing theme.tmTheme +554 -0
- package/themes/rose-pine-dawn.tmTheme +329 -0
- package/themes/rose-pine-moon.tmTheme +329 -0
- package/themes/rose-pine.tmTheme +329 -0
- package/themes/ryan-light.tmTheme +232 -0
- package/themes/wut.tmTheme +255 -0
- package/tools/build-pages-site.mjs +465 -0
- package/tools/generate-theme-metadata.mjs +680 -0
- package/tools/serve-pages-site.mjs +196 -0
- package/tools/validate-themes.mjs +272 -0
- package/types/index.d.ts +49 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
const defaultHost = "127.0.0.1";
|
|
6
|
+
const defaultPort = 4173;
|
|
7
|
+
const rootDirectory = process.cwd();
|
|
8
|
+
const docsDirectory = path.join(rootDirectory, "docs");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} docsRoot
|
|
12
|
+
*
|
|
13
|
+
* @returns {(
|
|
14
|
+
* request: import("node:http").IncomingMessage,
|
|
15
|
+
* response: import("node:http").ServerResponse
|
|
16
|
+
* ) => Promise<void>}
|
|
17
|
+
*/
|
|
18
|
+
function createRequestHandler(docsRoot) {
|
|
19
|
+
return async (request, response) => {
|
|
20
|
+
const requestUrl = request.url ?? "/";
|
|
21
|
+
const filePath = getRequestedFilePath(docsRoot, requestUrl);
|
|
22
|
+
|
|
23
|
+
if (filePath === null) {
|
|
24
|
+
sendNotFound(response);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await sendFile(response, filePath);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} filePath
|
|
34
|
+
*
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function getContentType(filePath) {
|
|
38
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
39
|
+
|
|
40
|
+
switch (extension) {
|
|
41
|
+
case ".css": {
|
|
42
|
+
return "text/css; charset=utf-8";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case ".html": {
|
|
46
|
+
return "text/html; charset=utf-8";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
case ".js": {
|
|
50
|
+
return "text/javascript; charset=utf-8";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case ".json": {
|
|
54
|
+
return "application/json; charset=utf-8";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case ".svg": {
|
|
58
|
+
return "image/svg+xml";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
default: {
|
|
62
|
+
return "application/octet-stream";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} optionName
|
|
69
|
+
* @param {number} fallback
|
|
70
|
+
*
|
|
71
|
+
* @returns {number}
|
|
72
|
+
*/
|
|
73
|
+
function getIntegerOption(optionName, fallback) {
|
|
74
|
+
const value = getStringOption(optionName, "");
|
|
75
|
+
const parsedValue = Number.parseInt(value, 10);
|
|
76
|
+
|
|
77
|
+
return Number.isInteger(parsedValue) && parsedValue > 0
|
|
78
|
+
? parsedValue
|
|
79
|
+
: fallback;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} docsRoot
|
|
84
|
+
* @param {string} requestUrl
|
|
85
|
+
*
|
|
86
|
+
* @returns {null | string}
|
|
87
|
+
*/
|
|
88
|
+
function getRequestedFilePath(docsRoot, requestUrl) {
|
|
89
|
+
const parsedUrl = new URL(requestUrl, "http://localhost");
|
|
90
|
+
const decodedPath = decodeURIComponent(parsedUrl.pathname);
|
|
91
|
+
const relativePath =
|
|
92
|
+
decodedPath === "/" ? "index.html" : decodedPath.replace(/^\/+/v, "");
|
|
93
|
+
const filePath = path.resolve(docsRoot, relativePath);
|
|
94
|
+
|
|
95
|
+
return isPathInside(docsRoot, filePath) ? filePath : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @returns {{ host: string; port: number }}
|
|
100
|
+
*/
|
|
101
|
+
function getServerOptions() {
|
|
102
|
+
return {
|
|
103
|
+
host: getStringOption("--host", defaultHost),
|
|
104
|
+
port: getIntegerOption("--port", defaultPort),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} optionName
|
|
110
|
+
* @param {string} fallback
|
|
111
|
+
*
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
function getStringOption(optionName, fallback) {
|
|
115
|
+
const prefix = `${optionName}=`;
|
|
116
|
+
const argument = process.argv.find((item) => item.startsWith(prefix));
|
|
117
|
+
|
|
118
|
+
return argument === undefined ? fallback : argument.slice(prefix.length);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {string} rootPath
|
|
123
|
+
* @param {string} candidatePath
|
|
124
|
+
*
|
|
125
|
+
* @returns {boolean}
|
|
126
|
+
*/
|
|
127
|
+
function isPathInside(rootPath, candidatePath) {
|
|
128
|
+
const relativePath = path.relative(rootPath, candidatePath);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
relativePath.length === 0 ||
|
|
132
|
+
(!relativePath.startsWith("..") && !path.isAbsolute(relativePath))
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @returns {void}
|
|
138
|
+
*/
|
|
139
|
+
function main() {
|
|
140
|
+
const { host, port } = getServerOptions();
|
|
141
|
+
const requestHandler = createRequestHandler(docsDirectory);
|
|
142
|
+
const server = createServer((request, response) => {
|
|
143
|
+
void requestHandler(request, response);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
server.listen(port, host, () => {
|
|
147
|
+
process.stdout.write(
|
|
148
|
+
`Serving docs at http://${host}:${String(port)}/\n`
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @returns {void}
|
|
155
|
+
*/
|
|
156
|
+
function run() {
|
|
157
|
+
try {
|
|
158
|
+
main();
|
|
159
|
+
} catch (error) {
|
|
160
|
+
process.stderr.write(`${String(error)}\n`);
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {import("node:http").ServerResponse} response
|
|
167
|
+
* @param {string} filePath
|
|
168
|
+
*
|
|
169
|
+
* @returns {Promise<void>}
|
|
170
|
+
*/
|
|
171
|
+
async function sendFile(response, filePath) {
|
|
172
|
+
try {
|
|
173
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- The request path is constrained to the repo-local docs directory.
|
|
174
|
+
const contents = await readFile(filePath);
|
|
175
|
+
response.writeHead(200, {
|
|
176
|
+
"Content-Type": getContentType(filePath),
|
|
177
|
+
});
|
|
178
|
+
response.end(contents);
|
|
179
|
+
} catch {
|
|
180
|
+
sendNotFound(response);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {import("node:http").ServerResponse} response
|
|
186
|
+
*
|
|
187
|
+
* @returns {void}
|
|
188
|
+
*/
|
|
189
|
+
function sendNotFound(response) {
|
|
190
|
+
response.writeHead(404, {
|
|
191
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
192
|
+
});
|
|
193
|
+
response.end("Not found\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
run();
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
import { SyntaxValidator } from "fast-xml-validator";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { once } from "node:events";
|
|
5
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
|
|
8
|
+
const rootDirectory = process.cwd();
|
|
9
|
+
const themeDirectory = path.join(rootDirectory, "themes");
|
|
10
|
+
const shouldBuildBatCache = process.argv.includes("--bat-cache");
|
|
11
|
+
|
|
12
|
+
const parser = new XMLParser({
|
|
13
|
+
ignoreAttributes: false,
|
|
14
|
+
preserveOrder: true,
|
|
15
|
+
trimValues: false,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{ filePath: string; ok: true; reason: "" }
|
|
20
|
+
* | { filePath: string; ok: false; reason: string }} ThemeValidationResult
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @returns {Promise<number>}
|
|
25
|
+
*/
|
|
26
|
+
async function buildBatCache() {
|
|
27
|
+
const childProcess = spawn("bat", ["cache", "--build"], {
|
|
28
|
+
stdio: "inherit",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const closeResult = /** @type {readonly unknown[]} */ (
|
|
32
|
+
await Promise.race([
|
|
33
|
+
once(childProcess, "close"),
|
|
34
|
+
once(childProcess, "error").then((errorResult) => {
|
|
35
|
+
const errorValues = /** @type {readonly unknown[]} */ (
|
|
36
|
+
errorResult
|
|
37
|
+
);
|
|
38
|
+
const error = errorValues[0];
|
|
39
|
+
throw error;
|
|
40
|
+
}),
|
|
41
|
+
])
|
|
42
|
+
);
|
|
43
|
+
const [exitCode] = closeResult;
|
|
44
|
+
|
|
45
|
+
return typeof exitCode === "number" ? exitCode : 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {Record<string, unknown>} record
|
|
50
|
+
* @param {string} key
|
|
51
|
+
*
|
|
52
|
+
* @returns {readonly unknown[] | undefined}
|
|
53
|
+
*/
|
|
54
|
+
function getArrayProperty(record, key) {
|
|
55
|
+
const value = record[key];
|
|
56
|
+
return isUnknownArray(value) ? value : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {unknown} value
|
|
61
|
+
*
|
|
62
|
+
* @returns {string | undefined}
|
|
63
|
+
*/
|
|
64
|
+
function getTextNodeValue(value) {
|
|
65
|
+
if (!isUnknownArray(value)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const firstValue = value[0];
|
|
70
|
+
if (!isRecord(firstValue)) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const textValue = firstValue["#text"];
|
|
75
|
+
return typeof textValue === "string" ? textValue : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} directory
|
|
80
|
+
*
|
|
81
|
+
* @returns {Promise<readonly string[]>}
|
|
82
|
+
*/
|
|
83
|
+
async function getThemeFiles(directory) {
|
|
84
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- The validator intentionally reads the repo-local themes directory.
|
|
85
|
+
const directoryEntries = await readdir(directory, { withFileTypes: true });
|
|
86
|
+
|
|
87
|
+
return directoryEntries
|
|
88
|
+
.filter(
|
|
89
|
+
(directoryEntry) =>
|
|
90
|
+
directoryEntry.isFile() &&
|
|
91
|
+
directoryEntry.name.endsWith(".tmTheme")
|
|
92
|
+
)
|
|
93
|
+
.map((directoryEntry) => directoryEntry.name)
|
|
94
|
+
.toSorted((left, right) => left.localeCompare(right))
|
|
95
|
+
.map((fileName) => path.join(directory, fileName));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {unknown} parsedDocument
|
|
100
|
+
*
|
|
101
|
+
* @returns {readonly string[]}
|
|
102
|
+
*/
|
|
103
|
+
function getTopLevelKeys(parsedDocument) {
|
|
104
|
+
if (!isUnknownArray(parsedDocument)) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const plistNode = parsedDocument.find((node) =>
|
|
109
|
+
hasOwnRecordKey(node, "plist")
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (plistNode === undefined) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const plistChildren = getArrayProperty(plistNode, "plist");
|
|
117
|
+
if (plistChildren === undefined) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const dictNode = plistChildren.find((node) =>
|
|
122
|
+
hasOwnRecordKey(node, "dict")
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (dictNode === undefined) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const dictChildren = getArrayProperty(dictNode, "dict");
|
|
130
|
+
if (dictChildren === undefined) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return dictChildren.flatMap((node) => {
|
|
135
|
+
if (!hasOwnRecordKey(node, "key")) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const keyName = getTextNodeValue(node.key);
|
|
140
|
+
return keyName === undefined ? [] : [keyName];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @param {unknown} value
|
|
146
|
+
* @param {string} key
|
|
147
|
+
*
|
|
148
|
+
* @returns {value is Record<string, unknown>}
|
|
149
|
+
*/
|
|
150
|
+
function hasOwnRecordKey(value, key) {
|
|
151
|
+
return isRecord(value) && Object.hasOwn(value, key);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {unknown} value
|
|
156
|
+
*
|
|
157
|
+
* @returns {value is Record<string, unknown>}
|
|
158
|
+
*/
|
|
159
|
+
function isRecord(value) {
|
|
160
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {unknown} value
|
|
165
|
+
*
|
|
166
|
+
* @returns {value is readonly unknown[]}
|
|
167
|
+
*/
|
|
168
|
+
function isUnknownArray(value) {
|
|
169
|
+
return Array.isArray(value);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @returns {Promise<number>}
|
|
174
|
+
*/
|
|
175
|
+
async function main() {
|
|
176
|
+
const themeDirectoryStats = await stat(themeDirectory).catch(() => null);
|
|
177
|
+
|
|
178
|
+
if (themeDirectoryStats?.isDirectory() === false) {
|
|
179
|
+
process.stderr.write(
|
|
180
|
+
`Theme path is not a directory: ${themeDirectory}\n`
|
|
181
|
+
);
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (themeDirectoryStats === null) {
|
|
186
|
+
process.stderr.write(
|
|
187
|
+
`Theme directory does not exist: ${themeDirectory}\n`
|
|
188
|
+
);
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const themeFiles = await getThemeFiles(themeDirectory);
|
|
193
|
+
const results = await Promise.all(
|
|
194
|
+
themeFiles.map((filePath) => validateTheme(filePath))
|
|
195
|
+
);
|
|
196
|
+
const failedResults = results.filter((result) => !result.ok);
|
|
197
|
+
|
|
198
|
+
if (failedResults.length > 0) {
|
|
199
|
+
for (const result of failedResults) {
|
|
200
|
+
process.stderr.write(
|
|
201
|
+
`${path.relative(rootDirectory, result.filePath)}: ${result.reason}\n`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
process.stdout.write(`Validated ${themeFiles.length} .tmTheme files.\n`);
|
|
209
|
+
|
|
210
|
+
if (shouldBuildBatCache) {
|
|
211
|
+
return buildBatCache();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @returns {Promise<void>}
|
|
219
|
+
*/
|
|
220
|
+
async function run() {
|
|
221
|
+
try {
|
|
222
|
+
process.exitCode = await main();
|
|
223
|
+
} catch (error) {
|
|
224
|
+
process.stderr.write(`${String(error)}\n`);
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {string} filePath
|
|
231
|
+
*
|
|
232
|
+
* @returns {Promise<ThemeValidationResult>}
|
|
233
|
+
*/
|
|
234
|
+
async function validateTheme(filePath) {
|
|
235
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Theme paths come from the repo-local themes directory listing.
|
|
236
|
+
const text = await readFile(filePath, "utf8");
|
|
237
|
+
const xmlValidation = SyntaxValidator.validate(text);
|
|
238
|
+
|
|
239
|
+
if (xmlValidation !== true) {
|
|
240
|
+
const { col, line, msg } = xmlValidation.err;
|
|
241
|
+
return {
|
|
242
|
+
filePath,
|
|
243
|
+
ok: false,
|
|
244
|
+
reason: `XML parse error at ${line}:${col}: ${msg}`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const parsedDocument = /** @type {unknown} */ (parser.parse(text));
|
|
249
|
+
const keys = getTopLevelKeys(parsedDocument);
|
|
250
|
+
const missingKeys = [
|
|
251
|
+
"name",
|
|
252
|
+
"uuid",
|
|
253
|
+
"settings",
|
|
254
|
+
].filter((key) => !keys.includes(key));
|
|
255
|
+
|
|
256
|
+
if (missingKeys.length > 0) {
|
|
257
|
+
return {
|
|
258
|
+
filePath,
|
|
259
|
+
ok: false,
|
|
260
|
+
reason: `Missing top-level key(s): ${missingKeys.join(", ")}`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
filePath,
|
|
266
|
+
ok: true,
|
|
267
|
+
reason: "",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// eslint-disable-next-line unicorn/prefer-top-level-await -- This published-module config also enforces n/no-top-level-await.
|
|
272
|
+
void run();
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type ThemeAppearance = "dark" | "light" | "unknown";
|
|
2
|
+
|
|
3
|
+
export interface ThemeColors {
|
|
4
|
+
readonly background: null | string;
|
|
5
|
+
readonly caret: null | string;
|
|
6
|
+
readonly foreground: null | string;
|
|
7
|
+
readonly invisibles: null | string;
|
|
8
|
+
readonly lineHighlight: null | string;
|
|
9
|
+
readonly selection: null | string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ThemeMetadata {
|
|
13
|
+
readonly appearance: ThemeAppearance;
|
|
14
|
+
readonly author: null | string;
|
|
15
|
+
readonly colors: ThemeColors;
|
|
16
|
+
readonly colorSpace: null | string;
|
|
17
|
+
readonly fileName: string;
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly path: string;
|
|
21
|
+
readonly scopes: readonly string[];
|
|
22
|
+
readonly semanticClass: null | string;
|
|
23
|
+
readonly statistics: ThemeStatistics;
|
|
24
|
+
readonly uuid: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ThemeMetadataManifest {
|
|
28
|
+
readonly $schema: "./themes.schema.json";
|
|
29
|
+
readonly consumers: readonly string[];
|
|
30
|
+
readonly description: string;
|
|
31
|
+
readonly duplicateUuidGroups: Readonly<Record<string, readonly string[]>>;
|
|
32
|
+
readonly generatedBy: "npm run metadata:write";
|
|
33
|
+
readonly name: "codex-terminal-themes";
|
|
34
|
+
readonly schemaVersion: 1;
|
|
35
|
+
readonly themeCount: number;
|
|
36
|
+
readonly themeDirectory: "themes";
|
|
37
|
+
readonly themes: readonly ThemeMetadata[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ThemeStatistics {
|
|
41
|
+
readonly colorReferences: number;
|
|
42
|
+
readonly scopedSettings: number;
|
|
43
|
+
readonly settings: number;
|
|
44
|
+
readonly uniqueScopes: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare const manifest: ThemeMetadataManifest;
|
|
48
|
+
|
|
49
|
+
export default manifest;
|