dexterm-cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/agents/src/bus.js +57 -6
- package/dist/packages/agents/src/executor.js +25 -0
- package/dist/packages/agents/src/follow.js +105 -0
- package/dist/packages/agents/src/gate.js +3 -0
- package/dist/packages/agents/src/identity.js +41 -2
- package/dist/packages/agents/src/index.js +5 -0
- package/dist/packages/agents/src/spectator-server.js +153 -0
- package/dist/packages/agents/src/spectator.js +212 -0
- package/dist/packages/agents/src/tiers.js +5 -0
- package/dist/packages/agents/src/tools.js +3 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +6 -1
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +11 -3
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +13 -3
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +3 -3
- package/dist/packages/cli/src/agent-cmd.js +107 -10
- package/dist/packages/cli/src/agent-spectator.js +37 -0
- package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
- package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
- package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
- package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
- package/dist/packages/cli/src/alerts/creds.js +83 -0
- package/dist/packages/cli/src/alerts/format.js +66 -0
- package/dist/packages/cli/src/alerts/index.js +10 -0
- package/dist/packages/cli/src/alerts/notifier.js +53 -0
- package/dist/packages/cli/src/alerts/route.js +19 -0
- package/dist/packages/cli/src/alerts/types.js +1 -0
- package/dist/packages/cli/src/alerts-cmd.js +212 -0
- package/dist/packages/cli/src/app-help-gen.js +365 -0
- package/dist/packages/cli/src/backtest-cmd.js +62 -3
- package/dist/packages/cli/src/coin-target.js +2 -0
- package/dist/packages/cli/src/doctor.js +21 -6
- package/dist/packages/cli/src/export.js +235 -0
- package/dist/packages/cli/src/follow-cmd.js +107 -1
- package/dist/packages/cli/src/help-cmd.js +48 -0
- package/dist/packages/cli/src/live/index.js +66 -3
- package/dist/packages/cli/src/live/screens/token-live.js +24 -0
- package/dist/packages/cli/src/live/signals.js +109 -0
- package/dist/packages/cli/src/live/trade.js +24 -1
- package/dist/packages/cli/src/main.js +596 -49
- package/dist/packages/cli/src/mcp-cmd.js +321 -0
- package/dist/packages/cli/src/pnl-app.js +221 -0
- package/dist/packages/cli/src/pnl-card.js +615 -0
- package/dist/packages/cli/src/record-cmd.js +455 -0
- package/dist/packages/cli/src/rules.js +69 -2
- package/dist/packages/cli/src/screens/follows.js +91 -2
- package/dist/packages/cli/src/screens/overlay.js +3 -0
- package/dist/packages/cli/src/screens/positions.js +1 -1
- package/dist/packages/cli/src/screens/rules.js +1 -1
- package/dist/packages/cli/src/skill-eval-build.js +109 -0
- package/dist/packages/cli/src/spectate-cmd.js +104 -0
- package/dist/packages/cli/src/state.js +1 -1
- package/dist/packages/cli/src/update.js +294 -0
- package/dist/packages/cli/src/version.js +3 -0
- package/dist/packages/cli/src/voice-cmd.js +194 -0
- package/dist/packages/cli/src/voice-hud.js +1 -1
- package/dist/packages/cli/src/watch-cmd.js +190 -0
- package/dist/packages/custom/src/rules.js +3 -2
- package/dist/packages/custom/src/schema.js +24 -1
- package/dist/packages/data/src/analyst-actions.js +130 -3
- package/dist/packages/data/src/coin-target.js +82 -0
- package/dist/packages/data/src/fast-lane.js +16 -2
- package/dist/packages/data/src/index.js +4 -0
- package/dist/packages/data/src/intel/helius-history.js +12 -1
- package/dist/packages/data/src/intel/live-signals.js +185 -0
- package/dist/packages/data/src/larp/link-baseline.js +196 -0
- package/dist/packages/data/src/sources/dexscreener.js +14 -0
- package/dist/packages/data/src/sources/pumpportal.js +6 -1
- package/dist/packages/data/src/trade/copy/index.js +1 -1
- package/dist/packages/data/src/trade/copy/stats.js +36 -2
- package/dist/packages/data/src/trade/copy/tracker.js +3 -2
- package/dist/packages/mcp/assets/kb/app-help/app-help-command.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +43 -0
- package/dist/packages/mcp/assets/kb/app-help/backtest.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/bubble-map.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/calm-mode.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/change-voice-speed.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/change-voice.md +30 -0
- package/dist/packages/mcp/assets/kb/app-help/chart-popup-selftest.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/coin-images.md +29 -0
- package/dist/packages/mcp/assets/kb/app-help/configure-rpc-key.md +28 -0
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent-registry.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/custom-keymap.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/edit-kb-or-skill.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/edit-rules.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/export-a-view.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/follow-a-wallet.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/getting-started.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/kol-lists.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/larp-check.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/login-with-x.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/narrative-board.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/open-a-coin.md +43 -0
- package/dist/packages/mcp/assets/kb/app-help/open-chart-popup.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/order-plans.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/record-headless.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/recorder-history.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/rug-check.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/run-doctor.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +190 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +180 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +199 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +240 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +192 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/set-a-filter.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/setup-neural-voice.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/share-packs.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/sort-the-screener.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/switch-theme.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/turn-on-alerts.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/update-dexterm.md +30 -0
- package/dist/packages/mcp/assets/kb/app-help/use-presets.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/use-voice-mode.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/watch-a-coin.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/watch-headless.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/what-dexterm-shows-agents.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/workspaces.md +38 -0
- package/dist/packages/mcp/src/app-help.js +135 -0
- package/dist/packages/mcp/src/index.js +3 -0
- package/dist/packages/mcp/src/server.js +179 -5
- package/dist/packages/mcp/src/spectator-sink.js +72 -0
- package/dist/packages/packs/src/index.js +3 -0
- package/dist/packages/packs/src/skill-backtest.js +300 -0
- package/dist/packages/packs/src/skill-care.js +206 -0
- package/dist/packages/popup/src/pages/spectator.css +45 -0
- package/dist/packages/popup/src/pages/spectator.html +25 -0
- package/dist/packages/popup/src/pages/spectator.js +74 -0
- package/dist/packages/popup/src/selftest.js +19 -0
- package/dist/packages/popup/src/server.js +7 -1
- package/dist/packages/tui/src/index.js +3 -0
- package/dist/packages/tui/src/keymap.js +12 -0
- package/dist/packages/tui/src/raster-font.js +195 -0
- package/dist/packages/tui/src/raster.js +213 -0
- package/dist/packages/voice/src/controller.js +57 -4
- package/dist/packages/voice/src/doctor.js +96 -11
- package/dist/packages/voice/src/download.js +184 -0
- package/dist/packages/voice/src/engines.js +55 -8
- package/dist/packages/voice/src/index.js +9 -3
- package/dist/packages/voice/src/speakable.js +111 -0
- package/dist/packages/voice/src/stt/whisper.js +14 -4
- package/dist/packages/voice/src/tts/espeak.js +131 -0
- package/dist/packages/voice/src/tts/piper-setup.js +229 -0
- package/dist/packages/voice/src/tts/piper.js +449 -0
- package/dist/packages/voice/src/tts/say.js +72 -35
- package/dist/packages/voice/src/tts/voices.js +209 -0
- package/package.json +5 -9
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// dexterm popup — spectator page (task-1406, "watch the agent cook"). Renders the SpectatorPopupVM: the
|
|
2
|
+
// screen the agent moved to, the connected agents with an 'agent' badge and status, and an activity feed
|
|
3
|
+
// with timing and data ages. Every string is already REDACTED TUI-side (wallet addresses/keys masked) and
|
|
4
|
+
// is rendered with textContent only — never innerHTML. The ONLY messages this page sends are the
|
|
5
|
+
// allow-listed spectator.pause / resume / takeover / kill events (by agent name); it can never arm, sign,
|
|
6
|
+
// raise a cap or touch a wallet (no such control exists and the server drops anything off the allow-list).
|
|
7
|
+
(() => {
|
|
8
|
+
'use strict';
|
|
9
|
+
const DT = window.DT; DT.page = 'spectator';
|
|
10
|
+
const $ = (id) => document.getElementById(id);
|
|
11
|
+
const el = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = String(text); return e; };
|
|
12
|
+
const ago = (t) => { if (!t) return '—'; const s = Math.max(0, Math.round((Date.now() - t) / 1000)); return s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s / 60)}m` : `${Math.floor(s / 3600)}h`; };
|
|
13
|
+
const age = (s) => (s == null ? '' : s < 60 ? `${Math.round(s)}s old` : s < 3600 ? `${Math.floor(s / 60)}m old` : `${Math.floor(s / 3600)}h old`);
|
|
14
|
+
|
|
15
|
+
let vm = null;
|
|
16
|
+
|
|
17
|
+
function render() {
|
|
18
|
+
if (!vm) return;
|
|
19
|
+
|
|
20
|
+
const scr = $('screen'); scr.textContent = '';
|
|
21
|
+
const s = vm.screen || {};
|
|
22
|
+
scr.appendChild(el('span', 'spec-screen-k', `screen: ${s.screen || '—'}`));
|
|
23
|
+
if (s.selectedMint) scr.appendChild(el('span', 'spec-screen-dim', ` · token ${s.selectedMint}`));
|
|
24
|
+
if (s.feed) scr.appendChild(el('span', 'spec-screen-dim', ` · feed ${s.feed}`));
|
|
25
|
+
if (s.filter) scr.appendChild(el('span', 'spec-screen-dim', ` · filter ${s.filter}`));
|
|
26
|
+
if (s.highlight) scr.appendChild(el('span', 'spec-screen-dim', ` · ★ ${s.highlight}`));
|
|
27
|
+
if (vm.control === 'user') scr.appendChild(el('span', 'spec-takeover', ' · YOU HAVE TAKEN OVER'));
|
|
28
|
+
|
|
29
|
+
const host = $('agents'); host.textContent = '';
|
|
30
|
+
if (!vm.agents || !vm.agents.length) host.appendChild(el('div', 'spec-dim', 'no agents connected yet'));
|
|
31
|
+
for (const a of (vm.agents || [])) {
|
|
32
|
+
const row = el('div', 'spec-agent' + (a.status !== 'active' ? ' off' : ''));
|
|
33
|
+
row.appendChild(el('span', 'spec-badge', 'agent')); // the 'agent' badge
|
|
34
|
+
row.appendChild(el('span', 'spec-name', a.name)); // textContent — redacted/firewalled, safe
|
|
35
|
+
if (a.tier) row.appendChild(el('span', 'spec-tier tier-' + a.tier, a.tier));
|
|
36
|
+
row.appendChild(el('span', 'spec-status status-' + a.status, a.status));
|
|
37
|
+
row.appendChild(el('span', 'spec-dim', ` · ${ago(a.lastAt)}`));
|
|
38
|
+
const ctl = el('div', 'spec-ctl');
|
|
39
|
+
const mk = (label, name, danger) => { const b = el('button', 'spec-btn' + (danger ? ' danger' : ''), label); b.type = 'button'; b.addEventListener('click', () => DT.send({ type: 'event', name, payload: { agent: a.name } })); return b; };
|
|
40
|
+
if (a.status === 'active') { ctl.appendChild(mk('pause', 'spectator.pause')); ctl.appendChild(mk('take over', 'spectator.takeover')); }
|
|
41
|
+
else ctl.appendChild(mk('resume', 'spectator.resume'));
|
|
42
|
+
ctl.appendChild(mk('kill', 'spectator.kill', true)); // panic
|
|
43
|
+
row.appendChild(ctl);
|
|
44
|
+
host.appendChild(row);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const act = $('activity'); act.textContent = '';
|
|
48
|
+
for (const r of (vm.activity || []).slice().reverse().slice(0, 80)) {
|
|
49
|
+
const line = el('div', 'spec-act' + (r.ok ? '' : ' bad') + (r.phase === 'nav' ? ' nav' : ''));
|
|
50
|
+
line.appendChild(el('span', 'spec-act-t', ago(r.t)));
|
|
51
|
+
line.appendChild(el('span', 'spec-badge sm', 'agent'));
|
|
52
|
+
line.appendChild(el('span', 'spec-act-agent', r.agent));
|
|
53
|
+
line.appendChild(el('span', 'spec-act-phase', r.phase));
|
|
54
|
+
if (r.tool) line.appendChild(el('span', 'spec-act-tool', r.tool));
|
|
55
|
+
line.appendChild(el('span', 'spec-act-sum', r.summary || '')); // textContent — redacted upstream
|
|
56
|
+
if (r.ms != null) line.appendChild(el('span', 'spec-act-ms', `${Math.round(r.ms)}ms`));
|
|
57
|
+
if (r.dataAgeSec != null) line.appendChild(el('span', 'spec-act-age', age(r.dataAgeSec)));
|
|
58
|
+
act.appendChild(line);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
$('redact').textContent = vm.redacted ? 'redacted' : 'local (full)';
|
|
62
|
+
const live = (vm.agents || []).filter((a) => a.status === 'active').length;
|
|
63
|
+
DT.status(`${live} cooking`, live ? 'up' : 'muted');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
DT.onHello(() => { DT.mountRain('rain'); const r = DT.rain(); if (r) { r.setActivity(0.3); r.setMood('rain'); } });
|
|
67
|
+
DT.onData((payload) => { vm = payload; render(); });
|
|
68
|
+
DT.onUpdate(() => { /* spectator is pushed as full `data` frames */ });
|
|
69
|
+
DT.wireChrome();
|
|
70
|
+
let lastTick = 0;
|
|
71
|
+
DT.rainEvery = 2; DT.fpsCap = 30;
|
|
72
|
+
DT.loop((now) => { if (now - lastTick > 1000) { lastTick = now; if (vm) render(); } });
|
|
73
|
+
DT.connect();
|
|
74
|
+
})();
|
|
@@ -62,6 +62,25 @@ export function fixtureNarrativeVM() {
|
|
|
62
62
|
limits: 'model output: none (deterministic lexicon v0.1.0). Name-based: X/Telegram-only narratives are missed; 6 copycat symbols.',
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
/** A deterministic, network-free spectator view for the popup self-test / demo (task-1406). Not real agents. */
|
|
66
|
+
export function fixtureSpectatorVM() {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
return {
|
|
69
|
+
title: 'watch the agent cook', now, redacted: true, control: 'agent',
|
|
70
|
+
screen: { screen: 'token', selectedMint: 'Fixt…AAAA', feed: 'new', filter: null, highlight: null },
|
|
71
|
+
agents: [
|
|
72
|
+
{ name: 'claude-code', tier: 'propose', status: 'active', lastAt: now - 1200 },
|
|
73
|
+
{ name: 'scanbot', tier: 'observe', status: 'paused', lastAt: now - 8000 },
|
|
74
|
+
],
|
|
75
|
+
activity: [
|
|
76
|
+
{ seq: 1, t: now - 8000, phase: 'connect', agent: 'claude-code', tier: 'propose', tool: null, summary: 'connected · claude-code 2.1', ms: null, dataAgeSec: null, ok: true, refusal: null },
|
|
77
|
+
{ seq: 2, t: now - 5000, phase: 'nav', agent: 'claude-code', tier: 'propose', tool: 'open_token', summary: 'open Fixt…AAAA', ms: 6, dataAgeSec: null, ok: true, refusal: null },
|
|
78
|
+
{ seq: 3, t: now - 4200, phase: 'result', agent: 'claude-code', tier: 'propose', tool: 'rugcheck', summary: 'rugcheck Fixt…', ms: 42, dataAgeSec: 3, ok: true, refusal: null },
|
|
79
|
+
{ seq: 4, t: now - 2000, phase: 'proposal', agent: 'claude-code', tier: 'propose', tool: 'propose_alert', summary: 'alert when mcap crosses 1M', ms: 2, dataAgeSec: null, ok: true, refusal: null },
|
|
80
|
+
{ seq: 5, t: now - 1000, phase: 'refused', agent: 'scanbot', tier: 'observe', tool: 'arm_plan', summary: 'refused: no agent may do that at any tier', ms: null, dataAgeSec: null, ok: false, refusal: 'never' },
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
65
84
|
/** Run the self-test and resolve with the measured result. Always cleans up the window host and server. */
|
|
66
85
|
export async function runSelfTest(opts = {}) {
|
|
67
86
|
const seconds = Math.max(2, Math.min(30, opts.seconds ?? 6));
|
|
@@ -52,6 +52,8 @@ export const POPUP_EVENTS = new Set([
|
|
|
52
52
|
'event:roster.pause', 'event:roster.resume', 'event:roster.kick',
|
|
53
53
|
// task-1355 order plans: cancel a plan by id (the ONLY write the plans popup can send — never arm/cap/sign)
|
|
54
54
|
'event:plan.cancel',
|
|
55
|
+
// task-1406 spectator: pause / resume / take over / kill an agent by name (the only writes this popup sends)
|
|
56
|
+
'event:spectator.pause', 'event:spectator.resume', 'event:spectator.takeover', 'event:spectator.kill',
|
|
55
57
|
]);
|
|
56
58
|
/**
|
|
57
59
|
* task-1358 r3: the ONLY messages a pack widget's socket may send — the generic set every popup has. A widget socket
|
|
@@ -104,6 +106,10 @@ export function validateEventPayload(name, payload) {
|
|
|
104
106
|
case 'roster.pause':
|
|
105
107
|
case 'roster.resume':
|
|
106
108
|
case 'roster.kick':
|
|
109
|
+
case 'spectator.pause':
|
|
110
|
+
case 'spectator.resume':
|
|
111
|
+
case 'spectator.takeover':
|
|
112
|
+
case 'spectator.kill':
|
|
107
113
|
return typeof p.agent === 'string' && ROSTER_AGENT_RE.test(p.agent) ? { agent: p.agent } : null;
|
|
108
114
|
case 'plan.cancel':
|
|
109
115
|
return typeof p.id === 'string' && PLAN_ID_RE.test(p.id) ? { id: p.id } : null;
|
|
@@ -117,7 +123,7 @@ export function lwcLogoStyleHash(lwcSource) {
|
|
|
117
123
|
return m ? `'sha256-${createHash('sha256').update(m[1], 'utf8').digest('base64')}'` : null;
|
|
118
124
|
}
|
|
119
125
|
// (r3) 'widget' is the WRAPPER page — ours, nonce-rendered like the rest; the pack html goes in its iframe
|
|
120
|
-
const PAGE_KINDS = ['chart', 'token', 'feeds', 'bubbles', 'roster', 'narrative', 'plans', 'widget'];
|
|
126
|
+
const PAGE_KINDS = ['chart', 'token', 'feeds', 'bubbles', 'roster', 'narrative', 'plans', 'widget', 'spectator'];
|
|
121
127
|
function b64url(buf) { return buf.toString('base64url'); }
|
|
122
128
|
function safeEq(a, b) {
|
|
123
129
|
const ba = Buffer.from(a), bb = Buffer.from(b);
|
|
@@ -13,3 +13,6 @@ export * from "./sound.js";
|
|
|
13
13
|
export * from "./bubbles.js";
|
|
14
14
|
export * from "./chrome.js";
|
|
15
15
|
export * from "./images/index.js";
|
|
16
|
+
// task-1401: rasterize a canvas to RGBA for the export/share PNG (bitmap font + procedural box/blocks/braille).
|
|
17
|
+
export * from "./raster.js";
|
|
18
|
+
export { glyphBits, fontChars, FONT_W, FONT_H } from "./raster-font.js";
|
|
@@ -206,6 +206,12 @@ export const DEFAULT_KEYMAP = new Keymap([
|
|
|
206
206
|
// task-1282 JARVIS voice: push-to-talk toggle (hold not possible in a terminal). Lowercase v is free
|
|
207
207
|
// on every screen; the loop auto-enables voice mode on first use and shows a live mic indicator.
|
|
208
208
|
{ keys: ['v'], action: 'voice.talk', label: 'voice' },
|
|
209
|
+
// task-1411: in-voice controls (loop-handled; only act while voice is on). `m` mute, `,` change voice,
|
|
210
|
+
// `<`/`>` slow down / speed up the spoken reply (saved to config.voice.rate). Per-screen `m` bindings
|
|
211
|
+
// (kol/bubbles/follows) override this global one on those screens, exactly like `voice.talk`.
|
|
212
|
+
{ keys: ['m'], action: 'voice.mute', label: 'mute voice' },
|
|
213
|
+
{ keys: [','], action: 'voice.cycle', label: 'change voice' },
|
|
214
|
+
{ keys: ['<', '>'], action: 'voice.rate', label: 'slower / faster voice' },
|
|
209
215
|
// task-1311: `i` focuses the chat input on the token / analyst screens (QA B1–B3); the loop only
|
|
210
216
|
// captures keystrokes into the input while it is focused, so the number keys and `q` never type.
|
|
211
217
|
{ keys: ['i'], action: 'input.focus', label: 'ask', screen: 'token' },
|
|
@@ -215,6 +221,12 @@ export const DEFAULT_KEYMAP = new Keymap([
|
|
|
215
221
|
{ keys: ['p'], action: 'pause.toggle', label: 'pause' },
|
|
216
222
|
// task-1366: `$` (unbound on every screen) toggles price ⇄ market cap on every screen + both charts; persisted.
|
|
217
223
|
{ keys: ['$'], action: 'display.priceMode', label: 'price ⇄ mcap' },
|
|
224
|
+
// task-1401: `E` exports the current view (PNG + text report, local only). Free on every screen.
|
|
225
|
+
{ keys: ['E'], action: 'export.view', label: 'export PNG + report' },
|
|
226
|
+
// task-1407: `H` (free capital everywhere) opens the PnL card preview on the screens that have fills to card.
|
|
227
|
+
{ keys: ['H'], action: 'pnl.card', label: 'PnL card (share)', screen: 'token' },
|
|
228
|
+
{ keys: ['H'], action: 'pnl.card', label: 'PnL card (share)', screen: 'positions' },
|
|
229
|
+
{ keys: ['H'], action: 'pnl.card', label: 'PnL card (share)', screen: 'follows' },
|
|
218
230
|
{ keys: ['/'], action: 'filter.edit', label: 'filter', screen: 'screener' },
|
|
219
231
|
{ keys: ['\\'], action: 'filter.clear', label: 'clear filter', screen: 'screener' },
|
|
220
232
|
{ keys: ['s'], action: 'sort.menu', label: 'sort', screen: 'screener' },
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// @dexterm/tui — a 5×9 bitmap font for the PNG exporter (task-1401). Drawn for dexterm (no third-party font data):
|
|
2
|
+
// rows 0–6 are the cap height, rows 7–8 the descenders. Box drawing, block elements and braille are not here:
|
|
3
|
+
// raster.ts draws those procedurally so they join up across cells exactly like a terminal does.
|
|
4
|
+
const G = {
|
|
5
|
+
' ': [],
|
|
6
|
+
'!': ['..#..', '..#..', '..#..', '..#..', '..#..', '.....', '..#..'],
|
|
7
|
+
'"': ['.#.#.', '.#.#.', '.#.#.'],
|
|
8
|
+
'#': ['.#.#.', '.#.#.', '#####', '.#.#.', '#####', '.#.#.', '.#.#.'],
|
|
9
|
+
'$': ['..#..', '.####', '#.#..', '.###.', '..#.#', '####.', '..#..'],
|
|
10
|
+
'%': ['##...', '##..#', '...#.', '..#..', '.#...', '#..##', '...##'],
|
|
11
|
+
'&': ['.##..', '#..#.', '#.#..', '.#...', '#.#.#', '#..#.', '.##.#'],
|
|
12
|
+
"'": ['..#..', '..#..', '.#...'],
|
|
13
|
+
'(': ['...#.', '..#..', '.#...', '.#...', '.#...', '..#..', '...#.'],
|
|
14
|
+
')': ['.#...', '..#..', '...#.', '...#.', '...#.', '..#..', '.#...'],
|
|
15
|
+
'*': ['.....', '..#..', '#.#.#', '.###.', '#.#.#', '..#..', '.....'],
|
|
16
|
+
'+': ['.....', '..#..', '..#..', '#####', '..#..', '..#..', '.....'],
|
|
17
|
+
',': ['.....', '.....', '.....', '.....', '.....', '..#..', '..#..', '.#...'],
|
|
18
|
+
'-': ['.....', '.....', '.....', '#####', '.....', '.....', '.....'],
|
|
19
|
+
'.': ['.....', '.....', '.....', '.....', '.....', '.##..', '.##..'],
|
|
20
|
+
'/': ['.....', '....#', '...#.', '..#..', '.#...', '#....', '.....'],
|
|
21
|
+
'0': ['.###.', '#...#', '#..##', '#.#.#', '##..#', '#...#', '.###.'],
|
|
22
|
+
'1': ['..#..', '.##..', '..#..', '..#..', '..#..', '..#..', '.###.'],
|
|
23
|
+
'2': ['.###.', '#...#', '....#', '...#.', '..#..', '.#...', '#####'],
|
|
24
|
+
'3': ['#####', '...#.', '..#..', '...#.', '....#', '#...#', '.###.'],
|
|
25
|
+
'4': ['...#.', '..##.', '.#.#.', '#..#.', '#####', '...#.', '...#.'],
|
|
26
|
+
'5': ['#####', '#....', '####.', '....#', '....#', '#...#', '.###.'],
|
|
27
|
+
'6': ['..##.', '.#...', '#....', '####.', '#...#', '#...#', '.###.'],
|
|
28
|
+
'7': ['#####', '....#', '...#.', '..#..', '.#...', '.#...', '.#...'],
|
|
29
|
+
'8': ['.###.', '#...#', '#...#', '.###.', '#...#', '#...#', '.###.'],
|
|
30
|
+
'9': ['.###.', '#...#', '#...#', '.####', '....#', '...#.', '.##..'],
|
|
31
|
+
':': ['.....', '.##..', '.##..', '.....', '.##..', '.##..', '.....'],
|
|
32
|
+
';': ['.....', '.##..', '.##..', '.....', '.##..', '.##..', '.#...'],
|
|
33
|
+
'<': ['...#.', '..#..', '.#...', '#....', '.#...', '..#..', '...#.'],
|
|
34
|
+
'=': ['.....', '.....', '#####', '.....', '#####', '.....', '.....'],
|
|
35
|
+
'>': ['.#...', '..#..', '...#.', '....#', '...#.', '..#..', '.#...'],
|
|
36
|
+
'?': ['.###.', '#...#', '....#', '...#.', '..#..', '.....', '..#..'],
|
|
37
|
+
'@': ['.###.', '#...#', '....#', '.##.#', '#.#.#', '#.#.#', '.###.'],
|
|
38
|
+
'A': ['.###.', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'],
|
|
39
|
+
'B': ['####.', '#...#', '#...#', '####.', '#...#', '#...#', '####.'],
|
|
40
|
+
'C': ['.###.', '#...#', '#....', '#....', '#....', '#...#', '.###.'],
|
|
41
|
+
'D': ['###..', '#..#.', '#...#', '#...#', '#...#', '#..#.', '###..'],
|
|
42
|
+
'E': ['#####', '#....', '#....', '####.', '#....', '#....', '#####'],
|
|
43
|
+
'F': ['#####', '#....', '#....', '####.', '#....', '#....', '#....'],
|
|
44
|
+
'G': ['.###.', '#...#', '#....', '#.###', '#...#', '#...#', '.####'],
|
|
45
|
+
'H': ['#...#', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'],
|
|
46
|
+
'I': ['.###.', '..#..', '..#..', '..#..', '..#..', '..#..', '.###.'],
|
|
47
|
+
'J': ['..###', '...#.', '...#.', '...#.', '...#.', '#..#.', '.##..'],
|
|
48
|
+
'K': ['#...#', '#..#.', '#.#..', '##...', '#.#..', '#..#.', '#...#'],
|
|
49
|
+
'L': ['#....', '#....', '#....', '#....', '#....', '#....', '#####'],
|
|
50
|
+
'M': ['#...#', '##.##', '#.#.#', '#.#.#', '#...#', '#...#', '#...#'],
|
|
51
|
+
'N': ['#...#', '#...#', '##..#', '#.#.#', '#..##', '#...#', '#...#'],
|
|
52
|
+
'O': ['.###.', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'],
|
|
53
|
+
'P': ['####.', '#...#', '#...#', '####.', '#....', '#....', '#....'],
|
|
54
|
+
'Q': ['.###.', '#...#', '#...#', '#...#', '#.#.#', '#..#.', '.##.#'],
|
|
55
|
+
'R': ['####.', '#...#', '#...#', '####.', '#.#..', '#..#.', '#...#'],
|
|
56
|
+
'S': ['.####', '#....', '#....', '.###.', '....#', '....#', '####.'],
|
|
57
|
+
'T': ['#####', '..#..', '..#..', '..#..', '..#..', '..#..', '..#..'],
|
|
58
|
+
'U': ['#...#', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'],
|
|
59
|
+
'V': ['#...#', '#...#', '#...#', '#...#', '#...#', '.#.#.', '..#..'],
|
|
60
|
+
'W': ['#...#', '#...#', '#...#', '#.#.#', '#.#.#', '#.#.#', '.#.#.'],
|
|
61
|
+
'X': ['#...#', '#...#', '.#.#.', '..#..', '.#.#.', '#...#', '#...#'],
|
|
62
|
+
'Y': ['#...#', '#...#', '.#.#.', '..#..', '..#..', '..#..', '..#..'],
|
|
63
|
+
'Z': ['#####', '....#', '...#.', '..#..', '.#...', '#....', '#####'],
|
|
64
|
+
'[': ['.###.', '.#...', '.#...', '.#...', '.#...', '.#...', '.###.'],
|
|
65
|
+
'\\': ['.....', '#....', '.#...', '..#..', '...#.', '....#', '.....'],
|
|
66
|
+
']': ['.###.', '...#.', '...#.', '...#.', '...#.', '...#.', '.###.'],
|
|
67
|
+
'^': ['..#..', '.#.#.', '#...#'],
|
|
68
|
+
'_': ['.....', '.....', '.....', '.....', '.....', '.....', '.....', '#####'],
|
|
69
|
+
'`': ['.#...', '..#..', '...#.'],
|
|
70
|
+
'a': ['.....', '.....', '.###.', '....#', '.####', '#...#', '.####'],
|
|
71
|
+
'b': ['#....', '#....', '#.##.', '##..#', '#...#', '#...#', '####.'],
|
|
72
|
+
'c': ['.....', '.....', '.###.', '#....', '#....', '#...#', '.###.'],
|
|
73
|
+
'd': ['....#', '....#', '.##.#', '#..##', '#...#', '#...#', '.####'],
|
|
74
|
+
'e': ['.....', '.....', '.###.', '#...#', '#####', '#....', '.###.'],
|
|
75
|
+
'f': ['..##.', '.#..#', '.#...', '###..', '.#...', '.#...', '.#...'],
|
|
76
|
+
'g': ['.....', '.....', '.####', '#...#', '#...#', '.####', '....#', '....#', '.###.'],
|
|
77
|
+
'h': ['#....', '#....', '#.##.', '##..#', '#...#', '#...#', '#...#'],
|
|
78
|
+
'i': ['..#..', '.....', '.##..', '..#..', '..#..', '..#..', '.###.'],
|
|
79
|
+
'j': ['...#.', '.....', '..##.', '...#.', '...#.', '...#.', '...#.', '#..#.', '.##..'],
|
|
80
|
+
'k': ['#....', '#....', '#..#.', '#.#..', '##...', '#.#..', '#..#.'],
|
|
81
|
+
'l': ['.##..', '..#..', '..#..', '..#..', '..#..', '..#..', '.###.'],
|
|
82
|
+
'm': ['.....', '.....', '##.#.', '#.#.#', '#.#.#', '#...#', '#...#'],
|
|
83
|
+
'n': ['.....', '.....', '#.##.', '##..#', '#...#', '#...#', '#...#'],
|
|
84
|
+
'o': ['.....', '.....', '.###.', '#...#', '#...#', '#...#', '.###.'],
|
|
85
|
+
'p': ['.....', '.....', '####.', '#...#', '#...#', '####.', '#....', '#....', '#....'],
|
|
86
|
+
'q': ['.....', '.....', '.####', '#...#', '#...#', '.####', '....#', '....#', '....#'],
|
|
87
|
+
'r': ['.....', '.....', '#.##.', '##..#', '#....', '#....', '#....'],
|
|
88
|
+
's': ['.....', '.....', '.####', '#....', '.###.', '....#', '####.'],
|
|
89
|
+
't': ['.#...', '.#...', '###..', '.#...', '.#...', '.#..#', '..##.'],
|
|
90
|
+
'u': ['.....', '.....', '#...#', '#...#', '#...#', '#..##', '.##.#'],
|
|
91
|
+
'v': ['.....', '.....', '#...#', '#...#', '#...#', '.#.#.', '..#..'],
|
|
92
|
+
'w': ['.....', '.....', '#...#', '#...#', '#.#.#', '#.#.#', '.#.#.'],
|
|
93
|
+
'x': ['.....', '.....', '#...#', '.#.#.', '..#..', '.#.#.', '#...#'],
|
|
94
|
+
'y': ['.....', '.....', '#...#', '#...#', '#...#', '.####', '....#', '....#', '.###.'],
|
|
95
|
+
'z': ['.....', '.....', '#####', '...#.', '..#..', '.#...', '#####'],
|
|
96
|
+
'{': ['...#.', '..#..', '..#..', '.#...', '..#..', '..#..', '...#.'],
|
|
97
|
+
'|': ['..#..', '..#..', '..#..', '..#..', '..#..', '..#..', '..#..'],
|
|
98
|
+
'}': ['.#...', '..#..', '..#..', '...#.', '..#..', '..#..', '.#...'],
|
|
99
|
+
'~': ['.....', '.....', '.#...', '#.#.#', '...#.', '.....', '.....'],
|
|
100
|
+
// symbols the screens use
|
|
101
|
+
'·': ['.....', '.....', '.....', '..#..', '.....', '.....', '.....'],
|
|
102
|
+
'•': ['.....', '.....', '.###.', '.###.', '.###.', '.....', '.....'],
|
|
103
|
+
'●': ['.....', '.###.', '#####', '#####', '#####', '.###.', '.....'],
|
|
104
|
+
'◉': ['.....', '.###.', '#...#', '#.#.#', '#...#', '.###.', '.....'],
|
|
105
|
+
'○': ['.....', '.###.', '#...#', '#...#', '#...#', '.###.', '.....'],
|
|
106
|
+
'◌': ['.....', '.#.#.', '#...#', '.....', '#...#', '.#.#.', '.....'],
|
|
107
|
+
'◐': ['.....', '.###.', '##..#', '##..#', '##..#', '.###.', '.....'],
|
|
108
|
+
'◈': ['..#..', '.#.#.', '#.#.#', '.#.#.', '..#..', '.....', '.....'],
|
|
109
|
+
'◆': ['..#..', '.###.', '#####', '.###.', '..#..', '.....', '.....'],
|
|
110
|
+
'◇': ['..#..', '.#.#.', '#...#', '.#.#.', '..#..', '.....', '.....'],
|
|
111
|
+
'■': ['.....', '#####', '#####', '#####', '#####', '#####', '.....'],
|
|
112
|
+
'□': ['.....', '#####', '#...#', '#...#', '#...#', '#####', '.....'],
|
|
113
|
+
'▪': ['.....', '.....', '.###.', '.###.', '.###.', '.....', '.....'],
|
|
114
|
+
'★': ['..#..', '..#..', '#####', '.###.', '.#.#.', '#...#', '.....'],
|
|
115
|
+
'☆': ['..#..', '..#..', '##.##', '.#.#.', '.#.#.', '#...#', '.....'],
|
|
116
|
+
'▲': ['.....', '..#..', '..#..', '.###.', '.###.', '#####', '.....'],
|
|
117
|
+
'▼': ['.....', '#####', '.###.', '.###.', '..#..', '..#..', '.....'],
|
|
118
|
+
'△': ['.....', '..#..', '..#..', '.#.#.', '.#.#.', '#####', '.....'],
|
|
119
|
+
'▽': ['.....', '#####', '.#.#.', '.#.#.', '..#..', '..#..', '.....'],
|
|
120
|
+
'▶': ['.#...', '.##..', '.###.', '.####', '.###.', '.##..', '.#...'],
|
|
121
|
+
'◀': ['...#.', '..##.', '.###.', '####.', '.###.', '..##.', '...#.'],
|
|
122
|
+
'▸': ['.....', '.#...', '.##..', '.###.', '.##..', '.#...', '.....'],
|
|
123
|
+
'◂': ['.....', '...#.', '..##.', '.###.', '..##.', '...#.', '.....'],
|
|
124
|
+
'✦': ['..#..', '..#..', '.###.', '#####', '.###.', '..#..', '..#..'],
|
|
125
|
+
'✧': ['..#..', '..#..', '.#.#.', '##.##', '.#.#.', '..#..', '..#..'],
|
|
126
|
+
'⚠': ['..#..', '.#.#.', '.#.#.', '#.#.#', '#...#', '#.#.#', '#####'],
|
|
127
|
+
'…': ['.....', '.....', '.....', '.....', '.....', '.....', '#.#.#'],
|
|
128
|
+
'→': ['.....', '..#..', '...#.', '#####', '...#.', '..#..', '.....'],
|
|
129
|
+
'←': ['.....', '..#..', '.#...', '#####', '.#...', '..#..', '.....'],
|
|
130
|
+
'↑': ['..#..', '.###.', '#.#.#', '..#..', '..#..', '..#..', '.....'],
|
|
131
|
+
'↓': ['.....', '..#..', '..#..', '..#..', '#.#.#', '.###.', '..#..'],
|
|
132
|
+
'↗': ['.....', '..###', '...##', '..#.#', '.#...', '#....', '.....'],
|
|
133
|
+
'↘': ['.....', '#....', '.#...', '..#.#', '...##', '..###', '.....'],
|
|
134
|
+
'↳': ['#....', '#....', '#..#.', '#...#', '#####', '....#', '...#.'],
|
|
135
|
+
'↪': ['.....', '.###.', '#...#', '#..#.', '#.###', '....#', '.....'],
|
|
136
|
+
'⎿': ['#....', '#....', '#....', '#....', '#####', '.....', '.....'],
|
|
137
|
+
'✔': ['.....', '....#', '...##', '#.##.', '###..', '.#...', '.....'],
|
|
138
|
+
'✖': ['.....', '##.##', '.###.', '..#..', '.###.', '##.##', '.....'],
|
|
139
|
+
'⚡': ['...##', '..##.', '.##..', '#####', '..##.', '.##..', '##...'],
|
|
140
|
+
'⇄': ['...#.', '#####', '...#.', '.#...', '#####', '.#...', '.....'],
|
|
141
|
+
'⏎': ['....#', '....#', '..#.#', '.####', '..#..', '.....', '.....'],
|
|
142
|
+
'✓': ['.....', '....#', '...#.', '#.#..', '.#...', '.....', '.....'],
|
|
143
|
+
'✗': ['.....', '#...#', '.#.#.', '..#..', '.#.#.', '#...#', '.....'],
|
|
144
|
+
'✕': ['.....', '#...#', '.#.#.', '..#..', '.#.#.', '#...#', '.....'],
|
|
145
|
+
'×': ['.....', '.....', '#...#', '.#.#.', '..#..', '.#.#.', '#...#'],
|
|
146
|
+
'—': ['.....', '.....', '.....', '#####', '.....', '.....', '.....'],
|
|
147
|
+
'–': ['.....', '.....', '.....', '.####', '.....', '.....', '.....'],
|
|
148
|
+
'≥': ['.#...', '..#..', '...#.', '..#..', '.#...', '.....', '#####'],
|
|
149
|
+
'≤': ['...#.', '..#..', '.#...', '..#..', '...#.', '.....', '#####'],
|
|
150
|
+
'≈': ['.....', '.#...', '#.#.#', '...#.', '.#...', '#.#.#', '...#.'],
|
|
151
|
+
'±': ['..#..', '..#..', '#####', '..#..', '..#..', '.....', '#####'],
|
|
152
|
+
'°': ['.##..', '#..#.', '.##..'],
|
|
153
|
+
'€': ['..###', '.#...', '####.', '.#...', '####.', '.#...', '..###'],
|
|
154
|
+
'£': ['..##.', '.#..#', '.#...', '###..', '.#...', '.#..#', '#.##.'],
|
|
155
|
+
'ᵖ': ['###..', '#.#..', '###..', '#....'],
|
|
156
|
+
'‹': ['.....', '...#.', '..#..', '.#...', '..#..', '...#.', '.....'],
|
|
157
|
+
'›': ['.....', '.#...', '..#..', '...#.', '..#..', '.#...', '.....'],
|
|
158
|
+
'«': ['.....', '..#.#', '.#.#.', '#.#..', '.#.#.', '..#.#', '.....'],
|
|
159
|
+
'»': ['.....', '#.#..', '.#.#.', '..#.#', '.#.#.', '#.#..', '.....'],
|
|
160
|
+
'‘': ['..#..', '.#...', '.#...'],
|
|
161
|
+
'’': ['..#..', '..#..', '.#...'],
|
|
162
|
+
'“': ['.#.#.', '#.#..', '#.#..'],
|
|
163
|
+
'”': ['.#.#.', '.#.#.', '#.#..'],
|
|
164
|
+
};
|
|
165
|
+
/** Glyph width and height in font pixels. */
|
|
166
|
+
export const FONT_W = 5;
|
|
167
|
+
export const FONT_H = 9;
|
|
168
|
+
// Small 3×5 digits for subscripts (₀-₉: the price formatter's zero-count, e.g. $0.0₅3105) and superscripts (⁰-⁹).
|
|
169
|
+
// A subscript must never render as a full-size digit: that would turn 0.0₅31 into 0.0531.
|
|
170
|
+
const SMALL = [
|
|
171
|
+
['###', '#.#', '#.#', '#.#', '###'], ['.#.', '##.', '.#.', '.#.', '###'], ['###', '..#', '###', '#..', '###'],
|
|
172
|
+
['###', '..#', '.##', '..#', '###'], ['#.#', '#.#', '###', '..#', '..#'], ['###', '#..', '###', '..#', '###'],
|
|
173
|
+
['###', '#..', '###', '#.#', '###'], ['###', '..#', '.#.', '.#.', '.#.'], ['###', '#.#', '###', '#.#', '###'],
|
|
174
|
+
['###', '#.#', '###', '..#', '###'],
|
|
175
|
+
];
|
|
176
|
+
const SUB = '₀₁₂₃₄₅₆₇₈₉', SUP = '⁰¹²³⁴⁵⁶⁷⁸⁹';
|
|
177
|
+
for (let d = 0; d < 10; d++) {
|
|
178
|
+
const rows = SMALL[d].map((r) => `.${r}.`);
|
|
179
|
+
G[SUB[d]] = ['.....', '.....', '.....', '.....', ...rows];
|
|
180
|
+
G[SUP[d]] = rows;
|
|
181
|
+
}
|
|
182
|
+
const BITS = new Map();
|
|
183
|
+
for (const [ch, rows] of Object.entries(G)) {
|
|
184
|
+
const b = new Uint8Array(FONT_W * FONT_H);
|
|
185
|
+
rows.forEach((r, y) => { for (let x = 0; x < FONT_W; x++)
|
|
186
|
+
if (r[x] === '#')
|
|
187
|
+
b[y * FONT_W + x] = 1; });
|
|
188
|
+
BITS.set(ch, b);
|
|
189
|
+
}
|
|
190
|
+
/** The 5×9 bitmap for a character (1 = ink), or null when the font has no glyph for it. */
|
|
191
|
+
export function glyphBits(ch) {
|
|
192
|
+
return BITS.get(ch) ?? null;
|
|
193
|
+
}
|
|
194
|
+
/** Every character with a bitmap glyph (tests check the printable ASCII range is complete). */
|
|
195
|
+
export function fontChars() { return [...BITS.keys()]; }
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { ATTR_BOLD, ATTR_DIM, ATTR_UNDERLINE } from "./canvas.js";
|
|
2
|
+
import { hexToRgb, mix } from "./color.js";
|
|
3
|
+
import { glyphBits, FONT_W, FONT_H } from "./raster-font.js";
|
|
4
|
+
class Px {
|
|
5
|
+
w;
|
|
6
|
+
h;
|
|
7
|
+
rgba;
|
|
8
|
+
constructor(w, h) { this.w = w; this.h = h; this.rgba = new Uint8Array(w * h * 4); }
|
|
9
|
+
rect(x, y, w, h, c, a = 1) {
|
|
10
|
+
const x0 = Math.max(0, Math.floor(x)), y0 = Math.max(0, Math.floor(y));
|
|
11
|
+
const x1 = Math.min(this.w, Math.floor(x + w)), y1 = Math.min(this.h, Math.floor(y + h));
|
|
12
|
+
for (let yy = y0; yy < y1; yy++)
|
|
13
|
+
for (let xx = x0; xx < x1; xx++) {
|
|
14
|
+
const i = (yy * this.w + xx) * 4;
|
|
15
|
+
if (a >= 1) {
|
|
16
|
+
this.rgba[i] = c[0];
|
|
17
|
+
this.rgba[i + 1] = c[1];
|
|
18
|
+
this.rgba[i + 2] = c[2];
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
this.rgba[i] = Math.round(this.rgba[i] * (1 - a) + c[0] * a);
|
|
22
|
+
this.rgba[i + 1] = Math.round(this.rgba[i + 1] * (1 - a) + c[1] * a);
|
|
23
|
+
this.rgba[i + 2] = Math.round(this.rgba[i + 2] * (1 - a) + c[2] * a);
|
|
24
|
+
}
|
|
25
|
+
this.rgba[i + 3] = 255;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Box drawing: up, down, left, right weights (0 none, 1 light, 2 heavy/double) and a dashed flag.
|
|
30
|
+
const BOX = {
|
|
31
|
+
'─': '0011', '━': '0022', '│': '1100', '┃': '2200',
|
|
32
|
+
'┌': '0101', '╭': '0101', '┐': '0110', '╮': '0110', '└': '1001', '╰': '1001', '┘': '1010', '╯': '1010',
|
|
33
|
+
'┏': '0202', '┓': '0220', '┗': '2002', '┛': '2020',
|
|
34
|
+
'├': '1101', '┤': '1110', '┬': '0111', '┴': '1011', '┼': '1111',
|
|
35
|
+
'┣': '2202', '┫': '2220', '┳': '0222', '┻': '2022', '╋': '2222',
|
|
36
|
+
'═': '0022', '║': '2200', '╔': '0202', '╗': '0220', '╚': '2002', '╝': '2020', '╠': '2202', '╣': '2220', '╦': '0222', '╩': '2022', '╬': '2222',
|
|
37
|
+
'╴': '0010', '╵': '1000', '╶': '0001', '╷': '0100', '╸': '0020', '╹': '2000', '╺': '0002', '╻': '0200',
|
|
38
|
+
};
|
|
39
|
+
const DASH_H = new Set(['┄', '┅', '┈', '┉', '╌', '╍']);
|
|
40
|
+
const DASH_V = new Set(['┆', '┇', '┊', '┋', '╎', '╏']);
|
|
41
|
+
const EIGHTHS_LOW = '▁▂▃▄▅▆▇█';
|
|
42
|
+
const EIGHTHS_LEFT = '▏▎▍▌▋▊▉█';
|
|
43
|
+
const SHADES = { '░': 0.25, '▒': 0.5, '▓': 0.75 };
|
|
44
|
+
// Quadrants: TL, TR, BL, BR.
|
|
45
|
+
const QUAD = {
|
|
46
|
+
'▘': [1, 0, 0, 0], '▝': [0, 1, 0, 0], '▖': [0, 0, 1, 0], '▗': [0, 0, 0, 1], '▚': [1, 0, 0, 1], '▞': [0, 1, 1, 0],
|
|
47
|
+
'▛': [1, 1, 1, 0], '▜': [1, 1, 0, 1], '▙': [1, 0, 1, 1], '▟': [0, 1, 1, 1], '▀': [1, 1, 0, 0], '▄': [0, 0, 1, 1], '▌': [1, 0, 1, 0], '▐': [0, 1, 0, 1],
|
|
48
|
+
};
|
|
49
|
+
/** Draw one non-text glyph procedurally. Returns false when the glyph is not one of these families. */
|
|
50
|
+
function drawSpecial(p, ch, x, y, cw, chh, fg, bg, scale) {
|
|
51
|
+
const box = BOX[ch];
|
|
52
|
+
if (box) {
|
|
53
|
+
const t1 = Math.max(1, scale), t2 = Math.max(2, scale * 2);
|
|
54
|
+
const [u, d, l, r] = [...box].map(Number);
|
|
55
|
+
const cx = x + Math.floor(cw / 2), cy = y + Math.floor(chh / 2);
|
|
56
|
+
const th = (wgt) => (wgt === 2 ? t2 : t1);
|
|
57
|
+
const hT = Math.max(th(l), th(r)), vT = Math.max(th(u), th(d));
|
|
58
|
+
if (l)
|
|
59
|
+
p.rect(x, cy - Math.floor(th(l) / 2), cx - x + Math.ceil(vT / 2), th(l), fg);
|
|
60
|
+
if (r)
|
|
61
|
+
p.rect(cx - Math.floor(vT / 2), cy - Math.floor(th(r) / 2), x + cw - (cx - Math.floor(vT / 2)), th(r), fg);
|
|
62
|
+
if (u)
|
|
63
|
+
p.rect(cx - Math.floor(th(u) / 2), y, th(u), cy - y + Math.ceil(hT / 2), fg);
|
|
64
|
+
if (d)
|
|
65
|
+
p.rect(cx - Math.floor(th(d) / 2), cy - Math.floor(hT / 2), th(d), y + chh - (cy - Math.floor(hT / 2)), fg);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (DASH_H.has(ch)) {
|
|
69
|
+
const t = Math.max(1, scale);
|
|
70
|
+
const seg = Math.max(2, Math.floor(cw / 4));
|
|
71
|
+
for (let k = 0; k < cw; k += seg * 2)
|
|
72
|
+
p.rect(x + k, y + Math.floor(chh / 2) - Math.floor(t / 2), seg, t, fg);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (DASH_V.has(ch)) {
|
|
76
|
+
const t = Math.max(1, scale);
|
|
77
|
+
const seg = Math.max(2, Math.floor(chh / 6));
|
|
78
|
+
for (let k = 0; k < chh; k += seg * 2)
|
|
79
|
+
p.rect(x + Math.floor(cw / 2) - Math.floor(t / 2), y + k, t, seg, fg);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
const low = EIGHTHS_LOW.indexOf(ch);
|
|
83
|
+
if (low >= 0) {
|
|
84
|
+
const hh = Math.round((chh * (low + 1)) / 8);
|
|
85
|
+
p.rect(x, y + chh - hh, cw, hh, fg);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
const left = EIGHTHS_LEFT.indexOf(ch);
|
|
89
|
+
if (left >= 0) {
|
|
90
|
+
p.rect(x, y, Math.round((cw * (left + 1)) / 8), chh, fg);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (ch === '▔') {
|
|
94
|
+
p.rect(x, y, cw, Math.max(1, Math.round(chh / 8)), fg);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (ch === '▕') {
|
|
98
|
+
const ww = Math.max(1, Math.round(cw / 8));
|
|
99
|
+
p.rect(x + cw - ww, y, ww, chh, fg);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
const shade = SHADES[ch];
|
|
103
|
+
if (shade !== undefined) {
|
|
104
|
+
p.rect(x, y, cw, chh, [Math.round(bg[0] + (fg[0] - bg[0]) * shade), Math.round(bg[1] + (fg[1] - bg[1]) * shade), Math.round(bg[2] + (fg[2] - bg[2]) * shade)]);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
const q = QUAD[ch];
|
|
108
|
+
if (q) {
|
|
109
|
+
const hw = Math.floor(cw / 2), hh = Math.floor(chh / 2);
|
|
110
|
+
if (q[0])
|
|
111
|
+
p.rect(x, y, hw, hh, fg);
|
|
112
|
+
if (q[1])
|
|
113
|
+
p.rect(x + hw, y, cw - hw, hh, fg);
|
|
114
|
+
if (q[2])
|
|
115
|
+
p.rect(x, y + hh, hw, chh - hh, fg);
|
|
116
|
+
if (q[3])
|
|
117
|
+
p.rect(x + hw, y + hh, cw - hw, chh - hh, fg);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
121
|
+
if (code >= 0x2800 && code <= 0x28ff) {
|
|
122
|
+
const bits = code - 0x2800;
|
|
123
|
+
// dot n → (column, row): 1(0,0) 2(0,1) 3(0,2) 4(1,0) 5(1,1) 6(1,2) 7(0,3) 8(1,3)
|
|
124
|
+
const dots = [[0x01, 0, 0], [0x02, 0, 1], [0x04, 0, 2], [0x08, 1, 0], [0x10, 1, 1], [0x20, 1, 2], [0x40, 0, 3], [0x80, 1, 3]];
|
|
125
|
+
const dw = Math.max(1, Math.floor(cw / 4)), dh = Math.max(1, Math.floor(chh / 8));
|
|
126
|
+
for (const [bit, col, row] of dots)
|
|
127
|
+
if (bits & bit)
|
|
128
|
+
p.rect(x + Math.floor((cw * (col * 2 + 1)) / 4) - Math.floor(dw / 2), y + Math.floor((chh * (row * 2 + 1)) / 8) - Math.floor(dh / 2), dw, dh, fg);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (ch === '╱' || ch === '╲' || ch === '╳') {
|
|
132
|
+
const t = Math.max(1, scale);
|
|
133
|
+
for (let k = 0; k < chh; k++) {
|
|
134
|
+
const fx = Math.floor((k * cw) / chh);
|
|
135
|
+
if (ch !== '╲')
|
|
136
|
+
p.rect(x + cw - 1 - fx - Math.floor(t / 2), y + k, t, 1, fg);
|
|
137
|
+
if (ch !== '╱')
|
|
138
|
+
p.rect(x + fx - Math.floor(t / 2), y + k, t, 1, fg);
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
/** Rasterize every cell of a canvas: background, glyph (font or procedural), attributes, then image slots. */
|
|
145
|
+
export function rasterizeCanvas(c, opts = {}) {
|
|
146
|
+
const cw = opts.cellW ?? 12, chh = opts.cellH ?? 24, scale = opts.scale ?? 2;
|
|
147
|
+
const p = new Px(c.w * cw, c.h * chh);
|
|
148
|
+
const gx = Math.floor((cw - FONT_W * scale) / 2), gy = Math.floor((chh - FONT_H * scale) / 2);
|
|
149
|
+
const rgbCache = new Map();
|
|
150
|
+
const rgb = (h) => { let v = rgbCache.get(h); if (!v) {
|
|
151
|
+
v = hexToRgb(h);
|
|
152
|
+
rgbCache.set(h, v);
|
|
153
|
+
} return v; };
|
|
154
|
+
for (let row = 0; row < c.h; row++)
|
|
155
|
+
for (let col = 0; col < c.w; col++) {
|
|
156
|
+
const i = row * c.w + col;
|
|
157
|
+
const x = col * cw, y = row * chh;
|
|
158
|
+
const bgHex = c.bg[i];
|
|
159
|
+
const bg = rgb(bgHex);
|
|
160
|
+
p.rect(x, y, cw, chh, bg);
|
|
161
|
+
const ch = c.ch[i];
|
|
162
|
+
if (ch === ' ' || ch === '')
|
|
163
|
+
continue;
|
|
164
|
+
const at = c.at[i];
|
|
165
|
+
const fg = rgb(at & ATTR_DIM ? mix(c.fg[i], bgHex, 0.45) : c.fg[i]);
|
|
166
|
+
if (drawSpecial(p, ch, x, y, cw, chh, fg, bg, scale))
|
|
167
|
+
continue;
|
|
168
|
+
let bits = glyphBits(ch);
|
|
169
|
+
// an accented LETTER falls back to its base letter (é → e); never a digit or symbol, whose compatibility form
|
|
170
|
+
// can change meaning (a subscript ₅ is not a 5)
|
|
171
|
+
if (!bits && /^\p{L}$/u.test(ch)) {
|
|
172
|
+
const base = ch.normalize('NFD').replace(/\p{M}+/gu, '');
|
|
173
|
+
if (base && base !== ch)
|
|
174
|
+
bits = glyphBits(base[0]);
|
|
175
|
+
}
|
|
176
|
+
if (!bits) {
|
|
177
|
+
// tofu: a hollow box so an unsupported glyph is visible, never silently dropped
|
|
178
|
+
const t = Math.max(1, Math.floor(scale / 2));
|
|
179
|
+
const bx = x + gx + scale, by = y + gy + scale * 2, bw = (FONT_W - 2) * scale, bh = (FONT_H - 4) * scale;
|
|
180
|
+
p.rect(bx, by, bw, t, fg);
|
|
181
|
+
p.rect(bx, by + bh - t, bw, t, fg);
|
|
182
|
+
p.rect(bx, by, t, bh, fg);
|
|
183
|
+
p.rect(bx + bw - t, by, t, bh, fg);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const bold = (at & ATTR_BOLD) !== 0;
|
|
187
|
+
for (let yy = 0; yy < FONT_H; yy++)
|
|
188
|
+
for (let xx = 0; xx < FONT_W; xx++) {
|
|
189
|
+
if (!bits[yy * FONT_W + xx])
|
|
190
|
+
continue;
|
|
191
|
+
p.rect(x + gx + xx * scale, y + gy + yy * scale, scale + (bold ? 1 : 0), scale, fg);
|
|
192
|
+
}
|
|
193
|
+
if (at & ATTR_UNDERLINE)
|
|
194
|
+
p.rect(x, y + chh - Math.max(1, scale), cw, Math.max(1, Math.floor(scale / 2)), fg);
|
|
195
|
+
}
|
|
196
|
+
// Pixel image slots (coin images) recorded by the canvas in 'pixels' mode: nearest-neighbour into the cell rect.
|
|
197
|
+
for (const s of c.images) {
|
|
198
|
+
const rx = s.x * cw, ry = s.y * chh, rw = s.w * cw, rh = s.h * chh;
|
|
199
|
+
const { w: iw, h: ih, rgba } = s.img;
|
|
200
|
+
if (!iw || !ih)
|
|
201
|
+
continue;
|
|
202
|
+
for (let yy = 0; yy < rh; yy++)
|
|
203
|
+
for (let xx = 0; xx < rw; xx++) {
|
|
204
|
+
const sx = Math.min(iw - 1, Math.floor((xx * iw) / rw)), sy = Math.min(ih - 1, Math.floor((yy * ih) / rh));
|
|
205
|
+
const si = (sy * iw + sx) * 4;
|
|
206
|
+
const a = rgba[si + 3] / 255;
|
|
207
|
+
if (a <= 0)
|
|
208
|
+
continue;
|
|
209
|
+
p.rect(rx + xx, ry + yy, 1, 1, [rgba[si], rgba[si + 1], rgba[si + 2]], a);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { w: p.w, h: p.h, rgba: p.rgba };
|
|
213
|
+
}
|