├── .editorconfig ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.md ├── icons ├── devopicons.woff2 ├── file-icons-colourless-icon-theme.json ├── file-icons-icon-theme.json ├── file-icons.woff2 ├── fontawesome.woff2 ├── mfixx.woff2 └── octicons.woff2 ├── package.json ├── scripts ├── content-types.xml ├── extension.xml ├── missing-filenames.txt └── update.mjs └── thumbnail.png /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = false 8 | indent_style = tab 9 | 10 | [*.{yaml,yml}] 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [{COMMIT_EDITMSG,MERGE_MSG}] 15 | trim_trailing_whitespace = true 16 | max_line_length = 72 17 | indent_style = space 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | Desktop.ini 4 | node_modules 5 | .nyc_output 6 | ._* 7 | .\#* 8 | \#*\# 9 | *~ 10 | *.log 11 | *.lock 12 | [._]*.s[a-v][a-z] 13 | [._]*.sw[a-p] 14 | [._]s[a-v][a-z] 15 | [._]sw[a-p] 16 | 17 | # Project-specific 18 | *.vsix 19 | *.zip 20 | /defs 21 | /tmp 22 | /version 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | **NOTE:** Since 1.0.10 (March 2018), all notable changes are being tracked in the change-logs of 5 | [`file-icons/atom`](https://github.com/file-icons/atom/blob/master/CHANGELOG.md). 6 | 7 | Only bug-fixes and updates specific to VSCode will continue to be documented here. 8 | 9 | 10 | ## 1.0.10 11 | New icons and extensions from upstream 12 | 13 | ## 1.0.6 14 | New icons and extensions from upstream 15 | - [Fix] Dockerfile, fileNames needed to be lower cased, thanks @costela 16 | 17 | ## 1.0.5 18 | New icons and extensions from upstream 19 | 20 | ## 1.0.4 21 | New icons and extensions from upstream 22 | Lots # TODO: add list here 23 | 24 | ## 1.0.3 25 | New icons and extensions from upstream 26 | - [Support] AngelScript (`.acs`, `.angelscript`), Bazel (`.bzl`, `BUILD`, `WORKSPACE`), BEM (`.bemjson.js`), Caddy (`Caddyfile`), DeviceTree (`.dts`, `.dtsi`), Franca (`.fdl`, `.fidl`, `.fdepl`), Jison (`.jison`, `.jisonlex`), Meson (`meson.build`, `meson_options.txt`), MiniZinc (`.mzn`, `.dzn`), Miranda (`.m`), Nanoc (`.nanoc.yaml`), P4 (`.p4`), Watchman (`.watchmanconfig`, `watchman.json`), ESDoc (`esdoc.json`), JSONT (`.jsont`), Twine (`.tw`), and Phoenix (`phoenix.{ex,js}`) 27 | - [Support] Generic code (`.bc`, `.dtd`, `.fo`, `.fidl`, `.stellaris`, `.spthy`, `.wlp4`), Markdown (`.gfm`, `.pfm`), PostCSS (`.postcssrc.{js,json,yaml}`) 28 | - [Change] 29 | 30 | ## 1.0.2 31 | * [Support] .cfignore as a default 32 | 33 | ## 1.0.1 34 | New icons and extensions from upstream 35 | * [Support] ABIF (`.abif`, `.ab1`, `.fsa`), EJS (`.ejs`), Hoplon (`.hl`), KiCad (`.kicad_pcb`), Mercurial (`.hg`), PlatformIO (`platformio.ini`), Polymer (`polymer.json`), Rhino3D (`.3dm`, `.rvb`), VirtualBox (`.vbox`), VMware (`.vmdk,` `.nvram`, `.vmsd`, `.vmsn`, `.vmss`, `.vmtm`, `.vmx`, `.vmxf`) 36 | * [Support] LookML (`.lkml`), SQL (`.hql`) 37 | * See https://github.com/file-icons/atom/releases/tag/v2.0.15 for full list 38 | 39 | ## 1.0.0 40 | Publish 41 | 42 | ## 0.0.5 43 | New icons and extensions from upstream 44 | * [Support] 33 new icons: Ansible, Aurelia, bitHound, Brunch, Buck, Bundler, CakePHP (updated logo), Chef, COBOL, CodeKit, Delphi, Doclets, DoneJS, Drone, GitLab, HaxeDevelop, Jasmine, Jest, KitchenCI, Lerna, Lime, Microsoft InfoPath, Nuclide, Octave, PHPUnit, Redux, RSpec, Sequelize, Shipit, Shippable, Swagger, Template Toolkit, Twig 45 | * [Support] Blade (.blade), Erlang (Emakefile), GraphViz (.plantuml, .iuml, .puml, .pu), Jekyll (_config.yml, .nojekyll), MkDocs (mkdocs.yml), Paket (Various paket.* configs, .paket folders), Process IDs (.pid), Puppet (.epp), Tcl (.exp), Terminal (.profile), Visual Studio (.vscodeignore, .vsix, .vssettings.json, .vscode folders), Yarn (.yarnrc, .yarn-metadata.json, .yarn-integrity, .yarnclean), WeChat (.wxml, .wxss) 46 | * See https://github.com/file-icons/atom/releases/tag/v2.0.14 for full list 47 | 48 | ## 0.0.4 49 | * [Fix] Remove BUILD file python icon, conflicts with build.* 50 | * [Support] Add colour to .git* icons 51 | 52 | ## 0.0.3 53 | * [Support] .gitignore and .gitattributes 54 | * [Support] Varying folder icons 55 | * [Fix] Colour variations of icons 56 | 57 | ## 0.0.2 58 | * [Fix] Devicons 59 | * [Fix] Updated Octicons Font 60 | * [Support] Added Atom's default icons definitions 61 | * [Feature] Colourless icon theme 62 | 63 | ## 0.0.1 64 | - Initial release 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Adding a new icon 2 | ================= 3 | 4 | Please [submit a request][1] to the `file-icons/icons` repository. Make sure the 5 | icon isn't already included in one of the other icon-fonts first: 6 | 7 | * [**Devicons**](https://github.com/file-icons/DevOpicons/blob/master/charmap.md) 8 | * [**Mfizz**](https://github.com/file-icons/MFixx/blob/master/charmap.md) 9 | * [**Octicons**](https://octicons.github.com/) 10 | 11 | 12 | Adding support for a new filetype 13 | ================================= 14 | 15 | This package pulls its icon-to-filetype mappings from the [`file-icons/atom`][2] 16 | repository using an [update script][3]. The `*-icon-theme.json` files themselves 17 | are auto-generated and shouldn't be edited by hand. Please add new extensions to 18 | the upstream [`config.cson`][4] file instead. 19 | 20 | If `config.cson` already lists the desired extension, then it's likely a problem 21 | with the [update script][3]. See below. 22 | 23 | 24 | Fixing a missing filetype 25 | ========================= 26 | 27 | The [update script][3] is unable to generate filetype mappings for patterns with 28 | an indefinite number of variants, such as `/^foo(.*)\.bar/`. In cases like this, 29 | a workaround is to add a new entry to [`missing-filenames.txt`][5]: 30 | 31 | ~~~text 32 | filename.extension 33 | ~~~ 34 | 35 | A caveat of this solution is that only fixed-length strings can be added; a more 36 | intuitive system for icon-mapping will eventually be developed in the future. 37 | 38 | 39 | 40 | [1]: https://github.com/file-icons/icons/issues/new 41 | [2]: https://github.com/file-icons/atom 42 | [3]: ./scripts/update.mjs 43 | [4]: https://github.com/file-icons/atom/blob/master/config.cson 44 | [5]: ./scripts/missing-filenames.txt 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2019 Daniel Brooker 2 | Copyright (c) 2019-2023 John Gardner 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PATH := ./node_modules/.bin:$(PATH) 2 | 3 | all: install update lint 4 | 5 | 6 | # Install or download project dependencies 7 | install: node_modules defs 8 | 9 | defs: 10 | test -d $@ || git clone \ 11 | --branch master \ 12 | --single-branch \ 13 | --filter=tree:0 \ 14 | 'https://github.com/file-icons/atom.git' $@ 15 | 16 | node_modules: 17 | npm install --legacy-peer-deps . 18 | 19 | 20 | # Pull the latest updates from upstream 21 | update: defs 22 | cd $^ && git pull -f origin master 23 | node scripts/update.mjs $^ ./icons 24 | 25 | 26 | # Check source for errors and style violations 27 | lint: node_modules 28 | eslint . 29 | 30 | .PHONY: lint 31 | 32 | 33 | 34 | # Package a VSIX bundle for uploading to VSCode's marketplace thingie 35 | release: tmp 36 | cp scripts/content-types.xml 'tmp/[Content_Types].xml' 37 | grep -e version package.json | tr -d '", \t' | cut -d: -f2 > version 38 | sed -e "s/%%VERSION%%/`cat version`/g" scripts/extension.xml > tmp/extension.vsixmanifest 39 | mkdir tmp/extension 40 | cp -r CHANGELOG.md LICENSE.md README.md icons package.json thumbnail.png tmp/extension 41 | vsix="file-icons.file-icons-`cat version`.vsix"; \ 42 | cd tmp && zip -r "$$vsix" * 43 | mv tmp/*.vsix . 44 | rm -rf version tmp 45 | open 'https://marketplace.visualstudio.com/manage/publishers/file-icons' 46 | 47 | tmp:; mkdir $@ 48 | 49 | 50 | # Wipe generated files and build artefacts 51 | clean: 52 | rm -rf tmp version 53 | 54 | .PHONY: clean 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | File Icons for VSCode 2 | ===================== 3 | 4 | [![Latest package version][VSC-version]][VSC-link] 5 | 6 | File-specific icons in VSCode for improved visual grepping. 7 | 8 | ![Icon previews][] 9 | 10 | * Vast array of icons for most languages and frameworks 11 | * Comes in coloured and colourless flavours 12 | * Uses same icon set as [file-icons for Atom](https://github.com/file-icons/atom) 13 | 14 | 15 | Installation 16 | ------------ 17 | Open the [command palette] and run `ext install file-icons`. 18 | 19 | Alternatively, install through the command-line: 20 | 21 | code --install-extension file-icons.file-icons 22 | 23 | Once installed, activate the theme by selecting `File Icons` from the **File Icon Theme** menu. 24 | 25 | 26 | Icon fonts 27 | ---------- 28 | 29 | * [**File-Icons**](https://github.com/file-icons/icons/blob/master/charmap.md) 30 | * [**FontAwesome**](https://fontawesome.com/v4/cheatsheet/) 31 | * [**Mfizz**](https://github.com/file-icons/MFixx/blob/master/charmap.md) 32 | * [**Devicons**](https://github.com/file-icons/DevOpicons/blob/master/charmap.md) 33 | * [**Octicons**](https://octicons.github.com/) 34 | 35 | 36 | New icons 37 | --------- 38 | Please request new icons over at [`file-icons/icons`](https://github.com/file-icons/icons). 39 | 40 | 41 | 42 | [VSC-version]: https://img.shields.io/visual-studio-marketplace/v/file-icons.file-icons?color=4c1&label=Visual%20Studio%20Marketplace 43 | [VSC-link]: https://marketplace.visualstudio.com/items?itemName=file-icons.file-icons 44 | [command palette]: https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette 45 | [Icon previews]: https://raw.githubusercontent.com/file-icons/atom/6714706f268e257100e03c9eb52819cb97ad570b/preview.png 46 | -------------------------------------------------------------------------------- /icons/devopicons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/file-icons/vscode/2cbaf95b609c2f08a50a32c18970c9b887086480/icons/devopicons.woff2 -------------------------------------------------------------------------------- /icons/file-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/file-icons/vscode/2cbaf95b609c2f08a50a32c18970c9b887086480/icons/file-icons.woff2 -------------------------------------------------------------------------------- /icons/fontawesome.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/file-icons/vscode/2cbaf95b609c2f08a50a32c18970c9b887086480/icons/fontawesome.woff2 -------------------------------------------------------------------------------- /icons/mfixx.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/file-icons/vscode/2cbaf95b609c2f08a50a32c18970c9b887086480/icons/mfixx.woff2 -------------------------------------------------------------------------------- /icons/octicons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/file-icons/vscode/2cbaf95b609c2f08a50a32c18970c9b887086480/icons/octicons.woff2 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "file-icons", 3 | "displayName": "file-icons", 4 | "description": "File-specific icons in VSCode for improved visual grepping.", 5 | "version": "1.1.0", 6 | "publisher": "file-icons", 7 | "license": "MIT", 8 | "author": "Daniel Brooker ", 9 | "maintainers": [{ 10 | "name": "John Gardner", 11 | "email": "gardnerjohng@gmail.com", 12 | "url": "https://github.com/Alhadis" 13 | }], 14 | "engines": { 15 | "node": ">=15", 16 | "vscode": "^1.5.0" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/file-icons/vscode" 21 | }, 22 | "bugs": "https://github.com/file-icons/vscode/issues", 23 | "homepage": "https://github.com/file-icons/vscode", 24 | "icon": "./thumbnail.png", 25 | "galleryBanner": { 26 | "color": "#15191B", 27 | "theme": "dark" 28 | }, 29 | "categories": [ 30 | "Themes", 31 | "Other" 32 | ], 33 | "keywords": [ 34 | "icons", 35 | "filetypes", 36 | "file-icons", 37 | "icon-theme", 38 | "theme" 39 | ], 40 | "contributes": { 41 | "iconThemes": [{ 42 | "id": "file-icons", 43 | "label": "File Icons", 44 | "path": "./icons/file-icons-icon-theme.json" 45 | },{ 46 | "id": "file-icons-colourless", 47 | "label": "File Icons (Colourless)", 48 | "path": "./icons/file-icons-colourless-icon-theme.json" 49 | }] 50 | }, 51 | "scripts": { 52 | "import": "make update", 53 | "lint": "make lint" 54 | }, 55 | "eslintConfig": { 56 | "extends": "@alhadis", 57 | "overrides": [{ 58 | "files": ["scripts/*.mjs"], 59 | "rules": {"no-irregular-whitespace": 0} 60 | }], 61 | "rules": {"camelcase": 0} 62 | }, 63 | "eslintIgnore": ["defs"], 64 | "devDependencies": { 65 | "@alhadis/eslint-config": "^2.3.4", 66 | "@babel/eslint-parser": "^7.15.0", 67 | "cson": "^7.20.0", 68 | "eslint": "^7.32.0", 69 | "eslint-plugin-import": "^2.24.2", 70 | "genex": "v1.1.0", 71 | "less": "^2.7.3" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /scripts/content-types.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /scripts/extension.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | file-icons 6 | File-specific icons in VSCode for improved visual grepping. 7 | icons,filetypes,file-icons,icon-theme,theme 8 | Themes,Other 9 | Public 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | extension/LICENSE.md 24 | extension/thumbnail.png 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /scripts/missing-filenames.txt: -------------------------------------------------------------------------------- 1 | babel.config.esm.js 2 | babel.config.js 3 | babel.config.json 4 | .cfignore 5 | .eslintrc.cjs 6 | .eslintrc.js 7 | .eslintrc.json 8 | .eslintrc.mjs 9 | .eslintrc.yaml 10 | .eslintrc.yml 11 | .gitattributes 12 | .git-blame-ignore-revs 13 | .gitkeep 14 | .gitmodules 15 | .global.gitattributes 16 | .global.gitignore 17 | gruntfile.babel.cjs 18 | gruntfile.babel.coffee 19 | gruntfile.babel.js 20 | gruntfile.babel.jsx 21 | gruntfile.babel.litcoffee 22 | gruntfile.babel.mjs 23 | gruntfile.ts 24 | gruntfile.tsx 25 | gulpfile.babel.cjs 26 | gulpfile.babel.coffee 27 | gulpfile.babel.js 28 | gulpfile.babel.jsx 29 | gulpfile.babel.litcoffee 30 | gulpfile.babel.mjs 31 | gulpfile.esm.cjs 32 | gulpfile.esm.coffee 33 | gulpfile.esm.js 34 | gulpfile.esm.jsx 35 | gulpfile.esm.litcoffee 36 | gulpfile.esm.mjs 37 | .jestrc 38 | .jestrc.cjs 39 | .jestrc.js 40 | .jestrc.json 41 | .jestrc.mjs 42 | postcss.config.cjs 43 | postcss.config.js 44 | postcss.config.mjs 45 | .stylelintrc.js 46 | .stylelintrc.json 47 | .stylelintrc.yaml 48 | .stylelintrc.yml 49 | tsconfig.base.json 50 | tsconfig.build.json 51 | tsconfig.es5.json 52 | tsconfig.eslint.json 53 | tsconfig.esm.json 54 | tsconfig.node.json 55 | tsconfig.spec.json 56 | webpack.analyze.js 57 | webpack.base.conf.cjs 58 | webpack.base.conf.coffee 59 | webpack.base.conf.js 60 | webpack.base.conf.mjs 61 | webpack.base.conf.ts 62 | webpack.common.cjs 63 | webpack.common.coffee 64 | webpack.common.js 65 | webpack.common.mjs 66 | webpack.common.ts 67 | webpack.config.babel.cjs 68 | webpack.config.babel.coffee 69 | webpack.config.babel.js 70 | webpack.config.babel.mjs 71 | webpack.config.babel.ts 72 | webpack.config.base.babel.cjs 73 | webpack.config.base.babel.coffee 74 | webpack.config.base.babel.js 75 | webpack.config.base.babel.mjs 76 | webpack.config.base.babel.ts 77 | webpack.config.base.cjs 78 | webpack.config.base.coffee 79 | webpack.config.base.js 80 | webpack.config.base.mjs 81 | webpack.config.base.ts 82 | webpack.config.cjs 83 | webpack.config.coffee 84 | webpack.config.common.babel.cjs 85 | webpack.config.common.babel.coffee 86 | webpack.config.common.babel.js 87 | webpack.config.common.babel.mjs 88 | webpack.config.common.babel.ts 89 | webpack.config.common.cjs 90 | webpack.config.common.coffee 91 | webpack.config.common.js 92 | webpack.config.common.mjs 93 | webpack.config.common.ts 94 | webpack.config.dev.babel.cjs 95 | webpack.config.dev.babel.coffee 96 | webpack.config.dev.babel.js 97 | webpack.config.dev.babel.mjs 98 | webpack.config.dev.babel.ts 99 | webpack.config.dev.cjs 100 | webpack.config.dev.coffee 101 | webpack.config.development.babel.cjs 102 | webpack.config.development.babel.coffee 103 | webpack.config.development.babel.js 104 | webpack.config.development.babel.mjs 105 | webpack.config.development.babel.ts 106 | webpack.config.development.cjs 107 | webpack.config.development.coffee 108 | webpack.config.development.js 109 | webpack.config.development.mjs 110 | webpack.config.development.ts 111 | webpack.config.dev.js 112 | webpack.config.dev.mjs 113 | webpack.config.dev.ts 114 | webpack.config.js 115 | webpack.config.mjs 116 | webpack.config.prod.babel.cjs 117 | webpack.config.prod.babel.coffee 118 | webpack.config.prod.babel.js 119 | webpack.config.prod.babel.mjs 120 | webpack.config.prod.babel.ts 121 | webpack.config.prod.cjs 122 | webpack.config.prod.coffee 123 | webpack.config.prod.js 124 | webpack.config.prod.mjs 125 | webpack.config.prod.ts 126 | webpack.config.production.babel.cjs 127 | webpack.config.production.babel.coffee 128 | webpack.config.production.babel.js 129 | webpack.config.production.babel.mjs 130 | webpack.config.production.babel.ts 131 | webpack.config.production.cjs 132 | webpack.config.production.coffee 133 | webpack.config.production.js 134 | webpack.config.production.mjs 135 | webpack.config.production.ts 136 | webpack.config.staging.babel.cjs 137 | webpack.config.staging.babel.coffee 138 | webpack.config.staging.babel.js 139 | webpack.config.staging.babel.mjs 140 | webpack.config.staging.babel.ts 141 | webpack.config.staging.cjs 142 | webpack.config.staging.coffee 143 | webpack.config.staging.js 144 | webpack.config.staging.mjs 145 | webpack.config.staging.ts 146 | webpack.config.test.babel.cjs 147 | webpack.config.test.babel.coffee 148 | webpack.config.test.babel.js 149 | webpack.config.test.babel.mjs 150 | webpack.config.test.babel.ts 151 | webpack.config.test.cjs 152 | webpack.config.test.coffee 153 | webpack.config.test.js 154 | webpack.config.test.mjs 155 | webpack.config.test.ts 156 | webpack.config.ts 157 | webpack.dev.cjs 158 | webpack.dev.coffee 159 | webpack.dev.conf.cjs 160 | webpack.dev.conf.coffee 161 | webpack.dev.conf.js 162 | webpack.dev.conf.mjs 163 | webpack.dev.conf.ts 164 | webpack.dev.js 165 | webpack.dev.mjs 166 | webpack.dev.ts 167 | webpack.main.config.cjs 168 | webpack.main.config.coffee 169 | webpack.main.config.js 170 | webpack.main.config.mjs 171 | webpack.main.config.ts 172 | webpack.mix.cjs 173 | webpack.mix.coffee 174 | webpack.mix.js 175 | webpack.mix.mjs 176 | webpack.mix.ts 177 | webpack.plugins.cjs 178 | webpack.plugins.coffee 179 | webpack.plugins.js 180 | webpack.plugins.mjs 181 | webpack.plugins.ts 182 | webpack.prod.cjs 183 | webpack.prod.coffee 184 | webpack.prod.conf.cjs 185 | webpack.prod.conf.coffee 186 | webpack.prod.conf.js 187 | webpack.prod.conf.mjs 188 | webpack.prod.conf.ts 189 | webpack.prod.js 190 | webpack.prod.mjs 191 | webpack.prod.ts 192 | webpack.renderer.config.cjs 193 | webpack.renderer.config.coffee 194 | webpack.renderer.config.js 195 | webpack.renderer.config.mjs 196 | webpack.renderer.config.ts 197 | webpack.rules.cjs 198 | webpack.rules.coffee 199 | webpack.rules.js 200 | webpack.rules.mjs 201 | webpack.rules.ts 202 | webpack.test.conf.cjs 203 | webpack.test.conf.coffee 204 | webpack.test.conf.js 205 | webpack.test.conf.mjs 206 | webpack.test.conf.ts 207 | -------------------------------------------------------------------------------- /scripts/update.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import Less from "less"; 4 | import Genex from "genex"; 5 | import {copyFileSync, linkSync, lstatSync, statSync, readFileSync, unlinkSync, writeFileSync} from "fs"; 6 | import {basename, dirname, join, resolve} from "path"; 7 | import {fileURLToPath} from "url"; 8 | import {inspect, isDeepStrictEqual} from "util"; 9 | import assert from "assert"; 10 | 11 | const $0 = fileURLToPath(import.meta.url); 12 | const root = dirname($0).replace(/\/scripts$/i, ""); 13 | const isObj = obj => "object" === typeof obj && null !== obj; 14 | const isKey = key => "string" === typeof key || "symbol" === typeof key; 15 | const isEnt = obj => Array.isArray(obj) && 2 === obj.length && isKey(obj[0]); 16 | const isOwn = Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty); 17 | 18 | const source = resolve(process.argv[2] || join(root, "..", "atom")); 19 | const output = resolve(process.argv[3] || join(root, "icons")); 20 | const missing = resolve(process.argv[4] || join(root, "scripts", "missing-filenames.txt")); 21 | const iconDB = join(source, "lib", "icons", ".icondb.js"); 22 | const icons = join(source, "styles", "icons.less"); 23 | const fonts = join(source, "styles", "fonts.less"); 24 | const colours = join(source, "styles", "colours.less"); 25 | assertDir(source, output); 26 | assertFile(fonts, colours); 27 | 28 | export default Promise.all([ 29 | loadDB(iconDB, missing), 30 | loadIcons(icons), 31 | loadFonts(fonts), 32 | loadColours(colours), 33 | ]).then(async ([iconDB, icons, fonts, colours]) => { 34 | const count = updateFonts(fonts, output); 35 | console.info(count ? `${count} font(s) updated` : "Fonts already up-to-date"); 36 | 37 | fonts.push({ 38 | id: "octicons regular", 39 | src: [{path: join(output, "octicons.woff2"), format: "woff2"}], 40 | weight: "normal", 41 | style: "normal", 42 | size: "100%", 43 | }); 44 | for(const font of fonts) 45 | font.id = icons.fonts[font.id].id || font.id; 46 | 47 | // Icons provided by Octicons and/or Atom's core stylesheets 48 | const defaultIcons = { 49 | _file: {fontId: "octicons", fontCharacter: "\\f011", fontSize: "114%"}, 50 | _folder: {fontId: "octicons", fontCharacter: "\\f016", fontSize: "114%"}, 51 | _repo: {fontId: "octicons", fontCharacter: "\\f001", fontSize: "114%"}, 52 | }; 53 | const unlistedIcons = { 54 | "circuit-board": {fontId: "octicons", fontCharacter: "\\f0d6", fontSize: "114%"}, 55 | mail: {fontId: "octicons", fontCharacter: "\\f03b", fontSize: "114%"}, 56 | paintcan: {fontId: "octicons", fontCharacter: "\\f0d1", fontSize: "114%"}, 57 | pdf: {fontId: "octicons", fontCharacter: "\\f014", fontSize: "114%"}, 58 | star: {fontId: "octicons", fontCharacter: "\\f02a", fontSize: "114%"}, 59 | text: {fontId: "octicons", fontCharacter: "\\f011", fontSize: "114%"}, 60 | }; 61 | ({icons} = icons); 62 | icons = {...unlistedIcons, ...icons}; 63 | 64 | // Truncate font paths to output directory 65 | for(const font of fonts) 66 | font.src.forEach(src => src.path = "./" + basename(src.path)); 67 | 68 | // Alphabetise font-families, but keep File-Icons at the front of array 69 | fonts = fonts.sort((a, b) => a.id.toLowerCase().localeCompare(b.id.toLowerCase())); 70 | const index = fonts.findIndex(font => "fi" === font.id); 71 | if(-1 !== index) 72 | fonts.unshift(...fonts.splice(index, 1)); 73 | else throw new ReferenceError("Failed to locate font with ID 'fi'"); 74 | 75 | const colouredTheme = buildTheme({icons, fonts, colours, iconDB}); 76 | colouredTheme.iconDefinitions = {...defaultIcons, ...colouredTheme.iconDefinitions}; 77 | saveJSON(colouredTheme, join(output, "file-icons-icon-theme.json")); 78 | 79 | const uncolouredTheme = desaturate(colouredTheme, colours); 80 | saveJSON(uncolouredTheme, join(output, "file-icons-colourless-icon-theme.json")); 81 | 82 | }).catch(error => { 83 | console.error(error); 84 | process.exit(1); 85 | }); 86 | 87 | 88 | // Section: Icon Database {{{1 89 | 90 | /** 91 | * Generate an icon-theme in the format required by VS Code. 92 | * @param {Object} source 93 | * @param {Array} source.iconDB 94 | * @param {Object} source.icons 95 | * @param {Object} source.fonts 96 | * @param {Object} source.colours 97 | * @param {String} [source.prefix="_"] 98 | * @return {IconTheme} 99 | * @internal 100 | */ 101 | function buildTheme({iconDB, icons, fonts, colours, prefix = "_"} = {}){ 102 | const theme = { 103 | __proto__: null, 104 | fonts, 105 | file: prefix + "file", 106 | folder: prefix + "folder", 107 | rootFolder: prefix + "repo", 108 | iconDefinitions: {}, 109 | fileExtensions: {}, 110 | fileNames: {}, 111 | folderNames: {}, 112 | languageIds: {}, 113 | light: {}, 114 | }; 115 | 116 | const getColourValue = colour => { 117 | if(!colour) return "#000000"; 118 | const index = colour.indexOf("-"); 119 | const brightness = colour.slice(0, index); 120 | const name = colour.slice(index + 1); 121 | const value = colours[name]?.[brightness]; 122 | if(null == value) 123 | throw new ReferenceError(`No such colour ${colour}`); 124 | return value; 125 | }; 126 | 127 | const [directoryIcons, fileIcons] = iconDB; 128 | for(const iconList of [directoryIcons, fileIcons]) 129 | for(let [ 130 | icon, 131 | colours, 132 | match,, 133 | matchPath,, 134 | scope, 135 | language, 136 | signature, 137 | ] of iconList[0]){ 138 | if(matchPath || !(match instanceof RegExp)) continue; 139 | 140 | // HACK 141 | if(/^\.atom-socket-.+\.\d$/.source === match.source) 142 | continue; 143 | 144 | // HACK: Conflicting file-extension: `.vh` (V, SystemVerilog) 145 | // Searching GitHub yields mostly Verilog results, so exclude V. 146 | if("v-icon" === icon && /\.vh$/i.source === match.source) 147 | continue; 148 | 149 | // Normalise icon ID: "pdf-icon" => "pdf", "icon-file-text" => "text" 150 | if(icon.startsWith("icon-file-")) icon = icon.slice(10); 151 | else if(icon.startsWith("icon-")) icon = icon.slice(5); 152 | else if(icon.endsWith("-icon")) icon = icon.slice(0, -5); 153 | if(icon.startsWith("_")) icon = icon.slice(1); 154 | validateIcon(icon, icons, fonts); 155 | 156 | // HACK: Manually add scopes to commonly-used generic formats like XML and YAML 157 | signature = String(signature); 158 | if("yaml" === icon && String(match) === String(/\.ya?ml$/i)){ 159 | language ||= /^YA?ML$/i; 160 | scope ||= /\.ya?ml$/i; 161 | } 162 | else if(signature === String(/^<\?xml /)){ 163 | language ||= /^XML$/i; 164 | scope ||= /^text\.xml$/i; 165 | } 166 | else if(signature === String(/^\xEF\xBB\xBF|^\xFF\xFE/)) 167 | scope ||= /^(text\.plain|plain[-_ ]?text|fundamental)$/i; 168 | 169 | // Normalise dark- and light-motif variants 170 | colours = Array.isArray(colours) ? [...colours].slice(0, 2) : [colours]; 171 | colours[0] === colours[1] && colours.pop(); 172 | 173 | const add = (listName, key) => { 174 | key = key.toLowerCase(); 175 | let list = theme[listName]; 176 | for(const colour of colours){ 177 | const uid = prefix + icon + (colour ? "_" + colour : ""); 178 | list[key] = uid; 179 | if(null == theme.iconDefinitions[uid]){ 180 | const def = {...icons[icon], fontColor: getColourValue(colour)}; 181 | if("#000000" === def.fontColor) 182 | delete def.fontColor; 183 | theme.iconDefinitions[uid] = def; 184 | } 185 | list = theme.light[listName] ??= {}; 186 | } 187 | }; 188 | try{ 189 | match = new RegExp( 190 | match.source 191 | .replace(/^\^stdlib\(\?:-\.\+\)\?/, "^stdlib") 192 | .replace(/(? [...set]).flat() 211 | .map(scope => scope.toLowerCase() 212 | .replace(/^\.+|\.+$/g, "") 213 | .replace(/^(?:source|text)\.+/g, "")); 214 | if(language){ 215 | language = parseRegExp(match = new RegExp( 216 | language.source, 217 | language.flags, 218 | )); 219 | langIDs.push(...Object.values(language).map(set => { 220 | set = [...set]; 221 | const downcase = set.map(lang => lang.toLowerCase()); 222 | const upcase = set.map(lang => lang.toUpperCase()); 223 | return set.concat(downcase, upcase); 224 | }).flat()); 225 | } 226 | for(const langID of new Set(langIDs)) 227 | theme.languageIds[langID] = iconID; 228 | } 229 | } 230 | catch(error){ 231 | if(error instanceof RangeError 232 | || error.message.includes("Unsupported lookbehind")){ 233 | console.warn("Skipping:", match); 234 | continue; 235 | } 236 | console.warn("Stopped at", match); 237 | throw error; 238 | } 239 | } 240 | 241 | // Make diffs less chaotic by enforcing alphanumeric order 242 | for(const obj of [theme, theme.light].filter(Boolean)) 243 | for(const key of "iconDefinitions fileExtensions fileNames folderNames folderNamesExpanded languageIds".split(" ")){ 244 | const value = obj[key]; 245 | if(isOwn(obj, key) && isObj(value)) 246 | obj[key] = sortProps(value); 247 | } 248 | return theme; 249 | } 250 | 251 | 252 | /** 253 | * Generate a colourless version of a coloured icon-theme. 254 | * @param {IconTheme} input 255 | * @param {Object} colours 256 | * @return {IconTheme} 257 | * @internal 258 | */ 259 | function desaturate(input, colours){ 260 | 261 | // Construct a regex for stripping the trailing colour name/shade 262 | const names = new Set(); 263 | for(const [colour, value] of Object.entries(colours)) 264 | for(const [luminosity] of Object.entries(value)) 265 | names.add([luminosity, colour].join("-")); 266 | const regex = new RegExp(`[-_]?(?:${[...names].join("|")})$`, "i"); 267 | 268 | // Start culling 269 | const result = JSON.parse(JSON.stringify(input)); 270 | const oldDefs = input.iconDefinitions; 271 | const newDefs = result.iconDefinitions = {__proto__: null}; 272 | 273 | for(const [oldID, props] of Object.entries(oldDefs)){ 274 | const newID = oldID.replace(regex, ""); 275 | delete props.fontColor; 276 | 277 | // Sanity check 278 | if(newID in newDefs) 279 | assert.deepStrictEqual(props, newDefs[newID]); 280 | else newDefs[newID] = props; 281 | } 282 | const remap = from => { 283 | const to = {__proto__: null}; 284 | for(let [key, iconID] of Object.entries(from)){ 285 | iconID = iconID.replace(regex, ""); 286 | assert(iconID in newDefs); 287 | to[key] = iconID; 288 | } 289 | return to; 290 | }; 291 | const isEmpty = obj => isObj(obj) && !Object.keys(obj).length; 292 | const cull = (...listNames) => { 293 | for(const listName of listNames){ 294 | if(!isObj(result?.light?.[listName]) 295 | || !isObj(result[listName])) continue; 296 | const darkIcons = result[listName]; 297 | const lightIcons = result.light[listName]; 298 | for(const [key, value] of Object.entries(lightIcons)){ 299 | if(key in darkIcons && isDeepStrictEqual(darkIcons[key], value)) 300 | delete lightIcons[key]; 301 | } 302 | if(isEmpty(result.light[listName])) 303 | delete result.light[listName]; 304 | } 305 | if(isEmpty(result.light)) 306 | delete result.light; 307 | }; 308 | for(const context of [result, result.light]){ 309 | if(!context) continue; 310 | context.fileExtensions = remap(context.fileExtensions); 311 | context.fileNames = remap(context.fileNames); 312 | context.folderNames = remap(context.folderNames); 313 | } 314 | cull(..."fileExtensions fileNames folderNames".split(" ")); 315 | return result; 316 | } 317 | 318 | 319 | /** 320 | * Load a compiled icon database. 321 | * 322 | * @example await loadDB("/path/to/.icondb.js"); 323 | * @param {String} from - Path to an `.icondb.js` file. 324 | * @param {String} [filenameList=null] 325 | * Path to a line-delimited list of filenames to include in a matching pattern. 326 | * This is necessary because some compiled regexes are too complex to be easily 327 | * enumerated by {@link parseRegExp}. 328 | * @return {Array} 329 | * @internal 330 | */ 331 | async function loadDB(from, filenameList = null){ 332 | const {default: db} = await import(resolve(from)); 333 | if(!filenameList) return db; 334 | 335 | // Add missing filenames to patterns too complex for `parseRegExp` to handle 336 | const filenames = readFileSync(resolve(filenameList), "utf8") 337 | .split(/\r?\n|\r|\x85|\u2028|\u2029/) 338 | .map((line, index) => (line = line.trim()) ? [index + 1, line] : null) 339 | .filter(Boolean); 340 | 341 | const unmatched = new Set(filenames); 342 | const matched = new Map(); 343 | 344 | for(const filename of filenames){ 345 | for(const icon of db[1][0]){ 346 | if(!icon[4] && icon[2].test(filename[1])){ 347 | unmatched.delete(filename); 348 | matched.has(icon) || matched.set(icon, new Set()); 349 | matched.get(icon).add(filename); 350 | break; 351 | } 352 | } 353 | } 354 | 355 | // Report any filenames that failed to match any icon whatsoever 356 | const {size} = unmatched; 357 | if(size){ 358 | const [prefix, path, dimOn, dimOff] = process.stderr.isTTY 359 | ? ["\x1B[33mWarning:\x1B[39m", `\x1B[4m${filenameList}\x1B[24m`, "\x1B[2m", "\x1B[22m"] 360 | : ["Warning:", filenameList]; 361 | console.warn(`${prefix} Unmatched ${1 === size ? "entry" : "entries"} in ${path}:`); 362 | for(const [lineNumber, filename] of unmatched) 363 | console.warn(`\t${dimOn}line ${lineNumber}:${dimOff} ${filename}`); 364 | } 365 | 366 | // Patch the existing database in-place 367 | for(const [icon, filenames] of matched){ 368 | const regex = icon[2]; 369 | const additions = [""]; 370 | for(const [, filename] of filenames) 371 | additions.push(`^${filename.replace(/[/\\^$*+?{}[\]().|]/g, "\\$&")}$`); 372 | regex.compile(regex.source + additions.join("|"), regex.flags); 373 | } 374 | return db; 375 | } 376 | 377 | 378 | /** 379 | * Extract a list of unique filename/extension matches from a regex. 380 | * @param {RegExp} input 381 | * @return {MatchesByType} 382 | * @internal 383 | */ 384 | function parseRegExp(input){ 385 | /** 386 | * @typedef {Object} MatchesByType 387 | * @property {Set} substrings - Substrings appearing anywhere in a filename 388 | * @property {Set} prefixes - Filename prefixes; i.e., /^foo…/ 389 | * @property {Set} suffixes - File extensions; i.e., /…foo$/ 390 | * @property {Set} full - Full-string matches; i.e., /^foo$/ 391 | */ 392 | const output = { 393 | __proto__: null, 394 | substrings: new Set(), 395 | prefixes: new Set(), 396 | suffixes: new Set(), 397 | full: new Set(), 398 | }; 399 | 400 | if("genex" !== input?.constructor?.name.toLowerCase()) 401 | input = Genex(input); 402 | 403 | const anchors = new Set(); 404 | const chars = new Set(); 405 | const killList = new Set(); 406 | const lists = new Set(); 407 | const walk = (obj, refs = new WeakSet()) => { 408 | if("object" !== typeof obj || null === obj || refs.has(obj)) 409 | return; 410 | refs.add(obj); 411 | if(Array.isArray(obj)){ 412 | lists.add(obj); 413 | for(const item of obj){ 414 | try{ walk(item, refs); } 415 | catch(e){ killList.add(item); } 416 | } 417 | } 418 | else{ 419 | if(Infinity === obj.max){ 420 | if(!obj.min) 421 | throw new RangeError("Bad range"); 422 | obj.max = ~~obj.min; 423 | } 424 | else switch(obj.type){ 425 | case 2: "^$".includes(obj.value) && anchors.add(obj); break; 426 | case 7: chars.add(obj.value); break; 427 | default: { 428 | const {options: opts, stack} = obj; 429 | if(Array.isArray(opts)){ 430 | lists.add(opts); 431 | for(const opt of opts) 432 | try{ walk(opt, refs); } 433 | catch(e){ killList.add(opt); } 434 | } 435 | else if(Array.isArray(stack)){ 436 | lists.add(stack); 437 | walk(stack, refs); 438 | } 439 | else walk(obj.value, refs); 440 | } 441 | } 442 | } 443 | }; 444 | 445 | walk(input.tokens); 446 | for(const token of killList){ 447 | for(const list of lists){ 448 | while(list.includes(token)) 449 | list.splice(list.indexOf(token), 1); 450 | } 451 | } 452 | 453 | const used = String.fromCodePoint(...chars); 454 | const unused = Array.from(getUnusedChar(used + "\\[]{}()?+*", 2)) 455 | .map(char => char.codePointAt(0)); 456 | 457 | for(const anchor of anchors){ 458 | anchor.type = 7; 459 | anchor.value = "^" === anchor.value 460 | ? unused[0] 461 | : unused[1]; 462 | } 463 | 464 | const cases = new Set(); 465 | input.generate(result => { 466 | if(cases.size > 1000) throw new RangeError("Too many cases to generate"); 467 | cases.add(result); 468 | }); 469 | 470 | for(let str of cases){ 471 | let anchoredToStart = false; 472 | let anchoredToEnd = false; 473 | str = [...str]; 474 | str = str.map((char, index) => { 475 | const code = char.codePointAt(0); 476 | if(code === unused[0]){ 477 | assert.strictEqual(index, 0); 478 | anchoredToStart = true; 479 | } 480 | else if(code === unused[1]){ 481 | assert.strictEqual(index, str.length - 1); 482 | anchoredToEnd = true; 483 | } 484 | else return char; 485 | return ""; 486 | }).join(""); 487 | const type = 488 | anchoredToStart && anchoredToEnd ? "full" : 489 | anchoredToStart ? "prefixes" : 490 | anchoredToEnd ? "suffixes" : 491 | "substrings"; 492 | output[type].add(str); 493 | } 494 | return output; 495 | } 496 | 497 | 498 | /** 499 | * Write an object to disk as a JSON file, preserving timestamps if identical. 500 | * @param {Object} input 501 | * @param {String} path 502 | * @return {void} 503 | * @internal 504 | */ 505 | function saveJSON(input, path){ 506 | path = resolve(path); 507 | let existingFile = null; 508 | try{ existingFile = readFileSync(path, "utf8"); } 509 | catch(e){} 510 | input = JSON.stringify(input, null, "\t").trim() + "\n"; 511 | input === existingFile 512 | ? console.info(`Theme already up-to-date: ${basename(path)}`) 513 | : writeFileSync(path, input, "utf8"); 514 | } 515 | 516 | 517 | /** 518 | * Ensure that a complete icon-definition with the given ID exists. 519 | * @param {String} name 520 | * @param {Object} icons 521 | * @param {Object} fonts 522 | * @return {void} 523 | * @internal 524 | */ 525 | function validateIcon(name, icons, fonts){ 526 | if(name in icons){ 527 | for(const key of ["fontCharacter", "fontId"]){ 528 | const value = icons[name][key]; 529 | if("string" !== typeof value || !value) 530 | throw new TypeError(`Missing "${key}" field in icon "${name}"`); 531 | } 532 | const {fontId} = icons[name]; 533 | if(!fonts.some(font => fontId === font.id)) 534 | throw new ReferenceError(`Icon "${name}" references undefined font "${fontId}"`); 535 | } 536 | else throw new ReferenceError(`Undefined icon: ${name}`); 537 | } 538 | 539 | 540 | // Section: Icons {{{1 541 | 542 | /** 543 | * Load icon definitions from the given stylesheet. 544 | * 545 | * @example loadIcons("/path/to/styles/icons.less"); 546 | * @param {String} from - Path to stylesheet 547 | * @return {Object} 548 | * @private 549 | */ 550 | async function loadIcons(from){ 551 | from = resolve(from); 552 | const icons = {__proto__: null}; 553 | const fonts = {__proto__: null}; 554 | const rules = parseRules(await loadStyleSheet(from)); 555 | for(const selector in rules){ 556 | const rule = rules[selector]; 557 | const font = (rule["font-family"] || "").toLowerCase(); 558 | if(!font) continue; 559 | if(/^\.((?:(?!-|\d)[-a-z0-9]+|_\d+[-a-z0-9]*)(? "string" === typeof x || Array.isArray(x) && "local" !== x[0]) ?? src; 684 | if("string" === typeof src) 685 | path = src; 686 | else for(const item of src){ 687 | if(null == path && "string" === typeof item) 688 | path = item; 689 | if(null == format && Array.isArray(item) && "format" === item[0]){ 690 | format = item[1]; 691 | if(Array.isArray(format)) 692 | format = format[0]; 693 | } 694 | } 695 | } 696 | else path = parse(src); 697 | format ||= /\.\w+$/.test(path) ? RegExp.lastMatch.slice(1) : "woff2"; 698 | if(path.toLowerCase().startsWith("atom://file-icons/")){ 699 | const head = resolve(dirname(from), ".."); 700 | const tail = path.slice(18); 701 | path = resolve(join(head, tail)); 702 | } 703 | fonts.push({ 704 | id: font["font-family"].toLowerCase(), 705 | src: [{path, format}], 706 | weight: font["font-weight"] || "normal", 707 | style: font["font-style"] || "normal", 708 | size: "100%", 709 | }); 710 | } 711 | return fonts; 712 | } 713 | 714 | /** 715 | * Update the VSCode package's icon-fonts with their upstream versions, if needed. 716 | * 717 | * @example updateFonts([octicons, ...fonts] "./vscode/icons/"); 718 | * @param {IconThemeFont[]} fontDefs - Icon-font definitions 719 | * @param {String} targetDir - Path of destination directory 720 | * @param {Object} [options={}] - Settings governing what to update 721 | * @param {Boolean} [options.force] - Ignore timestamps when copying 722 | * @param {Boolean} [options.noLink] - Copy files instead of linking 723 | * @return {Number} The number of fonts that were updated 724 | * @private 725 | */ 726 | function updateFonts(fontDefs, targetDir, {force, noLink} = {}){ 727 | let updates = 0; 728 | for(const font of fontDefs){ 729 | const srcPath = font.src[0].path; 730 | const srcStat = stat(srcPath); 731 | const dstPath = join(targetDir, basename(srcPath)); 732 | if(!exists(dstPath)){ 733 | const [str, fn] = srcStat.dev !== stat(targetDir).dev 734 | ? ["Copying", copyFileSync] 735 | : ["Linking", linkSync]; 736 | console.info(`${str}: ${srcPath} -> ${dstPath}`); 737 | fn(srcPath, dstPath); 738 | linkSync(srcPath, dstPath); 739 | ++updates; 740 | } 741 | else{ 742 | const dstStat = stat(dstPath); 743 | if(noLink || srcStat.dev !== dstStat.dev){ 744 | if(!force && dstStat.mtimeNs >= srcStat.mtimeNs){ 745 | console.info(`Already up-to-date: ${dstPath}`); 746 | continue; 747 | } 748 | console.info(`Copying: ${srcPath} -> ${dstPath}`); 749 | unlinkSync(dstPath); 750 | copyFileSync(srcPath, dstPath); 751 | ++updates; 752 | } 753 | else if(srcStat.ino !== dstStat.ino){ 754 | console.info(`Linking: ${srcPath} -> ${dstPath}`); 755 | unlinkSync(dstPath); 756 | linkSync(srcPath, dstPath); 757 | ++updates; 758 | } 759 | } 760 | } 761 | return updates; 762 | } 763 | 764 | /** 765 | * @typedef {Object} IconThemeFont 766 | * @property {String} id 767 | * @property {String} [weight="normal"] 768 | * @property {String} [style="normal"] 769 | * @property {String} [size="100%"] 770 | * @property {{path: String, format: String}[]} src 771 | */ 772 | 773 | 774 | // Section: Utilities {{{1 775 | 776 | /** 777 | * Throw an exception if one of the given paths isn't a directory. 778 | * @param {...String} paths 779 | * @return {void} 780 | */ 781 | function assertDir(...paths){ 782 | for(const path of paths){ 783 | const stats = stat(path, true); 784 | if(!stats) 785 | throw new Error("No such directory: " + path); 786 | if(!stats.isDirectory()) 787 | throw new Error("Not a directory: " + path); 788 | } 789 | } 790 | 791 | /** 792 | * Throw an exception if one of the given paths isn't a regular file. 793 | * @param {...String} paths 794 | * @return {void} 795 | */ 796 | function assertFile(...paths){ 797 | for(const path of paths){ 798 | const stats = stat(path, true); 799 | if(!stats) 800 | throw new Error("No such file: " + path); 801 | if(!stats.isFile()) 802 | throw new Error("Not a regular file: " + path); 803 | } 804 | } 805 | 806 | /** 807 | * Return true if a file exists on disk, even as a broken symbolic link. 808 | * @param {String} 809 | * @return {Boolean} 810 | * @see {@link fs.existsSync} 811 | */ 812 | function exists(path){ 813 | try{ return !!lstatSync(path); } 814 | catch(e){ return false; } 815 | } 816 | 817 | /** 818 | * Return one or more characters not contained in a string. 819 | * 820 | * @version Alhadis/Utils@c8ee57d 821 | * @example getUnusedChar("\x00\x02") == "\x01"; 822 | * @example getUnusedChar("\x00\x02", 2) == "\x01\x03"; 823 | * @param {String} input 824 | * @param {Number} [count=1] 825 | * @return {String} 826 | */ 827 | function getUnusedChar(input, count = 1){ 828 | let chars = ""; 829 | let next = "\x00"; 830 | let code = 0; 831 | for(let i = 0; i < count; ++i){ 832 | while(-1 !== input.indexOf(next) || -1 !== chars.indexOf(next)) 833 | next = String.fromCodePoint(++code); 834 | chars += next; 835 | } 836 | return chars; 837 | } 838 | 839 | /** 840 | * Synchronously lstat(2) a file without throwing an exception. 841 | * 842 | * @example Testing a symlink to `/dev/fd/0` 843 | * stat("/dev/stdin").isSymbolicLink() === true; 844 | * stat("/dev/stdin", true).isCharacterDevice() === true; 845 | * 846 | * @param {String} path - Pathname of the file being examined 847 | * @param {Boolean} [followLinks=false] - Use stat(2) instead 848 | * @return {?fs.BigIntStats} 849 | * @see {@link fs.lstatSync} 850 | */ 851 | function stat(path, followSymlinks = false){ 852 | try{ return (followSymlinks ? statSync : lstatSync)(path, {bigint: true}); } 853 | catch(e){ return null; } 854 | } 855 | 856 | /** 857 | * Render and reparse a Less stylesheet. 858 | * @param {String} path 859 | * @return {Less~Ruleset} 860 | * @private 861 | */ 862 | async function loadStyleSheet(path){ 863 | const file = readFileSync(path, "utf8"); 864 | const {css} = await Less.render(file, {filename: path}); 865 | path = path.replace(/\.less$/i, ".css"); 866 | return Less.parse(css, {filename: path}); 867 | } 868 | 869 | /** 870 | * Try to simplify Less's absurdly over-complicated parser output. 871 | * @param {String|Object|Array} node 872 | * @param {WeakSet} [refs] 873 | * @return {String|Object|Array} 874 | * @private 875 | */ 876 | function parse(node, refs = new WeakSet()){ 877 | if(null == node) return node; 878 | if(!isObj(node)) return String(node ?? ""); 879 | if(refs.has(node)) return; 880 | refs.add(node); 881 | if(Array.isArray(node)){ 882 | node = node.map(x => parse(x, refs)).filter(x => null != x); 883 | return node.length < 2 884 | ? parse(node[0], refs) ?? "" 885 | : node; 886 | } 887 | let {name, value, args, rules} = node; 888 | if("Comment" === node.type) return; 889 | if(name && value) return [parse(name, refs), parse(value, refs)]; 890 | if(!name && value) return parse(value, refs); 891 | if(!value && (value = rules || args)){ 892 | value = value.map(x => parse(x, refs)); 893 | if(value.every(item => isEnt(item))) 894 | value = Object.fromEntries(value); 895 | return name ? [parse(name, refs), value] : value; 896 | } 897 | console.error("Bad input:", node); 898 | throw new TypeError(`Unexpected input: ${inspect(node)}`); 899 | } 900 | 901 | /** 902 | * Extract a list of rulesets from a parsed stylesheet. 903 | * 904 | * @see {@link loadStyleSheet} 905 | * @example Parsing a stylesheet with 3 class selectors 906 | * const blue = await loadStyleSheet("colours/blue.less"); 907 | * parseRules(blue) == { 908 | * ".light-blue:before": {color: "#9dc0ce"}, 909 | * ".medium-blue:before": {color: "#6a9fb5"}, 910 | * ".dark-blue:before": {color: "#46788d"}, 911 | * }; 912 | * @param {Less~Ruleset} ruleset 913 | * @return {Object} A null-prototype object containing objects 914 | * keyed by selector, enumerated with parsed CSS properties. 915 | */ 916 | function parseRules(ruleset){ 917 | const rules = {__proto__: null}; 918 | for(const rule of ruleset.rules){ 919 | if(!rule || "Comment" === rule.type) 920 | continue; 921 | const selectors = rule.selectors.map(sel => 922 | sel.elements.map(el => el.combinator.value + el.value).join("").trim()); 923 | for(const name of selectors) 924 | rules[name] = {...rules[name], ...parse(rule)}; 925 | } 926 | return rules; 927 | } 928 | 929 | /** 930 | * Return a new object with the properties of another sorted alphanumerically. 931 | * @param {Object} input 932 | * @return {Object} 933 | * @internal 934 | */ 935 | function sortProps(input){ 936 | const alnum = /[^A-Za-z0-9]/g; 937 | input = Object.entries(input).sort(([a], [b]) => 938 | a.replace(alnum, "").localeCompare(b.replace(alnum, ""))); 939 | return Object.fromEntries(input); 940 | } 941 | 942 | // vim:fdm=marker:noet 943 | -------------------------------------------------------------------------------- /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/file-icons/vscode/2cbaf95b609c2f08a50a32c18970c9b887086480/thumbnail.png --------------------------------------------------------------------------------