diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 912d9646..f168a8da 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -15,7 +15,7 @@ "theme": { "type": "string", "title": "Theme", "description": "Theme name: glowup, aurora, dusk, classic, cyberpunk, vaporwave, high-contrast, or one in ~/.claude/glowup/themes", "default": "classic" }, "reducedMotion": { "type": "boolean", "title": "Reduced motion", "description": "No shimmer or spinner animation from glowup", "default": false }, "pack": { "type": "string", "title": "Pack", "description": "Pack name: classic, crt, cozy, arcade, or one in ~/.claude/glowup/packs", "default": "classic" }, - "spinner": { "type": "string", "title": "Spinner", "description": "Spinner name: stock, comet, eyes, orb-states, clawd, shimmer, or pack for the pack's own", "default": "pack" }, + "spinner": { "type": "string", "title": "Spinner", "description": "Spinner name: stock, comet, eyes, orb-states, clawd, shimmer, scanline, ring, glitch, signal, or pack for the pack's own", "default": "pack" }, "pet": { "type": "string", "title": "Pet", "description": "clawd, robot, off, or a pet in ~/.claude/glowup/pets", "default": "clawd" }, "bubbles": { "type": "string", "title": "Speech bubbles", "description": "on (template lines), off, or haiku (Claude Haiku writes some lines; small paid calls on your account)", "default": "on" }, "statusline": { "type": "string", "title": "Status line fields", "description": "Comma-separated, in order: activity, ctx, 5h, week, cost, model, effort, agents, plan, branch, changes, cwd", "default": "activity,ctx,effort,5h,week" } diff --git a/.docket/ledger.jsonl b/.docket/ledger.jsonl index b2a6e88a..25773150 100644 --- a/.docket/ledger.jsonl +++ b/.docket/ledger.jsonl @@ -113,8 +113,8 @@ {"answers": ["q7"], "author": "claude-code", "branch": "main", "cost_if_wrong": "The Konami egg is built on a key path that does not reach the mod", "depends_on": [], "evidence": [], "id": "c13", "kind": "claim", "migrated_from": "c108", "pinned": false, "rationale": "tmux spike with a throwaway mod (scratchpad/keyspike): after an SGR mouse click on the Client, ↑↑↓↓←→←→BA arrived as all ten keys; without the click, b and a landed in the prompt", "revisit": "A Claude Code release changes Client focus or onKey", "schema": 3, "scope": [], "session": "", "state": "accepted", "supersedes": [], "supports": [], "text": "On Claude Code 2.1.291 a Client's surface.onKey receives arrow keys (up, down, left, right) and letters once the person clicks the Client; before a click the keys go to the prompt, and Escape returns the focus", "ts": "2026-10-07T06:40:08+00:00"} {"alternatives": ["Crack overlays on one sheet", "Typing konami at the prompt to open a code dialog"], "answers": [], "author": "claude-code", "branch": "main", "choice": "Click Clawd's pet Client (it hops), type up up down down left right left right b a; the Client matches locally and posts once; the egg is a hidden built-in pet with four hand-drawn sheets, one per crack stage, cracking every 10 passing runs to stage 3", "cost_if_wrong": "Four sheets of art for one egg, and the code needs a terminal with mouse events", "decided_by": "owner", "depends_on": [], "evidence": [], "id": "d85", "kind": "decision", "migrated_from": "d109", "pinned": false, "rationale": "Owner picked 3 stages of 10 runs, full sheets per stage so cracks tilt with the wobble, and a hop on click with a quiet code", "revisit": "", "schema": 3, "scope": ["hooks/client/pet.tsx", "hooks/eggs.ts"], "session": "", "state": "adopted", "supersedes": [], "supports": [["c13"]], "text": "The Konami egg unlocks from a click on Clawd and the code typed into his Client", "ts": "2026-10-07T07:02:03+00:00"} {"answers": [], "author": "claude-code", "branch": "feat/konami-egg", "cost_if_wrong": "", "depends_on": [], "evidence": [], "id": "q12", "kind": "question", "migrated_from": "q110", "pinned": false, "rationale": "", "revisit": "", "schema": 3, "scope": ["hooks/eggs.ts"], "session": "", "state": "open", "supersedes": [], "supports": [], "text": "What does the Konami egg hatch into, and when?", "ts": "2026-10-07T08:54:10+00:00"} -{"alternatives": ["Hints in the config pane's Pet row", "Haiku-written hint lines"], "answers": [], "author": "claude-code", "branch": "feat/egg-hints", "choice": "pet list shows ○ ??? after the built-ins; the first done bubble of each 24 h with the pane shown says one of two fixed lines, taking turns by a stored count; hint turns never ask Haiku; the config Pet row is unchanged", "cost_if_wrong": "A done bubble a day is replaced by a hint until the person finds the egg", "decided_by": "owner", "depends_on": [], "evidence": [], "id": "d86", "kind": "decision", "migrated_from": "d111", "pinned": false, "rationale": "Owner wanted a nudge toward the code without giving it; Haiku could improvise or spoil the code", "revisit": "", "schema": 3, "scope": ["hooks/eggs.ts", "hooks/register.tsx", "hooks/command.ts"], "session": "", "state": "adopted", "supersedes": [], "supports": [], "text": "While the egg is locked, glowup hints at it with ??? in /glowup pet list and a fixed bubble line at most once a day", "ts": "2026-10-07T11:56:39+00:00"} -{"alternatives": [], "answers": [], "author": "claude-code", "branch": "feat/egg-hints", "choice": "eggHint runs inside say() after its gates, so bubbles off, pet off, pane hidden and done-mood off all suppress it", "cost_if_wrong": "Someone with done bubbles off never sees a hint", "decided_by": "", "depends_on": [], "evidence": [], "id": "d87", "kind": "decision", "migrated_from": "d112", "pinned": false, "rationale": "Reuses say()'s gates instead of a second path", "revisit": "", "schema": 3, "scope": ["hooks/register.tsx"], "session": "", "state": "adopted", "supersedes": [], "supports": [], "text": "A finished turn's done bubble while the hint is due shows the hint; the setup turning off done bubbles also turns off hints", "ts": "2026-10-07T11:56:40+00:00"} +{"alternatives":["Hints in the config pane's Pet row","Haiku-written hint lines"],"answers":[],"author":"claude-code","branch":"feat/egg-hints","choice":"pet list shows ○ ??? after the built-ins; the first done bubble of each 24 h with the pane shown says one of two fixed lines, taking turns by a stored count; hint turns never ask Haiku; the config Pet row is unchanged","cost_if_wrong":"A done bubble a day is replaced by a hint until the person finds the egg","decided_by":"owner","depends_on":[],"evidence":[],"id":"d86","kind":"decision","migrated_from":"d111","pinned":false,"rationale":"Owner wanted a nudge toward the code without giving it; Haiku could improvise or spoil the code","revisit":"","schema":3,"scope":["hooks/eggs.ts","hooks/register.tsx","hooks/command.ts"],"session":"","state":"adopted","supersedes":[],"supports":[],"text":"While the egg is locked, glowup hints at it with ??? in /glowup pet list and a fixed bubble line at most once a day","ts":"2026-10-07T11:56:39+00:00"} +{"alternatives":[],"answers":[],"author":"claude-code","branch":"feat/egg-hints","choice":"eggHint runs inside say() after its gates, so bubbles off, pet off, pane hidden and done-mood off all suppress it","cost_if_wrong":"Someone with done bubbles off never sees a hint","decided_by":"","depends_on":[],"evidence":[],"id":"d87","kind":"decision","migrated_from":"d112","pinned":false,"rationale":"Reuses say()'s gates instead of a second path","revisit":"","schema":3,"scope":["hooks/register.tsx"],"session":"","state":"adopted","supersedes":[],"supports":[],"text":"A finished turn's done bubble while the hint is due shows the hint; the setup turning off done bubbles also turns off hints","ts":"2026-10-07T11:56:40+00:00"} {"schema":3,"kind":"decision","id":"d88","text":"A file drops out of the Changes tab when git no longer reports it and tracks it.","state":"adopted","ts":"2026-10-07T13:17:02+00:00","author":"claude-code","session":"","branch":"feat/pane-and-hints","scope":["hooks/changes.ts","hooks/model.ts"],"rationale":"An uncommitted new file is also absent from git diff, so absence alone cannot tell a commit from an untracked file","supports":[],"depends_on":[],"answers":[],"supersedes":[],"evidence":[],"revisit":"","cost_if_wrong":"","pinned":false,"choice":"After the numstat diff, ask git ls-files which unreported files are tracked and drop those; untracked and ignored files keep their own counts","alternatives":["Drop every unreported file","Drop files only on a HEAD rebase"],"decided_by":""} {"schema":3,"kind":"decision","id":"d89","text":"A refresh that finds new counts for a file touches it again.","state":"adopted","ts":"2026-10-07T13:17:02+00:00","author":"claude-code","session":"","branch":"feat/pane-and-hints","scope":["hooks/changes.ts","hooks/model.ts"],"rationale":"The brief makes at the sort key for Changes and Diff, and a git-only edit has to reorder; max keeps an edit that landed while git ran","supports":[],"depends_on":[],"answers":[],"supersedes":[],"evidence":[],"revisit":"","cost_if_wrong":"","pinned":false,"choice":"refreshCounts stamps the refresh time when counts change; mergeCounts keeps the later of the two times","alternatives":["Keep the first-seen time for refreshed files"],"decided_by":""} {"schema":3,"kind":"decision","id":"d90","text":"The Diff tab reads hunks only while it is the visible tab.","state":"adopted","ts":"2026-10-07T13:17:02+00:00","author":"claude-code","session":"","branch":"feat/pane-and-hints","scope":["hooks/diff.ts","hooks/register.tsx","hooks/pane.tsx"],"rationale":"git diff is the costly call, and the render hook can only read published state","supports":[],"depends_on":[],"answers":[],"supersedes":[],"evidence":[],"revisit":"","cost_if_wrong":"","pinned":false,"choice":"Hunks live in the published pane view as a plain record, are read on the refresh queue while the tab shows and on switching to it, and untracked files are read with head -c 64 KB","alternatives":["Read the diff on every refresh","Keep hunks in module state outside the view"],"decided_by":""} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9be23865..cd3972c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Four spinners: `scanline` sweeps a bright column across a checkered bar and on through the spinner word, `ring` circles nine dots round an ellipse, `glitch` flickers the spinner word's letters into blocks in the pack's `read` and `agent` colors, and `signal` scrolls an oscilloscope trace. Each speeds up or changes shape while commands run or subagents work. Pick one with `motion.spinner` or `/glowup spinner `. +- A `slab` row style: your prompt is a full-width bar in the pack's accent, numbered by turn; tool calls are numbered within the turn with the tool in capitals and `OK` or `FAIL` on the right; a short rule opens each reply. - Hints that a secret pet exists: `???` in `/glowup pet list`, and now and then a line from Clawd. - A Diff tab shows this session's changes as themed hunks. diff --git a/docs/commands.md b/docs/commands.md index aed3dd58..107c03df 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -64,7 +64,7 @@ Each refusal prints a message naming the failed check. Adding a theme whose name | Command | What it does | | --- | --- | -| `/glowup spinner ` | Use one spinner (`stock`, `comet`, `eyes`, `orb-states`, `clawd` or `shimmer`) on top of the current pack. Prints `Spinner: `. An unknown name lists the valid ones. | +| `/glowup spinner ` | Use one spinner (`stock`, `comet`, `eyes`, `orb-states`, `clawd`, `shimmer`, `scanline`, `ring`, `glitch` or `signal`) on top of the current pack. Prints `Spinner: `. An unknown name lists the valid ones. | | `/glowup spinner list` | List the spinners. A filled dot marks the one showing now, whether it is your override or the pack's own. | | `/glowup spinner default` | Drop the override and go back to the pack's spinner. Prints `Spinner: pack default`. | diff --git a/docs/install.md b/docs/install.md index 694b10f4..402d94eb 100644 --- a/docs/install.md +++ b/docs/install.md @@ -36,7 +36,7 @@ curl -fsSL https://glowup.khimani.dev/install.sh | sh -s -- --yes --pack crt | `--yes` | Install without the picker, with the defaults or the options below. The script adds it when there is no terminal. | | `--pack NAME` | `classic`, `crt`, `cozy` or `arcade`. Default `classic`. | | `--theme NAME` | A theme on top of the pack: `classic`, `glowup`, `aurora`, `dusk`, `cyberpunk`, `vaporwave` or `high-contrast`. `classic` keeps the pack's own colors. Default: not set. | -| `--spinner NAME` | `stock`, `comet`, `eyes`, `orb-states`, `clawd`, `shimmer`, or `pack` for the pack's own. Default `pack`. | +| `--spinner NAME` | `stock`, `comet`, `eyes`, `orb-states`, `clawd`, `shimmer`, `scanline`, `ring`, `glitch`, `signal`, or `pack` for the pack's own. Default `pack`. | | `--pet clawd\|off` | Default `clawd`. | | `--bubbles on\|off\|haiku` | Speech bubbles. `haiku` lets Claude Haiku write some lines, as small calls on your account. Default `on`. | | `--reduced-motion` | Turn off glowup's animation. | @@ -92,7 +92,7 @@ The installer sets every setting except `statusline`. To change them afterwards, | `pet` | `clawd` or `off` | `clawd` | Shows Clawd, or no pet. | | `bubbles` | `on`, `off` or `haiku` | `on` | Speech bubbles from templates, none, or lines written by Haiku. | | `theme` | A theme name | `classic` | A theme on top of the pack. `classic` keeps the pack's own colors. | -| `spinner` | A spinner name | `pack` | A spinner on top of the pack: `stock`, `comet`, `eyes`, `orb-states`, `clawd` or `shimmer`. `pack` keeps the pack's own spinner. | +| `spinner` | A spinner name | `pack` | A spinner on top of the pack: `stock`, `comet`, `eyes`, `orb-states`, `clawd`, `shimmer`, `scanline`, `ring`, `glitch` or `signal`. `pack` keeps the pack's own spinner. | | `statusline` | Comma-separated field ids | `activity,ctx,effort,5h,week` | The status line fields and their order. Unknown ids are dropped. See [Status line](statusline.md#choosing-the-fields). | | `reducedMotion` | `true` or `false` | `false` | Turns off glowup's animation. | diff --git a/docs/pack-reference.md b/docs/pack-reference.md index 2d7ffadc..89d66cd2 100644 --- a/docs/pack-reference.md +++ b/docs/pack-reference.md @@ -19,7 +19,7 @@ Packs are read from `~/.claude/glowup/packs`, or `$CLAUDE_CONFIG_DIR/glowup/pack "theme": "classic", // a theme to start the palette from "palette": { "accent": "#ff3ec8", "text": "#f0e6ff" }, "bg": "#1a0b33", // what braille and half-block fades blend toward - "rows": "cards", // classic | cards | minimal | retro + "rows": "cards", // classic | cards | minimal | retro | slab "border": "bold", // round | single | double | bold | classic "borderColor": "#ff3ec8", "gradient": ["#ff3ec8", "#38e8ff"], @@ -33,7 +33,7 @@ Packs are read from `~/.claude/glowup/packs`, or `$CLAUDE_CONFIG_DIR/glowup/pack "pet": { "body": "#7aa2f7", "light": "#a9c1ff", "shade": "#4a6fc4" } // Clawd's colors }, "motion": { - "spinner": "orb-states", // stock | comet | eyes | orb-states | clawd | shimmer + "spinner": "orb-states", // stock | comet | eyes | orb-states | clawd | shimmer | scanline | ring | glitch | signal "shimmer": 2, // 0 none, 1 soft, 2 fast "color": "#38e8ff", // defaults to the palette's accent "field": { "shape": "simplex", "speed": 0.3, "scale": 0.36, "dither": "4x4" } @@ -82,6 +82,7 @@ Styles wrap or prefix a transcript row; the content Claude Code draws inside it | `cards` | An accent side bar `▎` and a `you` label | A dim side bar, and a `claude` label on the first block of a reply | A dim bordered card with a mark: `✓`, `✗`, `■` for interrupted, `…` while running | | `minimal` | A `› text` line | Claude Code's | A dim one-line `· Read src/auth.ts` once it finishes cleanly | | `retro` | A `[YOU]` tag on its own line above your prompt | A `[CLAUDE]` tag on the first block | A `[READ ]`-style tag, then `[ OK ]`, `[FAIL]`, `[STOP]` or `[....]` | +| `slab` | A full-width bar in `accent` with your prompt in the background color and `PROMPT 03` on the right | An 8-cell `─` rule in `accent` above the first block | `01 READ ` before the row, then `OK`, `FAIL`, `STOP` or `…` on the right; results indented under their call | Not styled: notifications, messages from other agents, prompts expanded with ctrl+o or `--verbose`, the folded "Read 3 files" line, and tool progress. Outside fullscreen, rows already in scrollback keep the style they were printed with. @@ -132,6 +133,10 @@ A [renderer plugin](packs.md#drawing-with-a-plugin) that answers for the pack dr | `orb-states` | 4×2 | A braille orb whose motion follows the activity: thinking, searching, working, running, agents. | | `clawd` | 5×2 | Clawd waving, in his own color. | | `shimmer` | 1×1 | A still `✻` with a shimmering word. | +| `scanline` | 10×1 | A bright sweep across a bar, and the spinner word lights as it passes. | +| `ring` | 6×2 | A bright braille arc with a fading tail circling an oval. | +| `glitch` | 1×1 | A still `◆` with a glitching word. | +| `signal` | 10×2 | A braille wave trace that scrolls and is brightest at its right edge. | The shimmer is a wave of color across the spinner word, between the gradient's two colors, or the accent and text colors when there is no gradient. `1` is soft and `2` is fast. diff --git a/docs/web/app/landing/SpinnersSection.tsx b/docs/web/app/landing/SpinnersSection.tsx index 1ec2a5b6..12a59d47 100644 --- a/docs/web/app/landing/SpinnersSection.tsx +++ b/docs/web/app/landing/SpinnersSection.tsx @@ -6,7 +6,7 @@ import { usePack } from './PackContext.tsx' import { SpinnerCanvas } from './SpinnerCanvas.tsx' const STATES: OrbState[] = ['think', 'search', 'work', 'run', 'agents'] -const IDS = (Object.keys(SPINNERS) as SpinnerId[]).filter(id => id !== 'shimmer') +const IDS = (Object.keys(SPINNERS) as SpinnerId[]).filter(id => id !== 'shimmer' && id !== 'glitch') export function SpinnersSection() { const { look } = usePack() diff --git a/hooks/motion.ts b/hooks/motion.ts index 59b7190f..5eb4f7eb 100644 --- a/hooks/motion.ts +++ b/hooks/motion.ts @@ -1,14 +1,17 @@ // JSX-free: the docs site and Client modules import it. +import { cellWidth } from './cells.ts' import { gradient, mix, wave, wave3, type Span } from './color.ts' export type Cell = { ch: string; fg: string; bg?: string } export type OrbState = 'think' | 'search' | 'work' | 'run' | 'agents' -export type SpinnerId = 'stock' | 'comet' | 'eyes' | 'orb-states' | 'clawd' | 'shimmer' +export type SpinnerId = 'stock' | 'comet' | 'eyes' | 'orb-states' | 'clawd' | 'shimmer' | 'scanline' | 'ring' | 'glitch' | 'signal' type Canvas = { w: number; h: number; d: number[] } type Spec = { name: string; cols: number; rows: number } & ( | { kind: 'glyph'; frames: string } | { kind: 'dots' | 'half'; draw: (cv: Canvas, t: number, st: OrbState) => void; useFg?: boolean } - | { kind: 'art'; art: string[][] }) + | { kind: 'art'; art: string[][] } + | { kind: 'scan' }) +type CellOpts = { color: string; bg: string; fg: string; wordLen?: number } const CLAWD = '#d77757' const canvas = (w: number, h: number): Canvas => ({ w, h, d: new Array(w * h).fill(0) }) @@ -103,6 +106,53 @@ function eyes(cv: Canvas, t: number) { } } +// One sweep crosses the bar, the one-cell gap the spinner leaves before the word, the word and a +// pause. spinnerWordSpans lights the word from the same position, so both must use this. +export const SCAN_BAR = 10 +const SCAN_MS = 45, SCAN_PAUSE = 6 +export function scanPos(tMs: number, wordLen: number, st: OrbState): number { + const speed = st === 'run' || st === 'agents' ? 1.5 : 1 + return ((Math.max(0, tMs || 0) * speed) / SCAN_MS) % (SCAN_BAR + 1 + wordLen + SCAN_PAUSE) +} + +const isLight = (hex: string) => { + const n = parseInt(hex.slice(1), 16) + return (0.299 * (n >> 16) + 0.587 * ((n >> 8) & 255) + 0.114 * (n & 255)) > 140 +} + +function scanCells(tMs: number, st: OrbState, o: CellOpts): Cell[][] { + const pos = scanPos(tMs, o.wordLen ?? 12, st), shift = Math.floor(tMs / 400) + // Toward white reads on a dark pack; on a light one it fades into the background, so text itself. + const hi = isLight(o.bg) ? o.fg : mix(o.fg, '#ffffff', 0.5), faint = mix(o.bg, o.color, 0.25) + return [Array.from({ length: SCAN_BAR }, (_, i) => { + const d = pos - i + if (d >= 0 && d < 1) return { ch: '█', fg: hi } + return { ch: (i + shift) % 2 ? '▚' : '▞', fg: d >= 1 && d < 3 ? o.color : faint } + })] +} + +function ringDraw(cv: Canvas, t: number, st: OrbState) { + const cx = (cv.w - 1) / 2, cy = (cv.h - 1) / 2 + const speed = st === 'work' || st === 'run' ? 1.5 : 1 + const [arcs, n] = st === 'agents' ? [[0, 2.094, 4.189], 3] : [[0], 9] + for (const off of arcs) for (let i = 0; i < n; i++) { + const a = t * Math.PI * speed + off - i * 0.55 + plot(cv, cx + Math.cos(a) * 5.2, cy + Math.sin(a) * 3.2, 1 - i / n) + } +} + +function signalDraw(cv: Canvas, t: number, st: OrbState) { + const amp = st === 'think' ? 0.55 : st === 'run' ? 1 : 0.8 + const trace = (a: number, off: number, v: (x: number) => number) => { + for (let x = 0; x < cv.w; x++) { + const p = t / 0.16 + x * 0.55 + off + plot(cv, x, 3.5 + a * 3.4 * (0.65 * Math.sin(p) + 0.35 * Math.sin(p * 2.7 + 1)), v(x)) + } + } + trace(amp, 0, x => (x >= cv.w - 3 ? 1 : 0.7)) + if (st === 'agents') trace(amp / 2, Math.PI, () => 0.45) +} + export const SPINNERS: Record = { stock: { name: 'stock ✻ cycle', cols: 1, rows: 1, kind: 'glyph', frames: '·✢✳✶✻✽' }, comet: { name: 'comet', cols: 3, rows: 2, kind: 'dots', draw: (cv, t) => { @@ -114,13 +164,18 @@ export const SPINNERS: Record = { 'orb-states': { name: 'orb states', cols: 4, rows: 2, kind: 'dots', draw: orbDraw }, clawd: { name: 'Clawd', cols: 5, rows: 2, kind: 'art', art: [['▐▛█▜▌', '▝▛█▜▘'], ['▐▛█▜▌', '▝▜█▛▘'], ['▐▛█▜▌', '▝▛█▜▘'], ['▐▛█▜▌', '▗▛█▜▖']] }, shimmer: { name: 'text shimmer wave', cols: 1, rows: 1, kind: 'glyph', frames: '✻' }, + scanline: { name: 'scanline sweep', cols: SCAN_BAR, rows: 1, kind: 'scan' }, + ring: { name: 'angel ring', cols: 6, rows: 2, kind: 'dots', draw: ringDraw }, + glitch: { name: 'glitch word', cols: 1, rows: 1, kind: 'glyph', frames: '◆' }, + signal: { name: 'signal trace', cols: 10, rows: 2, kind: 'dots', draw: signalDraw }, } -export function spinnerCells(id: SpinnerId, tMs: number, o: { color: string; bg: string; fg: string }, st: OrbState = 'think'): Cell[][] { +export function spinnerCells(id: SpinnerId, tMs: number, o: CellOpts, st: OrbState = 'think'): Cell[][] { tMs = Math.max(0, tMs || 0) const s = SPINNERS[id] if (s.kind === 'glyph') { const f = [...s.frames]; return [[{ ch: f[Math.floor(tMs / 120) % f.length]!, fg: o.color }]] } if (s.kind === 'art') return s.art[Math.floor(tMs / 220) % s.art.length]!.map(r => [...r].map(ch => ({ ch, fg: CLAWD }))) + if (s.kind === 'scan') return scanCells(tMs, st, o) if (s.kind === 'dots') { const cv = canvas(s.cols * 2, s.rows * 4); s.draw(cv, tMs / 1000, st); return dotCells(cv, s.cols, s.rows, o.color, o.bg) } const cv = canvas(s.cols, s.rows * 2); s.draw(cv, tMs / 1000, st) return halfCells(cv, s.cols, s.rows, s.useFg ? o.fg : o.color, o.bg) @@ -139,13 +194,54 @@ export type WordLook = { bg: string gradient?: [string, string] motion: { spinner: SpinnerId; shimmer: number; color: string } - theme: { colors: { text: string; accent: string } } + theme: { colors: { text: string; accent: string; read?: string; agent?: string } } +} + +// One span per run of equal style: each span is an element the surface diffs every frame. +function runs(spans: Span[]): Span[] { + const out: Span[] = [] + for (const s of spans) { + const last = out.at(-1) + if (last && last.color === s.color && last.bg === s.bg && !!last.bold === !!s.bold) last.text += s.text + else out.push({ ...s }) + } + return out +} + +function scanWord(look: WordLook, text: string, t: number, st: OrbState): Span[] { + const chars = [...text], pos = scanPos(t, chars.length, st), c = look.theme.colors + return runs(chars.map((ch, i) => { + const d = pos - (SCAN_BAR + 1 + i) + return d >= 0 && d < 2 ? { text: ch, color: look.bg, bg: c.accent, bold: true } : { text: ch, color: c.text, bold: true } + })) +} + +// Seeded by the 70 ms frame, so every redraw within a frame draws the same glitch. A tear repeats +// the first letter in front and drops the trailing space: the line never changes width. Only +// width-1 letters become blocks or get torn; wide and zero-width ones take the background variant. +function glitchWord(look: WordLook, text: string, t: number, st: OrbState): Span[] { + const c = look.theme.colors, a = c.read ?? c.accent, b = c.agent ?? c.text + let seed = (Math.floor(Math.max(0, t || 0) / 70) * 9301 + 49297) % 233280 + const rnd = () => (seed = (seed * 9301 + 49297) % 233280) / 233280 + const k = st === 'run' || st === 'agents' ? 2 : 1, chars = [...text], out: Span[] = [] + const tear = rnd() < 0.12 && chars[0] !== undefined && cellWidth(chars[0].codePointAt(0)!) === 1 + if (tear) out.push({ text: chars[0]!, color: b }) + for (const ch of chars) { + const r = rnd(), pick = rnd() < 0.5 ? a : b, block = '▓▒░█▚'[Math.floor(rnd() * 5)]! + if (r < 0.10 * k && cellWidth(ch.codePointAt(0)!) === 1) out.push({ text: block, color: pick }) + else if (r < 0.16 * k) out.push({ text: ch, color: look.bg, bg: pick }) + else out.push({ text: ch, color: c.text, bold: true }) + } + if (!tear) out.push({ text: ' ', color: c.text }) + return runs(out) } -export function spinnerWordSpans(look: WordLook, text: string, now: number): Span[] { +export function spinnerWordSpans(look: WordLook, text: string, now: number, at?: { t: number; st: OrbState }): Span[] { const { spinner, shimmer, color } = look.motion const c = look.theme.colors const [c1, c2] = look.gradient ?? [c.accent, c.text] + if (spinner === 'scanline') return scanWord(look, text, at?.t ?? now, at?.st ?? 'think') + if (spinner === 'glitch') return glitchWord(look, text, at?.t ?? now, at?.st ?? 'think') if (spinner === 'shimmer') return wave3(text, mix(color, look.bg, 0.45), mix(color, '#ffffff', 0.6), now, Math.max(1, shimmer)) if (shimmer > 0) return wave(text, c1, c2, now, shimmer * 1.2).map(s => ({ ...s, bold: true })) if (look.gradient) return gradient(text, c1, c2).map(s => ({ ...s, bold: true })) diff --git a/hooks/packexport.ts b/hooks/packexport.ts index 5db27e12..485a3b75 100644 --- a/hooks/packexport.ts +++ b/hooks/packexport.ts @@ -30,7 +30,12 @@ export type ClawdExport = { cols: number; rows: PetSpan[][] } export type Export = { packs: PackExport[]; themes: ThemeExport[]; spinners: SpinnerExport[]; clawd: ClawdExport } const FRAME_MS: Record = { stock: 120, clawd: 220 } -const FRAMES: Record = { stock: 6, clawd: 4 } +const FRAMES: Record = { stock: 6, clawd: 4, scanline: 29, ring: 16, signal: 67 } +// Spinners whose motion repeats on its own period get their frames spread over exactly that period, +// so the preview wraps without a jump. scanline: bar 10 + gap 1 + the default 12-letter word + pause 6 +// cells at 45 ms. ring: one turn. signal: 10 base cycles of 160*2pi ms, the first time its 2.7x +// harmonic lines up again; 67 frames land on a whole 150 ms, so ms * frames stays within 3 ms of it. +const PERIOD_MS: Record = { scanline: 29 * 45, ring: 2000, signal: 3200 * Math.PI } const MONO = { color: '#ffffff', bg: '#000000', fg: '#ffffff' } export function packExport(): PackExport[] { @@ -60,9 +65,10 @@ export const themeExport = (): ThemeExport[] => Object.keys(PRESETS).map(name => }) export const spinnerExport = (): SpinnerExport[] => SPINNER_IDS.map(id => { - const ms = FRAME_MS[id] ?? 150 + const n = FRAMES[id] ?? 12, period = PERIOD_MS[id] + const step = period === undefined ? FRAME_MS[id] ?? 150 : period / n const s = SPINNERS[id] - return { id, name: s.name, ms, text: 'useFg' in s && s.useFg === true, frames: Array.from({ length: FRAMES[id] ?? 12 }, (_, i) => spinnerCells(id, i * ms, MONO)) } + return { id, name: s.name, ms: Math.round(step), text: 'useFg' in s && s.useFg === true, frames: Array.from({ length: n }, (_, i) => spinnerCells(id, i * step, MONO)) } }) // The idle pose's first frame, as the band draws it: no outfit, not mirrored. diff --git a/hooks/packs.ts b/hooks/packs.ts index e47326ca..27c9816c 100644 --- a/hooks/packs.ts +++ b/hooks/packs.ts @@ -3,9 +3,9 @@ import { resolveTheme, PRESETS, COLOR_KEYS, GLYPH_KEYS, shown, isPlain, isUnsafe import { PACKS } from './packpresets.ts' import type { PetTint } from './pets.ts' -export const SPINNER_IDS = ['stock', 'comet', 'eyes', 'orb-states', 'clawd', 'shimmer'] as const +export const SPINNER_IDS = ['stock', 'comet', 'eyes', 'orb-states', 'clawd', 'shimmer', 'scanline', 'ring', 'glitch', 'signal'] as const export type SpinnerId = (typeof SPINNER_IDS)[number] -export const ROW_STYLES = ['classic', 'cards', 'minimal', 'retro'] as const +export const ROW_STYLES = ['classic', 'cards', 'minimal', 'retro', 'slab'] as const export type RowStyle = (typeof ROW_STYLES)[number] export const BORDERS = ['round', 'single', 'double', 'bold', 'classic'] as const export type Border = (typeof BORDERS)[number] diff --git a/hooks/register.tsx b/hooks/register.tsx index 4782d218..965c6955 100644 --- a/hooks/register.tsx +++ b/hooks/register.tsx @@ -19,6 +19,7 @@ import { renderBand } from './band.tsx' import { renderPane, bubbleBox, petStripCols, visibleTabs, type PaneExtra, type PaneView, type TabId } from './pane.tsx' import { spinnerWord, newTurnWord } from './restyle.ts' import { styleRow } from './rows.tsx' +import { makeTurns } from './turns.ts' import { orbStateOf, usesOwnSpinner, checkedSpinnerProps } from './spinner.ts' import type { PetClientProps } from './client/pet.tsx' import type { OrbState } from './motion.ts' @@ -233,20 +234,16 @@ function ask($: Engine, slot: Asked, key: string, call: () => Promise() +const turns = makeTurns() const dividerByMessage = new Map() const palette = () => ({ ...theme.colors }) // What a held answer was asked under: both layers' packs and the effects the look picked, // since a mix can take its colors and its motion from different packs. const lookKey = () => JSON.stringify([mix.colors, mix.motion, look.meters, look.dividers, look.motion.field]) // Asked while drawing: one short call per prompt and pack, kept for its redraws. -// A prompt first draws under the id "placeholder" until it is stored: it shows the number the -// stored row will take, but neither claims it nor is kept. async function dividerFor($: Engine, messageId: string): Promise { const pending = messageId === 'placeholder' - if (!pending && !turnByMessage.has(messageId)) turnByMessage.set(messageId, turnByMessage.size + 1) - const turn = turnByMessage.get(messageId) ?? turnByMessage.size + 1, pack = mix.colors, colors = palette() + const turn = turns.turnFor(messageId), pack = mix.colors, colors = palette() const key = JSON.stringify([lookKey(), colors]) const held = pending ? undefined : dividerByMessage.get(messageId) if (held?.key === key) return held.value @@ -1123,7 +1120,7 @@ export const register: Register = (on, options) => { const p = e.props const own = p.origin.kind === 'composer' && !p.from && !p.task const els = $.ui.resolve(e) - const styled = styleRow(els, look, { site: 'UserMessage', text: p.text, isExpanded: p.isExpanded, own }, row) as RenderElement + const styled = styleRow(els, look, { site: 'UserMessage', text: p.text, isExpanded: p.isExpanded, own, turn: own && !p.isExpanded ? turns.turnFor(e.requestId) : undefined }, row) as RenderElement if (!own || p.isExpanded) return styled const rule = await dividerFor($, e.requestId) if (!rule) return styled @@ -1154,7 +1151,7 @@ export const register: Register = (on, options) => { const row = await next(e) if (e.surface !== 'terminal') return row const p = e.props - return styleRow($.ui.resolve(e), look, { site: 'ToolUse', tool: p.tool, input: p.input, isRunning: p.isRunning, isErrored: p.isErrored, isInterrupted: p.isInterrupted }, row) as RenderElement + return styleRow($.ui.resolve(e), look, { site: 'ToolUse', tool: p.tool, input: p.input, isRunning: p.isRunning, isErrored: p.isErrored, isInterrupted: p.isInterrupted, seq: turns.toolSeq(e.requestId) }, row) as RenderElement }) on('ui.render', { component: 'ToolResult' }, async ($, e, next) => { if (off) return next(e) diff --git a/hooks/rows-text.ts b/hooks/rows-text.ts index 19054319..8774230a 100644 --- a/hooks/rows-text.ts +++ b/hooks/rows-text.ts @@ -1,7 +1,13 @@ // JSX-free: rows.tsx and the docs gallery draw the same marks and tags. +export const SLAB_MARKS = { done: 'OK', fail: 'FAIL', stop: 'STOP', run: '…' } + export const MARKS = { cards: { done: '✓', fail: '✗', stop: '■', run: '…' }, retro: { done: '[ OK ]', fail: '[FAIL]', stop: '[STOP]', run: '[....]' }, + slab: SLAB_MARKS, } +// An MCP tool's name is mcp__server__tool; the last part is the one a reader knows. +export const slabTag = (tool: string): string => (tool.startsWith('mcp__') ? tool.split('__').at(-1)! : tool).toUpperCase().slice(0, 6).padEnd(6) + export const retroTag = (tool: string): string => `[${tool.toUpperCase().slice(0, 6).padEnd(6)}] ` diff --git a/hooks/rows.tsx b/hooks/rows.tsx index d87052bb..4d18960c 100644 --- a/hooks/rows.tsx +++ b/hooks/rows.tsx @@ -1,13 +1,13 @@ import type { Look } from './packs.ts' -import { gradient } from './color.ts' +import { gradient, mix } from './color.ts' import { describeTool } from './events.ts' import { toolGlyph } from './restyle.ts' -import { MARKS, retroTag } from './rows-text.ts' +import { MARKS, retroTag, slabTag } from './rows-text.ts' export type RowInput = - | { site: 'ToolUse'; tool: string; input: unknown; isRunning: boolean; isErrored: boolean; isInterrupted: boolean } + | { site: 'ToolUse'; tool: string; input: unknown; isRunning: boolean; isErrored: boolean; isInterrupted: boolean; seq?: number } | { site: 'ToolResult' } - | { site: 'UserMessage'; text: string; isExpanded: boolean; own: boolean } + | { site: 'UserMessage'; text: string; isExpanded: boolean; own: boolean; turn?: number } | { site: 'AssistantMessage'; isFirstOfReply: boolean; xp?: number } type Els = { Box: any; Text: any } @@ -71,6 +71,18 @@ function xpTag({ Box, Text }: Els, xp: number, color: string) { return {`+${xp} XP`} } +function roleColor(look: Look, tool: string, input: Record): string { + const c = look.theme.colors + switch (describeTool(tool, input).kind) { + case 'read': case 'search': return c.read + case 'edit': return c.edit + case 'shell': return c.shell + case 'agent': return c.agent + case 'plan': return c.accent + default: return c.dim + } +} + export function styleRow(els: Els, look: Look, row: RowInput, engine: unknown, opts: { prefixCards?: boolean } = {}): unknown { const out = draw(els, look, row, engine, opts) if (look.rows === 'classic') return out @@ -128,6 +140,38 @@ function draw(els: Els, look: Look, row: RowInput, engine: unknown, opts: { pref const m = mark(look, row, MARKS.retro) return {label(els, retroTag(row.tool), look, c.accent)}{shrinker(Box, engine)}{fixed(els, m.color, ' ' + m.mark)} } + + case 'slab': { + if (row.site === 'UserMessage') { + // The engine row opens with a blank margin line (see card()); marginTop keeps that gap, + // where a divider overlay can still sit. + return ( + + {row.text} + {row.turn ? {` PROMPT ${String(row.turn).padStart(2, '0')} `} : null} + + ) + } + if (row.site === 'ToolResult') return {engine} + if (row.site === 'AssistantMessage') { + if (!row.isFirstOfReply) return engine + return ( + + {engine} + {'─'.repeat(8)} + + ) + } + const m = mark(look, row, MARKS.slab) + const n = row.seq ? String(row.seq).padStart(2, '0') + ' ' : '' + return ( + + {n}{slabTag(row.tool)}{' '} + {shrinker(Box, engine)} + {fixed(els, m.color, ' ' + m.mark)} + + ) + } } return engine } catch { diff --git a/hooks/spinner.ts b/hooks/spinner.ts index 742ea987..50490735 100644 --- a/hooks/spinner.ts +++ b/hooks/spinner.ts @@ -12,7 +12,7 @@ export type SpinLook = { bg: string gradient?: [string, string] motion: { spinner: SpinnerId; shimmer: 0 | 1 | 2; color: string } - theme: { colors: { text: string; accent: string; dim: string } } + theme: { colors: { text: string; accent: string; dim: string; read: string; agent: string } } } export type SpinnerProps = { look: SpinLook; input: SpinInput; reduced: boolean } @@ -36,15 +36,16 @@ export function spinnerLine(look: SpinLook, input: SpinInput, now: number): { ba const { spinner, color } = look.motion const c = look.theme.colors const t = now - input.turnAt - const badge = cellsToSpans(spinnerCells(spinner, t, { color, bg: look.bg, fg: c.text }, input.state)) - const word = spinnerWordSpans(look, input.word + '…', now) - return { badge, word, tail: `(${elapsed(t)} · esc to interrupt)`, ...(SPINNERS[spinner].rows >= 2 ? { detail: input.detail } : {}) } + const word = input.word + '…' + const badge = cellsToSpans(spinnerCells(spinner, t, { color, bg: look.bg, fg: c.text, wordLen: [...word].length }, input.state)) + const spans = spinnerWordSpans(look, word, now, { t, st: input.state }) + return { badge, word: spans, tail: `(${elapsed(t)} · esc to interrupt)`, ...(SPINNERS[spinner].rows >= 2 ? { detail: input.detail } : {}) } } export function spinnerProps(look: Look, input: SpinInput, reduced: boolean): SpinnerProps { - const { text, accent, dim } = look.theme.colors + const { text, accent, dim, read, agent } = look.theme.colors return { - look: { bg: look.bg, ...(look.gradient ? { gradient: look.gradient } : {}), motion: look.motion, theme: { colors: { text, accent, dim } } }, + look: { bg: look.bg, ...(look.gradient ? { gradient: look.gradient } : {}), motion: look.motion, theme: { colors: { text, accent, dim, read, agent } } }, input, reduced, } diff --git a/hooks/turns.ts b/hooks/turns.ts new file mode 100644 index 00000000..d5014b0f --- /dev/null +++ b/hooks/turns.ts @@ -0,0 +1,21 @@ +// Turns are numbered in the order their prompts are first drawn, which is transcript order. A +// prompt first draws under the id "placeholder" until it is stored: it shows the number the stored +// row will take, but neither claims it nor is kept. Claude Code redraws old rows on every +// invalidate, so a tool keeps the number it first drew with. +export function makeTurns() { + const turnByMessage = new Map() + const seqByTool = new Map() + let turn = 0, inTurn = 0 + return { + turnFor(messageId: string): number { + if (messageId === 'placeholder') return turnByMessage.size + 1 + if (!turnByMessage.has(messageId)) turnByMessage.set(messageId, turnByMessage.size + 1) + return turnByMessage.get(messageId)! + }, + toolSeq(toolId: string): number { + if (turnByMessage.size !== turn) { turn = turnByMessage.size; inTurn = 0 } + if (!seqByTool.has(toolId)) seqByTool.set(toolId, ++inTurn) + return seqByTool.get(toolId)! + }, + } +} diff --git a/installer/internal/packs/packs.json b/installer/internal/packs/packs.json index 85f6c56c..3ecf73c3 100644 --- a/installer/internal/packs/packs.json +++ b/installer/internal/packs/packs.json @@ -2589,6 +2589,8036 @@ ] ] ] + }, + { + "id": "scanline", + "name": "scanline sweep", + "ms": 45, + "text": false, + "frames": [ + [ + [ + { + "ch": "█", + "fg": "#ffffff" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#ffffff" + }, + { + "ch": "█", + "fg": "#ffffff" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#ffffff" + }, + { + "ch": "▚", + "fg": "#ffffff" + }, + { + "ch": "█", + "fg": "#ffffff" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#ffffff" + }, + { + "ch": "▞", + "fg": "#ffffff" + }, + { + "ch": "█", + "fg": "#ffffff" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#ffffff" + }, + { + "ch": "▚", + "fg": "#ffffff" + }, + { + "ch": "█", + "fg": "#ffffff" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#ffffff" + }, + { + "ch": "▞", + "fg": "#ffffff" + }, + { + "ch": "█", + "fg": "#ffffff" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#ffffff" + }, + { + "ch": "▚", + "fg": "#ffffff" + }, + { + "ch": "█", + "fg": "#ffffff" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#ffffff" + }, + { + "ch": "▞", + "fg": "#ffffff" + }, + { + "ch": "█", + "fg": "#ffffff" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#ffffff" + }, + { + "ch": "▚", + "fg": "#ffffff" + }, + { + "ch": "█", + "fg": "#ffffff" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#ffffff" + }, + { + "ch": "▚", + "fg": "#ffffff" + }, + { + "ch": "█", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#ffffff" + }, + { + "ch": "▞", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + } + ] + ], + [ + [ + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + }, + { + "ch": "▚", + "fg": "#404040" + }, + { + "ch": "▞", + "fg": "#404040" + } + ] + ] + ] + }, + { + "id": "ring", + "name": "angel ring", + "ms": 125, + "text": false, + "frames": [ + [ + [ + { + "ch": "⠠", + "fg": "#7f7f7f" + }, + { + "ch": "⠂", + "fg": "#9f9f9f" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠂", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#dfdfdf" + } + ], + [ + { + "ch": "⠁", + "fg": "#5f5f5f" + }, + { + "ch": "⠄", + "fg": "#5f5f5f" + }, + { + "ch": "⡀", + "fg": "#3f3f3f" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⡀", + "fg": "#5f5f5f" + }, + { + "ch": "⠂", + "fg": "#7f7f7f" + }, + { + "ch": "⠁", + "fg": "#9f9f9f" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠐", + "fg": "#bfbfbf" + }, + { + "ch": "⢀", + "fg": "#dfdfdf" + } + ], + [ + { + "ch": "⠈", + "fg": "#5f5f5f" + }, + { + "ch": "⠄", + "fg": "#3f3f3f" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠂", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⡠", + "fg": "#5f5f5f" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠂", + "fg": "#7f7f7f" + }, + { + "ch": "⠁", + "fg": "#9f9f9f" + }, + { + "ch": "⠐", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + } + ], + [ + { + "ch": "⠐", + "fg": "#3f3f3f" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠠", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#dfdfdf" + } + ] + ], + [ + [ + { + "ch": "⠠", + "fg": "#5f5f5f" + }, + { + "ch": "⠐", + "fg": "#5f5f5f" + }, + { + "ch": "⠈", + "fg": "#7f7f7f" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠂", + "fg": "#9f9f9f" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + } + ], + [ + { + "ch": "⠁", + "fg": "#3f3f3f" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠠", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠊", + "fg": "#dfdfdf" + } + ] + ], + [ + [ + { + "ch": "⢀", + "fg": "#3f3f3f" + }, + { + "ch": "⠂", + "fg": "#5f5f5f" + }, + { + "ch": "⠈", + "fg": "#5f5f5f" + }, + { + "ch": "⠐", + "fg": "#7f7f7f" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#ffffff" + }, + { + "ch": "⠄", + "fg": "#dfdfdf" + }, + { + "ch": "⠂", + "fg": "#bfbfbf" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠂", + "fg": "#3f3f3f" + }, + { + "ch": "⠁", + "fg": "#5f5f5f" + }, + { + "ch": "⠈", + "fg": "#5f5f5f" + }, + { + "ch": "⠐", + "fg": "#7f7f7f" + }, + { + "ch": "⢀", + "fg": "#9f9f9f" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠄", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#dfdfdf" + }, + { + "ch": "⠠", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠐", + "fg": "#3f3f3f" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠁", + "fg": "#5f5f5f" + }, + { + "ch": "⠐", + "fg": "#5f5f5f" + }, + { + "ch": "⠄", + "fg": "#7f7f7f" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠄", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#dfdfdf" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠠", + "fg": "#bfbfbf" + }, + { + "ch": "⠈", + "fg": "#9f9f9f" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#3f3f3f" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠂", + "fg": "#5f5f5f" + }, + { + "ch": "⢄", + "fg": "#7f7f7f" + } + ], + [ + { + "ch": "⠐", + "fg": "#ffffff" + }, + { + "ch": "⠄", + "fg": "#dfdfdf" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": "⠂", + "fg": "#9f9f9f" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#3f3f3f" + }, + { + "ch": "⠐", + "fg": "#5f5f5f" + }, + { + "ch": "⢀", + "fg": "#5f5f5f" + } + ], + [ + { + "ch": "⠑", + "fg": "#ffffff" + }, + { + "ch": "⠠", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": "⠠", + "fg": "#9f9f9f" + }, + { + "ch": "⠂", + "fg": "#7f7f7f" + } + ] + ], + [ + [ + { + "ch": "⠠", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠐", + "fg": "#3f3f3f" + }, + { + "ch": "⡀", + "fg": "#5f5f5f" + } + ], + [ + { + "ch": "⠁", + "fg": "#dfdfdf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": "⢀", + "fg": "#9f9f9f" + }, + { + "ch": "⠠", + "fg": "#7f7f7f" + }, + { + "ch": "⠈", + "fg": "#5f5f5f" + } + ] + ], + [ + [ + { + "ch": "⡀", + "fg": "#dfdfdf" + }, + { + "ch": "⠂", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠄", + "fg": "#3f3f3f" + } + ], + [ + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": "⢀", + "fg": "#9f9f9f" + }, + { + "ch": "⠠", + "fg": "#7f7f7f" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠊", + "fg": "#5f5f5f" + } + ] + ], + [ + [ + { + "ch": "⡠", + "fg": "#dfdfdf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠂", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#3f3f3f" + } + ], + [ + { + "ch": "⠐", + "fg": "#bfbfbf" + }, + { + "ch": "⠠", + "fg": "#9f9f9f" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#7f7f7f" + }, + { + "ch": "⠄", + "fg": "#5f5f5f" + }, + { + "ch": "⠂", + "fg": "#5f5f5f" + } + ] + ], + [ + [ + { + "ch": "⠠", + "fg": "#bfbfbf" + }, + { + "ch": "⠐", + "fg": "#dfdfdf" + }, + { + "ch": "⠈", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠄", + "fg": "#7f7f7f" + }, + { + "ch": "⡀", + "fg": "#5f5f5f" + }, + { + "ch": "⠠", + "fg": "#5f5f5f" + }, + { + "ch": "⠁", + "fg": "#3f3f3f" + } + ] + ], + [ + [ + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠂", + "fg": "#bfbfbf" + }, + { + "ch": "⠈", + "fg": "#dfdfdf" + }, + { + "ch": "⠐", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠁", + "fg": "#9f9f9f" + }, + { + "ch": "⠄", + "fg": "#7f7f7f" + }, + { + "ch": "⡀", + "fg": "#5f5f5f" + }, + { + "ch": "⢀", + "fg": "#5f5f5f" + }, + { + "ch": "⠠", + "fg": "#3f3f3f" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⡀", + "fg": "#9f9f9f" + }, + { + "ch": "⠂", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": "⠈", + "fg": "#dfdfdf" + }, + { + "ch": "⠐", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠐", + "fg": "#7f7f7f" + }, + { + "ch": "⠄", + "fg": "#5f5f5f" + }, + { + "ch": "⢀", + "fg": "#5f5f5f" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠄", + "fg": "#3f3f3f" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⠠", + "fg": "#9f9f9f" + }, + { + "ch": "⠐", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": "⠐", + "fg": "#dfdfdf" + }, + { + "ch": "⠄", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠑", + "fg": "#7f7f7f" + }, + { + "ch": "⠠", + "fg": "#5f5f5f" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#3f3f3f" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ] + ] + }, + { + "id": "glitch", + "name": "glitch word", + "ms": 150, + "text": false, + "frames": [ + [ + [ + { + "ch": "◆", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "◆", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "◆", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "◆", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "◆", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "◆", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "◆", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "◆", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "◆", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "◆", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "◆", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "◆", + "fg": "#ffffff" + } + ] + ] + ] + }, + { + "id": "signal", + "name": "signal trace", + "ms": 150, + "text": false, + "frames": [ + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠠", + "fg": "#ffffff" + }, + { + "ch": "⢄", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠐", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#ffffff" + }, + { + "ch": "⠊", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#ffffff" + }, + { + "ch": "⠤", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠂", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#ffffff" + }, + { + "ch": "⠊", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠠", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠂", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠠", + "fg": "#ffffff" + }, + { + "ch": "⢄", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠐", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#ffffff" + }, + { + "ch": "⠊", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠈", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#ffffff" + }, + { + "ch": "⠤", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠐", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#ffffff" + }, + { + "ch": "⠊", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#ffffff" + }, + { + "ch": "⠤", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + }, + { + "ch": "⠊", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠂", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#ffffff" + }, + { + "ch": "⠊", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#ffffff" + }, + { + "ch": "⢄", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠐", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠂", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠠", + "fg": "#ffffff" + }, + { + "ch": "⠤", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠐", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#ffffff" + }, + { + "ch": "⠊", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#ffffff" + }, + { + "ch": "⠤", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + }, + { + "ch": "⠊", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠂", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#ffffff" + }, + { + "ch": "⠊", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#ffffff" + }, + { + "ch": "⢄", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠒", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⡀", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#ffffff" + }, + { + "ch": "⡀", + "fg": "#ffffff" + } + ], + [ + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠁", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⢀", + "fg": "#bfbfbf" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⠄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠂", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠈", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠤", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠒", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#ffffff" + } + ] + ], + [ + [ + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⡠", + "fg": "#bfbfbf" + }, + { + "ch": "⣀", + "fg": "#bfbfbf" + }, + { + "ch": "⢄", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + } + ], + [ + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠉", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#bfbfbf" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": " ", + "fg": "#ffffff" + }, + { + "ch": "⠑", + "fg": "#bfbfbf" + }, + { + "ch": "⠊", + "fg": "#ffffff" + }, + { + "ch": "⠉", + "fg": "#ffffff" + } + ] + ] + ] } ], "clawd": { diff --git a/installer/internal/packs/packs_test.go b/installer/internal/packs/packs_test.go index ae6fb761..61eaa4ae 100644 --- a/installer/internal/packs/packs_test.go +++ b/installer/internal/packs/packs_test.go @@ -52,7 +52,7 @@ func TestThemesSpinnersAndClawd(t *testing.T) { if d, ok := ThemeByName("dusk"); !ok || d.Colors.Accent != "#b69cff" || d.Colors.Panel == "" { t.Fatalf("dusk = %+v", d) } - if want := []string{"stock", "comet", "eyes", "orb-states", "clawd", "shimmer"}; !slices.Equal(SpinnerIDs(), want) { + if want := []string{"stock", "comet", "eyes", "orb-states", "clawd", "shimmer", "scanline", "ring", "glitch", "signal"}; !slices.Equal(SpinnerIDs(), want) { t.Fatalf("SpinnerIDs() = %v, want %v", SpinnerIDs(), want) } for _, s := range Spinners() { diff --git a/installer/internal/tui/model.go b/installer/internal/tui/model.go index 004a767c..47de08fc 100644 --- a/installer/internal/tui/model.go +++ b/installer/internal/tui/model.go @@ -18,9 +18,9 @@ import ( const ( FormWidth = 30 // FormRows is the height the form column is padded to. The groups differ in height - // (four packs, seven themes) and bubbletea's inline renderer strands a line each - // time the view gets shorter, so the form never draws less than this. - FormRows = 14 + // (four packs, seven themes, eleven spinner options) and bubbletea's inline renderer + // strands a line each time the view gets shorter, so the form never draws less than this. + FormRows = 16 // With room for all three columns Clawd and his bubble stand left of the form. // Below it the form and preview sit side by side, and below that the preview diff --git a/skills/glowup-pack/reference.md b/skills/glowup-pack/reference.md index 55c543ff..75427c79 100644 --- a/skills/glowup-pack/reference.md +++ b/skills/glowup-pack/reference.md @@ -24,7 +24,7 @@ A pack is a JSON file at `~/.claude/glowup/packs/.json` (`$CLAUDE_CONFIG_D | `theme` | a theme name | Start the palette from this theme: `classic`, `glowup`, `aurora`, `dusk`, `cyberpunk`, `vaporwave`, `high-contrast`, or a file in `~/.claude/glowup/themes`. Default `classic`. | | `palette` | object of theme color keys | Colors that replace the theme's. Keys are listed under Theme colors. Set only what differs. | | `bg` | `#rrggbb` | The terminal background glowup fades toward. Match the user's terminal background. Default: the palette's `panel`. | -| `rows` | `classic`, `cards`, `minimal`, `retro` | Transcript row style. `classic` leaves rows alone, `cards` draws side bars and bordered tool cards, `minimal` is quiet one-liners, `retro` uses `[TAG]` labels. Default `classic`. | +| `rows` | `classic`, `cards`, `minimal`, `retro`, `slab` | Transcript row style. `classic` leaves rows alone, `cards` draws side bars and bordered tool cards, `minimal` is quiet one-liners, `retro` uses `[TAG]` labels, `slab` draws your prompt as an accent bar numbered by turn and numbers each tool call. Default `classic`. | | `border` | `round`, `single`, `double`, `bold`, `classic` | Border shape around cards. Also the shape of the status box and of the pane's section boxes. Default `round`. | | `borderColor` | `#rrggbb` | Border color. Default: the palette's `faint`. | | `gradient` | array of exactly two `#rrggbb` | Start and end color of the card gradient and spinner shimmer. Default: none. | @@ -48,7 +48,7 @@ A pack is a JSON file at `~/.claude/glowup/packs/.json` (`$CLAUDE_CONFIG_D | Field | Values | Meaning | | --- | --- | --- | -| `spinner` | `stock`, `comet`, `eyes`, `orb-states`, `clawd`, `shimmer` | The spinner. `stock` is Claude Code's own. Use only these: an id glowup lacks falls back to `stock` with a notice. Default `stock`. | +| `spinner` | `stock`, `comet`, `eyes`, `orb-states`, `clawd`, `shimmer`, `scanline`, `ring`, `glitch`, `signal` | The spinner. `stock` is Claude Code's own. Use only these: an id glowup lacks falls back to `stock` with a notice. Default `stock`. `scanline` and `glitch` also light or glitch the spinner word. | | `shimmer` | `0`, `1`, `2` | Shimmer across the spinner word: none, soft, fast. Default `1`. | | `color` | `#rrggbb` | Spinner color. Default: the palette's `accent`. | | `field` | `none`, `warp`, `simplex`, or an object | An animated texture in the docked pane's open rows, between the tab and the status box, dithered into braille dots of one color, the palette's `faint` unless `field.color` sets it. `simplex` is Paper's dithering shader's simplex shape, and Paper settings for it carry over as they are, except `size`; `warp` is noise folded through itself. A name uses the default knobs; an object takes a `shape` and any of the knobs below. It holds still under reduced motion. Default `none`. | diff --git a/test/command.test.ts b/test/command.test.ts index ca2dbe84..e291c149 100644 --- a/test/command.test.ts +++ b/test/command.test.ts @@ -306,7 +306,7 @@ test('an unknown spinner is refused and lists the valid ones', async () => { const { host, store } = fakeHost() const { calls, ctl: c } = ctl() const out = await runCommand(host, 'spinner nope', c) - for (const n of ['stock', 'comet', 'eyes', 'orb-states', 'clawd', 'shimmer']) expect(out).toContain(n) + for (const n of ['stock', 'comet', 'eyes', 'orb-states', 'clawd', 'shimmer', 'scanline', 'ring', 'glitch', 'signal']) expect(out).toContain(n) expect(store.mix).toBeUndefined() expect(calls).toEqual([]) }) diff --git a/test/engine-tree.test.ts b/test/engine-tree.test.ts index 6b963a6b..c75388ab 100644 --- a/test/engine-tree.test.ts +++ b/test/engine-tree.test.ts @@ -2,7 +2,7 @@ import { expect, mock } from 'claude-code/testing' import { fakeFs, runGlowup, test } from './kit.ts' import { REFUSED_ABOVE_ENGINE, engineViolations } from './engine-tree.ts' import { styleRow, type RowInput } from '../hooks/rows.tsx' -import { resolveLook, type Look } from '../hooks/packs.ts' +import { ROW_STYLES, resolveLook, type Look } from '../hooks/packs.ts' import { PACKS } from '../hooks/packpresets.ts' const ENGINE = { type: 'engine', ref: 0 } @@ -58,7 +58,7 @@ test('no row style, site or pack puts a refused Box prop above the engine node', let checked = 0 for (const name of Object.keys(PACKS)) { const base = resolveLook({ colors: name, motion: name }, {}, {}).look - for (const rows of ['classic', 'cards', 'minimal', 'retro'] as const) { + for (const rows of ROW_STYLES) { const look: Look = { ...base, rows } for (const prefixCards of [false, true]) { for (const row of rowsFor()) { @@ -69,7 +69,7 @@ test('no row style, site or pack puts a refused Box prop above the engine node', } } } - expect(checked).toBe(Object.keys(PACKS).length * 4 * 2 * rowsFor().length) + expect(checked).toBe(Object.keys(PACKS).length * ROW_STYLES.length * 2 * rowsFor().length) }) // The engine's own validator over the whole hook: what the owner's session ran. diff --git a/test/motion.test.ts b/test/motion.test.ts index a6144adc..08c27463 100644 --- a/test/motion.test.ts +++ b/test/motion.test.ts @@ -1,6 +1,6 @@ import { test, expect } from 'claude-code/testing' import { mix, gradient, wave } from '../hooks/color.ts' -import { SPINNERS, spinnerCells, cellsToSpans, type SpinnerId } from '../hooks/motion.ts' +import { SPINNERS, spinnerCells, scanPos, cellsToSpans, spinnerWordSpans, SCAN_BAR, type SpinnerId, type WordLook } from '../hooks/motion.ts' import { cellWidth } from '../hooks/cells.ts' const O = { color: '#38e8ff', bg: '#1a0b33', fg: '#f0e6ff' } @@ -25,8 +25,8 @@ test('wave moves with time', async () => { expect(at(0)).not.toBe(at(400)) }) -test('the 0.2 library is exactly these six', async () => { - expect(ids.sort()).toEqual(['clawd', 'comet', 'eyes', 'orb-states', 'shimmer', 'stock']) +test('the library is exactly these ten', async () => { + expect(ids.sort()).toEqual(['clawd', 'comet', 'eyes', 'glitch', 'orb-states', 'ring', 'scanline', 'shimmer', 'signal', 'stock']) }) test('every frame has its declared size and only width-1 BMP cells', async () => { @@ -46,13 +46,52 @@ test('every frame has its declared size and only width-1 BMP cells', async () => }) test('frames are deterministic and move over time', async () => { - for (const id of ['comet', 'eyes', 'orb-states', 'clawd'] as const) { + for (const id of ['comet', 'eyes', 'orb-states', 'clawd', 'scanline', 'ring', 'signal'] as const) { const at = (t: number) => JSON.stringify(spinnerCells(id, t, O)) expect(at(1000)).toBe(at(1000)) expect(new Set([0, 250, 1000, 3450].map(at)).size).toBeGreaterThan(1) } }) +test('every new spinner keeps its size in every state and is never blank', async () => { + for (const id of ['scanline', 'ring', 'glitch', 'signal'] as const) for (const st of ['think', 'search', 'work', 'run', 'agents'] as const) for (const t of [0, 1000, 10_000]) { + const rows = spinnerCells(id, t, O, st) + expect(rows.length).toBe(SPINNERS[id].rows) + for (const r of rows) expect(r.length).toBe(SPINNERS[id].cols) + expect(rows.flat().some(c => c.ch !== ' ')).toBe(true) + } +}) + +test('scanline: the scan cell is a full block that crosses the bar left to right', async () => { + const at = (t: number) => spinnerCells('scanline', t, { ...O, wordLen: 10 }).flat().findIndex(c => c.ch === '█') + expect(at(0)).toBe(0) + expect(at(45 * 4)).toBe(4) + expect(at(45 * 12)).toBe(-1) // past the bar, scanning the word +}) + +test('scanline: run and agents scan faster than think', async () => { + expect(scanPos(450, 10, 'run')).toBeGreaterThan(scanPos(450, 10, 'think')) + expect(scanPos(450, 10, 'agents')).toBe(scanPos(450, 10, 'run')) +}) + +test('scanline: the scan cell reads on a light pack', async () => { + const light = { color: '#0000f2', bg: '#f2f2f2', fg: '#000091' } + const hi = spinnerCells('scanline', 0, light).flat()[0]! + expect(hi.ch).toBe('█') + expect(hi.fg).toBe('#000091') +}) + +test('ring and signal react to the activity state', async () => { + for (const id of ['ring', 'signal'] as const) { + const frames = (['think', 'run', 'agents'] as const).map(st => JSON.stringify(spinnerCells(id, 1000, O, st))) + expect(new Set(frames).size).toBe(3) + } +}) + +test('glitch draws one diamond in the spinner color', async () => { + expect(spinnerCells('glitch', 500, O)).toEqual([[{ ch: '◆', fg: O.color }]]) +}) + test('orb states draw differently', async () => { const frames = (['think', 'search', 'work', 'run', 'agents'] as const).map(st => JSON.stringify(spinnerCells('orb-states', 1000, O, st))) expect(new Set(frames).size).toBe(5) @@ -88,3 +127,55 @@ test('cellsToSpans merges runs of one color', async () => { const spans = cellsToSpans([[{ ch: 'a', fg: '#111111' }, { ch: 'b', fg: '#111111' }, { ch: 'c', fg: '#222222', bg: '#000000' }]]) expect(spans[0]).toEqual([{ text: 'ab', color: '#111111' }, { text: 'c', color: '#222222', bg: '#000000' }]) }) + +const WL = (spinner: SpinnerId): WordLook => ({ + bg: '#0d0d24', motion: { spinner, shimmer: 1, color: '#8080ff' }, + theme: { colors: { text: '#e5e5ee', accent: '#8080ff', read: '#00f2f2', agent: '#af66f7' } }, +}) +const joined = (s: { text: string }[]) => s.map(x => x.text).join('') + +test('scanline word: every character comes back in order, the two under the scan lit', async () => { + const word = 'Ünïcode…' + for (const t of [0, 500, 1000, 1700]) expect(joined(spinnerWordSpans(WL('scanline'), word, 0, { t, st: 'think' }))).toBe(word) + // the scan reaches the word's first letter after the bar and the one-cell gap + const lit = spinnerWordSpans(WL('scanline'), word, 0, { t: (SCAN_BAR + 1) * 45, st: 'think' }) + expect(lit[0]).toMatchObject({ color: '#0d0d24', bg: '#8080ff' }) + expect(spinnerWordSpans(WL('scanline'), word, 0, { t: 0, st: 'think' }).every(s => !s.bg)).toBe(true) +}) + +test('glitch word: never changes the line length and keeps letters in place', async () => { + const word = 'Thinking…', n = [...word].length + let tore = 0, swapped = 0 + for (let f = 0; f < 400; f++) { + const spans = spinnerWordSpans(WL('glitch'), word, 0, { t: f * 70, st: 'run' }) + const chars = [...joined(spans)] + expect(chars.length).toBe(n + 1) + const torn = chars.at(-1) !== ' ' + if (torn) tore++ + const body = torn ? chars.slice(1) : chars.slice(0, n) + body.forEach((ch, i) => { if (ch !== [...word][i]) { expect('▓▒░█▚').toContain(ch); swapped++ } }) + } + expect(tore).toBeGreaterThan(0) + expect(swapped).toBeGreaterThan(0) +}) + +test('glitch word: the line keeps its cell width with wide and zero-width letters', async () => { + const width = (spans: { text: string }[]) => [...joined(spans)].reduce((s, ch) => s + cellWidth(ch.codePointAt(0)!), 0) + for (const word of ['Thé漢字ing…', '漢字Thinking']) { + const widths = new Set() + for (let f = 0; f < 400; f++) widths.add(width(spinnerWordSpans(WL('glitch'), word, 0, { t: f * 70, st: 'run' }))) + expect([word, [...widths]]).toEqual([word, [width([{ text: word + ' ' }])]]) + } +}) + +test('glitch word: a frame is the same on every redraw within its 70 ms', async () => { + const at = (t: number) => JSON.stringify(spinnerWordSpans(WL('glitch'), 'Thinking…', 0, { t, st: 'think' })) + expect(at(140)).toBe(at(209)) +}) + +test('word effects fall back to accent and text without read and agent colors', async () => { + const bare: WordLook = { ...WL('glitch'), theme: { colors: { text: '#e5e5ee', accent: '#8080ff' } } } + for (let f = 0; f < 50; f++) for (const s of spinnerWordSpans(bare, 'Thinking…', 0, { t: f * 70, st: 'run' })) { + expect(['#e5e5ee', '#8080ff', '#0d0d24']).toContain(s.color) + } +}) diff --git a/test/packexport.test.ts b/test/packexport.test.ts index 8691962d..ce39616a 100644 --- a/test/packexport.test.ts +++ b/test/packexport.test.ts @@ -2,6 +2,7 @@ import { test, expect } from 'claude-code/testing' import { PRESETS } from '../hooks/themes.ts' import { SPINNER_IDS } from '../hooks/packs.ts' import { CLAWD_SHEET, PET_ROWS } from '../hooks/pets.ts' +import { spinnerCells, type Cell } from '../hooks/motion.ts' import { packExport, packsJson, exportAll, themeExport, spinnerExport, clawdExport } from '../hooks/packexport.ts' test('exports the four built-in packs in preset order', async () => { @@ -58,6 +59,23 @@ test('every spinner is exported with frames in white on black', async () => { expect(sp.find(s => s.id === 'clawd')!.frames[0]![0]![0]!.fg).toBe('#d77757') }) +// The cycle the preview loops is the spinner's real period: moving a whole cycle on changes at +// most a few cells (rounding ms to an integer and float edges cost a cell or two). The scanline's +// background checker flips every 400 ms regardless, so only its sweep head is compared. +test('the looping spinners export one whole period, so the preview wraps without a jump', async () => { + const mono = { color: '#ffffff', bg: '#000000', fg: '#ffffff' } + const head = (f: Cell[][]) => f[0]!.findIndex(c => c.ch === '█') + const diff = (a: Cell[][], b: Cell[][]) => a.flat().filter((c, i) => c.ch !== b.flat()[i]!.ch).length + for (const s of spinnerExport().filter(s => ['scanline', 'ring', 'signal'].includes(s.id))) { + const cycle = s.ms * s.frames.length + const ts = Array.from({ length: 40 }, (_, i) => i * 97) + const off = ts.map(t => s.id === 'scanline' + ? Math.abs(head(spinnerCells('scanline', t, mono)) - head(spinnerCells('scanline', t + cycle, mono))) + : diff(spinnerCells(s.id, t, mono), spinnerCells(s.id, t + cycle, mono))) + expect([s.id, off.reduce((a, b) => a + b, 0) / ts.length < 1.5]).toEqual([s.id, true]) + } +}) + test('Clawd is exported as his idle half-block rows in his own palette', async () => { const c = clawdExport() expect(c.cols).toBe(CLAWD_SHEET.w) diff --git a/test/packs.test.ts b/test/packs.test.ts index f55a6c89..40724154 100644 --- a/test/packs.test.ts +++ b/test/packs.test.ts @@ -154,7 +154,7 @@ test('exportMix description is built from shown names and fits 80 characters', a test('stockMotion freezes motion', async () => { expect(stockMotion(look(pack('arcade')).look).motion).toEqual({ spinner: 'stock', shimmer: 0, color: '#38e8ff', field: { shape: 'none', ...FIELD_DEFAULTS } }) - expect(SPINNER_IDS).toHaveLength(6) + expect([...SPINNER_IDS]).toEqual(['stock', 'comet', 'eyes', 'orb-states', 'clawd', 'shimmer', 'scanline', 'ring', 'glitch', 'signal']) }) test('exportName prefixes a built-in name and keeps any other', () => { diff --git a/test/rows.test.ts b/test/rows.test.ts index 61393552..59bdb94d 100644 --- a/test/rows.test.ts +++ b/test/rows.test.ts @@ -179,7 +179,7 @@ test('prefixCards leaves message rows as they are without it', async () => { test('non-classic styles indent every row one column, classic does not', async () => { const rows: RowInput[] = [user(), user({ own: false }), asst(), asst(false), tool(), tool({ isErrored: true }), { site: 'ToolResult' }] - for (const style of ['cards', 'minimal', 'retro'] as const) { + for (const style of ['cards', 'minimal', 'retro', 'slab'] as const) { const l: Look = { ...look('arcade'), rows: style } for (const r of rows) { const s = styleRow(els, l, r, ENGINE) as any @@ -226,7 +226,7 @@ function beside(n: any): number { test('width budget: ToolResult and UserMessage rows add at most 4 columns beside the engine', async () => { for (const name of ['cozy', 'arcade', 'crt']) { - for (const rows of ['cards', 'retro', 'minimal'] as const) { + for (const rows of ['cards', 'retro', 'minimal', 'slab'] as const) { const l: Look = { ...look(name), rows } for (const r of [user(), asst(), asst(false), { site: 'ToolResult' } as RowInput]) { const s = styleRow(els, l, r, ENGINE) @@ -265,3 +265,65 @@ test('tool header marks and tags never shrink or wrap; the engine box gives up w const tag = walk(styleRow(els, look('crt'), tool(), ENGINE)).find(x => x.type === 'Box' && x.props?.flexShrink === 0 && text(x).startsWith('[READ')) expect(tag).toBeDefined() }) + +const slab = (): Look => ({ ...look('classic'), rows: 'slab' }) + +test('slab: your prompt is an accent bar with its own text and the turn number', async () => { + const l = slab(), c = l.theme.colors + const u = inner(styleRow(els, l, { ...user(), turn: 3 } as RowInput, ENGINE)) + expect(hasEngine(u)).toBe(false) + expect(walk(u).some(x => x.props?.backgroundColor === c.accent)).toBe(true) + expect(text(u)).toContain('fix the test') + expect(text(u)).toContain('PROMPT 03') + const prompt = walk(u).find(x => x.type === 'Text' && text(x).includes('fix the test')) + expect(prompt.props.color).toBe(l.bg) + expect(text(prompt)).toBe('fix the test') + expect(walk(u).find(x => x.type === 'Box' && x.children?.includes(prompt)).props.paddingX).toBe(1) +}) + +test('slab: a long prompt keeps all its text and is never truncated', async () => { + const long = 'make the slab bar wrap '.repeat(20).trim() + const u = styleRow(els, slab(), { ...user({ text: long }), turn: 1 } as RowInput, ENGINE) + expect(text(u)).toContain(long) + expect(walk(u).some(x => x.props?.wrap === 'truncate' && text(x).includes('make the slab'))).toBe(false) +}) + +test('slab: an expanded or foreign prompt stays the engine row', async () => { + for (const r of [user({ isExpanded: true }), user({ own: false })]) expect(inner(styleRow(els, slab(), r, ENGINE))).toBe(ENGINE) +}) + +test('slab: tool rows carry the number, the upper-case tool in its role color, the engine line and the mark', async () => { + const l = slab(), c = l.theme.colors + const cases: [Partial>, string, string][] = [ + [{}, 'OK', c.pass], + [{ isErrored: true }, 'FAIL', c.fail], + [{ isInterrupted: true }, 'STOP', c.dim], + [{ isRunning: true }, '…', c.dim], + ] + for (const [o, mark, color] of cases) { + const r = styleRow(els, l, { ...tool(o), seq: 2 } as RowInput, ENGINE) + expect(hasEngine(r)).toBe(true) + expect(text(r)).toContain('02') + expect(text(r)).toContain('READ') + expect(walk(r).find(x => x.type === 'Text' && text(x) === 'READ ')?.props.color).toBe(c.read) + expect(walk(r).find(x => x.type === 'Text' && text(x).trim() === mark)?.props.color).toBe(color) + } + const bash = styleRow(els, l, { ...tool({ tool: 'Bash', input: { command: 'pnpm test' } }), seq: 1 } as RowInput, ENGINE) + expect(walk(bash).find(x => x.type === 'Text' && text(x) === 'BASH ')?.props.color).toBe(c.shell) +}) + +test('slab: tool results sit indented under their call', async () => { + const r = inner(styleRow(els, slab(), { site: 'ToolResult' }, ENGINE)) as any + expect(r.props.paddingLeft).toBe(3) + expect(hasEngine(r)).toBe(true) +}) + +test('slab: the first block of a reply gets an accent rule on its margin line, later blocks nothing', async () => { + const l = slab() + const first = styleRow(els, l, asst(true), ENGINE) + const rule = walk(first).find(x => x.props?.position === 'absolute') + expect([rule.props.top, rule.props.left]).toEqual([0, 0]) + expect(text(rule)).toBe('────────') + expect(hasEngine(first)).toBe(true) + expect(inner(styleRow(els, l, asst(false), ENGINE))).toBe(ENGINE) +}) diff --git a/test/spinner.test.ts b/test/spinner.test.ts index 8de1fe8a..4f8fe76f 100644 --- a/test/spinner.test.ts +++ b/test/spinner.test.ts @@ -1,7 +1,7 @@ import { test, expect } from 'claude-code/testing' import { orbStateOf, usesOwnSpinner, spinnerLine, spinnerProps, checkedSpinnerProps, elapsed } from '../hooks/spinner.ts' import SpinnerClient from '../hooks/client/spinner.tsx' -import { SPINNERS } from '../hooks/motion.ts' +import { SPINNERS, SCAN_BAR } from '../hooks/motion.ts' import { SPINNER_IDS, resolveLook, stockMotion } from '../hooks/packs.ts' import { initialModel, applyEvent } from '../hooks/model.ts' @@ -103,3 +103,12 @@ test('client props carry only what the line reads', async () => { expect(JSON.stringify(p).length).toBeLessThan(1000) expect(spinnerLine(p.look, p.input, 777)).toEqual(spinnerLine(look('arcade'), IN, 777)) }) + +test('spinnerLine hands the word effect the same clock as the badge', async () => { + const l = look('classic') + const scan = { ...l, motion: { ...l.motion, spinner: 'scanline' as const } } + const props = spinnerProps(scan, { word: 'Thinking', turnAt: 1000, detail: '', state: 'think' }, false) + const line = spinnerLine(props.look, props.input, 1000 + (SCAN_BAR + 1) * 45) + expect(line.word[0]!.bg).toBe(props.look.theme.colors.accent) + expect(line.badge[0]!.some(s => s.text.includes('█'))).toBe(false) +}) diff --git a/test/turns.test.ts b/test/turns.test.ts new file mode 100644 index 00000000..f0ae27b2 --- /dev/null +++ b/test/turns.test.ts @@ -0,0 +1,33 @@ +import { test, expect } from 'claude-code/testing' +import { makeTurns } from '../hooks/turns.ts' + +test('turns number prompts in first-drawn order, and a redraw keeps its number', async () => { + const t = makeTurns() + expect(t.turnFor('p1')).toBe(1) + expect(t.turnFor('p2')).toBe(2) + expect(t.turnFor('p1')).toBe(1) +}) + +test('a placeholder shows the next number without claiming it', async () => { + const t = makeTurns() + t.turnFor('p1') + expect(t.turnFor('placeholder')).toBe(2) + expect(t.turnFor('placeholder')).toBe(2) + expect(t.turnFor('p2')).toBe(2) +}) + +test('tools count from 1 within each turn', async () => { + const t = makeTurns() + t.turnFor('p1') + expect([t.toolSeq('a'), t.toolSeq('b'), t.toolSeq('c')]).toEqual([1, 2, 3]) + t.turnFor('p2') + expect([t.toolSeq('d'), t.toolSeq('e')]).toEqual([1, 2]) +}) + +test('a tool redrawn turns later keeps the number it first showed', async () => { + const t = makeTurns() + t.turnFor('p1'); t.toolSeq('a'); t.toolSeq('b') + t.turnFor('p2'); t.toolSeq('c') + expect(t.toolSeq('b')).toBe(2) + expect(t.toolSeq('d')).toBe(2) +})