@nghyane/arcane-tui 0.1.0
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/CHANGELOG.md +3 -0
- package/README.md +704 -0
- package/package.json +72 -0
- package/src/autocomplete.ts +772 -0
- package/src/buffer/ansi-parser.ts +349 -0
- package/src/buffer/buffer.ts +120 -0
- package/src/buffer/cell.ts +103 -0
- package/src/buffer/index.ts +16 -0
- package/src/buffer/render.ts +149 -0
- package/src/components/box.ts +144 -0
- package/src/components/cancellable-loader.ts +39 -0
- package/src/components/editor.ts +2289 -0
- package/src/components/image.ts +86 -0
- package/src/components/input.ts +531 -0
- package/src/components/loader.ts +59 -0
- package/src/components/markdown.ts +858 -0
- package/src/components/select-list.ts +198 -0
- package/src/components/settings-list.ts +194 -0
- package/src/components/spacer.ts +28 -0
- package/src/components/tab-bar.ts +142 -0
- package/src/components/text.ts +110 -0
- package/src/components/truncated-text.ts +61 -0
- package/src/editor-component.ts +71 -0
- package/src/fuzzy.ts +143 -0
- package/src/index.ts +69 -0
- package/src/keybindings.ts +197 -0
- package/src/keys.ts +270 -0
- package/src/kill-ring.ts +46 -0
- package/src/mermaid.ts +140 -0
- package/src/stdin-buffer.ts +385 -0
- package/src/symbols.ts +24 -0
- package/src/terminal-capabilities.ts +393 -0
- package/src/terminal.ts +467 -0
- package/src/ttyid.ts +66 -0
- package/src/tui.ts +1134 -0
- package/src/utils.ts +149 -0
package/src/keys.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard input handling for terminal applications.
|
|
3
|
+
*
|
|
4
|
+
* Supports both legacy terminal sequences and Kitty keyboard protocol.
|
|
5
|
+
* See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
6
|
+
* Reference: https://github.com/sst/opentui/blob/7da92b4088aebfe27b9f691c04163a48821e49fd/packages/core/src/lib/parse.keypress.ts
|
|
7
|
+
*
|
|
8
|
+
* Symbol keys are also supported, however some ctrl+symbol combos
|
|
9
|
+
* overlap with ASCII codes, e.g. ctrl+[ = ESC.
|
|
10
|
+
* See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-ctrl-mapping-of-ascii-keys
|
|
11
|
+
* Those can still be * used for ctrl+shift combos
|
|
12
|
+
*
|
|
13
|
+
* API:
|
|
14
|
+
* - matchesKey(data, keyId) - Check if input matches a key identifier
|
|
15
|
+
* - parseKey(data) - Parse input and return the key identifier
|
|
16
|
+
* - Key - Helper object for creating typed key identifiers
|
|
17
|
+
* - setKittyProtocolActive(active) - Set global Kitty protocol state
|
|
18
|
+
* - isKittyProtocolActive() - Query global Kitty protocol state
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
type KeyEventType,
|
|
23
|
+
matchesKey as matchesKeyNative,
|
|
24
|
+
parseKey as parseKeyNative,
|
|
25
|
+
parseKittySequence as parseKittySequenceNative,
|
|
26
|
+
} from "@nghyane/arcane-natives";
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Global Kitty Protocol State
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
let kittyProtocolActive = false;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Set the global Kitty keyboard protocol state.
|
|
36
|
+
* Called by ProcessTerminal after detecting protocol support.
|
|
37
|
+
*/
|
|
38
|
+
export function setKittyProtocolActive(active: boolean): void {
|
|
39
|
+
kittyProtocolActive = active;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Query whether Kitty keyboard protocol is currently active.
|
|
44
|
+
*/
|
|
45
|
+
export function isKittyProtocolActive(): boolean {
|
|
46
|
+
return kittyProtocolActive;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// Type-Safe Key Identifiers
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
type Letter =
|
|
54
|
+
| "a"
|
|
55
|
+
| "b"
|
|
56
|
+
| "c"
|
|
57
|
+
| "d"
|
|
58
|
+
| "e"
|
|
59
|
+
| "f"
|
|
60
|
+
| "g"
|
|
61
|
+
| "h"
|
|
62
|
+
| "i"
|
|
63
|
+
| "j"
|
|
64
|
+
| "k"
|
|
65
|
+
| "l"
|
|
66
|
+
| "m"
|
|
67
|
+
| "n"
|
|
68
|
+
| "o"
|
|
69
|
+
| "p"
|
|
70
|
+
| "q"
|
|
71
|
+
| "r"
|
|
72
|
+
| "s"
|
|
73
|
+
| "t"
|
|
74
|
+
| "u"
|
|
75
|
+
| "v"
|
|
76
|
+
| "w"
|
|
77
|
+
| "x"
|
|
78
|
+
| "y"
|
|
79
|
+
| "z";
|
|
80
|
+
|
|
81
|
+
type SymbolKey =
|
|
82
|
+
| "`"
|
|
83
|
+
| "-"
|
|
84
|
+
| "="
|
|
85
|
+
| "["
|
|
86
|
+
| "]"
|
|
87
|
+
| "\\"
|
|
88
|
+
| ";"
|
|
89
|
+
| "'"
|
|
90
|
+
| ","
|
|
91
|
+
| "."
|
|
92
|
+
| "/"
|
|
93
|
+
| "!"
|
|
94
|
+
| "@"
|
|
95
|
+
| "#"
|
|
96
|
+
| "$"
|
|
97
|
+
| "%"
|
|
98
|
+
| "^"
|
|
99
|
+
| "&"
|
|
100
|
+
| "*"
|
|
101
|
+
| "("
|
|
102
|
+
| ")"
|
|
103
|
+
| "_"
|
|
104
|
+
| "+"
|
|
105
|
+
| "|"
|
|
106
|
+
| "~"
|
|
107
|
+
| "{"
|
|
108
|
+
| "}"
|
|
109
|
+
| ":"
|
|
110
|
+
| "<"
|
|
111
|
+
| ">"
|
|
112
|
+
| "?";
|
|
113
|
+
|
|
114
|
+
type SpecialKey =
|
|
115
|
+
| "escape"
|
|
116
|
+
| "esc"
|
|
117
|
+
| "enter"
|
|
118
|
+
| "return"
|
|
119
|
+
| "tab"
|
|
120
|
+
| "space"
|
|
121
|
+
| "backspace"
|
|
122
|
+
| "delete"
|
|
123
|
+
| "insert"
|
|
124
|
+
| "clear"
|
|
125
|
+
| "home"
|
|
126
|
+
| "end"
|
|
127
|
+
| "pageUp"
|
|
128
|
+
| "pageDown"
|
|
129
|
+
| "up"
|
|
130
|
+
| "down"
|
|
131
|
+
| "left"
|
|
132
|
+
| "right"
|
|
133
|
+
| "f1"
|
|
134
|
+
| "f2"
|
|
135
|
+
| "f3"
|
|
136
|
+
| "f4"
|
|
137
|
+
| "f5"
|
|
138
|
+
| "f6"
|
|
139
|
+
| "f7"
|
|
140
|
+
| "f8"
|
|
141
|
+
| "f9"
|
|
142
|
+
| "f10"
|
|
143
|
+
| "f11"
|
|
144
|
+
| "f12";
|
|
145
|
+
|
|
146
|
+
type BaseKey = Letter | SymbolKey | SpecialKey;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Union type of all valid key identifiers.
|
|
150
|
+
* Provides autocomplete and catches typos at compile time.
|
|
151
|
+
*/
|
|
152
|
+
export type KeyId =
|
|
153
|
+
| BaseKey
|
|
154
|
+
| `ctrl+${BaseKey}`
|
|
155
|
+
| `shift+${BaseKey}`
|
|
156
|
+
| `alt+${BaseKey}`
|
|
157
|
+
| `ctrl+shift+${BaseKey}`
|
|
158
|
+
| `shift+ctrl+${BaseKey}`
|
|
159
|
+
| `ctrl+alt+${BaseKey}`
|
|
160
|
+
| `alt+ctrl+${BaseKey}`
|
|
161
|
+
| `shift+alt+${BaseKey}`
|
|
162
|
+
| `alt+shift+${BaseKey}`
|
|
163
|
+
| `ctrl+shift+alt+${BaseKey}`
|
|
164
|
+
| `ctrl+alt+shift+${BaseKey}`
|
|
165
|
+
| `shift+ctrl+alt+${BaseKey}`
|
|
166
|
+
| `shift+alt+ctrl+${BaseKey}`
|
|
167
|
+
| `alt+ctrl+shift+${BaseKey}`
|
|
168
|
+
| `alt+shift+ctrl+${BaseKey}`;
|
|
169
|
+
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// Kitty Protocol Parsing
|
|
172
|
+
// =============================================================================
|
|
173
|
+
|
|
174
|
+
interface ParsedKittySequence {
|
|
175
|
+
codepoint: number;
|
|
176
|
+
shiftedKey?: number; // Shifted version of the key (when shift is pressed)
|
|
177
|
+
baseLayoutKey?: number; // Key in standard PC-101 layout (for non-Latin layouts)
|
|
178
|
+
modifier: number;
|
|
179
|
+
eventType?: KeyEventType;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Regex for Kitty protocol event type detection
|
|
183
|
+
// Matches CSI sequences with :2 (repeat) or :3 (release) event type
|
|
184
|
+
// Format: \x1b[...;modifier:event_type<terminator> where terminator is u, ~, or A-F/H
|
|
185
|
+
const KITTY_RELEASE_PATTERN = /^\x1b\[[\d:;]*:3[u~ABCDHF]$/;
|
|
186
|
+
const KITTY_REPEAT_PATTERN = /^\x1b\[[\d:;]*:2[u~ABCDHF]$/;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if the input is a key release event.
|
|
190
|
+
* Only meaningful when Kitty keyboard protocol with flag 2 is active.
|
|
191
|
+
* Returns false if Kitty protocol is not active.
|
|
192
|
+
*/
|
|
193
|
+
export function isKeyRelease(data: string): boolean {
|
|
194
|
+
// Only detect release events when Kitty protocol is active
|
|
195
|
+
if (!kittyProtocolActive) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Don't treat bracketed paste content as key release
|
|
200
|
+
if (data.includes("\x1b[200~")) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Match the full CSI sequence pattern for release events
|
|
205
|
+
return KITTY_RELEASE_PATTERN.test(data);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check if the input is a key repeat event.
|
|
210
|
+
* Only meaningful when Kitty keyboard protocol with flag 2 is active.
|
|
211
|
+
* Returns false if Kitty protocol is not active.
|
|
212
|
+
*/
|
|
213
|
+
export function isKeyRepeat(data: string): boolean {
|
|
214
|
+
// Only detect repeat events when Kitty protocol is active
|
|
215
|
+
if (!kittyProtocolActive) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Don't treat bracketed paste content as key repeat
|
|
220
|
+
if (data.includes("\x1b[200~")) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Match the full CSI sequence pattern for repeat events
|
|
225
|
+
return KITTY_REPEAT_PATTERN.test(data);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function parseKittySequence(data: string): ParsedKittySequence | null {
|
|
229
|
+
const result = parseKittySequenceNative(data);
|
|
230
|
+
if (!result) return null;
|
|
231
|
+
return {
|
|
232
|
+
codepoint: result.codepoint,
|
|
233
|
+
shiftedKey: result.shiftedKey ?? undefined,
|
|
234
|
+
baseLayoutKey: result.baseLayoutKey ?? undefined,
|
|
235
|
+
modifier: result.modifier,
|
|
236
|
+
eventType: result.eventType,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Match input data against a key identifier string.
|
|
242
|
+
*
|
|
243
|
+
* Supported key identifiers:
|
|
244
|
+
* - Single keys: "escape", "tab", "enter", "backspace", "delete", "home", "end", "space"
|
|
245
|
+
* - Arrow keys: "up", "down", "left", "right"
|
|
246
|
+
* - Ctrl combinations: "ctrl+c", "ctrl+z", etc.
|
|
247
|
+
* - Shift combinations: "shift+tab", "shift+enter"
|
|
248
|
+
* - Alt combinations: "alt+enter", "alt+backspace"
|
|
249
|
+
* - Combined modifiers: "shift+ctrl+p", "ctrl+alt+x"
|
|
250
|
+
*
|
|
251
|
+
* Use the Key helper for autocomplete: Key.ctrl("c"), Key.escape, Key.ctrlShift("p")
|
|
252
|
+
*
|
|
253
|
+
* @param data - Raw input data from terminal
|
|
254
|
+
* @param keyId - Key identifier (e.g., "ctrl+c", "escape", Key.ctrl("c"))
|
|
255
|
+
*/
|
|
256
|
+
export function matchesKey(data: string, keyId: KeyId): boolean {
|
|
257
|
+
return matchesKeyNative(data, keyId, kittyProtocolActive);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Parse terminal input and return a normalized key identifier.
|
|
262
|
+
*
|
|
263
|
+
* Returns key names like "escape", "ctrl+c", "shift+tab", "alt+enter".
|
|
264
|
+
* Returns undefined if the input is not a recognized key sequence.
|
|
265
|
+
*
|
|
266
|
+
* @param data - Raw input data from terminal
|
|
267
|
+
*/
|
|
268
|
+
export function parseKey(data: string): string | undefined {
|
|
269
|
+
return parseKeyNative(data, kittyProtocolActive) ?? undefined;
|
|
270
|
+
}
|
package/src/kill-ring.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ring buffer for Emacs-style kill/yank operations.
|
|
3
|
+
*
|
|
4
|
+
* Tracks killed (deleted) text entries. Consecutive kills can accumulate
|
|
5
|
+
* into a single entry. Supports yank (paste most recent) and yank-pop
|
|
6
|
+
* (cycle through older entries).
|
|
7
|
+
*/
|
|
8
|
+
export class KillRing {
|
|
9
|
+
#ring: string[] = [];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Add text to the kill ring.
|
|
13
|
+
*
|
|
14
|
+
* @param text - The killed text to add
|
|
15
|
+
* @param opts - Push options
|
|
16
|
+
* @param opts.prepend - If accumulating, prepend (backward deletion) or append (forward deletion)
|
|
17
|
+
* @param opts.accumulate - Merge with the most recent entry instead of creating a new one
|
|
18
|
+
*/
|
|
19
|
+
push(text: string, opts: { prepend: boolean; accumulate?: boolean }): void {
|
|
20
|
+
if (!text) return;
|
|
21
|
+
|
|
22
|
+
if (opts.accumulate && this.#ring.length > 0) {
|
|
23
|
+
const last = this.#ring.pop()!;
|
|
24
|
+
this.#ring.push(opts.prepend ? text + last : last + text);
|
|
25
|
+
} else {
|
|
26
|
+
this.#ring.push(text);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Get most recent entry without modifying the ring. */
|
|
31
|
+
peek(): string | undefined {
|
|
32
|
+
return this.#ring.length > 0 ? this.#ring[this.#ring.length - 1] : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Move last entry to front (for yank-pop cycling). */
|
|
36
|
+
rotate(): void {
|
|
37
|
+
if (this.#ring.length > 1) {
|
|
38
|
+
const last = this.#ring.pop()!;
|
|
39
|
+
this.#ring.unshift(last);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get length(): number {
|
|
44
|
+
return this.#ring.length;
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/mermaid.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { $ } from "bun";
|
|
5
|
+
|
|
6
|
+
export interface MermaidImage {
|
|
7
|
+
base64: string;
|
|
8
|
+
widthPx: number;
|
|
9
|
+
heightPx: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MermaidRenderOptions {
|
|
13
|
+
theme?: "default" | "dark" | "forest" | "neutral";
|
|
14
|
+
backgroundColor?: string;
|
|
15
|
+
width?: number;
|
|
16
|
+
scale?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Render mermaid diagram source to PNG.
|
|
21
|
+
*
|
|
22
|
+
* Uses `mmdc` (mermaid-cli) which must be installed and in PATH.
|
|
23
|
+
* Returns null if rendering fails or mmdc is unavailable.
|
|
24
|
+
*/
|
|
25
|
+
export async function renderMermaidToPng(
|
|
26
|
+
source: string,
|
|
27
|
+
options: MermaidRenderOptions = {},
|
|
28
|
+
): Promise<MermaidImage | null> {
|
|
29
|
+
const mmdc = Bun.which("mmdc");
|
|
30
|
+
if (!mmdc) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const tmpDir = path.join(os.tmpdir(), `mermaid-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
35
|
+
const inputPath = path.join(tmpDir, "input.mmd");
|
|
36
|
+
const outputPath = path.join(tmpDir, "output.png");
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await Bun.write(inputPath, source);
|
|
40
|
+
|
|
41
|
+
const args: string[] = ["-i", inputPath, "-o", outputPath, "-q"];
|
|
42
|
+
|
|
43
|
+
if (options.theme) {
|
|
44
|
+
args.push("-t", options.theme);
|
|
45
|
+
}
|
|
46
|
+
if (options.backgroundColor) {
|
|
47
|
+
args.push("-b", options.backgroundColor);
|
|
48
|
+
}
|
|
49
|
+
if (options.width) {
|
|
50
|
+
args.push("-w", String(options.width));
|
|
51
|
+
}
|
|
52
|
+
if (options.scale) {
|
|
53
|
+
args.push("-s", String(options.scale));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const result = await $`${mmdc} ${args}`.quiet().nothrow();
|
|
57
|
+
if (result.exitCode !== 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const outputFile = Bun.file(outputPath);
|
|
62
|
+
if (!(await outputFile.exists())) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const buffer = await outputFile.bytes();
|
|
67
|
+
const base64 = buffer.toBase64();
|
|
68
|
+
|
|
69
|
+
const dims = parsePngDimensions(buffer);
|
|
70
|
+
if (!dims) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
base64,
|
|
76
|
+
widthPx: dims.width,
|
|
77
|
+
heightPx: dims.height,
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
} finally {
|
|
82
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parsePngDimensions(buffer: Uint8Array): { width: number; height: number } | null {
|
|
87
|
+
if (buffer.length < 24) return null;
|
|
88
|
+
if (buffer[0] !== 0x89 || buffer[1] !== 0x50 || buffer[2] !== 0x4e || buffer[3] !== 0x47) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
92
|
+
return {
|
|
93
|
+
width: view.getUint32(16, false),
|
|
94
|
+
height: view.getUint32(20, false),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extract mermaid code blocks from markdown text.
|
|
100
|
+
* Returns array of { source, startIndex, endIndex } for each block.
|
|
101
|
+
*/
|
|
102
|
+
export function extractMermaidBlocks(markdown: string): { source: string; hash: string }[] {
|
|
103
|
+
const blocks: { source: string; hash: string }[] = [];
|
|
104
|
+
const regex = /```mermaid\s*\n([\s\S]*?)```/g;
|
|
105
|
+
|
|
106
|
+
for (let match = regex.exec(markdown); match !== null; match = regex.exec(markdown)) {
|
|
107
|
+
const source = match[1].trim();
|
|
108
|
+
const hash = Bun.hash(source).toString(16);
|
|
109
|
+
blocks.push({ source, hash });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return blocks;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Pre-render all mermaid blocks in markdown text.
|
|
117
|
+
* Returns a cache map: hash → MermaidImage.
|
|
118
|
+
*/
|
|
119
|
+
export async function prerenderMermaidBlocks(
|
|
120
|
+
markdown: string,
|
|
121
|
+
options: MermaidRenderOptions = {},
|
|
122
|
+
): Promise<Map<string, MermaidImage>> {
|
|
123
|
+
const blocks = extractMermaidBlocks(markdown);
|
|
124
|
+
const cache = new Map<string, MermaidImage>();
|
|
125
|
+
|
|
126
|
+
const results = await Promise.all(
|
|
127
|
+
blocks.map(async ({ source, hash }) => {
|
|
128
|
+
const image = await renderMermaidToPng(source, options);
|
|
129
|
+
return { hash, image };
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
for (const { hash, image } of results) {
|
|
134
|
+
if (image) {
|
|
135
|
+
cache.set(hash, image);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return cache;
|
|
140
|
+
}
|