Skip to content

File Icons API v1012+

Use acode.require("fileIcons") to register an icon pack. Acode owns matching and rendering; a pack supplies assets and association maps. Users select the active pack in Settings → App settings → Icon pack.

INFO

Available from versionCode 1012 (the next Acode release). Set "minVersionCode": 1012 in plugin.json when your plugin depends on it.

Import

Call acode.require("fileIcons") synchronously in your plugin main script and keep that API for later callbacks. The loader binds it to your plugin before executing the script.

js
const fileIcons = acode.require("fileIcons");

Feature detection on older Acode builds:

js
const fileIcons = acode.require("fileIcons");
if (!fileIcons?.register) {
  // Running on an older Acode build — skip pack registration
}

Do not call acode.require("fileIcons") for the first time in a timer, event handler, or after an await: there is no executing main-script context then.

You can also take the same bound API from the third initialization argument:

js
let registration;

acode.setPluginInit(plugin.id, async (baseUrl, page, { fileIcons }) => {
  // This API stays bound to this plugin across awaits and callbacks.
  registration = fileIcons.register({
    id: plugin.id,
    name: "My Icons",
    icons: `${baseUrl}icons/`,
    fileExtensions: { js: "javascript" },
  });
});

acode.setPluginUnmount(plugin.id, () => registration?.dispose());

Both forms expose only register, icon, and onChange. Acquisition outside a loader-bound context throws with instructions to use these entry points. Pack ownership never comes from the pack's id; a plugin can still provide several separately named packs.

Quick start

js
const fileIcons = acode.require("fileIcons");
let registration;

acode.setPluginInit(plugin.id, (baseUrl) => {
  registration = fileIcons.register({
    id: plugin.id,
    name: "My Icons",
    icons: `${baseUrl}icons/`,
    fileNames: { "package.json": "nodejs", ".gitignore": "git" },
    fileExtensions: {
      js: "javascript",
      ts: "typescript",
      "d.ts": "typescript-def",
    },
    folderNames: { src: "folder-src" },
    folderNamesExpanded: { src: "folder-src-open" },
    folder: "folder",
    folderExpanded: "folder-open",
  });
});

acode.setPluginUnmount(plugin.id, () => registration?.dispose());

Ship the referenced SVGs inside your plugin's icons/ directory. With the directory shorthand, javascript means icons/javascript.svg. Only explicitly referenced assets are used: Acode does not guess -open filenames. Omit folderNamesExpanded and folderExpanded to reuse closed icons.

Acode binds the API to the plugin whose main script is executing and supplies its owner ID automatically. The disposable also lets you remove a pack earlier. A plugin may register multiple packs with distinct IDs, preferably prefixed by its plugin ID.

Registration does not select a pack. When the user's previously selected pack registers after startup, Acode restores it automatically. Until then the Builtin pack remains usable.

Pack format

There is one format, with association maps and defaults at the top level.

FieldTypeMeaning
idstringRequired stable pack ID. Start with a letter; use letters, digits, ., _, or -. builtin is reserved.
pluginIdstringOptional compatibility field. If supplied, it must match the loading plugin; a mismatch throws. Acode supplies ownership internally.
namestringDisplay name; defaults to id.
schemaVersionnumberOptional; defaults to 1. Unsupported versions are rejected.
iconsstring | objectAn absolute SVG directory URL, or a map of icon IDs to definitions.
fileNamesRecord<string, string>Exact basenames mapped to icon IDs, such as package.json.
fileExtensionsRecord<string, string>Extensions without leading dots, including compounds such as test.ts.
languageIdsRecord<string, string>Acode CodeMirror mode names mapped to icon IDs.
folderNamesRecord<string, string>Folder basenames mapped to closed icon IDs.
folderNamesExpandedRecord<string, string>Folder basenames mapped to expanded icon IDs.
filestringDefault file icon ID. Omitted: built-in file fallback.
folderstringDefault closed folder icon ID. Omitted: built-in folder fallback.
folderExpandedstringExpanded folder icon ID. Omitted: reuse folder.
rootFolderstringWorkspace-root icon ID. Omitted: reuse folder.
rootFolderExpandedstringExpanded root icon ID. Falls back through rootFolder, folderExpanded, then folder.

Unknown fields, ambiguous definitions, conflicting normalized associations, and unknown icon references throw descriptive errors during registration. A rejected replacement leaves the previous pack active. Same-icon duplicate associations are allowed, which makes combining maps convenient.

There are no label, nested associations/defaults, iconDefinitions, iconPath, string definitions, or automatic expanded-asset aliases. Light/dark asset variants are not part of version 1. Use monochrome icons for automatic color adaptation. This format is inspired by editor icon packs; it is not a drop-in VS Code or Zed schema.

Icon definitions

An explicit definition has exactly one of:

  • src: an absolute image URL. Use plugin-local SVG, PNG, or WebP assets. Optional monochrome: true uses the image as a mask with the current text color.
  • className: existing CSS classes, for integration with an icon font or plugin stylesheet. The plugin owns that stylesheet and its cleanup.
js
icons: {
  javascript: { src: `${baseUrl}icons/javascript.svg` },
  folder: { src: `${baseUrl}icons/folder.svg`, monochrome: true },
  text: { className: "file file_type_default" },
}

Directory shorthand IDs use letters, digits, underscores, and hyphens. Explicit definition maps can use other non-empty IDs; generated CSS identifiers remain distinct.

Acode cannot detect missing glyphs in a custom CSS class. CSS class definitions are the plugin's responsibility.

Matching and fallback

Files match in this order:

  1. Filename, trying exact case before a case-insensitive lookup.
  2. Longest matching extension: button.test.ts tries test.ts before ts.
  3. Language ID, supplied by the caller or inferred from Acode's existing filename-to-mode registry.
  4. Pack default file, then Acode's built-in file icon.

Extension and language matching is case-insensitive. A standalone dotfile such as .env does not have extension env; match it through fileNames. Paths and patterns are not supported in associations. Case-equivalent filename keys may coexist only when they reference the same icon.

Folders first use a named expanded association when expanded, otherwise a named closed association. A named association takes precedence over workspace-root defaults. Unmatched roots use root defaults; other folders use folder defaults. Folder-name matching is case-insensitive.

While an image loads, or if it fails, resolution returns a usable fallback. An unavailable expanded asset falls back to the closed folder. Other unavailable assets fall back to the pack default, then the built-in icon. Failed URLs are reported once in the console per activation; reselecting or replacing the pack allows retrying them.

Methods

The examples below assume const fileIcons = acode.require("fileIcons"). These three methods and the returned cleanup functions are the complete plugin surface. An icon-pack-only plugin needs just register and its disposable. icon and onChange support plugins that render their own file lists.

MethodContract
register(pack)Validate and register a complete pack. Same ID and owner replaces the previous pack atomically. Returns { dispose() }.
icon(resource)CSS class string for the resource's current icon or fallback.
onChange(listener)Subscribe to active/preferred pack changes, active pack replacement/removal, and asset readiness. Returns an unsubscribe function. Listener receives { activeId, preferredId }.

Only register, icon, and onChange are exported to plugins. Pack selection, catalog queries, detailed/batch resolution, settings binding, and DOM refresh are internal. There is no public use, update, unregister, override system, or method alias. Users choose packs in settings. Register a complete replacement to update; dispose the registration to remove it.

Disposal is idempotent. Disposing an older registration cannot remove its replacement. Pack IDs owned by another plugin cannot be replaced. The loader binds lifecycle ownership independently of the pack definition. A retained registration API cannot register packs after its plugin unloads, including after that plugin reloads. This is lifecycle isolation, not a security sandbox between JavaScript plugins.

register(pack) — publish or replace a pack

Call this during plugin initialization with the complete pack object. It validates synchronously and returns a registration handle; it does not wait for image loading or select the pack. The quick start is a complete registration guide.

To update an existing pack, use the same id through the same plugin-bound API with the complete replacement:

js
let registration = fileIcons.register(pack);

// For example, a plugin preference changes its folder associations.
const replacement = {
  ...pack,
  folderNames: { ...pack.folderNames, tests: "folder-test" },
};
// folder-test must be defined in replacement.icons, or exist in its SVG directory.
registration = fileIcons.register(replacement);
pack = replacement;

Omitted associations are removed, rather than merged. Validation errors throw before the old pack is replaced. An active replacement refreshes Acode's icons; an inactive replacement remains inactive. The same ID cannot be taken over by another plugin.

registration.dispose() — remove your registration

js
acode.setPluginUnmount(plugin.id, () => {
  registration?.dispose();
  registration = undefined;
});

The returned dispose() takes no arguments and returns no value. Calling it repeatedly is safe. A handle for an older version cannot remove a newer registration with the same ID. Removing the active pack displays Builtin; the preferred ID is retained so the user's choice can resume if that pack registers again. Acode also removes registrations owned by a plugin when it unmounts.

icon(resource) — render a single icon

js
const iconElement = document.createElement("span");
const resource = { kind: "folder", name: "src", expanded: true };

function renderIcon() {
  iconElement.className = `my-file-icon ${fileIcons.icon(resource)}`;
}

renderIcon();
const unsubscribe = fileIcons.onChange(renderIcon);
// When the view is destroyed: unsubscribe();

Pass a filename string for a file, or the resource object. Returns a complete class string synchronously. Preserve your own sizing/layout classes separately, as shown. Treat generated classes as opaque: do not parse them or store them permanently. Resolve again when the resource is renamed, its folder expands/collapses, the view becomes visible, or onChange fires. A loading asset initially returns a fallback.

For language IDs, use the name values from acode.require("editorLanguages").list(). These are Acode's mode identifiers; do not assume another editor's IDs match.

For simple cases, acode.require("helpers") also wraps this:

js
const helpers = acode.require("helpers");
helpers.getIconForFile("app.ts");
helpers.getIconForFolder("src", { expanded: true, isRoot: false });

onChange(listener) — refresh a live view

js
function refreshVisibleRows() {
  for (const row of visibleRows) {
    // Read the current resource: recycled rows may now represent a different file.
    row.icon.className = `my-file-icon ${fileIcons.icon(row.resource)}`;
  }
}

const unsubscribe = fileIcons.onChange(({ activeId, preferredId }) => {
  refreshVisibleRows();
});
refreshVisibleRows();

// Also call refreshVisibleRows() in the view's show/reconnect handler.
// When destroying the view:
// unsubscribe();

Subscribe with a function; returns an idempotent unsubscribe function. Acode tracks subscriptions by plugin and removes them before pack-removal events during unload, reload, or failed initialization. Old API handles cannot subscribe after unload. Call the unsubscribe function when a view closes to release its listener earlier. Subscription does not invoke the listener immediately, so perform the first render yourself.

Events cover:

  • Active / preferred pack changes
  • Active pack replacement or removal
  • Completion of requested assets (success or failure)

Multiple asset completions may be grouped into one frame. activeId and preferredId can remain unchanged when only asset readiness changes.

Listeners run synchronously when notified. Keep them lightweight; resolve current rows instead of re-registering packs from the listener. One throwing listener is logged and does not prevent other listeners from running. Detached views can refresh their own elements directly; views that skip work while hidden should refresh when shown again.

Resource reference

For icon, a resource is a filename string, or an object:

FieldTypeMeaning
namestringRequired basename or path. URI decoding is not performed.
kind"file" | "folder"Defaults to file.
languageIdstringOptional known Acode mode name for a file; avoids language inference. Filename/extension associations still take precedence.
expandedbooleanFolder expansion state, false when omitted.
isRootbooleanWhether a folder is a workspace root, false when omitted.

Names may be passed as paths for convenience; only the basename is matched. URI parsing is the caller's responsibility.

The built-in ID builtin and stored setting key iconTheme remain unchanged. Icon pack and Builtin are the display names; existing saved selections continue to work.

Custom plugin views

Set your element's classes from icon(resource). Subscribe to onChange and resolve again when notified, since a returned fallback may later become a loaded image. Preserve any layout classes your element needs. Unsubscribe when the view closes. Recycled list rows must use their current resource when refreshing.

Matching is synchronous and never calls plugin code or reads file contents. Acode requests image assets only when resolved, shares requests within the active pack, and installs styles only for the active pack. Image decoding/caching is handled by the WebView. Prefer small local assets; the API does not impose a decoded-memory budget.

Reading packaged association files

Use acode.require("fs") with a filesystem path to load local JSON. Use baseUrl for image URLs. In Acode, the Cordova HTTP-backed fetch() path cannot read these packaged JSON files reliably.

js
const fileIcons = acode.require("fileIcons");
const fs = acode.require("fs");
const Url = acode.require("Url");
let registration;

acode.setPluginInit(plugin.id, async (baseUrl) => {
  const root = Url.join(PLUGIN_DIR, plugin.id);
  const files = await fs(Url.join(root, "file_icons.json")).readFile("json");
  const folders = await fs(Url.join(root, "folder_icons.json")).readFile("json");

  registration = fileIcons.register({
    id: plugin.id,
    name: "My Icons",
    icons: `${Url.join(baseUrl, "icons")}/`,
    fileNames: files.fileNames,
    fileExtensions: files.fileExtensions,
    folderNames: folders.folderNames,
    folderNamesExpanded: folders.folderNamesExpanded,
    folder: "folder",
    folderExpanded: "folder-open",
  });
});

The icon pack example plugin shows JSON loading and conversion from an existing pack. Install its plugin.zip, then select Icon Pack Example in Settings → App settings → Icon pack.

Migrating from the draft API

  • Omit pluginId and use name for the pack display name. If keeping pluginId for compatibility, it must match the loading plugin.
  • Capture acode.require("fileIcons") synchronously in the plugin main script, or take fileIcons from the initialization options.
  • Move association maps and defaults to the top level.
  • Replace iconDefinitions/iconPath with icons/src, using absolute image URLs.
  • Declare expanded associations explicitly; remove unsupported appearance fields.
  • Replace update with register, and unregister with the registration's dispose().
  • Remove method aliases and automatic pack selection from plugin initialization.
  • Use icon(resource) for rendering. Catalog queries and detailed/batch resolvers are no longer exported; loop over visible resources when drawing multiple icons.

Released under the MIT License.