A project explorer where a cursor walks the tree, opening and closing folders as it goes.
╭─ EXPLORER ────────────────────────────── ◆ main ───╮ │ api-gateway 3 changes │ │────────────────────────────────────────────────────│ │ ▼ api-gateway/ │ │ ├─ ▶ .github/ │ │▌░├─░▼░src/░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │ │ ├─ ▶ routes/ │ │ │ ├─ ▶ middleware/ │ │ │ ├─ • index.ts │ │ │ └─ • server.ts │ │ ├─ ▶ tests/ │ │ ├─ • .env.example │ │ ├─ • package.json M │ │ ├─ • tsconfig.json │ │ └─ • README.md │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │────────────────────────────────────────────────────│ │ ▼ src/ 4 items 3/12 │ ╰────────────────────────────────────────────────────╯
Paste two lines into any page, play it in a terminal, or put a GIF in a README. The code follows the look you pick above.
<script type="module" src="https://www.ascii-magic.com/ascii/ascii.js"></script>
<ascii-art piece="file-tree"></ascii-art>// copy file-tree.js and kit.js into your project, then:
import { useEffect, useRef } from "react";
import create from "./file-tree.js";
export function Piece() {
const ref = useRef(null);
useEffect(() => {
const draw = create(), t0 = performance.now();
let raf;
const tick = () => {
const f = draw((performance.now() - t0) / 1000);
ref.current.textContent = f.text ?? f;
raf = requestAnimationFrame(tick);
};
tick();
return () => cancelAnimationFrame(raf);
}, []);
return <pre ref={ref} style={{ lineHeight: 1, fontFamily: "Menlo, Consolas, monospace" }} />;
}npx --yes https://www.ascii-magic.com/ascii/cli.tgz file-tree<!-- Download GIF below, commit it, then: -->
<p align="center">
<img src="assets/file-tree.gif" alt="File Tree, animated ASCII art" width="720">
</p>Pause on the frame you like, pick a look, then download it at an exact size.
MIT licensed. It imports one small helper file, kit.js.
/* file-tree: a project explorer with folders expanding and collapsing under a moving cursor. */
import { Grid, palette } from "../lib/kit.js";
export const meta = {
slug: "file-tree",
name: "File Tree",
category: "screens",
note: "A project explorer where a cursor walks the tree, opening and closing folders as it goes.",
tags: ["ide", "editor", "explorer", "terminal", "readme"],
cols: 54, rows: 26, fps: 20,
palette: palette(["#e6edf3", "#8b949e", "#7ee787", "#79c0ff", "#ffa657", "#ff7b72", "#d2a8ff", "#484f58", "#30363d", "#f2cc60"]),
};
const D = (name, kids, open = false) => ({ name, kids, open });
const F = (name, git = "") => ({ name, git });
const tree = () => D("api-gateway", [
D(".github", [D("workflows", [F("ci.yml"), F("release.yml")])]),
D("src", [
D("routes", [F("auth.ts", "M"), F("billing.ts", "U"), F("users.ts")]),
D("middleware", [F("cors.ts"), F("rateLimit.ts", "M")]),
F("index.ts"), F("server.ts"),
]),
D("tests", [F("auth.test.ts"), F("users.test.ts")]),
F(".env.example"), F("package.json", "M"), F("tsconfig.json"), F("README.md"),
], true);
// d = down, u = up, o = toggle folder at cursor
const SCRIPT = "ddoddoddduuuoddoddduuuuuoddoddduuuuoduuu".split("");
const STEP = 0.42;
function flatten(node, depth, prefix, last, out) {
out.push({ node, depth, prefix, last });
if (node.kids && node.open) node.kids.forEach((k, i) => {
const l = i === node.kids.length - 1;
flatten(k, depth + 1, depth === 0 ? "" : prefix + (last ? " " : "│ "), l, out);
});
return out;
}
const extCol = (n) => n.endsWith(".ts") ? "3" : n.endsWith(".yml") ? "6" : n.endsWith(".json") ? "9" : n.endsWith(".md") ? "0" : "1";
export default function create() {
const g = new Grid(meta.cols, meta.rows);
const PERIOD = SCRIPT.length * STEP + 1.5;
return (t) => {
g.clear();
const lt = t % PERIOD;
const root = tree();
let cur = 0;
const steps = Math.min(SCRIPT.length, Math.floor(lt / STEP));
let rows = flatten(root, 0, "", true, []);
for (let i = 0; i < steps; i++) {
const a = SCRIPT[i];
if (a === "d") cur = Math.min(rows.length - 1, cur + 1);
else if (a === "u") cur = Math.max(0, cur - 1);
else if (rows[cur].node.kids) { rows[cur].node.open = !rows[cur].node.open; rows = flatten(root, 0, "", true, []); }
}
const W = meta.cols, H = meta.rows;
g.box(0, 0, W, H, "round", "7");
g.text(2, 0, " EXPLORER ", "1");
g.text(W - 12, 0, " ◆ main ", "6");
g.text(2, 1, "api-gateway", "0"); g.text(W - 14, 1, "3 changes", "4");
g.hline(1, W - 2, 2, "─", "8");
rows.forEach((r, i) => {
const y = 3 + i;
if (y > H - 4) return;
const sel = i === cur;
if (sel) { g.fill(1, y, W - 2, 1, "░", "8"); g.set(1, y, "▌", "3"); }
const n = r.node;
let x = 3;
if (r.depth > 0) { const p = r.prefix + (r.last ? "└─ " : "├─ "); for (let k = 0; k < p.length; k++) if (p[k] !== " ") g.set(x + k, y, p[k], "7"); x += p.length; }
if (n.kids) {
g.set(x, y, n.open ? "▼" : "▶", sel ? "0" : "1");
g.text(x + 2, y, n.name + "/", sel ? "0" : "3");
} else {
g.set(x, y, "•", extCol(n.name));
g.text(x + 2, y, n.name, sel ? "0" : n.git === "M" ? "4" : n.git === "U" ? "2" : "0");
if (n.git) g.set(W - 3, y, n.git, n.git === "M" ? "4" : "2");
}
});
const node = rows[cur].node;
g.hline(1, W - 2, H - 3, "─", "8");
g.text(2, H - 2, node.kids ? (node.open ? "▼ " : "▶ ") + node.name + "/" : "• " + node.name, "0");
g.text(W - 20, H - 2, node.kids ? `${node.kids.length} items` : "TypeScript", "1");
g.text(W - 9, H - 2, `${cur + 1}/${rows.length}`, "1");
return g.frame();
};
}
/YOUR OWN
The pieces here are hand-built animations. To turn your own photo, logo or video into animated ASCII, use the ASCII Magic editor.