72 |
73 |
91 |
94 |
97 |
100 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
Static:
113 |
114 | - ABYSS1.ANS
115 | - AN-D2.ANS
116 | - BK-KING.ANS
117 | - COMICS14.ANS
118 | - CT-DIE_HARD.ANS
119 | - CT-PIXELS.ANS
120 | - DONATELO.ANS
121 | - DW-FACES.ANS
122 | - DW-HAPPY_HOLIDAYS.ANS
123 | - ED-NS.ANS
124 | - JET.ANS
125 | - FROSTBBS.ANS
126 | - GAVEL30.ANS
127 | - GLOBE.ANS
128 | - LDA-GARFIELD.ANS
129 | - THEQ.ANS
130 | - US-UWU.ANS
131 | - WWANS54.ANS
132 |
133 |
Blinking:
134 |
135 | - CHRIST1.ANS
136 | - SMRFBONK.ANS
137 | - US-CANDLES.ANS
138 | - UTOPIA20.ANS
139 | - XMAS1.ANS
140 |
141 |
Animations:
142 |
143 | - BCACID7.ANS
144 | - BOGACID1.ANS
145 | - CC-ICE1.ICE
146 | - DT-GHETO.ANS
147 | - JD-BUTT.ANS
148 | - LM-OKC.ICE
149 | - SC-ACID5.ANS
150 | - SUBACID.ANS
151 | - UTOPIA86.ANS
152 |
153 |
154 |
157 |
158 | );
159 | }
160 |
--------------------------------------------------------------------------------
/src/dom-components.js:
--------------------------------------------------------------------------------
1 | import { useMemo, useRef, useEffect, createElement } from 'react';
2 | import { useAnsi } from './hooks.js';
3 | import { cgaPalette } from './dos-environment.js';
4 |
5 | export function AnsiText({ src, srcObject, palette = cgaPalette, className = 'AnsiText', ...options }) {
6 | // retrieve data if necessary
7 | const data = useMemo(() => srcObject ?? fetchBuffer(src), [ src, srcObject ]);
8 | // process data through hook
9 | const { lines, blinking, blinked } = useAnsi(data, options);
10 | // convert lines to spans
11 | const children = lines.map((segments) => {
12 | const spans = segments.map(({ text, fgColor, bgColor, blink }) => {
13 | const props = {};
14 | if (Array.isArray(palette)) {
15 | props.style = {
16 | backgroundColor: palette[bgColor],
17 | color: palette[(blink && blinked) ? bgColor : fgColor],
18 | };
19 | } else {
20 | const names = [];
21 | if (fgColor !== undefined) {
22 | names.push(`fgColor${fgColor}`)
23 | }
24 | if (bgColor !== undefined) {
25 | names.push(`bgColor${bgColor}`);
26 | }
27 | if (blink) {
28 | if (blinking === true) {
29 | // manual blinking
30 | if (blink && blinked) {
31 | names.push('blink');
32 | }
33 | } else {
34 | // blinking through css
35 | names.push('blinking');
36 | }
37 | }
38 | props.className = names.join(' ');
39 | }
40 | return createElement('span', props, text);
41 | });
42 | return createElement('div', {}, ...spans);
43 | });
44 | const style = {
45 | display: 'inline-block',
46 | whiteSpace: 'pre',
47 | width: 'fit-content',
48 | };
49 | return createElement('code', { className, style }, ...children);
50 | }
51 |
52 | /* c8 ignore start */
53 | export function AnsiCanvas({ src, srcObject, palette = cgaPalette, className = 'AnsiCanvas', ...options }) {
54 | const canvasRef = useRef();
55 | // retrieve data if necessary
56 | const data = useMemo(() => srcObject ?? fetchBuffer(src), [ src, srcObject ]);
57 | // process data through hook
58 | const { width, height, lines, blinked } = useAnsi(data, options);
59 | // draw into canvas in useEffect hook
60 | useEffect(() => {
61 | const canvas = canvasRef.current;
62 | if (!canvas) {
63 | return;
64 | }
65 | // get font applicable to canvas
66 | let { color, font } = getCanvasStyle(canvas);
67 | if (document.fonts.check(font)) {
68 | draw();
69 | // observe resizing of element so any font changes get applied
70 | const observer = new ResizeObserver(() => {
71 | const { font: newFont, color: newColor } = getCanvasStyle(canvas);
72 | if (newFont !== font || newColor !== color) {
73 | font = newFont;
74 | color = newColor;
75 | draw();
76 | }
77 | });
78 | observer.observe(canvas);
79 | return () => observer.disconnect();
80 | } else {
81 | // draw when the font has been loaded
82 | let cancelled = false;
83 | document.fonts.load(font).then(() => {
84 | if (!cancelled) {
85 | draw();
86 | }
87 | });
88 | return () => cancelled = true;
89 | }
90 |
91 | function draw() {
92 | const { charWidth, charHeight, ascent } = getFontMetrics(font);
93 | canvas.width = width * charWidth;
94 | canvas.height = height * charHeight;
95 | const cxt = canvas.getContext('2d');
96 | cxt.clearRect(0, 0, canvas.width, canvas.height);
97 | cxt.font = font;
98 | let x = 0, y = ascent;
99 | for (const line of lines) {
100 | for (const { text, bgColor, fgColor, blink } of line) {
101 | for (let i = 0; i < text.length; i++) {
102 | if (bgColor !== undefined) {
103 | // fill background with block character for more consistent appearance
104 | // if the full-block character doesn't quote fill the cell, then the gaps between
105 | // cells should appear everywhere
106 | cxt.fillStyle = palette[bgColor];
107 | cxt.fillText('\u2588', x, y);
108 | }
109 | if (!blink || !blinked) {
110 | // use black if foreground color isn't set
111 | cxt.fillStyle = (fgColor !== undefined) ? palette[fgColor] : color;
112 | cxt.fillText(text.charAt(i), x, y);
113 | }
114 | x += charWidth;
115 | }
116 | }
117 | y += charHeight;
118 | x = 0;
119 | }
120 | }
121 | }, [ width, height, lines, blinked, palette ]);
122 | return createElement('canvas', { ref: canvasRef, className });
123 | }
124 |
125 | function getCanvasStyle(node) {
126 | const { fontStyle, fontWeight, fontSize, fontFamily, color } = getComputedStyle(node);
127 | const font = `${fontStyle} ${fontWeight} ${fontSize} ${fontFamily}`;
128 | return { color, font };
129 | }
130 |
131 | const fontMetrics = {};
132 |
133 | function getFontMetrics(specifier) {
134 | let metrics = fontMetrics[specifier];
135 | if (!metrics) {
136 | const canvas = document.createElement('CANVAS');
137 | const cxt = canvas.getContext('2d');
138 | cxt.font = specifier;
139 | const m = cxt.measureText('\u2588');
140 | // support for fontBoundingBoxAscent and fontBoundingBoxDescent is spotty
141 | // (https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics#browser_compatibility)
142 | //
143 | // actualBoundingBoxAscent and actualBoundingBoxDescent wouldn't yield the exactly result
144 | // since the bounding box of the full-block character (U+2588) will likely stick out just a little
145 | const ascent = m.fontBoundingBoxAscent ?? m.actualBoundingBoxAscent;
146 | const descent = m.fontBoundingBoxDescent ?? m.actualBoundingBoxDescent;
147 | const charWidth = m.width;
148 | const charHeight = ascent + descent;
149 | metrics = fontMetrics[specifier] = { ascent, descent, charWidth, charHeight };
150 | }
151 | return metrics;
152 | }
153 | /* c8 ignore stop */
154 |
155 | async function fetchBuffer(src, options) {
156 | if (src) {
157 | const res = await fetch(src, options);
158 | if (res.status !== 200) {
159 | throw new Error(`HTTP ${res.status} - ${res.statusText}`);
160 | }
161 | return await res.arrayBuffer();
162 | }
163 | }
164 |
165 |
--------------------------------------------------------------------------------
/demo/ink/bin/SelectBox.mjs:
--------------------------------------------------------------------------------
1 | import { useFocus, useInput, Text, Box } from 'ink';
2 | import InkSelectInput from 'ink-select-input';
3 | import { jsx as _jsx } from "react/jsx-runtime";
4 | import { jsxs as _jsxs } from "react/jsx-runtime";
5 | const {
6 | default: SelectInput
7 | } = InkSelectInput;
8 | export default function SelectBox({
9 | id,
10 | items,
11 | label: labelWithAmp,
12 | value,
13 | home,
14 | onSelect: onSelectCaller
15 | }) {
16 | const {
17 | isFocused,
18 | focus
19 | } = useFocus({
20 | id
21 | });
22 | const initialIndex = items.findIndex(i => i.value === value);
23 | const [label, hotkey] = extractHotkey(labelWithAmp);
24 | useInput(input => {
25 | if (input.toUpperCase() === hotkey) {
26 | focus(id);
27 | }
28 | }, {
29 | isActive: !isFocused
30 | });
31 | if (isFocused) {
32 | const onSelect = item => {
33 | onSelectCaller?.(item);
34 | if (home) {
35 | // refocus main content (after this component has updated)
36 | setTimeout(() => focus(home), 0);
37 | }
38 | };
39 | return /*#__PURE__*/_jsxs(Box, {
40 | borderStyle: "round",
41 | borderColor: "blue",
42 | children: [/*#__PURE__*/_jsx(Text, {
43 | children: label
44 | }), /*#__PURE__*/_jsx(SelectInput, {
45 | items,
46 | initialIndex,
47 | onSelect
48 | })]
49 | });
50 | } else {
51 | const minWidth = items.reduce((w, i) => Math.max(w, i.label.length + 2), 2);
52 | const item = items[initialIndex];
53 | return /*#__PURE__*/_jsxs(Box, {
54 | borderStyle: "round",
55 | children: [/*#__PURE__*/_jsx(Text, {
56 | children: label
57 | }), /*#__PURE__*/_jsx(Box, {
58 | minWidth: minWidth,
59 | children: /*#__PURE__*/_jsxs(Text, {
60 | children: [": ", item?.label]
61 | })
62 | })]
63 | });
64 | }
65 | }
66 | function extractHotkey(labelWithAmp) {
67 | const m = /(.*)&(\w)(.*)/.exec(labelWithAmp);
68 | if (!m) {
69 | return [labelWithAmp];
70 | }
71 | return [/*#__PURE__*/_jsxs(Text, {
72 | children: [m[1], /*#__PURE__*/_jsx(Text, {
73 | underline: true,
74 | children: m[2]
75 | }), m[3]]
76 | }), m[2].toUpperCase()];
77 | }
78 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJ1c2VGb2N1cyIsInVzZUlucHV0IiwiVGV4dCIsIkJveCIsIklua1NlbGVjdElucHV0IiwiZGVmYXVsdCIsIlNlbGVjdElucHV0IiwiU2VsZWN0Qm94IiwiaWQiLCJpdGVtcyIsImxhYmVsIiwibGFiZWxXaXRoQW1wIiwidmFsdWUiLCJob21lIiwib25TZWxlY3QiLCJvblNlbGVjdENhbGxlciIsImlzRm9jdXNlZCIsImZvY3VzIiwiaW5pdGlhbEluZGV4IiwiZmluZEluZGV4IiwiaSIsImhvdGtleSIsImV4dHJhY3RIb3RrZXkiLCJpbnB1dCIsInRvVXBwZXJDYXNlIiwiaXNBY3RpdmUiLCJpdGVtIiwic2V0VGltZW91dCIsIm1pbldpZHRoIiwicmVkdWNlIiwidyIsIk1hdGgiLCJtYXgiLCJsZW5ndGgiLCJtIiwiZXhlYyJdLCJzb3VyY2VzIjpbIlNlbGVjdEJveC5qc3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgdXNlRm9jdXMsIHVzZUlucHV0LCBUZXh0LCBCb3ggfSBmcm9tICdpbmsnO1xuaW1wb3J0IElua1NlbGVjdElucHV0IGZyb20gJ2luay1zZWxlY3QtaW5wdXQnOyBjb25zdCB7IGRlZmF1bHQ6IFNlbGVjdElucHV0IH0gPSBJbmtTZWxlY3RJbnB1dDtcblxuZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gU2VsZWN0Qm94KHsgaWQsIGl0ZW1zLCBsYWJlbDogbGFiZWxXaXRoQW1wLCB2YWx1ZSwgaG9tZSwgb25TZWxlY3Q6IG9uU2VsZWN0Q2FsbGVyIH0pIHtcbiAgY29uc3QgeyBpc0ZvY3VzZWQsIGZvY3VzIH0gPSB1c2VGb2N1cyh7IGlkIH0pO1xuICBjb25zdCBpbml0aWFsSW5kZXggPSBpdGVtcy5maW5kSW5kZXgoaSA9PiBpLnZhbHVlID09PSB2YWx1ZSk7XG4gIGNvbnN0IFsgbGFiZWwsIGhvdGtleSBdID0gZXh0cmFjdEhvdGtleShsYWJlbFdpdGhBbXApO1xuICB1c2VJbnB1dCgoaW5wdXQpID0+IHtcbiAgICBpZiAoaW5wdXQudG9VcHBlckNhc2UoKSA9PT0gaG90a2V5KSB7XG4gICAgICBmb2N1cyhpZCk7XG4gICAgfVxuICB9LCB7IGlzQWN0aXZlOiAhaXNGb2N1c2VkIH0pO1xuICBpZiAoaXNGb2N1c2VkKSB7XG4gICAgY29uc3Qgb25TZWxlY3QgPSAoaXRlbSkgPT4ge1xuICAgICAgb25TZWxlY3RDYWxsZXI/LihpdGVtKTtcbiAgICAgIGlmIChob21lKSB7XG4gICAgICAgIC8vIHJlZm9jdXMgbWFpbiBjb250ZW50IChhZnRlciB0aGlzIGNvbXBvbmVudCBoYXMgdXBkYXRlZClcbiAgICAgICAgc2V0VGltZW91dCgoKSA9PiBmb2N1cyhob21lKSwgMCk7XG4gICAgICB9XG4gICAgfTtcbiAgICByZXR1cm4gKFxuICAgICAgPEJveCBib3JkZXJTdHlsZT1cInJvdW5kXCIgYm9yZGVyQ29sb3I9XCJibHVlXCI+XG4gICAgICAgIDxUZXh0PntsYWJlbH08L1RleHQ+XG4gICAgICAgIDxTZWxlY3RJbnB1dCB7Li4ueyBpdGVtcywgaW5pdGlhbEluZGV4LCBvblNlbGVjdCB9fSAvPlxuICAgICAgPC9Cb3g+XG4gICAgKTtcbiAgfSBlbHNlIHtcbiAgICBjb25zdCBtaW5XaWR0aCA9IGl0ZW1zLnJlZHVjZSgodywgaSkgPT4gTWF0aC5tYXgodywgaS5sYWJlbC5sZW5ndGggKyAyKSwgMik7XG4gICAgY29uc3QgaXRlbSA9IGl0ZW1zW2luaXRpYWxJbmRleF07XG4gICAgcmV0dXJuIChcbiAgICAgIDxCb3ggYm9yZGVyU3R5bGU9XCJyb3VuZFwiPlxuICAgICAgICA8VGV4dD57bGFiZWx9PC9UZXh0PlxuICAgICAgICA8Qm94IG1pbldpZHRoPXttaW5XaWR0aH0+PFRleHQ+OiB7aXRlbT8ubGFiZWx9PC9UZXh0PjwvQm94PlxuICAgICAgPC9Cb3g+XG4gICAgKTtcbiAgfVxufVxuXG5mdW5jdGlvbiBleHRyYWN0SG90a2V5KGxhYmVsV2l0aEFtcCkge1xuICBjb25zdCBtID0gLyguKikmKFxcdykoLiopLy5leGVjKGxhYmVsV2l0aEFtcCk7XG4gIGlmICghbSkge1xuICAgIHJldHVybiBbIGxhYmVsV2l0aEFtcCBdO1xuICB9XG4gIHJldHVybiBbXG4gICAgPFRleHQ+e21bMV19PFRleHQgdW5kZXJsaW5lPnttWzJdfTwvVGV4dD57bVszXX08L1RleHQ+LFxuICAgIG1bMl0udG9VcHBlckNhc2UoKVxuICBdO1xufSJdLCJtYXBwaW5ncyI6IkFBQUEsU0FBU0EsUUFBUSxFQUFFQyxRQUFRLEVBQUVDLElBQUksRUFBRUMsR0FBRyxRQUFRLEtBQUs7QUFDbkQsT0FBT0MsY0FBYyxNQUFNLGtCQUFrQjtBQUFDO0FBQUE7QUFBQyxNQUFNO0VBQUVDLE9BQU8sRUFBRUM7QUFBWSxDQUFDLEdBQUdGLGNBQWM7QUFFOUYsZUFBZSxTQUFTRyxTQUFTLENBQUM7RUFBRUMsRUFBRTtFQUFFQyxLQUFLO0VBQUVDLEtBQUssRUFBRUMsWUFBWTtFQUFFQyxLQUFLO0VBQUVDLElBQUk7RUFBRUMsUUFBUSxFQUFFQztBQUFlLENBQUMsRUFBRTtFQUMzRyxNQUFNO0lBQUVDLFNBQVM7SUFBRUM7RUFBTSxDQUFDLEdBQUdqQixRQUFRLENBQUM7SUFBRVE7RUFBRyxDQUFDLENBQUM7RUFDN0MsTUFBTVUsWUFBWSxHQUFHVCxLQUFLLENBQUNVLFNBQVMsQ0FBQ0MsQ0FBQyxJQUFJQSxDQUFDLENBQUNSLEtBQUssS0FBS0EsS0FBSyxDQUFDO0VBQzVELE1BQU0sQ0FBRUYsS0FBSyxFQUFFVyxNQUFNLENBQUUsR0FBR0MsYUFBYSxDQUFDWCxZQUFZLENBQUM7RUFDckRWLFFBQVEsQ0FBRXNCLEtBQUssSUFBSztJQUNsQixJQUFJQSxLQUFLLENBQUNDLFdBQVcsRUFBRSxLQUFLSCxNQUFNLEVBQUU7TUFDbENKLEtBQUssQ0FBQ1QsRUFBRSxDQUFDO0lBQ1g7RUFDRixDQUFDLEVBQUU7SUFBRWlCLFFBQVEsRUFBRSxDQUFDVDtFQUFVLENBQUMsQ0FBQztFQUM1QixJQUFJQSxTQUFTLEVBQUU7SUFDYixNQUFNRixRQUFRLEdBQUlZLElBQUksSUFBSztNQUN6QlgsY0FBYyxHQUFHVyxJQUFJLENBQUM7TUFDdEIsSUFBSWIsSUFBSSxFQUFFO1FBQ1I7UUFDQWMsVUFBVSxDQUFDLE1BQU1WLEtBQUssQ0FBQ0osSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDO01BQ2xDO0lBQ0YsQ0FBQztJQUNELG9CQUNFLE1BQUMsR0FBRztNQUFDLFdBQVcsRUFBQyxPQUFPO01BQUMsV0FBVyxFQUFDLE1BQU07TUFBQSx3QkFDekMsS0FBQyxJQUFJO1FBQUEsVUFBRUg7TUFBSyxFQUFRLGVBQ3BCLEtBQUMsV0FBVztRQUFPRCxLQUFLO1FBQUVTLFlBQVk7UUFBRUo7TUFBUSxFQUFNO0lBQUEsRUFDbEQ7RUFFVixDQUFDLE1BQU07SUFDTCxNQUFNYyxRQUFRLEdBQUduQixLQUFLLENBQUNvQixNQUFNLENBQUMsQ0FBQ0MsQ0FBQyxFQUFFVixDQUFDLEtBQUtXLElBQUksQ0FBQ0MsR0FBRyxDQUFDRixDQUFDLEVBQUVWLENBQUMsQ0FBQ1YsS0FBSyxDQUFDdUIsTUFBTSxHQUFHLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQztJQUMzRSxNQUFNUCxJQUFJLEdBQUdqQixLQUFLLENBQUNTLFlBQVksQ0FBQztJQUNoQyxvQkFDRSxNQUFDLEdBQUc7TUFBQyxXQUFXLEVBQUMsT0FBTztNQUFBLHdCQUN0QixLQUFDLElBQUk7UUFBQSxVQUFFUjtNQUFLLEVBQVEsZUFDcEIsS0FBQyxHQUFHO1FBQUMsUUFBUSxFQUFFa0IsUUFBUztRQUFBLHVCQUFDLE1BQUMsSUFBSTtVQUFBLFdBQUMsSUFBRSxFQUFDRixJQUFJLEVBQUVoQixLQUFLO1FBQUE7TUFBUSxFQUFNO0lBQUEsRUFDdkQ7RUFFVjtBQUNGO0FBRUEsU0FBU1ksYUFBYSxDQUFDWCxZQUFZLEVBQUU7RUFDbkMsTUFBTXVCLENBQUMsR0FBRyxlQUFlLENBQUNDLElBQUksQ0FBQ3hCLFlBQVksQ0FBQztFQUM1QyxJQUFJLENBQUN1QixDQUFDLEVBQUU7SUFDTixPQUFPLENBQUV2QixZQUFZLENBQUU7RUFDekI7RUFDQSxPQUFPLGNBQ0wsTUFBQyxJQUFJO0lBQUEsV0FBRXVCLENBQUMsQ0FBQyxDQUFDLENBQUMsZUFBQyxLQUFDLElBQUk7TUFBQyxTQUFTO01BQUEsVUFBRUEsQ0FBQyxDQUFDLENBQUM7SUFBQyxFQUFRLEVBQUNBLENBQUMsQ0FBQyxDQUFDLENBQUM7RUFBQSxFQUFRLEVBQ3REQSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUNWLFdBQVcsRUFBRSxDQUNuQjtBQUNIIn0=
--------------------------------------------------------------------------------
/demo/ink/bin/main.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from 'path';
2 | import { Box, Text } from 'ink';
3 | import ModemSpeedSelect from "./ModemSpeedSelect.mjs";
4 | import BlinkingSelect from "./BlinkingSelect.mjs";
5 | import ScrollingSelect from "./ScrollingSelect.mjs";
6 | import TransparencySelect from "./TransparencySelect.mjs";
7 | import AnsiDisplay from "./AnsiDisplay.mjs";
8 | import AnsiSlideShow from "./AnsiSlideShow.mjs";
9 | import FileList from "./FileList.mjs";
10 | import { jsx as _jsx } from "react/jsx-runtime";
11 | import { jsxs as _jsxs } from "react/jsx-runtime";
12 | export default async function* main(methods) {
13 | const {
14 | wrap,
15 | manageRoute,
16 | manageEvents,
17 | replacing
18 | } = methods;
19 | const [parts] = manageRoute();
20 | const [on, eventual] = manageEvents();
21 | wrap(children => {
22 | return /*#__PURE__*/_jsxs(Box, {
23 | children: [/*#__PURE__*/_jsxs(Box, {
24 | flexDirection: "column",
25 | children: [/*#__PURE__*/_jsx(ModemSpeedSelect, {}), /*#__PURE__*/_jsx(BlinkingSelect, {}), /*#__PURE__*/_jsx(ScrollingSelect, {}), /*#__PURE__*/_jsx(TransparencySelect, {})]
26 | }), /*#__PURE__*/_jsx(Box, {
27 | children: children
28 | })]
29 | });
30 | });
31 | for (;;) {
32 | try {
33 | if (parts[0] === undefined) {
34 | replacing(() => parts[0] = 'list');
35 | } else if (parts[0] === 'list') {
36 | yield /*#__PURE__*/_jsx(FileList, {
37 | folder: parts[1],
38 | selected: parts[2],
39 | onFileSelect: on.file,
40 | onFolderSelect: on.folder
41 | });
42 | const {
43 | file,
44 | folder
45 | } = await eventual.file.or.folder;
46 | parts.splice(0);
47 | if (folder) {
48 | parts.push('list', folder);
49 | } else if (file) {
50 | parts.push('show', file);
51 | }
52 | } else if (parts[0] === 'show') {
53 | const path = parts[1];
54 | yield /*#__PURE__*/_jsx(AnsiDisplay, {
55 | src: parts[1],
56 | onExit: on.exit
57 | });
58 | await eventual.exit;
59 | parts.splice(0);
60 | parts.push('list', dirname(path), path);
61 | } else if (parts[0] === 'loop') {
62 | const paths = parts.slice(1);
63 | yield /*#__PURE__*/_jsx(AnsiSlideShow, {
64 | srcList: paths,
65 | onExit: on.exit
66 | });
67 | await eventual.exit;
68 | process.exit(0);
69 | } else {
70 | throw new Error(`Unrecognized command: ${parts[0]}`);
71 | }
72 | } catch (err) {
73 | // not sure why Ink complains when nothing is yielded prior to program exit
74 | yield null;
75 | console.error(err.message);
76 | process.exit(1);
77 | }
78 | }
79 | }
80 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJkaXJuYW1lIiwiQm94IiwiVGV4dCIsIk1vZGVtU3BlZWRTZWxlY3QiLCJCbGlua2luZ1NlbGVjdCIsIlNjcm9sbGluZ1NlbGVjdCIsIlRyYW5zcGFyZW5jeVNlbGVjdCIsIkFuc2lEaXNwbGF5IiwiQW5zaVNsaWRlU2hvdyIsIkZpbGVMaXN0IiwibWFpbiIsIm1ldGhvZHMiLCJ3cmFwIiwibWFuYWdlUm91dGUiLCJtYW5hZ2VFdmVudHMiLCJyZXBsYWNpbmciLCJwYXJ0cyIsIm9uIiwiZXZlbnR1YWwiLCJjaGlsZHJlbiIsInVuZGVmaW5lZCIsImZpbGUiLCJmb2xkZXIiLCJvciIsInNwbGljZSIsInB1c2giLCJwYXRoIiwiZXhpdCIsInBhdGhzIiwic2xpY2UiLCJwcm9jZXNzIiwiRXJyb3IiLCJlcnIiLCJjb25zb2xlIiwiZXJyb3IiLCJtZXNzYWdlIl0sInNvdXJjZXMiOlsibWFpbi5qc3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgZGlybmFtZSB9IGZyb20gJ3BhdGgnO1xuaW1wb3J0IHsgQm94LCBUZXh0IH0gZnJvbSAnaW5rJztcbmltcG9ydCBNb2RlbVNwZWVkU2VsZWN0IGZyb20gJy4vTW9kZW1TcGVlZFNlbGVjdC5qc3gnO1xuaW1wb3J0IEJsaW5raW5nU2VsZWN0IGZyb20gJy4vQmxpbmtpbmdTZWxlY3QuanN4JztcbmltcG9ydCBTY3JvbGxpbmdTZWxlY3QgZnJvbSAnLi9TY3JvbGxpbmdTZWxlY3QuanN4JztcbmltcG9ydCBUcmFuc3BhcmVuY3lTZWxlY3QgZnJvbSAnLi9UcmFuc3BhcmVuY3lTZWxlY3QuanN4JztcbmltcG9ydCBBbnNpRGlzcGxheSBmcm9tICcuL0Fuc2lEaXNwbGF5LmpzeCc7XG5pbXBvcnQgQW5zaVNsaWRlU2hvdyBmcm9tICcuL0Fuc2lTbGlkZVNob3cuanN4JztcbmltcG9ydCBGaWxlTGlzdCBmcm9tICcuL0ZpbGVMaXN0LmpzeCc7XG5cbmV4cG9ydCBkZWZhdWx0IGFzeW5jIGZ1bmN0aW9uKiBtYWluKG1ldGhvZHMpIHtcbiAgY29uc3QgeyB3cmFwLCBtYW5hZ2VSb3V0ZSwgbWFuYWdlRXZlbnRzLCByZXBsYWNpbmcgfSA9IG1ldGhvZHM7XG4gIGNvbnN0IFsgcGFydHMgXSA9IG1hbmFnZVJvdXRlKCk7XG4gIGNvbnN0IFsgb24sIGV2ZW50dWFsIF0gPSBtYW5hZ2VFdmVudHMoKTtcbiAgd3JhcCgoY2hpbGRyZW4pID0+IHtcbiAgICByZXR1cm4gKFxuICAgICAgPEJveD5cbiAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICAgICAgPE1vZGVtU3BlZWRTZWxlY3QgLz5cbiAgICAgICAgICA8QmxpbmtpbmdTZWxlY3QgLz5cbiAgICAgICAgICA8U2Nyb2xsaW5nU2VsZWN0IC8+XG4gICAgICAgICAgPFRyYW5zcGFyZW5jeVNlbGVjdCAvPlxuICAgICAgICA8L0JveD5cbiAgICAgICAgPEJveD5cbiAgICAgICAgICB7Y2hpbGRyZW59XG4gICAgICAgIDwvQm94PlxuICAgICAgPC9Cb3g+XG4gICAgKTtcbiAgfSk7XG4gIGZvciAoOzspIHtcbiAgICB0cnkge1xuICAgICAgaWYgKHBhcnRzWzBdID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgcmVwbGFjaW5nKCgpID0+IHBhcnRzWzBdID0gJ2xpc3QnKVxuICAgICAgfSBlbHNlIGlmIChwYXJ0c1swXSA9PT0gJ2xpc3QnKSB7XG4gICAgICAgIHlpZWxkIDxGaWxlTGlzdCBmb2xkZXI9e3BhcnRzWzFdfSBzZWxlY3RlZD17cGFydHNbMl19IG9uRmlsZVNlbGVjdD17b24uZmlsZX0gb25Gb2xkZXJTZWxlY3Q9e29uLmZvbGRlcn0gLz47XG4gICAgICAgIGNvbnN0IHsgZmlsZSwgZm9sZGVyIH0gPSBhd2FpdCBldmVudHVhbC5maWxlLm9yLmZvbGRlcjtcbiAgICAgICAgcGFydHMuc3BsaWNlKDApO1xuICAgICAgICBpZiAoZm9sZGVyKSB7XG4gICAgICAgICAgcGFydHMucHVzaCgnbGlzdCcsIGZvbGRlcik7XG4gICAgICAgIH0gZWxzZSBpZiAoZmlsZSkge1xuICAgICAgICAgIHBhcnRzLnB1c2goJ3Nob3cnLCBmaWxlKTtcbiAgICAgICAgfVxuICAgICAgfSBlbHNlIGlmIChwYXJ0c1swXSA9PT0gJ3Nob3cnKSB7XG4gICAgICAgIGNvbnN0IHBhdGggPSBwYXJ0c1sxXTtcbiAgICAgICAgeWllbGQgPEFuc2lEaXNwbGF5IHNyYz17cGFydHNbMV19IG9uRXhpdD17b24uZXhpdH0gLz47XG4gICAgICAgIGF3YWl0IGV2ZW50dWFsLmV4aXQ7XG4gICAgICAgIHBhcnRzLnNwbGljZSgwKTtcbiAgICAgICAgcGFydHMucHVzaCgnbGlzdCcsIGRpcm5hbWUocGF0aCksIHBhdGgpO1xuICAgICAgfSBlbHNlIGlmIChwYXJ0c1swXSA9PT0gJ2xvb3AnKSB7XG4gICAgICAgIGNvbnN0IHBhdGhzID0gcGFydHMuc2xpY2UoMSk7XG4gICAgICAgIHlpZWxkIDxBbnNpU2xpZGVTaG93IHNyY0xpc3Q9e3BhdGhzfSBvbkV4aXQ9e29uLmV4aXR9IC8+O1xuICAgICAgICBhd2FpdCBldmVudHVhbC5leGl0O1xuICAgICAgICBwcm9jZXNzLmV4aXQoMCk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYFVucmVjb2duaXplZCBjb21tYW5kOiAke3BhcnRzWzBdfWApO1xuICAgICAgfVxuICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgLy8gbm90IHN1cmUgd2h5IEluayBjb21wbGFpbnMgd2hlbiBub3RoaW5nIGlzIHlpZWxkZWQgcHJpb3IgdG8gcHJvZ3JhbSBleGl0XG4gICAgICB5aWVsZCBudWxsO1xuICAgICAgY29uc29sZS5lcnJvcihlcnIubWVzc2FnZSk7ICAgICAgXG4gICAgICBwcm9jZXNzLmV4aXQoMSk7XG4gICAgfVxuICB9XG59XG5cblxuIl0sIm1hcHBpbmdzIjoiQUFBQSxTQUFTQSxPQUFPLFFBQVEsTUFBTTtBQUM5QixTQUFTQyxHQUFHLEVBQUVDLElBQUksUUFBUSxLQUFLO0FBQy9CLE9BQU9DLGdC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
--------------------------------------------------------------------------------
/demo/ink/bin/cli.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { render, useInput } from 'ink';
3 | import meow from 'meow';
4 | import meowhelp from 'cli-meow-help';
5 | import meowrev, { meowparse } from 'meow-reverse';
6 | import parse from 'shell-quote/parse.js';
7 | import quote from 'shell-quote/quote.js';
8 | import { createContext } from 'react';
9 | import { useSequential } from 'react-seq';
10 | import { useSequentialRouter } from 'array-router';
11 | import main from "./main.mjs";
12 | import { jsx as _jsx } from "react/jsx-runtime";
13 | const name = `ink-ansi-animation`;
14 | const commands = {
15 | 'show [FILE]': {
16 | desc: `Show an ANSI animation`
17 | },
18 | 'loop [FILE]...': {
19 | desc: `Show files in a loop`
20 | },
21 | 'list': {
22 | desc: `List ANSI files in current directory`
23 | }
24 | };
25 | const flags = {
26 | modemSpeed: {
27 | desc: `Emulate modem of specific baudrate`,
28 | alias: 'm',
29 | type: 'number',
30 | default: 56000
31 | },
32 | blinking: {
33 | desc: `Enable blinking text`,
34 | alias: 'b',
35 | type: 'boolean'
36 | },
37 | scrolling: {
38 | desc: `Enable scrolling`,
39 | alias: 's',
40 | type: 'boolean'
41 | },
42 | transparency: {
43 | desc: `Enable transparency`,
44 | alias: 't',
45 | type: 'boolean'
46 | }
47 | };
48 | const helpText = meowhelp({
49 | name,
50 | flags,
51 | commands
52 | });
53 | const options = {
54 | importMeta: import.meta,
55 | flags
56 | };
57 | const {
58 | input: parts,
59 | flags: query
60 | } = meow(helpText, options);
61 | function parseURL(_, {
62 | pathname
63 | }) {
64 | const argv = parse(pathname);
65 | const {
66 | input: parts,
67 | flags: query
68 | } = meowparse(argv, options);
69 | return {
70 | parts,
71 | query
72 | };
73 | }
74 | function createURL(_, {
75 | parts: input,
76 | query: flags
77 | }) {
78 | const argv = meowrev({
79 | input,
80 | flags
81 | }, options);
82 | const pathname = quote(argv);
83 | return new URL(`argv:${pathname}`);
84 | }
85 | function applyURL(currentURL) {
86 | globalThis.location.href = currentURL.href;
87 | }
88 | globalThis.location = createURL(null, {
89 | parts,
90 | query
91 | });
92 | const SpecialContext = createContext();
93 | function App() {
94 | useInput(input => {
95 | if (input.toLowerCase() === 'q') {
96 | process.exit(0);
97 | }
98 | });
99 | // use command-line URL
100 | const override = {
101 | createURL,
102 | parseURL,
103 | applyURL
104 | };
105 | const [parts, query, rMethods, {
106 | createContext,
107 | createBoundary
108 | }] = useSequentialRouter(override);
109 | return createContext(useSequential(sMethods => {
110 | const methods = {
111 | ...rMethods,
112 | ...sMethods
113 | };
114 | const {
115 | fallback,
116 | wrap,
117 | trap,
118 | reject
119 | } = methods;
120 | // default fallback (issue #142 in React-seq 0.9.0)
121 | fallback(null);
122 | // create error boundary
123 | wrap(children => createBoundary(children));
124 | // redirect error from boundary to generator function
125 | trap('error', err => reject(err));
126 | // method for managing route
127 | methods.manageRoute = () => [parts, query];
128 | return main(methods);
129 | }, [parts, query, rMethods, createBoundary]));
130 | }
131 | render( /*#__PURE__*/_jsx(App, {}));
132 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJyZW5kZXIiLCJ1c2VJbnB1dCIsIm1lb3ciLCJtZW93aGVscCIsIm1lb3dyZXYiLCJtZW93cGFyc2UiLCJwYXJzZSIsInF1b3RlIiwiY3JlYXRlQ29udGV4dCIsInVzZVNlcXVlbnRpYWwiLCJ1c2VTZXF1ZW50aWFsUm91dGVyIiwibWFpbiIsIm5hbWUiLCJjb21tYW5kcyIsImRlc2MiLCJmbGFncyIsIm1vZGVtU3BlZWQiLCJhbGlhcyIsInR5cGUiLCJkZWZhdWx0IiwiYmxpbmtpbmciLCJzY3JvbGxpbmciLCJ0cmFuc3BhcmVuY3kiLCJoZWxwVGV4dCIsIm9wdGlvbnMiLCJpbXBvcnRNZXRhIiwiaW1wb3J0IiwibWV0YSIsImlucHV0IiwicGFydHMiLCJxdWVyeSIsInBhcnNlVVJMIiwiXyIsInBhdGhuYW1lIiwiYXJndiIsImNyZWF0ZVVSTCIsIlVSTCIsImFwcGx5VVJMIiwiY3VycmVudFVSTCIsImdsb2JhbFRoaXMiLCJsb2NhdGlvbiIsImhyZWYiLCJTcGVjaWFsQ29udGV4dCIsIkFwcCIsInRvTG93ZXJDYXNlIiwicHJvY2VzcyIsImV4aXQiLCJvdmVycmlkZSIsInJNZXRob2RzIiwiY3JlYXRlQm91bmRhcnkiLCJzTWV0aG9kcyIsIm1ldGhvZHMiLCJmYWxsYmFjayIsIndyYXAiLCJ0cmFwIiwicmVqZWN0IiwiY2hpbGRyZW4iLCJlcnIiLCJtYW5hZ2VSb3V0ZSJdLCJzb3VyY2VzIjpbImNsaS5qc3giXSwic291cmNlc0NvbnRlbnQiOlsiIyEvdXNyL2Jpbi9lbnYgbm9kZVxuaW1wb3J0IHsgcmVuZGVyLCB1c2VJbnB1dCB9IGZyb20gJ2luayc7XG5pbXBvcnQgbWVvdyBmcm9tICdtZW93JztcbmltcG9ydCBtZW93aGVscCBmcm9tICdjbGktbWVvdy1oZWxwJztcbmltcG9ydCBtZW93cmV2LCB7IG1lb3dwYXJzZSB9IGZyb20gJ21lb3ctcmV2ZXJzZSc7XG5pbXBvcnQgcGFyc2UgZnJvbSAnc2hlbGwtcXVvdGUvcGFyc2UuanMnO1xuaW1wb3J0IHF1b3RlIGZyb20gJ3NoZWxsLXF1b3RlL3F1b3RlLmpzJztcbmltcG9ydCB7IGNyZWF0ZUNvbnRleHQgfSBmcm9tICdyZWFjdCc7XG5pbXBvcnQgeyB1c2VTZXF1ZW50aWFsIH0gZnJvbSAncmVhY3Qtc2VxJztcbmltcG9ydCB7IHVzZVNlcXVlbnRpYWxSb3V0ZXIgfSBmcm9tICdhcnJheS1yb3V0ZXInO1xuaW1wb3J0IG1haW4gZnJvbSAnLi9tYWluLmpzeCc7XG5cbmNvbnN0IG5hbWUgPSBgaW5rLWFuc2ktYW5pbWF0aW9uYDtcbmNvbnN0IGNvbW1hbmRzID0ge1xuICAnc2hvdyBbRklMRV0nOiB7IGRlc2M6IGBTaG93IGFuIEFOU0kgYW5pbWF0aW9uYCB9LFxuICAnbG9vcCBbRklMRV0uLi4nOiB7IGRlc2M6IGBTaG93IGZpbGVzIGluIGEgbG9vcGAgfSxcblx0J2xpc3QnOiB7IGRlc2M6IGBMaXN0IEFOU0kgZmlsZXMgaW4gY3VycmVudCBkaXJlY3RvcnlgIH0sXG59O1xuY29uc3QgZmxhZ3MgPSB7XG4gIG1vZGVtU3BlZWQ6IHtcbiAgICBkZXNjOiBgRW11bGF0ZSBtb2RlbSBvZiBzcGVjaWZpYyBiYXVkcmF0ZWAsXG4gICAgYWxpYXM6ICdtJyxcbiAgICB0eXBlOiAnbnVtYmVyJyxcbiAgICBkZWZhdWx0OiA1NjAwMFxuICB9LFxuICBibGlua2luZzoge1xuICAgIGRlc2M6IGBFbmFibGUgYmxpbmtpbmcgdGV4dGAsXG4gICAgYWxpYXM6ICdiJyxcbiAgICB0eXBlOiAnYm9vbGVhbicsXG4gIH0sXG4gIHNjcm9sbGluZzoge1xuICAgIGRlc2M6IGBFbmFibGUgc2Nyb2xsaW5nYCxcbiAgICBhbGlhczogJ3MnLFxuICAgIHR5cGU6ICdib29sZWFuJyxcbiAgfSxcbiAgdHJhbnNwYXJlbmN5OiB7XG4gICAgZGVzYzogYEVuYWJsZSB0cmFuc3BhcmVuY3lgLFxuICAgIGFsaWFzOiAndCcsXG4gICAgdHlwZTogJ2Jvb2xlYW4nLFxuICB9LFxufTtcblxuY29uc3QgaGVscFRleHQgPSBtZW93aGVscCh7XHRuYW1lLFx0ZmxhZ3MsIGNvbW1hbmRzIH0pO1xuY29uc3Qgb3B0aW9ucyA9IHsgaW1wb3J0TWV0YTogaW1wb3J0Lm1ldGEsIGZsYWdzIH07XG5jb25zdCB7IGlucHV0OiBwYXJ0cywgZmxhZ3M6IHF1ZXJ5IH0gPSBtZW93KGhlbHBUZXh0LCBvcHRpb25zKTtcblxuZnVuY3Rpb24gcGFyc2VVUkwoXywgeyBwYXRobmFtZSB9KSB7XG4gIGNvbnN0IGFyZ3YgPSBwYXJzZShwYXRobmFtZSk7XG4gIGNvbnN0IHsgaW5wdXQ6IHBhcnRzLCBmbGFnczogcXVlcnkgfSA9IG1lb3dwYXJzZShhcmd2LCBvcHRpb25zKTtcbiAgcmV0dXJuIHsgcGFydHMsIHF1ZXJ5IH07XG59XG5cbmZ1bmN0aW9uIGNyZWF0ZVVSTChfLCB7IHBhcnRzOiBpbnB1dCwgcXVlcnk6IGZsYWdzIH0pIHtcbiAgY29uc3QgYXJndiA9IG1lb3dyZXYoeyBpbnB1dCwgZmxhZ3MgfSwgb3B0aW9ucyk7XG4gIGNvbnN0IHBhdGhuYW1lID0gcXVvdGUoYXJndik7XG4gIHJldHVybiBuZXcgVVJMKGBhcmd2OiR7cGF0aG5hbWV9YCk7XG59XG5cbmZ1bmN0aW9uIGFwcGx5VVJMKGN1cnJlbnRVUkwpIHtcbiAgZ2xvYmFsVGhpcy5sb2NhdGlvbi5ocmVmID0gY3VycmVudFVSTC5ocmVmO1xufVxuXG5nbG9iYWxUaGlzLmxvY2F0aW9uID0gY3JlYXRlVVJMKG51bGwsIHsgcGFydHMsIHF1ZXJ5IH0pO1xuXG5jb25zdCBTcGVjaWFsQ29udGV4dCA9IGNyZWF0ZUNvbnRleHQoKTtcblxuZnVuY3Rpb24gQXBwKCkge1xuICB1c2VJbnB1dCgoaW5wdXQpID0+IHtcbiAgICBpZiAoaW5wdXQudG9Mb3dlckNhc2UoKSA9PT0gJ3EnKSB7XG4gICAgICBwcm9jZXNzLmV4aXQoMCk7XG4gICAgfVxuICB9KVxuICAvLyB1c2UgY29tbWFuZC1saW5lIFVSTFxuICBjb25zdCBvdmVycmlkZSA9IHsgY3JlYXRlVVJMLCBwYXJzZVVSTCwgYXBwbHlVUkwgfTtcbiAgY29uc3QgWyBwYXJ0cywgcXVlcnksIHJNZXRob2RzLCB7IGNyZWF0ZUNvbnRleHQsIGNyZWF0ZUJvdW5kYXJ5IH0gXSA9IHVzZVNlcXVlbnRpYWxSb3V0ZXIob3ZlcnJpZGUpO1xuICByZXR1cm4gY3JlYXRlQ29udGV4dCh1c2VTZXF1ZW50aWFsKChzTWV0aG9kcykgPT4ge1xuICAgIGNvbnN0IG1ldGhvZHMgPSB7IC4uLnJNZXRob2RzLCAuLi5zTWV0aG9kcyB9O1xuICAgIGNvbnN0IHsgZmFsbGJhY2ssIHdyYXAsIHRyYXAsIHJlamVjdCB9ID0gbWV0aG9kcztcbiAgICAvLyBkZWZhdWx0IGZhbGxiYWNrIChpc3N1ZSAjMTQyIGluIFJlYWN0LXNlcSAwLjkuMClcbiAgICBmYWxsYmFjayhudWxsKTtcbiAgICAvLyBjcmVhdGUgZXJyb3IgYm91bmRhcnlcbiAgICB3cmFwKGNoaWxkcmVuID0+IGNyZWF0ZUJvdW5kYXJ5KGNoaWxkcmVuKSk7XG4gICAgLy8gcmVkaXJlY3QgZXJyb3IgZnJvbSBib3VuZGFyeSB0byBnZW5lcmF0b3IgZnVuY3Rpb25cbiAgICB0cmFwKCdlcnJvcicsIGVyciA9PiByZWplY3QoZXJyKSk7XG4gICAgLy8gbWV0aG9kIGZvciBtYW5hZ2luZyByb3V0ZVxuICAgIG1ldGhvZHMubWFuYWdlUm91dGUgPSAoKSA9PiBbIHBhcnRzLCBxdWVyeSBdO1xuICAgIHJldHVybiBtYWluKG1ldGhvZHMpO1xuICB9LCBbIHBhcnRzLCBxdWVyeSwgck1ldGhvZHMsIGNyZWF0ZUJvdW5kYXJ5IF0pKTtcbn1cblxucmVuZGVyKDxBcHAgLz4pO1xuIl0sIm1hcHBpbmdzIjoiQUFBQTtBQUNBLFNBQVNBLE1BQU0sRUFBRUMsUUFBUSxRQUFRLEtBQUs7QUFDdEMsT0FBT0MsSUFBSSxNQUFNLE1BQU07QUFDdkIsT0FBT0MsUUFBUSxNQUFNLGVBQWU7QUFDcEMsT0FBT0MsT0FBTyxJQUFJQyxTQUFTLFFBQVEsY0FBYztBQUNqRCxPQUFPQyxLQUFLLE1BQU0sc0JBQXNCO0FBQ3hDLE9BQU9DLEtBQUss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
--------------------------------------------------------------------------------
/test/dom-components.test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { readFile, stat } from 'fs/promises';
3 | import { createElement } from 'react';
4 | import { delay } from 'react-seq';
5 | import { withTestRenderer } from './test-renderer.js';
6 |
7 | import {
8 | AnsiText
9 | } from '../index.js';
10 |
11 | describe('DOM components', function() {
12 | describe('#AnsiText', function() {
13 | it('should produce empty block of text at min width and height when neither src or srcObject is provided', async function() {
14 | await withTestRenderer(async ({ render, toJSON }) => {
15 | const el = createElement(AnsiText, { minHeight: 4 });
16 | await render(el);
17 | const node = toJSON();
18 | expect(node).to.have.property('type', 'code');
19 | expect(node.props).to.have.property('className', 'AnsiText');
20 | expect(node.children).to.have.lengthOf(4);
21 | for (const { children: line } of node.children) {
22 | const segment = line[0];
23 | expect(segment).to.have.property('type', 'span');
24 | expect(segment.props.style).to.have.property('color', undefined);
25 | expect(segment.props.style).to.have.property('backgroundColor', undefined);
26 | const text = segment.children[0];
27 | expect(text).to.have.lengthOf(79);
28 | expect(text).to.match(/^\s+$/);
29 | }
30 | });
31 | })
32 | it('should output error message when fetch throws', async function() {
33 | await withTestRenderer(async ({ render, toJSON }) => {
34 | try {
35 | global.fetch = () => {
36 | // checking handling of non-ASCII characters
37 | throw new Error('Stało się coś strasznego');
38 | };
39 | const src = 'http://whatever';
40 | let error;
41 | const el = createElement(AnsiText, { src, minHeight: 4, onError: (err) => error = err });
42 | await render(el);
43 | const node = toJSON();
44 | expect(node).to.have.property('type', 'code');
45 | expect(node.props).to.have.property('className', 'AnsiText');
46 | expect(node.children).to.have.lengthOf(4);
47 | const segment = node.children[0].children[0];
48 | expect(segment).to.have.property('type', 'span');
49 | expect(segment.props.style).to.have.property('color', '#aaaaaa');
50 | expect(segment.props.style).to.have.property('backgroundColor', '#000000');
51 | const text = segment.children[0];
52 | expect(text).to.match(/^Sta\?o si\? co\? strasznego\s+/);
53 | expect(error).to.be.an('error');
54 | } finally {
55 | delete global.fetch;
56 | }
57 | });
58 | })
59 | it('should accept a buffer as srcObject', async function() {
60 | await withTestRenderer(async ({ render, toJSON }) => {
61 | const srcObject = await readFile(resolve('./ansi/LDA-GARFIELD.ANS'));
62 | const el = createElement(AnsiText, { srcObject, maxHeight: 1024 });
63 | await render(el);
64 | const node = toJSON();
65 | expect(node).to.have.property('type', 'code');
66 | expect(node.props).to.have.property('className', 'AnsiText');
67 | expect(node.children).to.have.lengthOf(40);
68 | });
69 | })
70 | it('should call onMetadata when file contains metadata', async function() {
71 | await withTestRenderer(async ({ render, toJSON }) => {
72 | const srcObject = await readFile(resolve('./ansi/LDA-GARFIELD.ANS'));
73 | let metadata = null;
74 | const el = createElement(AnsiText, { srcObject, modemSpeed: Infinity, onMetadata: (m) => metadata = m });
75 | await render(el);
76 | expect(metadata).to.be.an('array');
77 | });
78 | })
79 | it('should accept a promise as srcObject', async function() {
80 | await withTestRenderer(async ({ render, toJSON }) => {
81 | const srcObject = readFile(resolve('./ansi/LDA-GARFIELD.ANS'));
82 | const el = createElement(AnsiText, { srcObject, maxHeight: 1024 });
83 | await render(el);
84 | await delay(50);
85 | const node = toJSON();
86 | expect(node).to.have.property('type', 'code');
87 | expect(node.props).to.have.property('className', 'AnsiText');
88 | expect(node.children).to.have.lengthOf(40);
89 | });
90 | })
91 | it('should return status through onStatus', async function() {
92 | await withTestRenderer(async ({ render, toJSON }) => {
93 | const srcObject = await readFile(resolve('./ansi/LDA-GARFIELD.ANS'));
94 | let status = null;
95 | const el = createElement(AnsiText, {
96 | srcObject,
97 | maxHeight: 1024,
98 | onStatus: s => status = s,
99 | });
100 | await render(el);
101 | await delay(50);
102 | const node = toJSON();
103 | expect(node).to.have.property('type', 'code');
104 | expect(node.props).to.have.property('className', 'AnsiText');
105 | expect(node.children).to.have.lengthOf(40);
106 | expect(status.position).to.be.at.least(0).and.at.most(0.5);
107 | });
108 | })
109 | it('should display blinking text', async function() {
110 | await withTestRenderer(async ({ render, toJSON }) => {
111 | const srcObject = await readFile(resolve('./ansi/US-CANDLES.ANS'));
112 | const el = createElement(AnsiText, { srcObject, blinking: true, modemSpeed: Infinity, blinkDuration: 100 });
113 | await render(el);
114 | const node1 = toJSON();
115 | await delay(120);
116 | const node2 = toJSON();
117 | expect(node2).to.not.eql(node1);
118 | await delay(120);
119 | const node3 = toJSON();
120 | expect(node3).to.not.eql(node2);
121 | expect(node3).to.eql(node1);
122 | });
123 | })
124 | it('should leave out background color from undrawn area when transparency is on', async function() {
125 | await withTestRenderer(async ({ render, toJSON }) => {
126 | const srcObject = await readFile(resolve('./ansi/LDA-GARFIELD.ANS'));
127 | const el = createElement(AnsiText, { srcObject, transparency: true });
128 | await render(el);
129 | const node = toJSON();
130 | let transparentSegment;
131 | for (const line of node.children) {
132 | for (const segment of line.children) {
133 | if (!segment.props.style.backgroundColor) {
134 | transparentSegment = segment;
135 | }
136 | }
137 | }
138 | expect(transparentSegment).to.not.be.null;
139 | });
140 | })
141 | it('should load data through fetch when src is given', async function() {
142 | await withTestRenderer(async ({ render, toJSON }) => {
143 | let called = false;
144 | global.fetch = async function(path) {
145 | called = true;
146 | const data = await readFile(resolve(path));
147 | return {
148 | status: 200,
149 | statusText: 'OK',
150 | arrayBuffer: async () => data,
151 | };
152 | };
153 | try {
154 | const el = createElement(AnsiText, { src: './ansi/LDA-GARFIELD.ANS', maxHeight: 1024 });
155 | await render(el);
156 | await delay(10);
157 | expect(called).to.be.true;
158 | const node = toJSON();
159 | expect(node).to.have.property('type', 'code');
160 | expect(node.props).to.have.property('className', 'AnsiText');
161 | expect(node.children).to.have.lengthOf(40);
162 | } finally {
163 | delete global.fetch;
164 | }
165 | });
166 | })
167 | it('should display error when fetch does not return 200 OK', async function() {
168 | await withTestRenderer(async ({ render, toJSON }) => {
169 | let called = false;
170 | global.fetch = async function(path) {
171 | called = true;
172 | await delay(10);
173 | return {
174 | status: 404,
175 | statusText: 'Not Found',
176 | };
177 | };
178 | try {
179 | const el = createElement(AnsiText, { src: './ansi/LDA-GARFIELD.ANS' });
180 | await render(el);
181 | await delay(30);
182 | expect(called).to.be.true;
183 | const node = toJSON();
184 | expect(node).to.have.property('type', 'code');
185 | expect(node.props).to.have.property('className', 'AnsiText');
186 | expect(node.children).to.have.lengthOf(22);
187 | const segment = node.children[0].children[0];
188 | expect(segment).to.have.property('type', 'span');
189 | expect(segment.props.style).to.have.property('color', '#aaaaaa');
190 | expect(segment.props.style).to.have.property('backgroundColor', '#000000');
191 | const text = segment.children[0];
192 | expect(text).to.match(/^HTTP 404 \- Not Found\s+$/);
193 | } finally {
194 | delete global.fetch;
195 | }
196 | });
197 | })
198 | it('should blink text using CSS when palette is set to css', async function() {
199 | await withTestRenderer(async ({ render, toJSON }) => {
200 | const srcObject = await readFile(resolve('./ansi/US-CANDLES.ANS'));
201 | const el = createElement(AnsiText, {
202 | srcObject,
203 | blinking: true,
204 | modemSpeed: Infinity,
205 | blinkDuration: 100 ,
206 | palette: 'css',
207 | });
208 | await render(el);
209 | const node1 = toJSON();
210 | const segment = node1.children[0].children[0];
211 | expect(segment.props).to.not.have.property('style');
212 | expect(segment.props).to.have.property('className').that.matches(/fgColor\d+ bgColor\d+/);
213 | await delay(120);
214 | let blinkingSegment = null;
215 | for (const line of node1.children) {
216 | for (const segment of line.children) {
217 | if (/\bblink\b/.test(segment.props.className)) {
218 | blinkingSegment = segment;
219 | }
220 | }
221 | }
222 | expect(blinkingSegment).to.be.null;
223 | const node2 = toJSON();
224 | expect(node2).to.not.eql(node1);
225 | for (const line of node2.children) {
226 | for (const segment of line.children) {
227 | if (/\bblink\b/.test(segment.props.className)) {
228 | blinkingSegment = segment;
229 | }
230 | }
231 | }
232 | expect(blinkingSegment).to.not.be.null;
233 | expect(blinkingSegment.props.className).to.match(/fgColor\d+ bgColor\d+ blink\b/);
234 | });
235 | })
236 | it('should use CSS for blinking when blinking is also set to css', async function() {
237 | await withTestRenderer(async ({ render, toJSON }) => {
238 | const srcObject = await readFile(resolve('./ansi/US-CANDLES.ANS'));
239 | const el = createElement(AnsiText, {
240 | srcObject,
241 | blinking: 'css',
242 | modemSpeed: Infinity,
243 | blinkDuration: 100 ,
244 | palette: 'css',
245 | });
246 | await render(el);
247 | const node1 = toJSON();
248 | const segment = node1.children[0].children[0];
249 | expect(segment.props).to.not.have.property('style');
250 | expect(segment.props).to.have.property('className').that.matches(/fgColor\d+ bgColor\d+/);
251 | let blinkingSegment = null;
252 | for (const line of node1.children) {
253 | for (const segment of line.children) {
254 | if (/\bblinking\b/.test(segment.props.className)) {
255 | blinkingSegment = segment;
256 | }
257 | }
258 | }
259 | expect(blinkingSegment).to.not.be.null;
260 | expect(blinkingSegment.props.className).to.match(/fgColor\d+ bgColor\d+ blinking\b/);
261 | await delay(120);
262 | const node2 = toJSON();
263 | expect(node2).to.eql(node1);
264 | });
265 | })
266 | })
267 | })
268 |
--------------------------------------------------------------------------------
/demo/ink/bin/FileList.mjs:
--------------------------------------------------------------------------------
1 | import { readdir, open } from 'fs/promises';
2 | import { basename, normalize } from 'path';
3 | import { useProgressive } from 'react-seq';
4 | import { useFocus, Box, Text } from 'ink';
5 | import MulticolumnSelectInput from 'ink-multicolumn-select-input';
6 | import InkSpinner from 'ink-spinner';
7 | import { jsx as _jsx } from "react/jsx-runtime";
8 | import { jsxs as _jsxs } from "react/jsx-runtime";
9 | const {
10 | default: Spinner
11 | } = InkSpinner;
12 | export default function FileList({
13 | folder = '.',
14 | selected = '',
15 | onFileSelect,
16 | onFolderSelect
17 | }) {
18 | return useProgressive(async ({
19 | fallback,
20 | type,
21 | usable,
22 | defer
23 | }) => {
24 | fallback( /*#__PURE__*/_jsx(LoadingScreen, {}));
25 | type(FileListUI);
26 | usable(5);
27 | defer(100);
28 | return {
29 | folders: findSubfolder(folder),
30 | files: findAnsiFiles(folder),
31 | selected,
32 | onFileSelect,
33 | onFolderSelect
34 | };
35 | }, [folder]);
36 | }
37 |
38 | // typical ANSI files are 79x24
39 | const width = 81;
40 | const height = 26;
41 | const borderStyle = 'round';
42 | function FileListUI({
43 | folders = [],
44 | files = [],
45 | selected,
46 | onFileSelect,
47 | onFolderSelect
48 | }) {
49 | const {
50 | isFocused
51 | } = useFocus({
52 | id: 'main',
53 | autoFocus: true
54 | });
55 | const items = [];
56 | for (const folder of folders) {
57 | items.push({
58 | label: '\u{1F5C0} ' + basename(folder),
59 | value: folder,
60 | type: 'folder'
61 | });
62 | }
63 | for (const file of files) {
64 | items.push({
65 | label: '\u{1F5CF} ' + basename(file),
66 | value: file,
67 | type: 'file'
68 | });
69 | }
70 | // ensure that columns are wide enough for the longest filename
71 | const maxWidth = items.reduce((m, i) => m = Math.max(m, i.label.length), 0);
72 | const columnCount = Math.max(1, Math.floor(80 / (maxWidth + 2)));
73 | const limit = height - 2;
74 | const initialIndex = Math.max(0, items.findIndex(i => i.value === selected));
75 | const borderColor = isFocused ? 'blue' : undefined;
76 | const onSelect = ({
77 | value,
78 | type
79 | }) => {
80 | if (type === 'folder') {
81 | onFolderSelect?.(value);
82 | } else if (type === 'file') {
83 | onFileSelect?.(value);
84 | }
85 | };
86 | return /*#__PURE__*/_jsx(Box, {
87 | borderStyle,
88 | borderColor,
89 | width,
90 | children: /*#__PURE__*/_jsx(MulticolumnSelectInput, {
91 | items,
92 | limit,
93 | columnCount,
94 | isFocused,
95 | initialIndex,
96 | onSelect
97 | })
98 | });
99 | }
100 | export function LoadingScreen() {
101 | const alignItems = 'center';
102 | const justifyContent = 'center';
103 | return /*#__PURE__*/_jsx(Box, {
104 | borderStyle,
105 | width,
106 | height,
107 | alignItems,
108 | justifyContent,
109 | children: /*#__PURE__*/_jsxs(Text, {
110 | children: [" ", /*#__PURE__*/_jsx(Spinner, {}), " Loading"]
111 | })
112 | });
113 | }
114 | async function* findSubfolder(folder) {
115 | const list = await readdir(folder, {
116 | withFileTypes: true
117 | });
118 | yield normalize(`${folder}/..`);
119 | for (const entry of list) {
120 | if (entry.isDirectory() && !entry.name.startsWith('.')) {
121 | yield `${folder}/${entry.name}`;
122 | }
123 | }
124 | }
125 | async function* findAnsiFiles(folder) {
126 | const list = await readdir(folder, {
127 | withFileTypes: true
128 | });
129 | for (const entry of list) {
130 | if (entry.isFile() && !entry.name.startsWith('.')) {
131 | const path = `${folder}/${entry.name}`;
132 | if (await isAnsiFile(path)) {
133 | yield path;
134 | }
135 | }
136 | }
137 | }
138 | export async function isAnsiFile(path) {
139 | let file;
140 | try {
141 | file = await open(path);
142 | const buffer = Buffer.alloc(1024);
143 | const {
144 | bytesRead
145 | } = await file.read(buffer, 0, buffer.length);
146 | const array = new Uint8Array(buffer, 0, bytesRead);
147 | for (let i = 0; i < array.length - 1; i++) {
148 | if (array[i] === 0x1B && array[i + 1] === 0x5B) {
149 | return true;
150 | }
151 | }
152 | } catch (err) {} finally {
153 | await file?.close();
154 | }
155 | return false;
156 | }
157 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJyZWFkZGlyIiwib3BlbiIsImJhc2VuYW1lIiwibm9ybWFsaXplIiwidXNlUHJvZ3Jlc3NpdmUiLCJ1c2VGb2N1cyIsIkJveCIsIlRleHQiLCJNdWx0aWNvbHVtblNlbGVjdElucHV0IiwiSW5rU3Bpbm5lciIsImRlZmF1bHQiLCJTcGlubmVyIiwiRmlsZUxpc3QiLCJmb2xkZXIiLCJzZWxlY3RlZCIsIm9uRmlsZVNlbGVjdCIsIm9uRm9sZGVyU2VsZWN0IiwiZmFsbGJhY2siLCJ0eXBlIiwidXNhYmxlIiwiZGVmZXIiLCJGaWxlTGlzdFVJIiwiZm9sZGVycyIsImZpbmRTdWJmb2xkZXIiLCJmaWxlcyIsImZpbmRBbnNpRmlsZXMiLCJ3aWR0aCIsImhlaWdodCIsImJvcmRlclN0eWxlIiwiaXNGb2N1c2VkIiwiaWQiLCJhdXRvRm9jdXMiLCJpdGVtcyIsInB1c2giLCJsYWJlbCIsInZhbHVlIiwiZmlsZSIsIm1heFdpZHRoIiwicmVkdWNlIiwibSIsImkiLCJNYXRoIiwibWF4IiwibGVuZ3RoIiwiY29sdW1uQ291bnQiLCJmbG9vciIsImxpbWl0IiwiaW5pdGlhbEluZGV4IiwiZmluZEluZGV4IiwiYm9yZGVyQ29sb3IiLCJ1bmRlZmluZWQiLCJvblNlbGVjdCIsIkxvYWRpbmdTY3JlZW4iLCJhbGlnbkl0ZW1zIiwianVzdGlmeUNvbnRlbnQiLCJsaXN0Iiwid2l0aEZpbGVUeXBlcyIsImVudHJ5IiwiaXNEaXJlY3RvcnkiLCJuYW1lIiwic3RhcnRzV2l0aCIsImlzRmlsZSIsInBhdGgiLCJpc0Fuc2lGaWxlIiwiYnVmZmVyIiwiQnVmZmVyIiwiYWxsb2MiLCJieXRlc1JlYWQiLCJyZWFkIiwiYXJyYXkiLCJVaW50OEFycmF5IiwiZXJyIiwiY2xvc2UiXSwic291cmNlcyI6WyJGaWxlTGlzdC5qc3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgcmVhZGRpciwgb3BlbiB9IGZyb20gJ2ZzL3Byb21pc2VzJztcbmltcG9ydCB7IGJhc2VuYW1lLCBub3JtYWxpemUgfSBmcm9tICdwYXRoJztcbmltcG9ydCB7IHVzZVByb2dyZXNzaXZlIH0gZnJvbSAncmVhY3Qtc2VxJztcbmltcG9ydCB7IHVzZUZvY3VzLCBCb3gsIFRleHQgfSBmcm9tICdpbmsnO1xuaW1wb3J0IE11bHRpY29sdW1uU2VsZWN0SW5wdXQgZnJvbSAnaW5rLW11bHRpY29sdW1uLXNlbGVjdC1pbnB1dCc7XG5pbXBvcnQgSW5rU3Bpbm5lciBmcm9tICdpbmstc3Bpbm5lcic7IGNvbnN0IHsgZGVmYXVsdDogU3Bpbm5lciB9ID0gSW5rU3Bpbm5lcjtcblxuZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gRmlsZUxpc3QoeyBmb2xkZXIgPSAnLicsIHNlbGVjdGVkID0gJycsIG9uRmlsZVNlbGVjdCwgb25Gb2xkZXJTZWxlY3QgfSkge1xuICByZXR1cm4gdXNlUHJvZ3Jlc3NpdmUoYXN5bmMgKHsgZmFsbGJhY2ssIHR5cGUsIHVzYWJsZSwgZGVmZXIgfSkgPT4ge1xuICAgIGZhbGxiYWNrKDxMb2FkaW5nU2NyZWVuIC8+KTtcbiAgICB0eXBlKEZpbGVMaXN0VUkpO1xuICAgIHVzYWJsZSg1KTtcbiAgICBkZWZlcigxMDApO1xuICAgIHJldHVybiB7XG4gICAgICBmb2xkZXJzOiBmaW5kU3ViZm9sZGVyKGZvbGRlciksXG4gICAgICBmaWxlczogZmluZEFuc2lGaWxlcyhmb2xkZXIpLFxuICAgICAgc2VsZWN0ZWQsXG4gICAgICBvbkZpbGVTZWxlY3QsIFxuICAgICAgb25Gb2xkZXJTZWxlY3QsXG4gICAgfTtcbiAgfSwgWyBmb2xkZXIgXSk7XG59XG5cbi8vIHR5cGljYWwgQU5TSSBmaWxlcyBhcmUgNzl4MjRcbmNvbnN0IHdpZHRoID0gODE7XG5jb25zdCBoZWlnaHQgPSAyNjtcbmNvbnN0IGJvcmRlclN0eWxlID0gJ3JvdW5kJztcblxuZnVuY3Rpb24gRmlsZUxpc3RVSSh7IGZvbGRlcnMgPSBbXSwgZmlsZXMgPSBbXSwgc2VsZWN0ZWQsIG9uRmlsZVNlbGVjdCwgb25Gb2xkZXJTZWxlY3QgfSkge1xuICBjb25zdCB7IGlzRm9jdXNlZCB9ID0gdXNlRm9jdXMoeyBpZDogJ21haW4nLCBhdXRvRm9jdXM6IHRydWUgfSk7XG4gIGNvbnN0IGl0ZW1zID0gW107XG4gIGZvciAoY29uc3QgZm9sZGVyIG9mIGZvbGRlcnMpIHtcbiAgICBpdGVtcy5wdXNoKHsgbGFiZWw6ICdcXHV7MUY1QzB9ICcgKyBiYXNlbmFtZShmb2xkZXIpLCB2YWx1ZTogZm9sZGVyLCB0eXBlOiAnZm9sZGVyJyB9KVxuICB9XG4gIGZvciAoY29uc3QgZmlsZSBvZiBmaWxlcykge1xuICAgIGl0ZW1zLnB1c2goeyBsYWJlbDogJ1xcdXsxRjVDRn0gJyArIGJhc2VuYW1lKGZpbGUpLCB2YWx1ZTogZmlsZSwgdHlwZTogJ2ZpbGUnIH0pXG4gIH1cbiAgLy8gZW5zdXJlIHRoYXQgY29sdW1ucyBhcmUgd2lkZSBlbm91Z2ggZm9yIHRoZSBsb25nZXN0IGZpbGVuYW1lXG4gIGNvbnN0IG1heFdpZHRoID0gaXRlbXMucmVkdWNlKChtLCBpKSA9PiBtID0gTWF0aC5tYXgobSwgaS5sYWJlbC5sZW5ndGgpLCAwKTtcbiAgY29uc3QgY29sdW1uQ291bnQgPSBNYXRoLm1heCgxLCBNYXRoLmZsb29yKDgwIC8gKG1heFdpZHRoICsgMikpKTtcbiAgY29uc3QgbGltaXQgPSBoZWlnaHQgLSAyO1xuICBjb25zdCBpbml0aWFsSW5kZXggPSBNYXRoLm1heCgwLCBpdGVtcy5maW5kSW5kZXgoaSA9PiBpLnZhbHVlID09PSBzZWxlY3RlZCkpO1xuICBjb25zdCBib3JkZXJDb2xvciA9IChpc0ZvY3VzZWQpID8gJ2JsdWUnIDogdW5kZWZpbmVkO1xuICBjb25zdCBvblNlbGVjdCA9ICh7IHZhbHVlLCB0eXBlIH0pID0+IHtcbiAgICBpZiAodHlwZSA9PT0gJ2ZvbGRlcicpIHtcbiAgICAgIG9uRm9sZGVyU2VsZWN0Py4odmFsdWUpO1xuICAgIH0gZWxzZSBpZiAodHlwZSA9PT0gJ2ZpbGUnKSB7XG4gICAgICBvbkZpbGVTZWxlY3Q/Lih2YWx1ZSk7XG4gICAgfVxuICB9O1xuICByZXR1cm4gKFxuICAgIDxCb3ggey4uLnsgYm9yZGVyU3R5bGUsIGJvcmRlckNvbG9yLCB3aWR0aCB9fT5cbiAgICAgIDxNdWx0aWNvbHVtblNlbGVjdElucHV0IHsuLi57IGl0ZW1zLCBsaW1pdCwgY29sdW1uQ291bnQsIGlzRm9jdXNlZCwgaW5pdGlhbEluZGV4LCBvblNlbGVjdCB9fSAvPlxuICAgIDwvQm94PlxuICApO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gTG9hZGluZ1NjcmVlbigpIHtcbiAgY29uc3QgYWxpZ25JdGVtcyA9ICdjZW50ZXInO1xuICBjb25zdCBqdXN0aWZ5Q29udGVudCA9ICdjZW50ZXInO1xuICByZXR1cm4gKFxuICAgIDxCb3ggey4uLnsgYm9yZGVyU3R5bGUsIHdpZHRoLCBoZWlnaHQsIGFsaWduSXRlbXMsIGp1c3RpZnlDb250ZW50IH19PlxuICAgICAgPFRleHQ+IDxTcGlubmVyIC8+IExvYWRpbmc8L1RleHQ+XG4gICAgPC9Cb3g+XG4gICk7XG59XG5cbmFzeW5jIGZ1bmN0aW9uKiBmaW5kU3ViZm9sZGVyKGZvbGRlcikge1xuICBjb25zdCBsaXN0ID0gYXdhaXQgcmVhZGRpcihmb2xkZXIsIHsgd2l0aEZpbGVUeXBlczogdHJ1ZSB9KTtcbiAgeWllbGQgbm9ybWFsaXplKGAke2ZvbGRlcn0vLi5gKTtcbiAgZm9yIChjb25zdCBlbnRyeSBvZiBsaXN0KSB7XG4gICAgaWYgKGVudHJ5LmlzRGlyZWN0b3J5KCkgJiYgIWVudHJ5Lm5hbWUuc3RhcnRzV2l0aCgnLicpKSB7XG4gICAgICB5aWVsZCBgJHtmb2xkZXJ9LyR7ZW50cnkubmFtZX1gO1xuICAgIH1cbiAgfVxufVxuXG5hc3luYyBmdW5jdGlvbiogZmluZEFuc2lGaWxlcyhmb2xkZXIpIHtcbiAgY29uc3QgbGlzdCA9IGF3YWl0IHJlYWRkaXIoZm9sZGVyLCB7IHdpdGhGaWxlVHlwZXM6IHRydWUgfSk7XG4gIGZvciAoY29uc3QgZW50cnkgb2YgbGlzdCkge1xuICAgIGlmIChlbnRyeS5pc0ZpbGUoKSAmJiAhZW50cnkubmFtZS5zdGFydHNXaXRoKCcuJykpIHtcbiAgICAgIGNvbnN0IHBhdGggPSBgJHtmb2xkZXJ9LyR7ZW50cnkubmFtZX1gO1xuICAgICAgaWYgKGF3YWl0IGlzQW5zaUZpbGUocGF0aCkpIHtcbiAgICAgICAgeWllbGQgcGF0aDtcbiAgICAgIH1cbiAgICB9XG4gIH1cbn1cblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGlzQW5zaUZpbGUocGF0aCkge1xuICBsZXQgZmlsZTtcbiAgdHJ5IHtcbiAgICBmaWxlID0gYXdhaXQgb3BlbihwYXRoKTtcbiAgICBjb25zdCBidWZmZXIgPSBCdWZmZXIuYWxsb2MoMTAyNCk7XG4gICAgY29uc3QgeyBieXRlc1JlYWQgfSA9IGF3YWl0IGZpbGUucmVhZChidWZmZXIsIDAsIGJ1ZmZlci5sZW5ndGgpO1xuICAgIGNvbnN0IGFycmF5ID0gbmV3IFVpbnQ4QXJyYXkoYnVmZmVyLCAwLCBieXRlc1JlYWQpO1xuICAgIGZvciAobGV0IGkgPSAwOyBpIDwgYXJyYXkubGVuZ3RoIC0gMTsgaSsrKSB7XG4gICAgICBpZiAoYXJyYXlbaV0gPT09IDB4MUIgJiYgYXJyYXlbaSArIDFdID09PSAweDVCKSB7XG4gICAgICAgIHJldHVybiB0cnVlO1xuICAgICAgfVxuICAgIH1cbiAgfSBjYXRjaCAoZXJyKSB7XG4gIH0gZmluYWxseSB7XG4gICAgYXdhaXQgZmlsZT8uY2xvc2UoKTtcbiAgfVxuICByZXR1cm4gZmFsc2U7XG59Il0sIm1hcHBpbmdzIjoiQUFBQSxTQUFTQSxPQUFPLEVBQUVDLElBQUksUUFBUSxhQUFhO0FBQzNDLFNBQVNDLFFBQVEsRUFBRUMsU0FBUyxRQUFRLE1BQU07QUFDMUMsU0FBU0MsY0FBYyxRQUFRLFdBQVc7QUFDMUMsU0FBU0MsUUFBUSxFQUFFQyxHQUFHLEVBQUVDLElBQUksUUFBUSxLQUFLO0FBQ3pDLE9BQU9DLHNCQUFzQixNQUFNLDhCQUE4QjtBQUNqRSxPQUFPQyxVQUFVLE1BQU0sYUFBYTtBQUFDO0FBQUE7QUFB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
--------------------------------------------------------------------------------
/demo/ink/bin/MulticolumnSelectInput.mjs:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect, createElement } from 'react';
2 | import { Text, Box, useInput } from 'ink';
3 | export default function MulticolumnSelectInput(props) {
4 | const {
5 | items = [],
6 | initialIndex = 0,
7 | isFocused = true,
8 | limit = 24,
9 | columnCount = 4,
10 | width = '100%',
11 | indicatorComponent = Indicator,
12 | itemComponent = Item,
13 | onSelect,
14 | onHighlight
15 | } = props;
16 | const [selectedIndex, setSelectedIndex] = useState(initialIndex);
17 | const [columnOffset, setColumnOffset] = useState(calculateColumnOffset(selectedIndex, columnCount, limit));
18 | useInput((input, key) => {
19 | let newIndex = -1;
20 | if (input === 'k' || key.upArrow) {
21 | newIndex = selectedIndex - 1;
22 | } else if (input === 'j' || key.downArrow) {
23 | newIndex = selectedIndex + 1;
24 | } else if (input === 'h' || key.leftArrow) {
25 | newIndex = selectedIndex - limit;
26 | } else if (input === 'l' || key.rightArrow) {
27 | newIndex = selectedIndex + limit;
28 | if (newIndex >= items.length) {
29 | // jump to the last item only if we're in the second to last column
30 | const currentColumn = Math.floor((selectedIndex + 1) / limit);
31 | const lastColumn = Math.floor(items.length / limit);
32 | if (lastColumn > currentColumn) {
33 | newIndex = items.length - 1;
34 | }
35 | }
36 | } else if (input === '^' || input === '\u005BH') {
37 | newIndex = 0;
38 | } else if (input === '$' || input === '\u005BF') {
39 | newIndex = items.length - 1;
40 | } else if (key.return) {
41 | if (items[selectedIndex]) {
42 | onSelect?.(items[selectedIndex]);
43 | }
44 | }
45 | if (newIndex >= 0 && newIndex < items.length) {
46 | setSelectedIndex(newIndex);
47 | // adjust column offset so the selected item is in view
48 | setColumnOffset(calculateColumnOffset(initialIndex, columnCount, limit, columnOffset));
49 | onHighlight?.(items[newIndex]);
50 | }
51 | }, {
52 | isActive: isFocused
53 | });
54 | // reset selectIndex when items are different
55 | const previousItems = useRef();
56 | useEffect(() => {
57 | const {
58 | current
59 | } = previousItems;
60 | if (current && items.some((item, i) => item.value !== current[i].value)) {
61 | setSelectedIndex(initialIndex);
62 | setColumnOffset(calculateColumnOffset(initialIndex, columnCount, limit));
63 | }
64 | previousItems.current = items;
65 | }, [items, initialIndex, columnCount, limit]);
66 | const columns = [];
67 | for (let i = 0; i < columnCount; i++) {
68 | const rows = [];
69 | for (let j = 0, index = (columnOffset + i) * limit; j < limit && index < items.length; j++, index++) {
70 | const isSelected = index === selectedIndex;
71 | const indicator = createElement(indicatorComponent, {
72 | isSelected
73 | });
74 | const item = createElement(itemComponent, {
75 | ...items[index],
76 | isSelected
77 | });
78 | const row = createElement(Box, {
79 | key: index
80 | }, indicator, item);
81 | rows.push(row);
82 | }
83 | // use flex basis 0 so columns have the same width
84 | const column = createElement(Box, {
85 | key: i,
86 | flexDirection: 'column',
87 | flexGrow: 1,
88 | flexBasis: 0
89 | }, rows);
90 | columns.push(column);
91 | }
92 | return createElement(Box, {
93 | width
94 | }, columns);
95 | }
96 | function calculateColumnOffset(index, count, limit, offset = 0) {
97 | const columnIndex = Math.floor(Math.max(1, index + 1) / limit);
98 | let newOffset = offset;
99 | while (newOffset + count <= columnIndex) {
100 | newOffset++;
101 | }
102 | while (newOffset > columnIndex) {
103 | newOffset--;
104 | }
105 | return newOffset;
106 | }
107 | function Item({
108 | isSelected = false,
109 | label
110 | }) {
111 | return createElement(Text, {
112 | inverse: isSelected
113 | }, ` ${label} `);
114 | }
115 | function Indicator({
116 | isSelected
117 | }) {
118 | return null;
119 | }
120 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJ1c2VTdGF0ZSIsInVzZVJlZiIsInVzZUVmZmVjdCIsImNyZWF0ZUVsZW1lbnQiLCJUZXh0IiwiQm94IiwidXNlSW5wdXQiLCJNdWx0aWNvbHVtblNlbGVjdElucHV0IiwicHJvcHMiLCJpdGVtcyIsImluaXRpYWxJbmRleCIsImlzRm9jdXNlZCIsImxpbWl0IiwiY29sdW1uQ291bnQiLCJ3aWR0aCIsImluZGljYXRvckNvbXBvbmVudCIsIkluZGljYXRvciIsIml0ZW1Db21wb25lbnQiLCJJdGVtIiwib25TZWxlY3QiLCJvbkhpZ2hsaWdodCIsInNlbGVjdGVkSW5kZXgiLCJzZXRTZWxlY3RlZEluZGV4IiwiY29sdW1uT2Zmc2V0Iiwic2V0Q29sdW1uT2Zmc2V0IiwiY2FsY3VsYXRlQ29sdW1uT2Zmc2V0IiwiaW5wdXQiLCJrZXkiLCJuZXdJbmRleCIsInVwQXJyb3ciLCJkb3duQXJyb3ciLCJsZWZ0QXJyb3ciLCJyaWdodEFycm93IiwibGVuZ3RoIiwiY3VycmVudENvbHVtbiIsIk1hdGgiLCJmbG9vciIsImxhc3RDb2x1bW4iLCJyZXR1cm4iLCJpc0FjdGl2ZSIsInByZXZpb3VzSXRlbXMiLCJjdXJyZW50Iiwic29tZSIsIml0ZW0iLCJpIiwidmFsdWUiLCJjb2x1bW5zIiwicm93cyIsImoiLCJpbmRleCIsImlzU2VsZWN0ZWQiLCJpbmRpY2F0b3IiLCJyb3ciLCJwdXNoIiwiY29sdW1uIiwiZmxleERpcmVjdGlvbiIsImZsZXhHcm93IiwiZmxleEJhc2lzIiwiY291bnQiLCJvZmZzZXQiLCJjb2x1bW5JbmRleCIsIm1heCIsIm5ld09mZnNldCIsImxhYmVsIiwiaW52ZXJzZSJdLCJzb3VyY2VzIjpbIk11bHRpY29sdW1uU2VsZWN0SW5wdXQuanN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IHVzZVN0YXRlLCB1c2VSZWYsIHVzZUVmZmVjdCwgY3JlYXRlRWxlbWVudCB9IGZyb20gJ3JlYWN0JztcbmltcG9ydCB7IFRleHQsIEJveCwgdXNlSW5wdXQgfSBmcm9tICdpbmsnO1xuXG5leHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBNdWx0aWNvbHVtblNlbGVjdElucHV0KHByb3BzKSB7XG4gIGNvbnN0IHsgXG4gICAgaXRlbXMgPSBbXSxcbiAgICBpbml0aWFsSW5kZXggPSAwLFxuICAgIGlzRm9jdXNlZCA9IHRydWUsXG4gICAgbGltaXQgPSAyNCxcbiAgICBjb2x1bW5Db3VudCA9IDQsXG4gICAgd2lkdGggPSAnMTAwJScsXG4gICAgaW5kaWNhdG9yQ29tcG9uZW50ID0gSW5kaWNhdG9yLFxuICAgIGl0ZW1Db21wb25lbnQgPSBJdGVtLCAgICBcbiAgICBvblNlbGVjdCxcbiAgICBvbkhpZ2hsaWdodCxcbiAgfSA9IHByb3BzO1xuICBjb25zdCBbIHNlbGVjdGVkSW5kZXgsIHNldFNlbGVjdGVkSW5kZXggXSA9IHVzZVN0YXRlKGluaXRpYWxJbmRleCk7XG4gIGNvbnN0IFsgY29sdW1uT2Zmc2V0LCBzZXRDb2x1bW5PZmZzZXQgXSA9IHVzZVN0YXRlKGNhbGN1bGF0ZUNvbHVtbk9mZnNldChzZWxlY3RlZEluZGV4LCBjb2x1bW5Db3VudCwgbGltaXQpKTtcbiAgdXNlSW5wdXQoKGlucHV0LCBrZXkpID0+IHtcbiAgICBsZXQgbmV3SW5kZXggPSAtMTtcbiAgICBpZiAoaW5wdXQgPT09ICdrJyB8fCBrZXkudXBBcnJvdykge1xuICAgICAgbmV3SW5kZXggPSBzZWxlY3RlZEluZGV4IC0gMTtcbiAgICB9IGVsc2UgaWYgKGlucHV0ID09PSAnaicgfHwga2V5LmRvd25BcnJvdykge1xuICAgICAgbmV3SW5kZXggPSBzZWxlY3RlZEluZGV4ICsgMTtcbiAgICB9IGVsc2UgaWYgKGlucHV0ID09PSAnaCcgfHwga2V5LmxlZnRBcnJvdykge1xuICAgICAgbmV3SW5kZXggPSBzZWxlY3RlZEluZGV4IC0gbGltaXQ7XG4gICAgfSBlbHNlIGlmIChpbnB1dCA9PT0gJ2wnIHx8IGtleS5yaWdodEFycm93KSB7XG4gICAgICBuZXdJbmRleCA9IHNlbGVjdGVkSW5kZXggKyBsaW1pdDtcbiAgICAgIGlmIChuZXdJbmRleCA+PSBpdGVtcy5sZW5ndGgpIHtcbiAgICAgICAgLy8ganVtcCB0byB0aGUgbGFzdCBpdGVtIG9ubHkgaWYgd2UncmUgaW4gdGhlIHNlY29uZCB0byBsYXN0IGNvbHVtblxuICAgICAgICBjb25zdCBjdXJyZW50Q29sdW1uID0gTWF0aC5mbG9vcigoc2VsZWN0ZWRJbmRleCArIDEpIC8gbGltaXQpO1xuICAgICAgICBjb25zdCBsYXN0Q29sdW1uID0gTWF0aC5mbG9vcihpdGVtcy5sZW5ndGggLyBsaW1pdCk7XG4gICAgICAgIGlmIChsYXN0Q29sdW1uID4gY3VycmVudENvbHVtbikge1xuICAgICAgICAgIG5ld0luZGV4ID0gaXRlbXMubGVuZ3RoIC0gMTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0gZWxzZSBpZiAoaW5wdXQgPT09ICdeJyB8fCBpbnB1dCA9PT0gJ1xcdTAwNUJIJykge1xuICAgICAgbmV3SW5kZXggPSAwO1xuICAgIH0gZWxzZSBpZiAoaW5wdXQgPT09ICckJyB8fCBpbnB1dCA9PT0gJ1xcdTAwNUJGJykge1xuICAgICAgbmV3SW5kZXggPSBpdGVtcy5sZW5ndGggLSAxO1xuICAgIH0gZWxzZSBpZiAoa2V5LnJldHVybikge1xuICAgICAgaWYgKGl0ZW1zW3NlbGVjdGVkSW5kZXhdKSB7XG4gICAgICAgIG9uU2VsZWN0Py4oaXRlbXNbc2VsZWN0ZWRJbmRleF0pO1xuICAgICAgfVxuICAgIH0gICBcbiAgICBpZiAobmV3SW5kZXggPj0gMCAmJiBuZXdJbmRleCA8IGl0ZW1zLmxlbmd0aCkge1xuICAgICAgc2V0U2VsZWN0ZWRJbmRleChuZXdJbmRleCk7XG4gICAgICAvLyBhZGp1c3QgY29sdW1uIG9mZnNldCBzbyB0aGUgc2VsZWN0ZWQgaXRlbSBpcyBpbiB2aWV3XG4gICAgICBzZXRDb2x1bW5PZmZzZXQoY2FsY3VsYXRlQ29sdW1uT2Zmc2V0KGluaXRpYWxJbmRleCwgY29sdW1uQ291bnQsIGxpbWl0LCBjb2x1bW5PZmZzZXQpKTtcbiAgICAgIG9uSGlnaGxpZ2h0Py4oaXRlbXNbbmV3SW5kZXhdKTtcbiAgICB9XG4gIH0sIHsgaXNBY3RpdmU6IGlzRm9jdXNlZCB9KTtcbiAgLy8gcmVzZXQgc2VsZWN0SW5kZXggd2hlbiBpdGVtcyBhcmUgZGlmZmVyZW50IFxuXHRjb25zdCBwcmV2aW91c0l0ZW1zID0gdXNlUmVmKCk7XG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgY29uc3QgeyBjdXJyZW50IH0gPSBwcmV2aW91c0l0ZW1zO1xuICAgIGlmIChjdXJyZW50ICYmIGl0ZW1zLnNvbWUoKGl0ZW0sIGkpID0+IGl0ZW0udmFsdWUgIT09IGN1cnJlbnRbaV0udmFsdWUpKSB7XG4gICAgICBzZXRTZWxlY3RlZEluZGV4KGluaXRpYWxJbmRleCk7XG4gICAgICBzZXRDb2x1bW5PZmZzZXQoY2FsY3VsYXRlQ29sdW1uT2Zmc2V0KGluaXRpYWxJbmRleCwgY29sdW1uQ291bnQsIGxpbWl0KSk7XG4gICAgfVxuICAgIHByZXZpb3VzSXRlbXMuY3VycmVudCA9IGl0ZW1zO1xuICB9LCBbIGl0ZW1zLCBpbml0aWFsSW5kZXgsIGNvbHVtbkNvdW50LCBsaW1pdCBdKTtcbiAgY29uc3QgY29sdW1ucyA9IFtdO1xuICBmb3IgKGxldCBpID0gMDsgaSA8IGNvbHVtbkNvdW50OyBpKyspIHsgICAgXG4gICAgY29uc3Qgcm93cyA9IFtdO1xuICAgIGZvciAobGV0IGogPSAwLCBpbmRleCA9IChjb2x1bW5PZmZzZXQgKyBpKSAqIGxpbWl0OyBqIDwgbGltaXQgJiYgaW5kZXggPCBpdGVtcy5sZW5ndGg7IGorKywgaW5kZXgrKykge1xuICAgICAgY29uc3QgaXNTZWxlY3RlZCA9IChpbmRleCA9PT0gc2VsZWN0ZWRJbmRleCk7XG4gICAgICBjb25zdCBpbmRpY2F0b3IgPSBjcmVhdGVFbGVtZW50KGluZGljYXRvckNvbXBvbmVudCwgeyBpc1NlbGVjdGVkIH0pO1xuICAgICAgY29uc3QgaXRlbSA9IGNyZWF0ZUVsZW1lbnQoaXRlbUNvbXBvbmVudCwgeyAuLi5pdGVtc1tpbmRleF0sIGlzU2VsZWN0ZWQgfSk7XG4gICAgICBjb25zdCByb3cgPSBjcmVhdGVFbGVtZW50KEJveCwgeyBrZXk6IGluZGV4IH0sIGluZGljYXRvciwgaXRlbSk7XG4gICAgICByb3dzLnB1c2gocm93KTtcbiAgICB9XG4gICAgLy8gdXNlIGZsZXggYmFzaXMgMCBzbyBjb2x1bW5zIGhhdmUgdGhlIHNhbWUgd2lkdGhcbiAgICBjb25zdCBjb2x1bW4gPSBjcmVhdGVFbGVtZW50KEJveCwgeyBrZXk6IGksIGZsZXhEaXJlY3Rpb246ICdjb2x1bW4nLCBmbGV4R3JvdzogMSwgZmxleEJhc2lzOiAwIH0sIHJvd3MpO1xuICAgIGNvbHVtbnMucHVzaChjb2x1bW4pO1xuICB9XG4gIHJldHVybiBjcmVhdGVFbGVtZW50KEJveCwgeyB3aWR0aCB9LCBjb2x1bW5zKTtcbn1cblxuZnVuY3Rpb24gY2FsY3VsYXRlQ29sdW1uT2Zmc2V0KGluZGV4LCBjb3VudCwgbGltaXQsIG9mZnNldCA9IDApIHtcbiAgY29uc3QgY29sdW1uSW5kZXggPSBNYXRoLmZsb29yKE1hdGgubWF4KDEsIGluZGV4ICsgMSkgLyBsaW1pdCk7XG4gIGxldCBuZXdPZmZzZXQgPSBvZmZzZXQ7XG4gIHdoaWxlIChuZXdPZmZzZXQgKyBjb3VudCA8PSBjb2x1bW5JbmRleCkge1xuICAgIG5ld09mZnNldCsrO1xuICB9XG4gIHdoaWxlIChuZXdPZmZzZXQgPiBjb2x1bW5JbmRleCkge1xuICAgIG5ld09mZnNldC0tO1xuICB9XG4gIHJldHVybiBuZXdPZmZzZXQ7XG59XG5cblxuXG5mdW5jdGlvbiBJdGVtKHsgaXNTZWxlY3RlZCA9IGZhbHNlLCBsYWJlbCB9KSB7XG5cdHJldHVybiBjcmVhdGVFbGVtZW50KFRleHQsIHsgaW52ZXJzZTogaXNTZWxlY3RlZCB9LCBgICR7bGFiZWx9IGApO1xufVxuXG5mdW5jdGlvbiBJbmRpY2F0b3IoeyBpc1NlbGVjdGVkIH0pIHtcbiAgcmV0dXJuIG51bGw7XG59Il0sIm1h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
--------------------------------------------------------------------------------
/src/hooks.js:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from 'react';
2 | import { useSequentialState, delay } from 'react-seq';
3 | import { toCP437, cp437Chars } from './dos-environment.js';
4 |
5 | const defaultStatus = { position: 0, playing: true };
6 | const promisedData = new WeakMap();
7 |
8 | export function useAnsi(dataSource, options = {}) {
9 | const {
10 | modemSpeed = 56000,
11 | frameDuration = 50,
12 | blinkDuration = 500,
13 | blinking = false,
14 | transparency = false,
15 | minWidth = 79,
16 | minHeight = 22,
17 | maxWidth = 80,
18 | maxHeight = 25,
19 | initialStatus = defaultStatus,
20 | onStatus,
21 | onError,
22 | onMetadata,
23 | beep,
24 | } = options;
25 | const state = useSequentialState(async function*({ initial, mount, signal }) {
26 | // screen is at minimum dimensions and empty initially
27 | let state = {
28 | width: minWidth,
29 | height: minHeight,
30 | blinked: false,
31 | lines: Array(minHeight).fill([ { text: ' '.repeat(minWidth), fgColor: undefined, bgColor: undefined, blinking, blink: false } ]),
32 | willBlink: false,
33 | status: initialStatus,
34 | metadata: null,
35 | error: null,
36 | };
37 | let data = null, initialized = false, error = null;
38 | if (typeof(dataSource?.then) === 'function') {
39 | data = promisedData.get(dataSource);
40 | if (!data) {
41 | try {
42 | // set initial state now, since we need to wait for data to show up
43 | initial(state);
44 | initialized = true;
45 | data = await dataSource;
46 | } catch (err) {
47 | data = err.message;
48 | error = err;
49 | }
50 | // remember the data
51 | promisedData.set(dataSource, data);
52 | }
53 | } else {
54 | data = dataSource;
55 | }
56 | if (typeof(data) === 'string') {
57 | data = toCP437(data);
58 | }
59 | let chars = new Uint8Array(data);
60 | let detectedWidth = 0, detectedHeight = 0;
61 | // process data in two passes: the first determines the maximum extent of the contents
62 | // while the second pass actually outputs them
63 | for (let pass = 1; pass <= 2; pass++) {
64 | // screen states
65 | let width = detectedWidth, height = detectedHeight;
66 | let cursorX = 0, cursorY = 0, savedCursorX = 0, savedCursorY = 0, maxCursorX = 0, maxCursorY = 0;
67 | let bgColor = 0, fgColor = 7, bgColorBase = 0, fgColorBase = 7, bgBright = false, fgBright = false;
68 | let transparencyFlags = 0, bgSet = false, fgSet = false;
69 | let buffer = null, blinked = false, willBlink = false;
70 | let escapeSeq = null, eof = false, metadata = null, metaString = '';
71 | if (pass === 1) {
72 | // there's no need to do the first pass if the minimum dimensions match the maximum
73 | if (minWidth !== maxWidth || minHeight !== maxHeight) {
74 | chars.map(processCharacter);
75 | }
76 | detectedWidth = Math.max(minWidth, maxCursorX + 1);
77 | detectedHeight = Math.max(minHeight, maxCursorY + 1);
78 | } else if (pass === 2) {
79 | // calculate the number of frames during which blinking text stays visible or invisible
80 | const blinkFrameCount = Math.ceil(blinkDuration / frameDuration);
81 | let blinkFramesRemaining = blinkFrameCount;
82 | // create buffer, using 32-bit integers when handling transparency
83 | buffer = (transparency) ? new Uint32Array(width * height) : new Uint16Array(width * height);
84 | // fill buffer with default attributes
85 | buffer.fill(cell(0));
86 | metadata = [];
87 | // process data in a multiple chunks
88 | const animationSpeed = modemSpeed / 10 / 1000;
89 | const chunks = [];
90 | let i = 0;
91 | if (initialStatus.position > 0) {
92 | // add initial chunk
93 | i = Math.floor(initialStatus.position * chars.length);
94 | chunks.push(chars.subarray(0, i));
95 | }
96 | if (initialStatus.playing) {
97 | // add remaining chunks
98 | const chunkLength = Math.floor(animationSpeed * frameDuration);
99 | while (i < chars.length) {
100 | chunks.push(chars.subarray(i, i + chunkLength));
101 | i += chunkLength;
102 | }
103 | }
104 | let processed = 0;
105 | for (const [ index, chunk ] of chunks.entries()) {
106 | chunk.map(processCharacter);
107 | // time to output what's held in the screen buffer to the hook consumer,
108 | // consolidating characters with identical attributes into segments
109 | const lines = scanBuffer();
110 | // calculate status
111 | processed += chunk.length;
112 | const playing = (index !== chunks.length - 1);
113 | const position = processed / chars.length;
114 | const status = { position, playing };
115 | state = { width, height, blinking, blinked, lines, willBlink, status, metadata, error };
116 | if (!initialized) {
117 | // initialize with real contents
118 | initial(state);
119 | initialized = true;
120 | await mount();
121 | } else {
122 | yield state;
123 | }
124 | if (playing) {
125 | // wait for frame to end
126 | await delay(frameDuration, { signal });
127 | if (blinking) {
128 | // update blink states
129 | blinkFramesRemaining--;
130 | if (blinkFramesRemaining === 0) {
131 | blinked = !blinked;
132 | blinkFramesRemaining = blinkFrameCount;
133 | }
134 | }
135 | }
136 | }
137 | data = chars = buffer = null;
138 |
139 | // go into an endless loop if there's blinking text (unless blinking is just truthy and not true)
140 | if (state.willBlink && blinking === true) {
141 | // wait out the remaining blink period
142 | await delay(frameDuration * blinkFramesRemaining, { signal });
143 | for (;;) {
144 | blinked = !blinked;
145 | yield { ...state, blinked };
146 | await delay(blinkDuration, { signal });
147 | }
148 | }
149 | }
150 |
151 | // --- helper functions below ----
152 |
153 | function cell(c) {
154 | // pack text attributes and codepoint into 16-bit cell
155 | return (bgColor << 8) | (fgColor << 12) | c | transparencyFlags;
156 | }
157 |
158 | function setCharacter(c) {
159 | if (cursorY >= maxHeight) {
160 | if (buffer) {
161 | // scroll up
162 | processCommand('S', `${cursorY - maxHeight + 1}`);
163 | }
164 | cursorY = maxHeight - 1;
165 | }
166 | if (!buffer) {
167 | if (cursorX > maxCursorX) {
168 | maxCursorX = cursorX;
169 | }
170 | if (cursorY > maxCursorY) {
171 | maxCursorY = cursorY;
172 | }
173 | } else {
174 | buffer[cursorY * width + cursorX] = cell(c);
175 | }
176 | cursorX++;
177 | if (cursorX >= maxWidth) {
178 | cursorX = 0;
179 | cursorY++;
180 | }
181 | }
182 |
183 | function parseOne(text, def1) {
184 | return (text) ? parseInt(text) : def1;
185 | }
186 |
187 | function parseTwo(text, def1, def2) {
188 | const parts = text.split(';');
189 | return [ parseOne(parts[0], def1), parseOne(parts[1], def2) ];
190 | }
191 |
192 | function parseMultiple(text, def) {
193 | const parts = text.split(';');
194 | return parts.map(p => parseOne(p, def));
195 | }
196 |
197 | function processCharacter(c) {
198 | if (escapeSeq) {
199 | if (escapeSeq.length === 1) {
200 | escapeSeq.push(c);
201 | if (c !== 0x5b) {
202 | // invalid sequence
203 | for (const c of escapeSeq) {
204 | setCharacter(c);
205 | }
206 | escapeSeq = null;
207 | }
208 | } else {
209 | if (c >= 0x40 && c <= 0x7e) {
210 | // @ to ~
211 | const cmd = cp437Chars[c];
212 | const params = escapeSeq.slice(2).map(c => cp437Chars[c]).join('');
213 | processCommand(cmd, params);
214 | escapeSeq = null;
215 | } else {
216 | escapeSeq.push(c);
217 | }
218 | }
219 | } else if (!eof) {
220 | if (c === 0x07) {
221 | beep?.();
222 | } else if (c === 0x08) {
223 | // backspace
224 | cursorX--;
225 | if (cursorX < 0) {
226 | cursorX = 0;
227 | }
228 | } else if (c === 0x09) {
229 | // tabs
230 | cursorX = ((cursorX >> 3) << 3) + 8;
231 | } else if (c === 0x0a) {
232 | // linefeed
233 | cursorY++;
234 | } else if (c === 0x0c) {
235 | // clear screen
236 | processCommand('J', 2);
237 | } else if (c === 0x0d) {
238 | // carriage return
239 | cursorX = 0;
240 | } else if (c === 0x1a) {
241 | eof = true;
242 | } else if (c === 0x1b) {
243 | escapeSeq = [ c ];
244 | } else {
245 | setCharacter(c);
246 | }
247 | } else {
248 | // metadata
249 | if (metadata) {
250 | if (c === 0 || c === 0x1a) {
251 | if (metaString) {
252 | metadata.push(metaString);
253 | metaString = '';
254 | }
255 | } else {
256 | metaString += cp437Chars[c];
257 | }
258 | }
259 | }
260 | }
261 |
262 | function processCommand(cmd, params = '') {
263 | if (cmd === 'A') {
264 | const count = parseOne(params, 1);
265 | cursorY -= count;
266 | if (cursorY < 0) {
267 | cursorY = 0;
268 | }
269 | } else if (cmd === 'B') {
270 | const count = parseOne(params, 1);
271 | cursorY += count;
272 | if (cursorY >= maxHeight) {
273 | cursorY = maxHeight - 1;
274 | }
275 | } else if (cmd === 'C') {
276 | const count = parseOne(params, 1);
277 | cursorX += count;
278 | if (cursorX >= maxWidth) {
279 | cursorX = maxWidth - 1;
280 | }
281 | } else if (cmd === 'D') {
282 | const count = parseOne(params, 1);
283 | cursorX -= count;
284 | if (cursorX < 0) {
285 | cursorX = 0;
286 | }
287 | } else if (cmd === 'H' || cmd === 'f') {
288 | const [ row, col ] = parseTwo(params, 1, 1);
289 | cursorX = Math.min(col, maxWidth) - 1;
290 | cursorY = Math.min(row, maxHeight)- 1;
291 | } else if (cmd === 'J') {
292 | // clear screen
293 | const mode = parseOne(params, 0);
294 | if (buffer) {
295 | let start, end;
296 | if (mode === 0) {
297 | start = cursorY * width + cursorX;
298 | end = width * height;
299 | } else if (mode === 1) {
300 | start = 0;
301 | end = cursorY * width + cursorX;
302 | } else if (mode === 2) {
303 | start = 0;
304 | end = width * height;
305 | }
306 | buffer.fill(cell(0), start, end);;
307 | }
308 | if (mode === 2) {
309 | cursorX = 0;
310 | cursorY = 0;
311 | }
312 | } else if (cmd === 'K') {
313 | // clear line to end
314 | const mode = parseOne(params, 0);
315 | if (buffer) {
316 | let start, end;
317 | if (mode === 0) {
318 | start = cursorY * width + cursorX;
319 | end = (cursorY + 1) * width;
320 | } else if (mode === 1) {
321 | start = cursorY * width;
322 | end = start + cursorX;
323 | } else if (mode === 2) {
324 | start = cursorY * width;
325 | end = start + width;
326 | }
327 | buffer.fill(cell(0), start, end);
328 | }
329 | } else if (cmd === 'L') {
330 | // insert line
331 | const count = parseOne(params, 1);
332 | if (buffer) {
333 | const target = (cursorY + count) * width;
334 | const source = cursorY * width;
335 | buffer.copyWithin(target, source);
336 | const start = source;
337 | const end = target;
338 | buffer.fill(cell(0), start, end);
339 | }
340 | if (cursorY <= maxCursorY) {
341 | maxCursorY += count;
342 | }
343 | } else if (cmd === 'M') {
344 | // delete line
345 | const count = parseOne(params, 1);
346 | if (buffer) {
347 | const target = cursorY * width;
348 | const source = (cursorY + count) * width;
349 | buffer.copyWithin(target, source);
350 | const start = source;
351 | const end = width * height;
352 | buffer.fill(cell(0), start, end);
353 | }
354 | } else if (cmd === 'P') {
355 | // delete characters
356 | const count = parseOne(params, 1);
357 | if (buffer) {
358 | const target = cursorY * width + cursorX;
359 | const source = target + count;
360 | const last = (cursorY + 1) * width;
361 | buffer.copyWithin(target, source, last);
362 | const start = last - count;
363 | const end = last;
364 | buffer.fill(cell(0), start, end);
365 | }
366 | } else if (cmd === 'S') {
367 | // scroll up
368 | const count = parseOne(params, 1);
369 | if (buffer) {
370 | const target = 0;
371 | const source = count * width;
372 | buffer.copyWithin(target, source);
373 | const start = width * (height - count);
374 | const end = width * height;
375 | buffer.fill(cell(0), start, end);
376 | }
377 | } else if (cmd === 'T') {
378 | // scroll down
379 | const count = parseOne(params, 1);
380 | if (buffer) {
381 | const target = count * width;
382 | const source = 0;
383 | buffer.copyWithin(target, source);
384 | const start = 0;
385 | const end = count * width;
386 | buffer.fill(cell(0), start, end);
387 | }
388 | } else if (cmd === 'X') {
389 | // clear characters
390 | const count = parseOne(params, 1);
391 | if (buffer) {
392 | const start = cursorY * width + cursorX;
393 | const end = Math.min(start + count, (cursorY + 1) * width);
394 | buffer.fill(cell(0), start, end);
395 | }
396 | } else if (cmd === 'm') {
397 | // modify text properties
398 | const modifiers = parseMultiple(params, 0);
399 | for (const m of modifiers) {
400 | if (m === 0) {
401 | fgBright = false;
402 | bgBright = false;
403 | fgColorBase = 7;
404 | bgColorBase = 0;
405 | fgSet = false;
406 | bgSet = false;
407 | } else if (m === 1) {
408 | fgBright = true;
409 | } else if (m === 2 || m === 22) {
410 | fgBright = false;
411 | } else if (m === 5 || m === 6) {
412 | bgBright = true;
413 | } else if (m === 7) {
414 | const fgColorBefore = fgColorBase;
415 | fgColorBase = bgColorBase;
416 | bgColorBase = fgColorBefore;
417 | fgSet = true;
418 | bgSet = true;
419 | } else if (m === 8) {
420 | fgColorBase = bgColorBase;
421 | fgSet = true;
422 | } else if (m === 25) {
423 | bgBright = false;
424 | } else if (m >= 30 && m <= 37) {
425 | fgColorBase = m - 30;
426 | fgSet = true;
427 | } else if (m >= 40 && m <= 47) {
428 | bgColorBase = m - 40;
429 | bgSet = true;
430 | }
431 | }
432 | fgColor = fgColorBase + (fgBright ? 8 : 0);
433 | bgColor = bgColorBase + (bgBright ? 8 : 0);
434 | if (transparency) {
435 | transparencyFlags = (fgSet ? 0x00010000 : 0) | (bgSet ? 0x00020000 : 0);
436 | }
437 | } else if (cmd === 's') {
438 | savedCursorX = cursorX;
439 | savedCursorY = cursorY;
440 | } else if (cmd === 'u') {
441 | cursorX = savedCursorX;
442 | cursorY = savedCursorY;
443 | }
444 | }
445 |
446 | function scanBuffer() {
447 | const lines = [];
448 | const blinkMask = (blinking) ? 0x0800 : 0x0000;
449 | const bgColorMask = (blinking) ? 0x0700 : 0x0F00;
450 | const fgColorMask = 0xF000;
451 | const fgMask = 0x00010000;
452 | const bgMask = 0x00020000;
453 | for (let row = 0; row < height; row++) {
454 | const segments = [];
455 | const first = row * width;
456 | const last = first + width;
457 | let attr = 0x00FF; // invalid attributes
458 | let text = '';
459 | // find where there's a change in attributes
460 | for (let i = first; i < last; i++) {
461 | const cp = buffer[i] & 0x00FF;
462 | const newAttr = buffer[i] & 0x000FFF00;
463 | if (attr !== newAttr) {
464 | // add preceding text
465 | if (text.length > 0) {
466 | segments.push({ attr, text });
467 | }
468 | attr = newAttr;
469 | text = '';
470 | }
471 | // map codepoint 0 to space
472 | text += cp437Chars[cp || 0x20];
473 | }
474 | // add leftover at end of line
475 | if (text.length > 0) {
476 | segments.push({ attr, text });
477 | }
478 | const line = [];
479 | for (const { attr, text } of segments) {
480 | const blink = (attr & blinkMask) !== 0;
481 | const fgColor = (!transparency || (attr & fgMask)) ? (attr & fgColorMask) >> 12 : undefined;
482 | const bgColor = (!transparency || (attr & bgMask)) ? (attr & bgColorMask) >> 8 : undefined;
483 | line.push({ text, fgColor, bgColor, blink });
484 | willBlink = willBlink || blink;
485 | }
486 | lines.push(line);
487 | }
488 | return lines;
489 | }
490 | }
491 | if (!initialized) {
492 | // the data source was empty--initialize with empty screen
493 | initial(state);
494 | }
495 | }, [ dataSource, modemSpeed, frameDuration, blinkDuration, blinking, minWidth, minHeight, maxWidth, maxHeight, transparency, initialStatus, beep ]);
496 | // saving handlers into a ref so we don't trigger useEffect when they're different
497 | const handlerRef = useRef();
498 | handlerRef.current = { onStatus, onMetadata, onError };
499 | // relay events to event handlers
500 | const { status, metadata, error } = state;
501 | useEffect(() => {
502 | handlerRef.current.onStatus?.(status);
503 | }, [ status ]);
504 | useEffect(() => {
505 | handlerRef.current.onMetadata?.(metadata);
506 | }, [ metadata ]);
507 | useEffect(() => {
508 | if (error) {
509 | handlerRef.current.onError?.(error);
510 | }
511 | }, [ error ]);
512 | return state;
513 | }
514 |
515 |
--------------------------------------------------------------------------------