mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-26 11:12:03 +00:00
Browse + install color themes from the VS Code Marketplace straight from Cmd-K and Settings → Appearance. The Electron main process resolves the extension, unzips the .vsix with a hand-rolled zip reader (zlib only, no new deps), and hands back the raw theme JSON; the renderer converts it to a DesktopTheme with a small seed → color-mix mapping. - Folds an extension's light + dark variants into one theme family, so the light/dark toggle switches Solarized/GitHub variants and installing in dark mode stays dark. - Guarantees accent contrast (WCAG AA) so imported sidebar labels read instead of vanishing into the surface. - Filters icon/product-icon packs out of the Themes-category search. - "Install theme…" lives atop the Cmd-K theme picker; imports fold into the Light/Dark groups by the modes they support.
113 lines
4.1 KiB
JavaScript
113 lines
4.1 KiB
JavaScript
'use strict'
|
|
|
|
const assert = require('node:assert')
|
|
const test = require('node:test')
|
|
|
|
const { __testing, extractThemes, readCentralDirectory } = require('./vscode-marketplace.cjs')
|
|
|
|
// Build a minimal zip with stored (uncompressed) entries so the test controls
|
|
// the bytes exactly — exercises the central-directory reader + theme extraction
|
|
// without a deflate dependency.
|
|
function makeZip(entries) {
|
|
const locals = []
|
|
const centrals = []
|
|
let offset = 0
|
|
|
|
for (const { name, data } of entries) {
|
|
const nameBuf = Buffer.from(name, 'utf8')
|
|
const body = Buffer.from(data, 'utf8')
|
|
|
|
const local = Buffer.alloc(30 + nameBuf.length)
|
|
local.writeUInt32LE(0x04034b50, 0)
|
|
local.writeUInt16LE(0, 8) // method: stored
|
|
local.writeUInt32LE(body.length, 18) // compressed size
|
|
local.writeUInt32LE(body.length, 22) // uncompressed size
|
|
local.writeUInt16LE(nameBuf.length, 26)
|
|
nameBuf.copy(local, 30)
|
|
|
|
locals.push(local, body)
|
|
|
|
const central = Buffer.alloc(46 + nameBuf.length)
|
|
central.writeUInt32LE(0x02014b50, 0)
|
|
central.writeUInt16LE(0, 10) // method: stored
|
|
central.writeUInt32LE(body.length, 20)
|
|
central.writeUInt32LE(body.length, 24)
|
|
central.writeUInt16LE(nameBuf.length, 28)
|
|
central.writeUInt32LE(offset, 42) // local header offset
|
|
nameBuf.copy(central, 46)
|
|
|
|
centrals.push(central)
|
|
offset += local.length + body.length
|
|
}
|
|
|
|
const centralStart = offset
|
|
const centralBuf = Buffer.concat(centrals)
|
|
|
|
const eocd = Buffer.alloc(22)
|
|
eocd.writeUInt32LE(0x06054b50, 0)
|
|
eocd.writeUInt16LE(entries.length, 8)
|
|
eocd.writeUInt16LE(entries.length, 10)
|
|
eocd.writeUInt32LE(centralBuf.length, 12)
|
|
eocd.writeUInt32LE(centralStart, 16)
|
|
|
|
return Buffer.concat([...locals, centralBuf, eocd])
|
|
}
|
|
|
|
test('readCentralDirectory finds every entry', () => {
|
|
const zip = makeZip([
|
|
{ name: 'extension/package.json', data: '{}' },
|
|
{ name: 'extension/themes/x.json', data: '{}' }
|
|
])
|
|
|
|
const records = readCentralDirectory(zip)
|
|
assert.ok(records.has('extension/package.json'))
|
|
assert.ok(records.has('extension/themes/x.json'))
|
|
})
|
|
|
|
test('extractThemes reads contributed color themes (resolving ./ paths)', () => {
|
|
const pkg = JSON.stringify({
|
|
name: 'theme-dracula',
|
|
displayName: 'Dracula',
|
|
contributes: {
|
|
themes: [{ label: 'Dracula', uiTheme: 'vs-dark', path: './themes/dracula.json' }]
|
|
}
|
|
})
|
|
const themeJson = JSON.stringify({ name: 'Dracula', type: 'dark', colors: { 'editor.background': '#282a36' } })
|
|
|
|
const zip = makeZip([
|
|
{ name: 'extension/package.json', data: pkg },
|
|
{ name: 'extension/themes/dracula.json', data: themeJson }
|
|
])
|
|
|
|
const themes = extractThemes(zip)
|
|
assert.strictEqual(themes.length, 1)
|
|
assert.strictEqual(themes[0].label, 'Dracula')
|
|
assert.strictEqual(themes[0].uiTheme, 'vs-dark')
|
|
assert.match(themes[0].contents, /editor\.background/)
|
|
})
|
|
|
|
test('extractThemes returns empty when the extension contributes no themes', () => {
|
|
const zip = makeZip([{ name: 'extension/package.json', data: JSON.stringify({ name: 'x', contributes: {} }) }])
|
|
assert.deepStrictEqual(extractThemes(zip), [])
|
|
})
|
|
|
|
test('extractThemes throws when the manifest is missing', () => {
|
|
const zip = makeZip([{ name: 'extension/other.txt', data: 'hi' }])
|
|
assert.throws(() => extractThemes(zip), /manifest missing/i)
|
|
})
|
|
|
|
test('looksLikeIconTheme filters icon/product-icon packs out of theme search', () => {
|
|
const { looksLikeIconTheme } = __testing
|
|
|
|
// Tagged contribution points are the strongest signal.
|
|
assert.strictEqual(looksLikeIconTheme({ tags: ['theme', 'icon-theme'] }), true)
|
|
assert.strictEqual(looksLikeIconTheme({ tags: ['product-icon-theme'] }), true)
|
|
|
|
// Name/description fallback for packs that don't tag themselves.
|
|
assert.strictEqual(looksLikeIconTheme({ displayName: 'Material Icon Theme' }), true)
|
|
assert.strictEqual(looksLikeIconTheme({ shortDescription: 'A pack of file icons.' }), true)
|
|
|
|
// Real color themes survive.
|
|
assert.strictEqual(looksLikeIconTheme({ displayName: 'Dracula Official', tags: ['theme', 'color-theme'] }), false)
|
|
assert.strictEqual(looksLikeIconTheme({ displayName: 'One Dark Pro' }), false)
|
|
})
|