shortcutxl 0.2.12 → 0.2.13
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/README.md +26 -26
- package/agent-docs/README.md +397 -397
- package/agent-docs/docs/compaction.md +390 -390
- package/agent-docs/docs/custom-provider.md +580 -580
- package/agent-docs/docs/extensions.md +1971 -1971
- package/agent-docs/docs/packages.md +209 -209
- package/agent-docs/docs/rpc.md +1317 -1317
- package/agent-docs/docs/sdk.md +962 -962
- package/agent-docs/docs/session.md +412 -412
- package/agent-docs/docs/termux.md +127 -127
- package/agent-docs/docs/tui.md +887 -887
- package/agent-docs/examples/README.md +25 -25
- package/agent-docs/examples/extensions/README.md +205 -205
- package/agent-docs/examples/extensions/antigravity-image-gen.ts +447 -447
- package/agent-docs/examples/extensions/auto-commit-on-exit.ts +49 -49
- package/agent-docs/examples/extensions/bash-spawn-hook.ts +30 -30
- package/agent-docs/examples/extensions/bookmark.ts +50 -50
- package/agent-docs/examples/extensions/built-in-tool-renderer.ts +256 -256
- package/agent-docs/examples/extensions/claude-rules.ts +86 -86
- package/agent-docs/examples/extensions/commands.ts +75 -75
- package/agent-docs/examples/extensions/confirm-destructive.ts +59 -59
- package/agent-docs/examples/extensions/custom-compaction.ts +126 -126
- package/agent-docs/examples/extensions/custom-footer.ts +63 -63
- package/agent-docs/examples/extensions/custom-header.ts +73 -73
- package/agent-docs/examples/extensions/custom-provider-anthropic/index.ts +660 -660
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/index.ts +362 -362
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/test.ts +88 -88
- package/agent-docs/examples/extensions/custom-provider-qwen-cli/index.ts +349 -349
- package/agent-docs/examples/extensions/dirty-repo-guard.ts +56 -56
- package/agent-docs/examples/extensions/doom-overlay/doom-component.ts +133 -133
- package/agent-docs/examples/extensions/doom-overlay/doom-keys.ts +108 -108
- package/agent-docs/examples/extensions/doom-overlay/index.ts +74 -74
- package/agent-docs/examples/extensions/dynamic-resources/index.ts +15 -15
- package/agent-docs/examples/extensions/dynamic-tools.ts +77 -77
- package/agent-docs/examples/extensions/event-bus.ts +43 -43
- package/agent-docs/examples/extensions/file-trigger.ts +41 -41
- package/agent-docs/examples/extensions/git-checkpoint.ts +53 -53
- package/agent-docs/examples/extensions/handoff.ts +155 -155
- package/agent-docs/examples/extensions/hello.ts +25 -25
- package/agent-docs/examples/extensions/inline-bash.ts +94 -94
- package/agent-docs/examples/extensions/input-transform.ts +43 -43
- package/agent-docs/examples/extensions/interactive-shell.ts +209 -209
- package/agent-docs/examples/extensions/mac-system-theme.ts +47 -47
- package/agent-docs/examples/extensions/message-renderer.ts +59 -59
- package/agent-docs/examples/extensions/minimal-mode.ts +430 -430
- package/agent-docs/examples/extensions/modal-editor.ts +90 -90
- package/agent-docs/examples/extensions/model-status.ts +31 -31
- package/agent-docs/examples/extensions/notify.ts +55 -55
- package/agent-docs/examples/extensions/overlay-qa-tests.ts +936 -936
- package/agent-docs/examples/extensions/overlay-test.ts +159 -159
- package/agent-docs/examples/extensions/permission-gate.ts +37 -37
- package/agent-docs/examples/extensions/pirate.ts +47 -47
- package/agent-docs/examples/extensions/plan-mode/index.ts +363 -363
- package/agent-docs/examples/extensions/preset.ts +418 -418
- package/agent-docs/examples/extensions/protected-paths.ts +30 -30
- package/agent-docs/examples/extensions/qna.ts +122 -122
- package/agent-docs/examples/extensions/question.ts +278 -278
- package/agent-docs/examples/extensions/questionnaire.ts +440 -440
- package/agent-docs/examples/extensions/rainbow-editor.ts +90 -90
- package/agent-docs/examples/extensions/reload-runtime.ts +37 -37
- package/agent-docs/examples/extensions/rpc-demo.ts +124 -124
- package/agent-docs/examples/extensions/sandbox/index.ts +324 -324
- package/agent-docs/examples/extensions/send-user-message.ts +97 -97
- package/agent-docs/examples/extensions/session-name.ts +27 -27
- package/agent-docs/examples/extensions/shutdown-command.ts +69 -69
- package/agent-docs/examples/extensions/snake.ts +343 -343
- package/agent-docs/examples/extensions/space-invaders.ts +566 -566
- package/agent-docs/examples/extensions/ssh.ts +233 -233
- package/agent-docs/examples/extensions/status-line.ts +40 -40
- package/agent-docs/examples/extensions/subagent/agents.ts +130 -130
- package/agent-docs/examples/extensions/subagent/index.ts +1068 -1068
- package/agent-docs/examples/extensions/summarize.ts +206 -206
- package/agent-docs/examples/extensions/system-prompt-header.ts +17 -17
- package/agent-docs/examples/extensions/timed-confirm.ts +72 -72
- package/agent-docs/examples/extensions/titlebar-spinner.ts +58 -58
- package/agent-docs/examples/extensions/todo.ts +314 -314
- package/agent-docs/examples/extensions/tool-override.ts +146 -146
- package/agent-docs/examples/extensions/tools.ts +145 -145
- package/agent-docs/examples/extensions/trigger-compact.ts +40 -40
- package/agent-docs/examples/extensions/truncated-tool.ts +194 -194
- package/agent-docs/examples/extensions/widget-placement.ts +17 -17
- package/agent-docs/examples/extensions/with-deps/index.ts +37 -37
- package/agent-docs/examples/rpc-extension-ui.ts +654 -654
- package/agent-docs/examples/sdk/01-minimal.ts +22 -22
- package/agent-docs/examples/sdk/02-custom-model.ts +48 -48
- package/agent-docs/examples/sdk/03-custom-prompt.ts +55 -55
- package/agent-docs/examples/sdk/04-skills.ts +53 -53
- package/agent-docs/examples/sdk/05-tools.ts +56 -56
- package/agent-docs/examples/sdk/06-extensions.ts +88 -88
- package/agent-docs/examples/sdk/07-context-files.ts +40 -40
- package/agent-docs/examples/sdk/08-prompt-templates.ts +47 -47
- package/agent-docs/examples/sdk/09-api-keys-and-oauth.ts +48 -48
- package/agent-docs/examples/sdk/10-settings.ts +54 -54
- package/agent-docs/examples/sdk/11-sessions.ts +48 -48
- package/agent-docs/examples/sdk/12-full-control.ts +82 -82
- package/agent-docs/examples/sdk/README.md +144 -144
- package/agent-docs/xll-spec.md +110 -110
- package/dist/core/auth-storage.js +21 -2
- package/package.json +1 -1
- package/xll/ShortcutXL.xll +0 -0
- package/xll/modules/debug_render.py +272 -272
- package/xll/modules/gameboy.py +241 -241
- package/xll/modules/pong.py +188 -188
- package/xll/modules/shortcut_xl/_diff_highlight.py +176 -0
- package/xll/modules/shortcut_xl/_log.py +12 -12
- package/xll/modules/shortcut_xl/_registry.py +44 -44
- package/xll/modules/stocks.py +100 -100
- /package/skills/{com-advanced-api → COM-advanced-api}/SKILL.md +0 -0
- /package/skills/{com-advanced-api → COM-advanced-api}/excel-type-library.py +0 -0
- /package/skills/{com-advanced-api → COM-advanced-api}/office-type-library.py +0 -0
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dirty Repo Guard Extension
|
|
3
|
-
*
|
|
4
|
-
* Prevents session changes when there are uncommitted git changes.
|
|
5
|
-
* Useful to ensure work is committed before switching context.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { ExtensionAPI, ExtensionContext } from 'shortcutxl';
|
|
9
|
-
|
|
10
|
-
async function checkDirtyRepo(
|
|
11
|
-
shortcut: ExtensionAPI,
|
|
12
|
-
ctx: ExtensionContext,
|
|
13
|
-
action: string
|
|
14
|
-
): Promise<{ cancel: boolean } | undefined> {
|
|
15
|
-
// Check for uncommitted changes
|
|
16
|
-
const { stdout, code } = await shortcut.exec('git', ['status', '--porcelain']);
|
|
17
|
-
|
|
18
|
-
if (code !== 0) {
|
|
19
|
-
// Not a git repo, allow the action
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const hasChanges = stdout.trim().length > 0;
|
|
24
|
-
if (!hasChanges) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (!ctx.hasUI) {
|
|
29
|
-
// In non-interactive mode, block by default
|
|
30
|
-
return { cancel: true };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Count changed files
|
|
34
|
-
const changedFiles = stdout.trim().split('\n').filter(Boolean).length;
|
|
35
|
-
|
|
36
|
-
const choice = await ctx.ui.select(
|
|
37
|
-
`You have ${changedFiles} uncommitted file(s). ${action} anyway?`,
|
|
38
|
-
['Yes, proceed anyway', 'No, let me commit first']
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
if (choice !== 'Yes, proceed anyway') {
|
|
42
|
-
ctx.ui.notify('Commit your changes first', 'warning');
|
|
43
|
-
return { cancel: true };
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export default function (shortcut: ExtensionAPI) {
|
|
48
|
-
shortcut.on('session_before_switch', async (event, ctx) => {
|
|
49
|
-
const action = event.reason === 'new' ? 'new session' : 'switch session';
|
|
50
|
-
return checkDirtyRepo(shortcut, ctx, action);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
shortcut.on('session_before_fork', async (_event, ctx) => {
|
|
54
|
-
return checkDirtyRepo(shortcut, ctx, 'fork');
|
|
55
|
-
});
|
|
56
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Dirty Repo Guard Extension
|
|
3
|
+
*
|
|
4
|
+
* Prevents session changes when there are uncommitted git changes.
|
|
5
|
+
* Useful to ensure work is committed before switching context.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI, ExtensionContext } from 'shortcutxl';
|
|
9
|
+
|
|
10
|
+
async function checkDirtyRepo(
|
|
11
|
+
shortcut: ExtensionAPI,
|
|
12
|
+
ctx: ExtensionContext,
|
|
13
|
+
action: string
|
|
14
|
+
): Promise<{ cancel: boolean } | undefined> {
|
|
15
|
+
// Check for uncommitted changes
|
|
16
|
+
const { stdout, code } = await shortcut.exec('git', ['status', '--porcelain']);
|
|
17
|
+
|
|
18
|
+
if (code !== 0) {
|
|
19
|
+
// Not a git repo, allow the action
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const hasChanges = stdout.trim().length > 0;
|
|
24
|
+
if (!hasChanges) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!ctx.hasUI) {
|
|
29
|
+
// In non-interactive mode, block by default
|
|
30
|
+
return { cancel: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Count changed files
|
|
34
|
+
const changedFiles = stdout.trim().split('\n').filter(Boolean).length;
|
|
35
|
+
|
|
36
|
+
const choice = await ctx.ui.select(
|
|
37
|
+
`You have ${changedFiles} uncommitted file(s). ${action} anyway?`,
|
|
38
|
+
['Yes, proceed anyway', 'No, let me commit first']
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (choice !== 'Yes, proceed anyway') {
|
|
42
|
+
ctx.ui.notify('Commit your changes first', 'warning');
|
|
43
|
+
return { cancel: true };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function (shortcut: ExtensionAPI) {
|
|
48
|
+
shortcut.on('session_before_switch', async (event, ctx) => {
|
|
49
|
+
const action = event.reason === 'new' ? 'new session' : 'switch session';
|
|
50
|
+
return checkDirtyRepo(shortcut, ctx, action);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
shortcut.on('session_before_fork', async (_event, ctx) => {
|
|
54
|
+
return checkDirtyRepo(shortcut, ctx, 'fork');
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DOOM Component for overlay mode
|
|
3
|
-
*
|
|
4
|
-
* Renders DOOM frames using half-block characters (▀) with 24-bit color.
|
|
5
|
-
* Height is calculated from width to maintain DOOM's aspect ratio.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { Component } from 'shortcutxl';
|
|
9
|
-
import { isKeyRelease, type TUI } from 'shortcutxl';
|
|
10
|
-
import type { DoomEngine } from './doom-engine.js';
|
|
11
|
-
import { DoomKeys, mapKeyToDoom } from './doom-keys.js';
|
|
12
|
-
|
|
13
|
-
function renderHalfBlock(
|
|
14
|
-
rgba: Uint8Array,
|
|
15
|
-
width: number,
|
|
16
|
-
height: number,
|
|
17
|
-
targetCols: number,
|
|
18
|
-
targetRows: number
|
|
19
|
-
): string[] {
|
|
20
|
-
const lines: string[] = [];
|
|
21
|
-
const scaleX = width / targetCols;
|
|
22
|
-
const scaleY = height / (targetRows * 2);
|
|
23
|
-
|
|
24
|
-
for (let row = 0; row < targetRows; row++) {
|
|
25
|
-
let line = '';
|
|
26
|
-
const srcY1 = Math.floor(row * 2 * scaleY);
|
|
27
|
-
const srcY2 = Math.floor((row * 2 + 1) * scaleY);
|
|
28
|
-
|
|
29
|
-
for (let col = 0; col < targetCols; col++) {
|
|
30
|
-
const srcX = Math.floor(col * scaleX);
|
|
31
|
-
const idx1 = (srcY1 * width + srcX) * 4;
|
|
32
|
-
const idx2 = (srcY2 * width + srcX) * 4;
|
|
33
|
-
const r1 = rgba[idx1] ?? 0,
|
|
34
|
-
g1 = rgba[idx1 + 1] ?? 0,
|
|
35
|
-
b1 = rgba[idx1 + 2] ?? 0;
|
|
36
|
-
const r2 = rgba[idx2] ?? 0,
|
|
37
|
-
g2 = rgba[idx2 + 1] ?? 0,
|
|
38
|
-
b2 = rgba[idx2 + 2] ?? 0;
|
|
39
|
-
line += `\x1b[38;2;${r1};${g1};${b1}m\x1b[48;2;${r2};${g2};${b2}m▀`;
|
|
40
|
-
}
|
|
41
|
-
line += '\x1b[0m';
|
|
42
|
-
lines.push(line);
|
|
43
|
-
}
|
|
44
|
-
return lines;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export class DoomOverlayComponent implements Component {
|
|
48
|
-
private engine: DoomEngine;
|
|
49
|
-
private tui: TUI;
|
|
50
|
-
private interval: ReturnType<typeof setInterval> | null = null;
|
|
51
|
-
private onExit: () => void;
|
|
52
|
-
|
|
53
|
-
// Opt-in to key release events for smooth movement
|
|
54
|
-
wantsKeyRelease = true;
|
|
55
|
-
|
|
56
|
-
constructor(tui: TUI, engine: DoomEngine, onExit: () => void, resume = false) {
|
|
57
|
-
this.tui = tui;
|
|
58
|
-
this.engine = engine;
|
|
59
|
-
this.onExit = onExit;
|
|
60
|
-
|
|
61
|
-
// Unpause if resuming
|
|
62
|
-
if (resume) {
|
|
63
|
-
this.engine.pushKey(true, DoomKeys.KEY_PAUSE);
|
|
64
|
-
this.engine.pushKey(false, DoomKeys.KEY_PAUSE);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
this.startGameLoop();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private startGameLoop(): void {
|
|
71
|
-
this.interval = setInterval(() => {
|
|
72
|
-
try {
|
|
73
|
-
this.engine.tick();
|
|
74
|
-
this.tui.requestRender();
|
|
75
|
-
} catch {
|
|
76
|
-
// WASM error (e.g., exit via DOOM menu) - treat as quit
|
|
77
|
-
this.dispose();
|
|
78
|
-
this.onExit();
|
|
79
|
-
}
|
|
80
|
-
}, 1000 / 35);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
handleInput(data: string): void {
|
|
84
|
-
// Q to pause and exit (but not on release)
|
|
85
|
-
if (!isKeyRelease(data) && (data === 'q' || data === 'Q')) {
|
|
86
|
-
// Send DOOM's pause key before exiting
|
|
87
|
-
this.engine.pushKey(true, DoomKeys.KEY_PAUSE);
|
|
88
|
-
this.engine.pushKey(false, DoomKeys.KEY_PAUSE);
|
|
89
|
-
this.dispose();
|
|
90
|
-
this.onExit();
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const doomKeys = mapKeyToDoom(data);
|
|
95
|
-
if (doomKeys.length === 0) return;
|
|
96
|
-
|
|
97
|
-
const released = isKeyRelease(data);
|
|
98
|
-
|
|
99
|
-
for (const key of doomKeys) {
|
|
100
|
-
this.engine.pushKey(!released, key);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
render(width: number): string[] {
|
|
105
|
-
// DOOM renders at 640x400 (1.6:1 ratio)
|
|
106
|
-
// With half-block characters, each terminal row = 2 pixels
|
|
107
|
-
// So effective ratio is 640:200 = 3.2:1 (width:height in terminal cells)
|
|
108
|
-
// Add 1 row for footer
|
|
109
|
-
const ASPECT_RATIO = 3.2;
|
|
110
|
-
const MIN_HEIGHT = 10;
|
|
111
|
-
const height = Math.max(MIN_HEIGHT, Math.floor(width / ASPECT_RATIO));
|
|
112
|
-
|
|
113
|
-
const rgba = this.engine.getFrameRGBA();
|
|
114
|
-
const lines = renderHalfBlock(rgba, this.engine.width, this.engine.height, width, height);
|
|
115
|
-
|
|
116
|
-
// Footer
|
|
117
|
-
const footer =
|
|
118
|
-
' DOOM | Q=Pause | WASD=Move | Shift+WASD=Run | Space=Use | F=Fire | 1-7=Weapons';
|
|
119
|
-
const truncatedFooter = footer.length > width ? footer.slice(0, width) : footer;
|
|
120
|
-
lines.push(`\x1b[2m${truncatedFooter}\x1b[0m`);
|
|
121
|
-
|
|
122
|
-
return lines;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
invalidate(): void {}
|
|
126
|
-
|
|
127
|
-
dispose(): void {
|
|
128
|
-
if (this.interval) {
|
|
129
|
-
clearInterval(this.interval);
|
|
130
|
-
this.interval = null;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* DOOM Component for overlay mode
|
|
3
|
+
*
|
|
4
|
+
* Renders DOOM frames using half-block characters (▀) with 24-bit color.
|
|
5
|
+
* Height is calculated from width to maintain DOOM's aspect ratio.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Component } from 'shortcutxl';
|
|
9
|
+
import { isKeyRelease, type TUI } from 'shortcutxl';
|
|
10
|
+
import type { DoomEngine } from './doom-engine.js';
|
|
11
|
+
import { DoomKeys, mapKeyToDoom } from './doom-keys.js';
|
|
12
|
+
|
|
13
|
+
function renderHalfBlock(
|
|
14
|
+
rgba: Uint8Array,
|
|
15
|
+
width: number,
|
|
16
|
+
height: number,
|
|
17
|
+
targetCols: number,
|
|
18
|
+
targetRows: number
|
|
19
|
+
): string[] {
|
|
20
|
+
const lines: string[] = [];
|
|
21
|
+
const scaleX = width / targetCols;
|
|
22
|
+
const scaleY = height / (targetRows * 2);
|
|
23
|
+
|
|
24
|
+
for (let row = 0; row < targetRows; row++) {
|
|
25
|
+
let line = '';
|
|
26
|
+
const srcY1 = Math.floor(row * 2 * scaleY);
|
|
27
|
+
const srcY2 = Math.floor((row * 2 + 1) * scaleY);
|
|
28
|
+
|
|
29
|
+
for (let col = 0; col < targetCols; col++) {
|
|
30
|
+
const srcX = Math.floor(col * scaleX);
|
|
31
|
+
const idx1 = (srcY1 * width + srcX) * 4;
|
|
32
|
+
const idx2 = (srcY2 * width + srcX) * 4;
|
|
33
|
+
const r1 = rgba[idx1] ?? 0,
|
|
34
|
+
g1 = rgba[idx1 + 1] ?? 0,
|
|
35
|
+
b1 = rgba[idx1 + 2] ?? 0;
|
|
36
|
+
const r2 = rgba[idx2] ?? 0,
|
|
37
|
+
g2 = rgba[idx2 + 1] ?? 0,
|
|
38
|
+
b2 = rgba[idx2 + 2] ?? 0;
|
|
39
|
+
line += `\x1b[38;2;${r1};${g1};${b1}m\x1b[48;2;${r2};${g2};${b2}m▀`;
|
|
40
|
+
}
|
|
41
|
+
line += '\x1b[0m';
|
|
42
|
+
lines.push(line);
|
|
43
|
+
}
|
|
44
|
+
return lines;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class DoomOverlayComponent implements Component {
|
|
48
|
+
private engine: DoomEngine;
|
|
49
|
+
private tui: TUI;
|
|
50
|
+
private interval: ReturnType<typeof setInterval> | null = null;
|
|
51
|
+
private onExit: () => void;
|
|
52
|
+
|
|
53
|
+
// Opt-in to key release events for smooth movement
|
|
54
|
+
wantsKeyRelease = true;
|
|
55
|
+
|
|
56
|
+
constructor(tui: TUI, engine: DoomEngine, onExit: () => void, resume = false) {
|
|
57
|
+
this.tui = tui;
|
|
58
|
+
this.engine = engine;
|
|
59
|
+
this.onExit = onExit;
|
|
60
|
+
|
|
61
|
+
// Unpause if resuming
|
|
62
|
+
if (resume) {
|
|
63
|
+
this.engine.pushKey(true, DoomKeys.KEY_PAUSE);
|
|
64
|
+
this.engine.pushKey(false, DoomKeys.KEY_PAUSE);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.startGameLoop();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private startGameLoop(): void {
|
|
71
|
+
this.interval = setInterval(() => {
|
|
72
|
+
try {
|
|
73
|
+
this.engine.tick();
|
|
74
|
+
this.tui.requestRender();
|
|
75
|
+
} catch {
|
|
76
|
+
// WASM error (e.g., exit via DOOM menu) - treat as quit
|
|
77
|
+
this.dispose();
|
|
78
|
+
this.onExit();
|
|
79
|
+
}
|
|
80
|
+
}, 1000 / 35);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
handleInput(data: string): void {
|
|
84
|
+
// Q to pause and exit (but not on release)
|
|
85
|
+
if (!isKeyRelease(data) && (data === 'q' || data === 'Q')) {
|
|
86
|
+
// Send DOOM's pause key before exiting
|
|
87
|
+
this.engine.pushKey(true, DoomKeys.KEY_PAUSE);
|
|
88
|
+
this.engine.pushKey(false, DoomKeys.KEY_PAUSE);
|
|
89
|
+
this.dispose();
|
|
90
|
+
this.onExit();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const doomKeys = mapKeyToDoom(data);
|
|
95
|
+
if (doomKeys.length === 0) return;
|
|
96
|
+
|
|
97
|
+
const released = isKeyRelease(data);
|
|
98
|
+
|
|
99
|
+
for (const key of doomKeys) {
|
|
100
|
+
this.engine.pushKey(!released, key);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
render(width: number): string[] {
|
|
105
|
+
// DOOM renders at 640x400 (1.6:1 ratio)
|
|
106
|
+
// With half-block characters, each terminal row = 2 pixels
|
|
107
|
+
// So effective ratio is 640:200 = 3.2:1 (width:height in terminal cells)
|
|
108
|
+
// Add 1 row for footer
|
|
109
|
+
const ASPECT_RATIO = 3.2;
|
|
110
|
+
const MIN_HEIGHT = 10;
|
|
111
|
+
const height = Math.max(MIN_HEIGHT, Math.floor(width / ASPECT_RATIO));
|
|
112
|
+
|
|
113
|
+
const rgba = this.engine.getFrameRGBA();
|
|
114
|
+
const lines = renderHalfBlock(rgba, this.engine.width, this.engine.height, width, height);
|
|
115
|
+
|
|
116
|
+
// Footer
|
|
117
|
+
const footer =
|
|
118
|
+
' DOOM | Q=Pause | WASD=Move | Shift+WASD=Run | Space=Use | F=Fire | 1-7=Weapons';
|
|
119
|
+
const truncatedFooter = footer.length > width ? footer.slice(0, width) : footer;
|
|
120
|
+
lines.push(`\x1b[2m${truncatedFooter}\x1b[0m`);
|
|
121
|
+
|
|
122
|
+
return lines;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
invalidate(): void {}
|
|
126
|
+
|
|
127
|
+
dispose(): void {
|
|
128
|
+
if (this.interval) {
|
|
129
|
+
clearInterval(this.interval);
|
|
130
|
+
this.interval = null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DOOM key codes (from doomkeys.h)
|
|
3
|
-
*/
|
|
4
|
-
export const DoomKeys = {
|
|
5
|
-
KEY_RIGHTARROW: 0xae,
|
|
6
|
-
KEY_LEFTARROW: 0xac,
|
|
7
|
-
KEY_UPARROW: 0xad,
|
|
8
|
-
KEY_DOWNARROW: 0xaf,
|
|
9
|
-
KEY_STRAFE_L: 0xa0,
|
|
10
|
-
KEY_STRAFE_R: 0xa1,
|
|
11
|
-
KEY_USE: 0xa2,
|
|
12
|
-
KEY_FIRE: 0xa3,
|
|
13
|
-
KEY_ESCAPE: 27,
|
|
14
|
-
KEY_ENTER: 13,
|
|
15
|
-
KEY_TAB: 9,
|
|
16
|
-
KEY_F1: 0x80 + 0x3b,
|
|
17
|
-
KEY_F2: 0x80 + 0x3c,
|
|
18
|
-
KEY_F3: 0x80 + 0x3d,
|
|
19
|
-
KEY_F4: 0x80 + 0x3e,
|
|
20
|
-
KEY_F5: 0x80 + 0x3f,
|
|
21
|
-
KEY_F6: 0x80 + 0x40,
|
|
22
|
-
KEY_F7: 0x80 + 0x41,
|
|
23
|
-
KEY_F8: 0x80 + 0x42,
|
|
24
|
-
KEY_F9: 0x80 + 0x43,
|
|
25
|
-
KEY_F10: 0x80 + 0x44,
|
|
26
|
-
KEY_F11: 0x80 + 0x57,
|
|
27
|
-
KEY_F12: 0x80 + 0x58,
|
|
28
|
-
KEY_BACKSPACE: 127,
|
|
29
|
-
KEY_PAUSE: 0xff,
|
|
30
|
-
KEY_EQUALS: 0x3d,
|
|
31
|
-
KEY_MINUS: 0x2d,
|
|
32
|
-
KEY_RSHIFT: 0x80 + 0x36,
|
|
33
|
-
KEY_RCTRL: 0x80 + 0x1d,
|
|
34
|
-
KEY_RALT: 0x80 + 0x38
|
|
35
|
-
} as const;
|
|
36
|
-
|
|
37
|
-
import { Key, matchesKey, parseKey } from 'shortcutxl';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Map terminal key input to DOOM key codes
|
|
41
|
-
* Supports both raw terminal input and Kitty protocol sequences
|
|
42
|
-
*/
|
|
43
|
-
export function mapKeyToDoom(data: string): number[] {
|
|
44
|
-
// Arrow keys
|
|
45
|
-
if (matchesKey(data, Key.up)) return [DoomKeys.KEY_UPARROW];
|
|
46
|
-
if (matchesKey(data, Key.down)) return [DoomKeys.KEY_DOWNARROW];
|
|
47
|
-
if (matchesKey(data, Key.right)) return [DoomKeys.KEY_RIGHTARROW];
|
|
48
|
-
if (matchesKey(data, Key.left)) return [DoomKeys.KEY_LEFTARROW];
|
|
49
|
-
|
|
50
|
-
// WASD - check both raw char and Kitty sequences
|
|
51
|
-
if (data === 'w' || matchesKey(data, 'w')) return [DoomKeys.KEY_UPARROW];
|
|
52
|
-
if (data === 'W' || matchesKey(data, Key.shift('w')))
|
|
53
|
-
return [DoomKeys.KEY_UPARROW, DoomKeys.KEY_RSHIFT];
|
|
54
|
-
if (data === 's' || matchesKey(data, 's')) return [DoomKeys.KEY_DOWNARROW];
|
|
55
|
-
if (data === 'S' || matchesKey(data, Key.shift('s')))
|
|
56
|
-
return [DoomKeys.KEY_DOWNARROW, DoomKeys.KEY_RSHIFT];
|
|
57
|
-
if (data === 'a' || matchesKey(data, 'a')) return [DoomKeys.KEY_STRAFE_L];
|
|
58
|
-
if (data === 'A' || matchesKey(data, Key.shift('a')))
|
|
59
|
-
return [DoomKeys.KEY_STRAFE_L, DoomKeys.KEY_RSHIFT];
|
|
60
|
-
if (data === 'd' || matchesKey(data, 'd')) return [DoomKeys.KEY_STRAFE_R];
|
|
61
|
-
if (data === 'D' || matchesKey(data, Key.shift('d')))
|
|
62
|
-
return [DoomKeys.KEY_STRAFE_R, DoomKeys.KEY_RSHIFT];
|
|
63
|
-
|
|
64
|
-
// Fire - F key
|
|
65
|
-
if (data === 'f' || data === 'F' || matchesKey(data, 'f') || matchesKey(data, Key.shift('f'))) {
|
|
66
|
-
return [DoomKeys.KEY_FIRE];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Use/Open
|
|
70
|
-
if (data === ' ' || matchesKey(data, Key.space)) return [DoomKeys.KEY_USE];
|
|
71
|
-
|
|
72
|
-
// Menu/UI keys
|
|
73
|
-
if (matchesKey(data, Key.enter)) return [DoomKeys.KEY_ENTER];
|
|
74
|
-
if (matchesKey(data, Key.escape)) return [DoomKeys.KEY_ESCAPE];
|
|
75
|
-
if (matchesKey(data, Key.tab)) return [DoomKeys.KEY_TAB];
|
|
76
|
-
if (matchesKey(data, Key.backspace)) return [DoomKeys.KEY_BACKSPACE];
|
|
77
|
-
|
|
78
|
-
// Ctrl keys (except Ctrl+C) = fire (legacy support)
|
|
79
|
-
const parsed = parseKey(data);
|
|
80
|
-
if (parsed?.startsWith('ctrl+') && parsed !== 'ctrl+c') {
|
|
81
|
-
return [DoomKeys.KEY_FIRE];
|
|
82
|
-
}
|
|
83
|
-
if (data.length === 1 && data.charCodeAt(0) < 32 && data !== '\x03') {
|
|
84
|
-
return [DoomKeys.KEY_FIRE];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Weapon selection (0-9)
|
|
88
|
-
if (data >= '0' && data <= '9') return [data.charCodeAt(0)];
|
|
89
|
-
|
|
90
|
-
// Plus/minus for screen size
|
|
91
|
-
if (data === '+' || data === '=') return [DoomKeys.KEY_EQUALS];
|
|
92
|
-
if (data === '-') return [DoomKeys.KEY_MINUS];
|
|
93
|
-
|
|
94
|
-
// Y/N for prompts
|
|
95
|
-
if (data === 'y' || data === 'Y' || matchesKey(data, 'y') || matchesKey(data, Key.shift('y'))) {
|
|
96
|
-
return ['y'.charCodeAt(0)];
|
|
97
|
-
}
|
|
98
|
-
if (data === 'n' || data === 'N' || matchesKey(data, 'n') || matchesKey(data, Key.shift('n'))) {
|
|
99
|
-
return ['n'.charCodeAt(0)];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Other printable characters (for cheats)
|
|
103
|
-
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
104
|
-
return [data.toLowerCase().charCodeAt(0)];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return [];
|
|
108
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* DOOM key codes (from doomkeys.h)
|
|
3
|
+
*/
|
|
4
|
+
export const DoomKeys = {
|
|
5
|
+
KEY_RIGHTARROW: 0xae,
|
|
6
|
+
KEY_LEFTARROW: 0xac,
|
|
7
|
+
KEY_UPARROW: 0xad,
|
|
8
|
+
KEY_DOWNARROW: 0xaf,
|
|
9
|
+
KEY_STRAFE_L: 0xa0,
|
|
10
|
+
KEY_STRAFE_R: 0xa1,
|
|
11
|
+
KEY_USE: 0xa2,
|
|
12
|
+
KEY_FIRE: 0xa3,
|
|
13
|
+
KEY_ESCAPE: 27,
|
|
14
|
+
KEY_ENTER: 13,
|
|
15
|
+
KEY_TAB: 9,
|
|
16
|
+
KEY_F1: 0x80 + 0x3b,
|
|
17
|
+
KEY_F2: 0x80 + 0x3c,
|
|
18
|
+
KEY_F3: 0x80 + 0x3d,
|
|
19
|
+
KEY_F4: 0x80 + 0x3e,
|
|
20
|
+
KEY_F5: 0x80 + 0x3f,
|
|
21
|
+
KEY_F6: 0x80 + 0x40,
|
|
22
|
+
KEY_F7: 0x80 + 0x41,
|
|
23
|
+
KEY_F8: 0x80 + 0x42,
|
|
24
|
+
KEY_F9: 0x80 + 0x43,
|
|
25
|
+
KEY_F10: 0x80 + 0x44,
|
|
26
|
+
KEY_F11: 0x80 + 0x57,
|
|
27
|
+
KEY_F12: 0x80 + 0x58,
|
|
28
|
+
KEY_BACKSPACE: 127,
|
|
29
|
+
KEY_PAUSE: 0xff,
|
|
30
|
+
KEY_EQUALS: 0x3d,
|
|
31
|
+
KEY_MINUS: 0x2d,
|
|
32
|
+
KEY_RSHIFT: 0x80 + 0x36,
|
|
33
|
+
KEY_RCTRL: 0x80 + 0x1d,
|
|
34
|
+
KEY_RALT: 0x80 + 0x38
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
import { Key, matchesKey, parseKey } from 'shortcutxl';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Map terminal key input to DOOM key codes
|
|
41
|
+
* Supports both raw terminal input and Kitty protocol sequences
|
|
42
|
+
*/
|
|
43
|
+
export function mapKeyToDoom(data: string): number[] {
|
|
44
|
+
// Arrow keys
|
|
45
|
+
if (matchesKey(data, Key.up)) return [DoomKeys.KEY_UPARROW];
|
|
46
|
+
if (matchesKey(data, Key.down)) return [DoomKeys.KEY_DOWNARROW];
|
|
47
|
+
if (matchesKey(data, Key.right)) return [DoomKeys.KEY_RIGHTARROW];
|
|
48
|
+
if (matchesKey(data, Key.left)) return [DoomKeys.KEY_LEFTARROW];
|
|
49
|
+
|
|
50
|
+
// WASD - check both raw char and Kitty sequences
|
|
51
|
+
if (data === 'w' || matchesKey(data, 'w')) return [DoomKeys.KEY_UPARROW];
|
|
52
|
+
if (data === 'W' || matchesKey(data, Key.shift('w')))
|
|
53
|
+
return [DoomKeys.KEY_UPARROW, DoomKeys.KEY_RSHIFT];
|
|
54
|
+
if (data === 's' || matchesKey(data, 's')) return [DoomKeys.KEY_DOWNARROW];
|
|
55
|
+
if (data === 'S' || matchesKey(data, Key.shift('s')))
|
|
56
|
+
return [DoomKeys.KEY_DOWNARROW, DoomKeys.KEY_RSHIFT];
|
|
57
|
+
if (data === 'a' || matchesKey(data, 'a')) return [DoomKeys.KEY_STRAFE_L];
|
|
58
|
+
if (data === 'A' || matchesKey(data, Key.shift('a')))
|
|
59
|
+
return [DoomKeys.KEY_STRAFE_L, DoomKeys.KEY_RSHIFT];
|
|
60
|
+
if (data === 'd' || matchesKey(data, 'd')) return [DoomKeys.KEY_STRAFE_R];
|
|
61
|
+
if (data === 'D' || matchesKey(data, Key.shift('d')))
|
|
62
|
+
return [DoomKeys.KEY_STRAFE_R, DoomKeys.KEY_RSHIFT];
|
|
63
|
+
|
|
64
|
+
// Fire - F key
|
|
65
|
+
if (data === 'f' || data === 'F' || matchesKey(data, 'f') || matchesKey(data, Key.shift('f'))) {
|
|
66
|
+
return [DoomKeys.KEY_FIRE];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Use/Open
|
|
70
|
+
if (data === ' ' || matchesKey(data, Key.space)) return [DoomKeys.KEY_USE];
|
|
71
|
+
|
|
72
|
+
// Menu/UI keys
|
|
73
|
+
if (matchesKey(data, Key.enter)) return [DoomKeys.KEY_ENTER];
|
|
74
|
+
if (matchesKey(data, Key.escape)) return [DoomKeys.KEY_ESCAPE];
|
|
75
|
+
if (matchesKey(data, Key.tab)) return [DoomKeys.KEY_TAB];
|
|
76
|
+
if (matchesKey(data, Key.backspace)) return [DoomKeys.KEY_BACKSPACE];
|
|
77
|
+
|
|
78
|
+
// Ctrl keys (except Ctrl+C) = fire (legacy support)
|
|
79
|
+
const parsed = parseKey(data);
|
|
80
|
+
if (parsed?.startsWith('ctrl+') && parsed !== 'ctrl+c') {
|
|
81
|
+
return [DoomKeys.KEY_FIRE];
|
|
82
|
+
}
|
|
83
|
+
if (data.length === 1 && data.charCodeAt(0) < 32 && data !== '\x03') {
|
|
84
|
+
return [DoomKeys.KEY_FIRE];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Weapon selection (0-9)
|
|
88
|
+
if (data >= '0' && data <= '9') return [data.charCodeAt(0)];
|
|
89
|
+
|
|
90
|
+
// Plus/minus for screen size
|
|
91
|
+
if (data === '+' || data === '=') return [DoomKeys.KEY_EQUALS];
|
|
92
|
+
if (data === '-') return [DoomKeys.KEY_MINUS];
|
|
93
|
+
|
|
94
|
+
// Y/N for prompts
|
|
95
|
+
if (data === 'y' || data === 'Y' || matchesKey(data, 'y') || matchesKey(data, Key.shift('y'))) {
|
|
96
|
+
return ['y'.charCodeAt(0)];
|
|
97
|
+
}
|
|
98
|
+
if (data === 'n' || data === 'N' || matchesKey(data, 'n') || matchesKey(data, Key.shift('n'))) {
|
|
99
|
+
return ['n'.charCodeAt(0)];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Other printable characters (for cheats)
|
|
103
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
104
|
+
return [data.toLowerCase().charCodeAt(0)];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return [];
|
|
108
|
+
}
|