Skip to content

Commit

Permalink
feat: add monochrome icons configuration option (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
prazdevs authored Jan 29, 2024
1 parent f2fb54a commit 95e35e1
Show file tree
Hide file tree
Showing 17 changed files with 294 additions and 80 deletions.
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

0 comments on commit 95e35e1

Please sign in to comment.