An editor debugger steps line by line through a loop, pausing on breakpoints while locals, watches and the call stack change.
╭─ checkout.js ─ ● Paused on step ─────────────────────────────────────────────────────────────────────╮ │ ► Continue F5 ↓ Step Over F10 → Step Into F11 ■ Stop │ │──────────────────────────────────────────────────────────────────────────────────────────────────────│ │ 1 function applyDiscounts(cart, rules) { │ VARIABLES │ │░░► 2░ let total = 0;░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ ▼ Local │ │ 3 for (const item of cart) { │ total:undefined │ │ ● 4 let price = item.price * item.qty; │ │ │ 5 const rule = rules[item.sku]; │ ▼ Watch │ │ 6 if (rule) { │ cart.length: 3 │ │ ● 7 price = price * (1 - rule.pct); │ total > 20: false │ │ 8 } │ │ │ ● 9 total += price; │ CALL STACK │ │ 10 } │ applyDiscounts :2 │ │ 11 return Math.round(total * 100) / 100; │ <module> :19 │ │ 12 } │ run :1 │ │ 13 │ │ │ 14 const cart = [ │ BREAKPOINTS │ │ 15 { sku: "tea", price: 4.5, qty: 2 }, │ ● checkout.js:4 │ │ 16 { sku: "mug", price: 12, qty: 1 }, │ ● checkout.js:7 │ │ 17 { sku: "jam", price: 6.25, qty: 3 }, │ ● checkout.js:9 │ │ 18 ]; │ │ │ 19 applyDiscounts(cart, { mug: { pct: 0.25 } }); │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯
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="ide-debugger-stepping"></ascii-art>// copy ide-debugger-stepping.js and kit.js into your project, then:
import { useEffect, useRef } from "react";
import create from "./ide-debugger-stepping.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 ide-debugger-stepping<!-- Download GIF below, commit it, then: -->
<p align="center">
<img src="assets/ide-debugger-stepping.gif" alt="Debugger Stepping, 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.
/* ide-debugger-stepping: a debugger stepping through code past breakpoints while variables and the call stack update. */
import { Grid, palette } from "../lib/kit.js";
export const meta = {
slug: "ide-debugger-stepping",
name: "Debugger Stepping",
category: "screens",
note: "An editor debugger steps line by line through a loop, pausing on breakpoints while locals, watches and the call stack change.",
tags: ["debugger", "ide", "breakpoints", "code", "developer", "editor"],
cols: 104, rows: 28, fps: 24,
palette: palette(["#e6edf3", "#8b949e", "#7ee787", "#79c0ff", "#ffa657", "#ff7b72", "#d2a8ff", "#484f58", "#30363d", "#f2cc60", "#a5d6ff", "#3a3200"]),
};
const SRC = [
"§6function §3applyDiscounts§0(cart, rules) {",
" §6let §0total = §40§0;",
" §6for §0(§6const §0item §6of §0cart) {",
" §6let §0price = item.price * item.qty;",
" §6const §0rule = rules[item.sku];",
" §6if §0(rule) {",
" price = price * (§41§0 - rule.pct);",
" }",
" total += price;",
" }",
" §6return §0Math.round(total * §4100§0) / §4100§0;",
"}",
"",
"§6const §0cart = [",
" { sku: §a\"tea\"§0, price: §44.5§0, qty: §42§0 },",
" { sku: §a\"mug\"§0, price: §412§0, qty: §41§0 },",
" { sku: §a\"jam\"§0, price: §46.25§0, qty: §43§0 },",
"];",
"§3applyDiscounts§0(cart, { mug: { pct: §40.25§0 } });",
];
const BP = new Set([4, 7, 9]);
// execution trace: [line(1-based), locals]
const TRACE = [];
(function build() {
const cart = [["tea", 4.5, 2], ["mug", 12, 1], ["jam", 6.25, 3]];
const rules = { mug: 0.25 };
let total = 0;
TRACE.push([2, { total: "undefined" }]);
TRACE.push([3, { total: "0" }]);
for (const [sku, p, q] of cart) {
let price = p * q;
const it = `{sku:"${sku}"}`;
TRACE.push([4, { total: String(total), item: it }]);
TRACE.push([5, { total: String(total), item: it, price: String(price) }]);
const r = rules[sku];
TRACE.push([6, { total: String(total), item: it, price: String(price), rule: r ? `{pct:${r}}` : "undefined" }]);
if (r) { TRACE.push([7, { total: String(total), item: it, price: String(price), rule: `{pct:${r}}` }]); price = price * (1 - r); }
TRACE.push([9, { total: String(total), item: it, price: String(price) }]);
total += price;
}
TRACE.push([11, { total: String(total) }]);
})();
const ST = 0.75;
function parse(s) { const ch = [], co = []; let c = "0"; for (let i = 0; i < s.length; i++) { if (s[i] === "§") { c = s[++i]; continue; } ch.push(s[i]); co.push(c); } return { ch, co }; }
export default function create() {
const g = new Grid(meta.cols, meta.rows);
const W = meta.cols, H = meta.rows;
const P = SRC.map(parse);
const PERIOD = TRACE.length * ST + 2;
const CW = 64;
return (t) => {
g.clear();
const lt = t % PERIOD;
const si = Math.min(TRACE.length - 1, Math.floor(lt / ST));
const finished = lt >= TRACE.length * ST;
const [line, locals] = TRACE[si];
const prevLocals = si > 0 ? TRACE[si - 1][1] : {};
g.box(0, 0, W, H, "round", "7");
g.text(2, 0, " checkout.js ", "0");
const paused = !finished;
g.text(16, 0, paused ? (BP.has(line) ? " ● Paused on breakpoint " : " ● Paused on step ") : " ► Running ", paused ? "4" : "2");
const tb = ["► Continue F5", "↓ Step Over F10", "→ Step Into F11", "■ Stop"];
let x = 2; tb.forEach((s, i) => { g.text(x, 1, s, i === 1 && paused ? "3" : "1"); x += s.length + 3; });
g.hline(1, W - 2, 2, "─", "8");
P.forEach((p, i) => {
const y = 3 + i;
if (y >= H - 1) return;
const ln = i + 1, cur = paused && ln === line;
if (cur) g.fill(1, y, CW - 1, 1, "░", "b");
if (BP.has(ln)) g.set(2, y, "●", "5");
if (cur) g.set(3, y, "►", "9");
g.text(4, y, String(ln).padStart(3), cur ? "9" : "7");
p.ch.forEach((c, j) => { if (8 + j < CW) g.set(8 + j, y, c, p.co[j]); });
// inline value hint
if (cur && locals.price) g.text(Math.min(CW - 14, 9 + p.ch.length + 2), y, "price=" + locals.price, "6");
});
g.vline(CW, 3, H - 2, "│", "8");
const rx = CW + 2;
g.text(rx, 3, "VARIABLES", "1");
let y = 4;
g.text(rx, y++, "▼ Local", "6");
Object.entries(locals).forEach(([k, v]) => {
const changed = prevLocals[k] !== v;
g.text(rx + 2, y, k + ":", "3");
g.text(rx + 3 + k.length, y, v.slice(0, W - rx - 6 - k.length), changed ? "9" : "0");
y++;
});
y++;
g.text(rx, y++, "▼ Watch", "6");
g.text(rx + 2, y, "cart.length:", "3"); g.text(rx + 15, y++, "3", "0");
g.text(rx + 2, y, "total > 20:", "3"); g.text(rx + 14, y++, String(parseFloat(locals.total) > 20), "0");
y++;
g.text(rx, y++, "CALL STACK", "1");
g.text(rx + 2, y++, "applyDiscounts :" + line, "0");
g.text(rx + 2, y++, "<module> :19", "1");
g.text(rx + 2, y++, "run :1", "7");
y++;
g.text(rx, y++, "BREAKPOINTS", "1");
[...BP].forEach((b) => { g.text(rx + 2, y, "● checkout.js:" + b, b === line && paused ? "4" : "1"); y++; });
if (finished) g.text(rx, H - 2, "→ returned 27.75", "2");
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.