From ec60591d1910eb1480d37c58ec5f7e73a5ccb655 Mon Sep 17 00:00:00 2001 From: subframe7536 <1667077010@qq.com> Date: Sun, 13 Oct 2024 10:59:59 +0800 Subject: [PATCH] feat: make it works --- .vscode/tasks.json | 38 +++++++++++---- eslint.config.mjs | 4 +- package.json | 107 ++++++++++++++++++++++++++++++++++++++--- pnpm-lock.yaml | 33 ++++++++++++- src/config.ts | 10 ++++ src/index.ts | 23 +++++++-- src/manager/base.ts | 47 ++++++++++++++++++ src/manager/css.ts | 69 ++++++++++++++++++++++++++ src/manager/index.ts | 24 +++++++++ src/manager/js.ts | 39 +++++++++++++++ src/manager/webview.ts | 53 ++++++++++++++++++++ src/path.ts | 67 ++++++++++++++++++++++++++ src/utils.ts | 59 +++++++++++++++++++++++ tsconfig.json | 13 +---- 14 files changed, 550 insertions(+), 36 deletions(-) create mode 100644 src/manager/base.ts create mode 100644 src/manager/css.ts create mode 100644 src/manager/index.ts create mode 100644 src/manager/js.ts create mode 100644 src/manager/webview.ts create mode 100644 src/path.ts diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b56d899..61ddb42 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -10,17 +10,35 @@ "presentation": { "reveal": "never" }, - "problemMatcher": [ - { - "base": "$ts-webpack-watch", - "background": { - "activeOnStart": true, - "beginsPattern": "Build start", - "endsPattern": "Build success" - } + "problemMatcher": { + "owner": "typescript", + "fileLocation": "relative", + "pattern": { + // TODO: correct "regexp" + "regexp": "^([a-zA-Z]\\:/?([\\w\\-]/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$", + "file": 1, + "line": 3, + "column": 4, + "code": 5, + "message": 6 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "Build start", + "endsPattern": "Build success" } - ], - "group": "build" + } + // "problemMatcher": [ + // { + // "owner": "typescript", + // "fileLocation": "relative", + // "background": { + // "activeOnStart": true, + // "beginsPattern": "Build start", + // "endsPattern": "Build success" + // } + // } + // ] } ] } diff --git a/eslint.config.mjs b/eslint.config.mjs index 40553c4..fae254e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,3 +1,5 @@ import { defineEslintConfig } from '@subframe7536/eslint-config' -export default defineEslintConfig() +export default defineEslintConfig({ + type: 'app', +}) diff --git a/package.json b/package.json index 71262f1..41539a3 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "publisher": "1667077010@qq.com", "name": "custom-ui-style", - "displayName": "custom-ui-style", + "displayName": "Custom UI Style", "version": "0.0.0", "private": true, "packageManager": "pnpm@9.12.1", - "description": "", + "description": "Custom ui css style in both editor and webview, unify global font family, setup background image", "author": "subframe7536 <1667077010@qq.com>", "license": "MIT", "homepage": "https://github.com/subframe7536/custom-ui-style#readme", @@ -32,17 +32,106 @@ "activationEvents": [ "onStartupFinished" ], + "extensionKind": [ + "ui", + "workspace" + ], "contributes": { - "commands": [], + "commands": [ + { + "command": "custom-ui-style.reload", + "title": "Custom UI Style: Load custom style" + }, + { + "command": "custom-ui-style.rollback", + "title": "Custom UI Style: Rollback" + } + ], "configuration": { "type": "object", - "title": "custom-ui-style", - "properties": {} + "title": "Custom UI Style", + "properties": { + "custom-ui-style.monospace": { + "type": "string", + "title": "Custom global monospace font family" + }, + "custom-ui-style.sansSerif": { + "type": "string", + "title": "Custom global sans-serif font family" + }, + "custom-ui-style.backgroundUrl": { + "type": "string", + "pattern": "^(https://|file://|data:)", + "patternErrorMessage": "Only allow https: file:// data:", + "title": "Custom background image url" + }, + "custom-ui-style.backgroundUrlWin32": { + "type": "string", + "pattern": "^(https://|file://|data:)", + "patternErrorMessage": "Only allow https: file:// data:", + "title": "Custom background image url (For Windows)" + }, + "custom-ui-style.backgroundUrlDarWin": { + "type": "string", + "pattern": "^(https://|file://|data:)", + "patternErrorMessage": "Only allow https: file:// data:", + "title": "Custom background image url (For MacOS)" + }, + "custom-ui-style.backgroundUrlLinux": { + "type": "string", + "pattern": "^(https://|file://|data:)", + "patternErrorMessage": "Only allow https: file:// data:", + "title": "Custom background image url (For Linux)" + }, + "custom-ui-style.backgroundOpacity": { + "type": "number", + "title": "Custom background image opacity", + "default": 0.9 + }, + "custom-ui-style.backgroundSize": { + "type": "string", + "enum": [ + "cover", + "contain" + ], + "title": "Custom background image size", + "default": "cover" + }, + "custom-ui-style.backgroundPosition": { + "type": "string", + "title": "Custom background image size", + "default": "center" + }, + "custom-ui-style.stylesheet": { + "type": "object", + "title": "Custom css", + "description": "support nest selectors" + }, + "custom-ui-style.webviewMonospaceSelector": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Custom monospace selector in webview" + }, + "custom-ui-style.webviewSansSerifSelector": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Custom sans-serif selector in webview" + }, + "custom-ui-style.webviewStylesheet": { + "type": "object", + "title": "Custom css", + "description": "support nest selectors" + } + } } }, "scripts": { - "build": "tsup src/index.ts --external vscode", - "dev": "pnpm run build --watch", + "build": "tsup --treeshake", + "dev": "tsup --watch", "prepare": "pnpm run update", "update": "vscode-ext-gen --output src/generated/meta.ts", "format": "eslint . --fix", @@ -50,13 +139,15 @@ "publish": "vsce publish --no-dependencies", "pack": "vsce package --no-dependencies", "typecheck": "tsc --noEmit", - "release": "bumpp && pnpm publish" + "release": "bumpp && pnpm run publish" }, "devDependencies": { "@subframe7536/eslint-config": "^0.9.4", + "@subframe7536/type-utils": "^0.1.6", "@types/node": "^20.16.11", "@types/vscode": "^1.94.0", "@vscode/vsce": "^3.1.1", + "atomically": "^2.0.3", "bumpp": "^9.7.1", "eslint": "^9.12.0", "reactive-vscode": "^0.2.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5b3a29..9cb21e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@subframe7536/eslint-config': specifier: ^0.9.4 version: 0.9.4(@typescript-eslint/utils@8.8.1(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(@vue/compiler-sfc@3.4.27)(eslint@9.12.0(jiti@1.21.6))(local-pkg@0.5.0)(typescript@5.6.3)(vitest@2.0.5(@types/node@20.16.11)) + '@subframe7536/type-utils': + specifier: ^0.1.6 + version: 0.1.6 '@types/node': specifier: ^20.16.11 version: 20.16.11 @@ -20,6 +23,9 @@ importers: '@vscode/vsce': specifier: ^3.1.1 version: 3.1.1 + atomically: + specifier: ^2.0.3 + version: 2.0.3 bumpp: specifier: ^9.7.1 version: 9.7.1 @@ -824,6 +830,9 @@ packages: peerDependencies: local-pkg: '*' + '@subframe7536/type-utils@0.1.6': + resolution: {integrity: sha512-7tUH6F6q7RluUGNZ3iB2oxnhJAW5w4aCVky6mBTajpuyiBPnO2v2E+lsaNI6oGeUVsGo0rsB1u3akSIQcOKXhA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1099,6 +1108,9 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomically@2.0.3: + resolution: {integrity: sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==} + azure-devops-node-api@12.5.0: resolution: {integrity: sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==} @@ -2805,6 +2817,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + stubborn-fs@1.2.5: + resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} + sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} @@ -3100,6 +3115,9 @@ packages: whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + when-exit@2.1.3: + resolution: {integrity: sha512-uVieSTccFIr/SFQdFWN/fFaQYmV37OKtuaGphMAzi4DmmUlrvRBJW5WSLkHyjNQY/ePJMz3LoiX9R3yy1Su6Hw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3774,6 +3792,8 @@ snapshots: - typescript - vitest + '@subframe7536/type-utils@0.1.6': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -4119,6 +4139,11 @@ snapshots: asynckit@0.4.0: {} + atomically@2.0.3: + dependencies: + stubborn-fs: 1.2.5 + when-exit: 2.1.3 + azure-devops-node-api@12.5.0: dependencies: tunnel: 0.0.6 @@ -4833,7 +4858,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 optional: true esutils@2.0.3: {} @@ -5889,7 +5914,7 @@ snapshots: postcss@8.4.41: dependencies: nanoid: 3.3.7 - picocolors: 1.0.1 + picocolors: 1.1.0 source-map-js: 1.2.0 prebuild-install@7.1.1: @@ -6167,6 +6192,8 @@ snapshots: strip-json-comments@3.1.1: {} + stubborn-fs@1.2.5: {} + sucrase@3.35.0: dependencies: '@jridgewell/gen-mapping': 0.3.5 @@ -6483,6 +6510,8 @@ snapshots: tr46: 1.0.1 webidl-conversions: 4.0.2 + when-exit@2.1.3: {} + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/src/config.ts b/src/config.ts index 5d32d6f..b16369c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,3 +5,13 @@ export const config = defineConfigObject( Meta.scopedConfigs.scope, Meta.scopedConfigs.defaults, ) + +export const editorConfig = defineConfigObject('editor', { + fontFamily: String, +}) + +export function getFamilies() { + let { monospace, sansSerif } = config + monospace ||= editorConfig.fontFamily + return { monospace, sansSerif } +} diff --git a/src/index.ts b/src/index.ts index 9fab6e5..ab46f4b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,25 @@ -import { defineExtension } from 'reactive-vscode' -import { window } from 'vscode' +import { defineExtension, useCommand, watch } from 'reactive-vscode' +import { config, editorConfig } from './config' +import * as Meta from './generated/meta' +import { createFileManagers } from './manager' const { activate, deactivate } = defineExtension(() => { - window.showInformationMessage('Hello') + const { reload, rollback } = createFileManagers() + useCommand(Meta.commands.reload, () => { + reload('UI style changed') + }) + useCommand(Meta.commands.rollback, () => { + rollback('UI style rollback') + }) + + watch( + () => editorConfig.fontFamily, + () => !config.monospace && reload('Configuration changed, reload'), + ) + watch( + config, + () => reload('Configuration changed, reload'), + ) }) export { activate, deactivate } diff --git a/src/manager/base.ts b/src/manager/base.ts new file mode 100644 index 0000000..105062e --- /dev/null +++ b/src/manager/base.ts @@ -0,0 +1,47 @@ +import type { Promisable } from '@subframe7536/type-utils' +import { cpSync, existsSync } from 'node:fs' +import { readFileSync, writeFileSync } from 'atomically' +import { logger } from '../utils' + +export interface FileManager { + hasBakFile: boolean + reload: () => Promise + rollback: () => Promise +} + +export abstract class BaseFileManager implements FileManager { + constructor( + private srcPath: string, + private bakPath: string, + ) { + if (!this.hasBakFile) { + cpSync(this.srcPath, this.bakPath) + logger.info('Create', this.bakPath) + } + } + + get hasBakFile() { + return existsSync(this.bakPath) + } + + async reload() { + if (!this.hasBakFile) { + logger.warn(`bak file [${this.bakPath}] does not exist, skip reload`) + } else { + writeFileSync(this.srcPath, await this.patch(readFileSync(this.bakPath, 'utf-8'))) + logger.info(`Config reload [${this.srcPath}]`) + } + } + + async rollback() { + if (!this.hasBakFile) { + logger.warn(`bak file [${this.bakPath}] does not exist, skip rollback`) + } else { + const originJS = readFileSync(this.bakPath, 'utf-8') + writeFileSync(this.srcPath, originJS) + logger.info(`Config rollback [${this.srcPath}]`) + } + } + + abstract patch(content: string): Promisable +} diff --git a/src/manager/css.ts b/src/manager/css.ts new file mode 100644 index 0000000..59c8e91 --- /dev/null +++ b/src/manager/css.ts @@ -0,0 +1,69 @@ +import type { Promisable } from '@subframe7536/type-utils' +import type { ConfigShorthandMap } from '../generated/meta' +import { config, getFamilies } from '../config' +import { normalizeUrl } from '../path' +import { captialize, generateStyleFromObject } from '../utils' +import { BaseFileManager } from './base' +import { VSC_DFAULT_SANS_FONT, VSC_NOTEBOOK_MONO_FONT } from './js' + +const banner = '/* Custom UI Style Start */' +const footer = '/* Custom UI Style End */' + +function generateBackgroundCSS() { + const plt = process?.platform + + const url = ( + config[`backgroundUrl${captialize(plt)}` as keyof ConfigShorthandMap] || config.backgroundUrl + ) as string + + if (!url) { + return '' + } + return `body { + background-size: ${config.backgroundSize}; + background-repeat: no-repeat; + background-attachment: fixed; // for code-server + background-position: ${config.backgroundPosition}; + opacity: ${config.backgroundOpacity}; + background-image: url('${normalizeUrl(url)}'); +}` +} + +function generateFontCSS() { + let result = '' + const { monospace, sansSerif } = getFamilies() + if (monospace) { + result += `span.monaco-keybinding-key, .setting-list-row { + font-family: ${monospace}, ${VSC_NOTEBOOK_MONO_FONT} !important; +} +.windows, +.mac, +.linux { + --monaco-monospace-font: ${monospace}, ${VSC_NOTEBOOK_MONO_FONT} !important;"; +}` + } + if (sansSerif) { + result += `.windows { + font-family: ${sansSerif}, ${VSC_DFAULT_SANS_FONT.win} !important;"; +} +.mac { + font-family: ${sansSerif}, ${VSC_DFAULT_SANS_FONT.mac} !important;"; +} +.linux { + font-family: ${sansSerif}, ${VSC_DFAULT_SANS_FONT.linux} !important;"; +}` + } + return result +} + +export class CssFileManager extends BaseFileManager { + patch(content: string): Promisable { + return `${content} +${banner} +${generateBackgroundCSS()} +${generateFontCSS()} +${generateStyleFromObject(config.stylesheet)} +${footer} +` + } +} diff --git a/src/manager/index.ts b/src/manager/index.ts new file mode 100644 index 0000000..cc1f4f2 --- /dev/null +++ b/src/manager/index.ts @@ -0,0 +1,24 @@ +import type { FileManager } from './base' +import { cssBakPath, cssPath, jsBakPath, jsPath, webviewHTMLBakPath, webviewHTMLPath } from '../path' +import { runAndRestart } from '../utils' +import { CssFileManager } from './css' +import { JsFileManager } from './js' +import { WebViewFileManager } from './webview' + +export function createFileManagers() { + const managers: FileManager[] = [ + new CssFileManager(cssPath, cssBakPath), + new JsFileManager(jsPath, jsBakPath), + new WebViewFileManager(webviewHTMLPath, webviewHTMLBakPath), + ] + return { + reload: (text: string) => runAndRestart( + text, + () => Promise.all(managers.map(m => m.reload())), + ), + rollback: (text: string) => runAndRestart( + text, + () => Promise.all(managers.map(m => m.rollback())), + ), + } +} diff --git a/src/manager/js.ts b/src/manager/js.ts new file mode 100644 index 0000000..4dfed6e --- /dev/null +++ b/src/manager/js.ts @@ -0,0 +1,39 @@ +import type { Promisable } from '@subframe7536/type-utils' +import { getFamilies } from '../config' +import { escapeQuote } from '../utils' +import { BaseFileManager } from './base' + +export const VSC_DFAULT_MONO_FONT = { + win: `Consolas, 'Courier New'`, + mac: `Menlo, Monaco, 'Courier New'`, + linux: `'Droid Sans Mono', 'monospace'`, +} +export const VSC_DFAULT_SANS_FONT = { + win: `"Segoe WPC", "Segoe UI"`, + mac: `-apple-system, BlinkMacSystemFont`, + linux: `system-ui, "Ubuntu", "Droid Sans"`, +} + +export const VSC_NOTEBOOK_MONO_FONT = `"SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace` + +export class JsFileManager extends BaseFileManager { + patch(content: string): Promisable { + let { monospace, sansSerif } = getFamilies() + if (monospace) { + monospace = escapeQuote(monospace) + content = content + .replaceAll(VSC_DFAULT_MONO_FONT.win, monospace) + .replaceAll(VSC_DFAULT_MONO_FONT.mac, monospace) + .replaceAll(VSC_DFAULT_MONO_FONT.linux, monospace) + .replaceAll(VSC_NOTEBOOK_MONO_FONT, monospace) + } + if (sansSerif) { + sansSerif = escapeQuote(sansSerif) + content = content + .replaceAll(VSC_DFAULT_SANS_FONT.win, sansSerif) + .replaceAll(VSC_DFAULT_SANS_FONT.mac, sansSerif) + .replaceAll(VSC_DFAULT_SANS_FONT.linux, sansSerif) + } + return content + } +} diff --git a/src/manager/webview.ts b/src/manager/webview.ts new file mode 100644 index 0000000..d73e33e --- /dev/null +++ b/src/manager/webview.ts @@ -0,0 +1,53 @@ +import type { Promisable } from '@subframe7536/type-utils' +import { createHash } from 'node:crypto' +import { config, getFamilies } from '../config' +import { escapeQuote, generateStyleFromObject, logger } from '../utils' +import { BaseFileManager } from './base' + +const entry = `'\\n' + newDocument.documentElement.outerHTML` + +const defaultMonospaceSelector: string[] = ['.font-mono', 'code', 'pre', '.mono', '.monospace'] +const defaultSansSerifSelector: string[] = ['.font-sans', '.github-markdown-body'] + +function getCSS() { + const { monospace, sansSerif } = getFamilies() + const { + webviewSansSerifSelector = [], + webviewMonospaceSelector = [], + webviewStylesheet, + } = config + let result = '' + if (monospace) { + const monoSelectors = [...defaultMonospaceSelector, ...webviewMonospaceSelector] + result += `${monoSelectors}{font-family:${escapeQuote(monospace)}!important}` + } + if (sansSerif) { + const sansSelectors = [...defaultSansSerifSelector, ...webviewSansSerifSelector] + result += `${sansSelectors}{font-family:${escapeQuote(sansSerif)}!important}` + } + if (webviewStylesheet) { + result += generateStyleFromObject(webviewStylesheet) + } + return result +} + +export function fixSha256(html: string) { + const [,scriptString] = html.match(/