Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: monochrome feature #87

Merged
merged 6 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
"type": "boolean",
"default": true,
"description": "Display specific folder icons. Disable to only use default folder icon."
},
"catppuccin-icons.monochrome": {
"type": "boolean",
"default": false,
"description": "Only use Text color for icons."
}
}
},
Expand Down Expand Up @@ -101,6 +106,7 @@
"lint:fix": "eslint . --fix",
"pack": "vsce package --no-dependencies",
"release": "changelogen --release --push",
"typecheck": "tsc",
"vscode:prepublish": "pnpm build"
},
"devDependencies": {
Expand Down
15 changes: 8 additions & 7 deletions scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ import { compileTheme } from '~/utils/themes'
const DIST = 'dist'
const flavors = flavorEntries.map(([f]) => f)

// CLEANUP
// cleanup
await rimraf(DIST)

// COPY ICONS TO DIST
// copy icons to dist
await Promise.all(flavors.map(async (f) => {
await cp(join('icons', f), join(DIST, f, 'icons'), { recursive: true })
}))

// GENERATE ICON DEFINITIONS AND SAVE THEM TO DIST
// copy css-vars/unflavored icons to dist
await cp(join('icons', 'css-variables'), join(DIST, 'unflavored'), { recursive: true })

// generate iconDefinitions.json file and save to dist
const icons = await readdir(join(DIST, flavors[0], 'icons'))
const iconDefinitions = icons.reduce((d, i) => ({
...d,
Expand All @@ -27,18 +30,16 @@ await writeFile(
JSON.stringify(iconDefinitions, null, 2),
)

// CREATE THEME AND INJECT ICON DEFINITIONS
// compile theme.json and write to dist
const theme = compileTheme({}, iconDefinitions)

// WRITE THEMES
await Promise.all(flavors.map(async (f) => {
await writeFile(
join(DIST, f, 'theme.json'),
JSON.stringify(theme, null, 2),
)
}))

// BUILD EXTENSION RUNTIME
// build extension runtime
await build({
entry: ['src/main.ts', 'src/browser.ts'],
format: ['cjs'],
Expand Down
8 changes: 7 additions & 1 deletion scripts/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import { resolve } from 'node:path'
import { SVG, parseColors } from '@iconify/tools'
import { palettes } from '~/utils/palettes'

const flavors = Object.keys(palettes) as Array<keyof typeof palettes>
const flavors = [
'css-variables',
'frappe',
'latte',
'macchiato',
'mocha',
] satisfies Array<keyof typeof palettes>

for (const origin of flavors) {
const originPath = resolve('icons', origin)
Expand Down
7 changes: 6 additions & 1 deletion src/browser.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import type { ConfigurationChangeEvent, ExtensionContext } from 'vscode'
import { window, workspace } from 'vscode'
import { CONFIG_ROOT } from '~/constants'

/**
* Web extension entrypoint
* @see https://code.visualstudio.com/api/extension-guides/web-extensions
*/
export function activate(context: ExtensionContext) {
context.subscriptions.push(
workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => {
if (event.affectsConfiguration('catppuccin-icons')) {
if (event.affectsConfiguration(CONFIG_ROOT)) {
window.showErrorMessage(
'VSCode Web doesn\'t support advanced Catppuccin Icons options at the moment.',
)
Expand Down
12 changes: 12 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const CONFIG_ROOT = 'catppuccin-icons' as const

export enum CONFIG_KEYS {
HidesExplorerArrows = 'hidesExplorerArrows',
SpecificFolders = 'specificFolders',
Monochrome = 'monochrome',
Associations = 'associations',
}

export enum COMMANDS {
Reset = 'reset',
}
4 changes: 4 additions & 0 deletions src/defaults/fileIcons.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* Default file icon associations
* Keys are icon file basenames
*/
const fileIcons: Record<string, {
languageIds?: Array<string>
fileExtensions?: Array<string>
Expand Down
4 changes: 4 additions & 0 deletions src/defaults/folderIcons.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* Default folder icon associations
* Keys are icon file basenames (without folder_ prefix)
*/
const folderIcons: Record<string, {
folderNames?: Array<string>
}> = {
Expand Down
39 changes: 29 additions & 10 deletions src/hooks/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
import defu from 'defu'
import { ConfigurationTarget, workspace } from 'vscode'
import { defaultConfig } from '~/defaults'
import { CONFIG_KEYS, CONFIG_ROOT } from '~/constants'
import type { Config } from '~/types'

/**
* Get user catppuccin-icons configuration
* @returns explicitly set configuration keys
*/
export function getConfig(): Partial<Config> {
const config = workspace.getConfiguration('catppuccin-icons')
const config = workspace.getConfiguration(CONFIG_ROOT)

return {
hidesExplorerArrows: config.get('hidesExplorerArrows'),
specificFolders: config.get('specificFolders'),
associations: config.get('associations'),
monochrome: config.get('monochrome'),
hidesExplorerArrows: config.get(CONFIG_KEYS.HidesExplorerArrows),
specificFolders: config.get(CONFIG_KEYS.SpecificFolders),
associations: config.get(CONFIG_KEYS.Associations),
monochrome: config.get(CONFIG_KEYS.Monochrome),
}
}

/**
* Reset catppuccin-icons configuration
* Deletes keys from `settings.json`
*/
export async function resetConfig() {
const config = workspace.getConfiguration('catppuccin-icons')
await config.update('hidesExplorerArrows', undefined, ConfigurationTarget.Global)
await config.update('specificFolders', undefined, ConfigurationTarget.Global)
await config.update('associations', undefined, ConfigurationTarget.Global)
await config.update('monochrome', undefined, ConfigurationTarget.Global)
const config = workspace.getConfiguration(CONFIG_ROOT)
for (const k in Object.values(CONFIG_KEYS))
await config.update(k, undefined, ConfigurationTarget.Global)
}

/**
* Compares current user config to factory defaults
* @returns `true` if parsed config === defaults
*/
export function isDefaultConfig() {
const config = defu(getConfig(), defaultConfig)

return JSON.stringify(config) === JSON.stringify(defaultConfig)
}
5 changes: 5 additions & 0 deletions src/hooks/iconDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { Uri, workspace } from 'vscode'
import type { ExtensionContext } from 'vscode'
import type { IconDefinitions } from '~/types'

/**
* Reads `iconDefinitions.json` file from dist
* @param context current extension context
* @returns parsed IconDefinitions
*/
export async function getIconDefinitions(context: ExtensionContext) {
const path = Uri.joinPath(context.extensionUri, 'dist', 'iconDefinitions.json')
return workspace.fs.readFile(path).then(f => JSON.parse(f.toString()) as IconDefinitions)
Expand Down
46 changes: 37 additions & 9 deletions src/hooks/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@ import { Buffer } from 'node:buffer'
import { Uri, commands, window, workspace } from 'vscode'
import type { ExtensionContext } from 'vscode'

/**
* Inform the user changes happened and reload is required
*/
export async function promptToReload() {
const message = `Catppuccin Icons: Theme changed - Reload required.`
const action = 'Reload window'
return window.showInformationMessage(message, action).then(async (selectedAction) => {
await window.showInformationMessage(message, action).then(async (selectedAction) => {
if (selectedAction === action)
commands.executeCommand('workbench.action.reloadWindow')
})
};

/**
* Check if the extension was installed already (uses flag file)
* @param context current extension context
* @returns `true` if extension was just insalled
*/
export async function isFreshInstall(context: ExtensionContext) {
const flag = Uri.joinPath(context.extensionUri, 'dist', '.flag')
return await workspace.fs.stat(flag).then(
Expand All @@ -22,13 +30,33 @@ export async function isFreshInstall(context: ExtensionContext) {
)
}

export async function writeJsonFile(uri: Uri, content: unknown) {
/**
* Read a file using `workspace.fs`
* @param uri file path
* @returns file content as string
*/
export async function readFile(uri: Uri) {
return workspace.fs
.writeFile(uri, Buffer.from(JSON.stringify(content, null, 2)))
.then(
() => {},
(error: Error) => {
window.showErrorMessage(error.message)
},
)
.readFile(uri)
.then(b => b.toString())
}

/**
* Write a file using `workspace.fs`
* @param uri file path
* @param content file content to write
*/
export async function writeFile(uri: Uri, content: string) {
await workspace.fs
.writeFile(uri, Buffer.from(content))
.then(() => {})
}

/**
* Write a json object to a file using `workspace.fs`
* @param uri file path
* @param json json object
*/
export async function writeJsonFile(uri: Uri, json: Record<string, any>) {
await writeFile(uri, JSON.stringify(json, null, 2))
}
25 changes: 25 additions & 0 deletions src/hooks/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,40 @@ import { Uri } from 'vscode'
import type { ExtensionContext } from 'vscode'
import type { ThemePaths } from '~/types'

/**
* Get extension runtime root (`dist`)
* @param context current extension context
* @returns root uri
*/
function getRootPath(context: ExtensionContext) {
return Uri.joinPath(context.extensionUri, 'dist')
}

/**
* Get (fresh install) flag path
* @param context current extension context
* @returns flag uri
*/
export function getFlagPath(context: ExtensionContext) {
const root = getRootPath(context)
return Uri.joinPath(root, '.flag')
}

/**
* Get unflavored (css-vars) icons folder path
* @param context current extension context
* @returns unflavored icons folder uri
*/
export function getUnflavoredPath(context: ExtensionContext) {
const root = getRootPath(context)
return Uri.joinPath(root, 'unflavored')
}

/**
* Get flavored folder paths
* @param context current extension context
* @returns flavored icons/theme paths
*/
export function getThemePaths(context: ExtensionContext): ThemePaths {
const root = getRootPath(context)
return {
Expand Down
69 changes: 43 additions & 26 deletions src/hooks/updateThemes.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,56 @@
import { Buffer } from 'node:buffer'
import type { ExtensionContext, Uri } from 'vscode'
import { window, workspace } from 'vscode'
import type { ExtensionContext } from 'vscode'
import { Uri, window, workspace } from 'vscode'
import { flavorEntries } from '@catppuccin/palette'
import { promptToReload } from './interactions'
import { readFile, writeFile, writeJsonFile } from '~/hooks/interactions'
import { getConfig } from '~/hooks/configuration'
import { getIconDefinitions } from '~/hooks/iconDefinitions'
import { compileTheme } from '~/utils/themes'
import { getThemePaths } from '~/hooks/paths'
import { getThemePaths, getUnflavoredPath } from '~/hooks/paths'
import { compileIcon, hashedSvgPath, iconHash } from '~/utils/icons'

export async function updateThemes(context: ExtensionContext) {
/**
* Update themes and icons according to configuration
* @param context current extension context
* @param icons should icon files be regenerated
*/
export async function updateThemes(context: ExtensionContext, icons = false) {
const iconDefinitions = await getIconDefinitions(context)
const paths = getThemePaths(context)
const config = getConfig()
const theme = compileTheme(config, iconDefinitions)
const hash = iconHash(config)
const flavors = flavorEntries.map(([f]) => f)

return Promise.all(flavors.map(async flavor =>
workspace.fs.writeFile(
paths[flavor].theme,
Buffer.from(JSON.stringify(theme, null, 2)),
),
)).then(async () => {
await promptToReload()
}).catch((e: Error) => {
if (icons) {
const unflavored = getUnflavoredPath(context)
const unflavoredIcons = await workspace.fs.readDirectory(unflavored)

await Promise.all(flavors.map(async (flavor) => {
// delete flavored icons
await workspace.fs.delete(paths[flavor].icons, { recursive: true })
// recreate flavored icon folder
await workspace.fs.createDirectory(paths[flavor].icons)
// recreate flavored icons with hashed paths
await Promise.all(unflavoredIcons.map(async ([i]) => {
const icon = await readFile(Uri.joinPath(unflavored, i))
await writeFile(
Uri.joinPath(paths[flavor].icons, hashedSvgPath(i, hash)),
compileIcon(icon, flavor, { monochrome: config.monochrome }),
)
}))
})).catch((e: Error) => {
window.showErrorMessage(`Failed to save re-compiled icons: \n${e.message}`)
})
}

// add hashed paths to iconDefs
for (const i in iconDefinitions)
iconDefinitions[i].iconPath = hashedSvgPath(iconDefinitions[i].iconPath, hash)

// create and write `theme.json` files
const theme = compileTheme(config, iconDefinitions)
await Promise.all(flavors.map(async (flavor) => {
await writeJsonFile(paths[flavor].theme, theme)
})).catch((e: Error) => {
window.showErrorMessage(`Failed to save re-compiled theme: \n${e.message}`)
})
}

export async function writeFile(uri: Uri, content: unknown) {
return workspace.fs
.writeFile(uri, Buffer.from(JSON.stringify(content, null, 2)))
.then(
() => {},
(error: Error) => {
window.showErrorMessage(error.message)
},
)
}
Loading
Loading