@xxmachina/components 19.21.8 → 19.25.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/extras/flow/index.d.ts +13 -3
- package/extras/flow/index.d.ts.map +1 -1
- package/features/query/index.d.ts.map +1 -1
- package/fesm2022/xxmachina-components-extras-flow.mjs +110 -11
- package/fesm2022/xxmachina-components-extras-flow.mjs.map +1 -1
- package/fesm2022/xxmachina-components-groups-query-form.mjs.map +1 -1
- package/fesm2022/xxmachina-components-molecules-inline-edit-field.mjs +117 -0
- package/fesm2022/xxmachina-components-molecules-inline-edit-field.mjs.map +1 -0
- package/fesm2022/xxmachina-components-molecules-weekly-header.mjs +2 -2
- package/fesm2022/xxmachina-components-molecules-weekly-header.mjs.map +1 -1
- package/fesm2022/xxmachina-components-organisms-calendar-section.mjs +2 -2
- package/fesm2022/xxmachina-components-organisms-calendar-section.mjs.map +1 -1
- package/fesm2022/xxmachina-components-organisms-terminal-input-section.mjs +19 -4
- package/fesm2022/xxmachina-components-organisms-terminal-input-section.mjs.map +1 -1
- package/fesm2022/xxmachina-components-organisms-xterm.mjs +849 -49
- package/fesm2022/xxmachina-components-organisms-xterm.mjs.map +1 -1
- package/fesm2022/xxmachina-components-pages-command-harness.mjs +28 -0
- package/fesm2022/xxmachina-components-pages-command-harness.mjs.map +1 -0
- package/fesm2022/xxmachina-components-pages-command.mjs +10 -6
- package/fesm2022/xxmachina-components-pages-command.mjs.map +1 -1
- package/fesm2022/xxmachina-components-pages-query.mjs +2 -2
- package/fesm2022/xxmachina-components-pages-query.mjs.map +1 -1
- package/fesm2022/xxmachina-components-pages-thread.mjs +2 -2
- package/fesm2022/xxmachina-components-pages-thread.mjs.map +1 -1
- package/fesm2022/xxmachina-components-services-message.mjs.map +1 -1
- package/fesm2022/xxmachina-components-templates-agent.mjs +151 -123
- package/fesm2022/xxmachina-components-templates-agent.mjs.map +1 -1
- package/fesm2022/xxmachina-components-templates-background.mjs +376 -242
- package/fesm2022/xxmachina-components-templates-background.mjs.map +1 -1
- package/fesm2022/xxmachina-components-templates-flow-nodes-group.mjs +164 -0
- package/fesm2022/xxmachina-components-templates-flow-nodes-group.mjs.map +1 -0
- package/fesm2022/xxmachina-components-templates-flow-nodes-issue.mjs +157 -0
- package/fesm2022/xxmachina-components-templates-flow-nodes-issue.mjs.map +1 -0
- package/fesm2022/xxmachina-components-templates-flow-nodes-task.mjs +154 -0
- package/fesm2022/xxmachina-components-templates-flow-nodes-task.mjs.map +1 -0
- package/fesm2022/xxmachina-components-templates-flow.mjs +337 -0
- package/fesm2022/xxmachina-components-templates-flow.mjs.map +1 -0
- package/fesm2022/xxmachina-components.mjs +2 -2
- package/fesm2022/xxmachina-components.mjs.map +1 -1
- package/groups/query-form/index.d.ts +3 -4
- package/groups/query-form/index.d.ts.map +1 -1
- package/index.d.ts.map +1 -1
- package/molecules/inline-edit-field/index.d.ts +32 -0
- package/molecules/inline-edit-field/index.d.ts.map +1 -0
- package/organisms/terminal-input-section/index.d.ts +2 -1
- package/organisms/terminal-input-section/index.d.ts.map +1 -1
- package/organisms/xterm/index.d.ts +176 -4
- package/organisms/xterm/index.d.ts.map +1 -1
- package/package.json +25 -9
- package/pages/command/harness/index.d.ts +14 -0
- package/pages/command/harness/index.d.ts.map +1 -0
- package/pages/command/index.d.ts +12 -4
- package/pages/command/index.d.ts.map +1 -1
- package/pages/query/index.d.ts +2 -2
- package/pages/query/index.d.ts.map +1 -1
- package/pages/query-v2/index.d.ts.map +1 -1
- package/services/command/index.d.ts.map +1 -1
- package/services/message/index.d.ts +3 -3
- package/services/message/index.d.ts.map +1 -1
- package/templates/agent/index.d.ts +11 -2
- package/templates/agent/index.d.ts.map +1 -1
- package/templates/background/index.d.ts +14 -20
- package/templates/background/index.d.ts.map +1 -1
- package/templates/flow/index.d.ts +61 -0
- package/templates/flow/index.d.ts.map +1 -0
- package/templates/flow/nodes/group/index.d.ts +44 -0
- package/templates/flow/nodes/group/index.d.ts.map +1 -0
- package/templates/flow/nodes/issue/index.d.ts +46 -0
- package/templates/flow/nodes/issue/index.d.ts.map +1 -0
- package/templates/flow/nodes/task/index.d.ts +37 -0
- package/templates/flow/nodes/task/index.d.ts.map +1 -0
- package/fesm2022/xxmachina-components-services-calendar.mjs +0 -25
- package/fesm2022/xxmachina-components-services-calendar.mjs.map +0 -1
- package/fesm2022/xxmachina-components-services-schedule.mjs +0 -51
- package/fesm2022/xxmachina-components-services-schedule.mjs.map +0 -1
- package/services/calendar/index.d.ts +0 -14
- package/services/calendar/index.d.ts.map +0 -1
- package/services/schedule/index.d.ts +0 -27
- package/services/schedule/index.d.ts.map +0 -1
|
@@ -1,8 +1,128 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { viewChild, output, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
2
|
+
import { input, Directive, viewChild, inject, ElementRef, output, signal, effect, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
3
|
import { Terminal } from '@xterm/xterm';
|
|
4
4
|
import { FitAddon } from '@xterm/addon-fit';
|
|
5
|
-
import {
|
|
5
|
+
import { InjectableComponent, NgAtomicComponent } from '@ng-atomic/core';
|
|
6
|
+
import { makeDI } from '@ng-atomic/common/services/ui';
|
|
7
|
+
|
|
8
|
+
const URL_REGEX = /https?:\/\/[^\s<>'")\]]+/g;
|
|
9
|
+
const MAX_SCAN_LINES = 30;
|
|
10
|
+
/**
|
|
11
|
+
* Link provider that detects URLs spanning multiple terminal lines.
|
|
12
|
+
*
|
|
13
|
+
* Handles two wrapping scenarios:
|
|
14
|
+
* 1. Terminal wrapping (isWrapped=true) - xterm splits long output
|
|
15
|
+
* 2. Application wrapping (isWrapped=false) - e.g. Claude Code/Ink explicitly
|
|
16
|
+
* breaks lines at some width. Detected by heuristic: previous line ends
|
|
17
|
+
* without whitespace and current line starts without whitespace.
|
|
18
|
+
*/
|
|
19
|
+
class WebLinkProvider {
|
|
20
|
+
_terminal;
|
|
21
|
+
_handler;
|
|
22
|
+
constructor(_terminal, _handler = (_, uri) => {
|
|
23
|
+
window.open(uri, '_blank', 'noopener');
|
|
24
|
+
}) {
|
|
25
|
+
this._terminal = _terminal;
|
|
26
|
+
this._handler = _handler;
|
|
27
|
+
}
|
|
28
|
+
provideLinks(bufferLineNumber, callback) {
|
|
29
|
+
const buffer = this._terminal.buffer.active;
|
|
30
|
+
// Find the start of this line group (0-indexed)
|
|
31
|
+
let startY = bufferLineNumber - 1;
|
|
32
|
+
while (startY > 0
|
|
33
|
+
&& (bufferLineNumber - 1 - startY) < MAX_SCAN_LINES
|
|
34
|
+
&& this._isLineContinuation(startY)) {
|
|
35
|
+
startY--;
|
|
36
|
+
}
|
|
37
|
+
// Collect text from all lines in the group
|
|
38
|
+
const lineTexts = [];
|
|
39
|
+
let y = startY;
|
|
40
|
+
do {
|
|
41
|
+
const line = buffer.getLine(y);
|
|
42
|
+
if (!line)
|
|
43
|
+
break;
|
|
44
|
+
lineTexts.push(line.translateToString(true));
|
|
45
|
+
y++;
|
|
46
|
+
} while (y < buffer.length
|
|
47
|
+
&& (y - startY) < MAX_SCAN_LINES
|
|
48
|
+
&& this._isLineContinuation(y));
|
|
49
|
+
const fullText = lineTexts.join('');
|
|
50
|
+
URL_REGEX.lastIndex = 0;
|
|
51
|
+
let match;
|
|
52
|
+
const links = [];
|
|
53
|
+
while ((match = URL_REGEX.exec(fullText)) !== null) {
|
|
54
|
+
const startPos = this._offsetToPos(match.index, startY, lineTexts);
|
|
55
|
+
const endPos = this._offsetToPos(match.index + match[0].length - 1, startY, lineTexts);
|
|
56
|
+
if (!startPos || !endPos)
|
|
57
|
+
continue;
|
|
58
|
+
links.push({
|
|
59
|
+
text: match[0],
|
|
60
|
+
range: {
|
|
61
|
+
start: { x: startPos.x + 1, y: startPos.y + 1 },
|
|
62
|
+
end: { x: endPos.x + 1, y: endPos.y + 1 },
|
|
63
|
+
},
|
|
64
|
+
activate: (event, text) => this._handler(event, text),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
callback(links.length > 0 ? links : undefined);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Determine if a line is a continuation of the previous line.
|
|
71
|
+
* True when either:
|
|
72
|
+
* - xterm marks it as wrapped (isWrapped=true), OR
|
|
73
|
+
* - no whitespace at line boundary (previous line ends without space,
|
|
74
|
+
* current line starts without space) suggesting mid-token line break
|
|
75
|
+
*/
|
|
76
|
+
_isLineContinuation(lineIdx) {
|
|
77
|
+
const buffer = this._terminal.buffer.active;
|
|
78
|
+
const line = buffer.getLine(lineIdx);
|
|
79
|
+
if (!line)
|
|
80
|
+
return false;
|
|
81
|
+
if (line.isWrapped)
|
|
82
|
+
return true;
|
|
83
|
+
if (lineIdx === 0)
|
|
84
|
+
return false;
|
|
85
|
+
const prevLine = buffer.getLine(lineIdx - 1);
|
|
86
|
+
if (!prevLine)
|
|
87
|
+
return false;
|
|
88
|
+
const prevText = prevLine.translateToString(true);
|
|
89
|
+
const currentText = line.translateToString(true);
|
|
90
|
+
return (prevText.length > 0
|
|
91
|
+
&& !/\s$/.test(prevText)
|
|
92
|
+
&& currentText.length > 0
|
|
93
|
+
&& !/^\s/.test(currentText));
|
|
94
|
+
}
|
|
95
|
+
/** Convert a character offset in the concatenated text to a buffer position. */
|
|
96
|
+
_offsetToPos(offset, startY, lineTexts) {
|
|
97
|
+
let remaining = offset;
|
|
98
|
+
for (let i = 0; i < lineTexts.length; i++) {
|
|
99
|
+
if (remaining < lineTexts[i].length) {
|
|
100
|
+
return { x: remaining, y: startY + i };
|
|
101
|
+
}
|
|
102
|
+
remaining -= lineTexts[i].length;
|
|
103
|
+
}
|
|
104
|
+
const lastIdx = lineTexts.length - 1;
|
|
105
|
+
return { x: lineTexts[lastIdx].length, y: startY + lastIdx };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Drop-in replacement for @xterm/addon-web-links that supports
|
|
111
|
+
* URLs spanning wrapped terminal lines.
|
|
112
|
+
*/
|
|
113
|
+
class WebLinksAddon {
|
|
114
|
+
_handler;
|
|
115
|
+
_linkProvider;
|
|
116
|
+
constructor(_handler) {
|
|
117
|
+
this._handler = _handler;
|
|
118
|
+
}
|
|
119
|
+
activate(terminal) {
|
|
120
|
+
this._linkProvider = terminal.registerLinkProvider(new WebLinkProvider(terminal, this._handler));
|
|
121
|
+
}
|
|
122
|
+
dispose() {
|
|
123
|
+
this._linkProvider?.dispose();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
6
126
|
|
|
7
127
|
class GitHubLinkProvider {
|
|
8
128
|
_terminal;
|
|
@@ -23,13 +143,8 @@ class GitHubLinkProvider {
|
|
|
23
143
|
const text = line.translateToString(true);
|
|
24
144
|
const links = [];
|
|
25
145
|
this.GITHUB_ISSUE_REGEX.lastIndex = 0;
|
|
26
|
-
// Debug: check if text contains potential issue references
|
|
27
|
-
if (text.includes('#')) {
|
|
28
|
-
console.log('[GitHubLinkProvider] Line contains #:', text);
|
|
29
|
-
}
|
|
30
146
|
let match;
|
|
31
147
|
while ((match = this.GITHUB_ISSUE_REGEX.exec(text)) !== null) {
|
|
32
|
-
console.log('[GitHubLinkProvider] Found match:', match[0], 'issue:', match[1]);
|
|
33
148
|
const issueNumber = parseInt(match[1], 10);
|
|
34
149
|
const fullMatch = match[0];
|
|
35
150
|
const issueRef = `#${match[1]}`;
|
|
@@ -84,50 +199,385 @@ class GitHubLinksAddon {
|
|
|
84
199
|
_linkProvider;
|
|
85
200
|
constructor(_handler) {
|
|
86
201
|
this._handler = _handler;
|
|
87
|
-
console.log('[GitHubLinksAddon] constructor called');
|
|
88
202
|
}
|
|
89
203
|
activate(terminal) {
|
|
90
|
-
console.log('[GitHubLinksAddon] activate called');
|
|
91
204
|
this._terminal = terminal;
|
|
92
205
|
this._linkProvider = terminal.registerLinkProvider(new GitHubLinkProvider(terminal, this._handler));
|
|
93
|
-
console.log('[GitHubLinksAddon] linkProvider registered');
|
|
94
206
|
}
|
|
95
207
|
dispose() {
|
|
96
208
|
this._linkProvider?.dispose();
|
|
97
209
|
}
|
|
98
210
|
}
|
|
99
211
|
|
|
100
|
-
class
|
|
212
|
+
class FileLinkProvider {
|
|
213
|
+
_terminal;
|
|
214
|
+
_handler;
|
|
215
|
+
// ファイルパスを検出する正規表現
|
|
216
|
+
// - 絶対パス: /path/to/file.ts, /path/to/file.ts:10:5
|
|
217
|
+
// - 相対パス: ./src/file.ts, ../lib/utils.ts:20
|
|
218
|
+
// - 拡張子を持つファイル名
|
|
219
|
+
FILE_PATH_REGEX = /(?:^|[\s'"`(])((\.\.?\/|\/)?[\w.@-]+(?:\/[\w.@-]+)*\.[a-zA-Z0-9]+(?::\d+(?::\d+)?)?)/g;
|
|
220
|
+
constructor(_terminal, _handler) {
|
|
221
|
+
this._terminal = _terminal;
|
|
222
|
+
this._handler = _handler;
|
|
223
|
+
}
|
|
224
|
+
provideLinks(bufferLineNumber, callback) {
|
|
225
|
+
// bufferLineNumber は 1-indexed、buffer.getLine() は 0-indexed を期待
|
|
226
|
+
const lineIndex = bufferLineNumber - 1;
|
|
227
|
+
const line = this._terminal.buffer.active.getLine(lineIndex);
|
|
228
|
+
if (!line) {
|
|
229
|
+
callback(undefined);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const text = line.translateToString(true);
|
|
233
|
+
const links = [];
|
|
234
|
+
// Reset regex lastIndex
|
|
235
|
+
this.FILE_PATH_REGEX.lastIndex = 0;
|
|
236
|
+
let match;
|
|
237
|
+
while ((match = this.FILE_PATH_REGEX.exec(text)) !== null) {
|
|
238
|
+
const filePath = match[1];
|
|
239
|
+
if (this._isLikelyFilePath(filePath) && !this._isUrl(filePath)) {
|
|
240
|
+
// 文字列インデックスから表示位置(セル位置)を計算
|
|
241
|
+
const stringStartIndex = match.index + (match[0].length - match[1].length);
|
|
242
|
+
// WebLinksAddon と同じ方式で位置をマッピング(0-indexed lineIndexを渡す)
|
|
243
|
+
const [, startX] = this._mapStrIdx(lineIndex, 0, stringStartIndex);
|
|
244
|
+
const [, endX] = this._mapStrIdx(lineIndex, startX, filePath.length);
|
|
245
|
+
if (startX === -1 || endX === -1) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
links.push({
|
|
249
|
+
text: filePath,
|
|
250
|
+
range: {
|
|
251
|
+
// IBufferCellPosition は 1-indexed
|
|
252
|
+
start: { x: startX + 1, y: bufferLineNumber },
|
|
253
|
+
end: { x: endX, y: bufferLineNumber }
|
|
254
|
+
},
|
|
255
|
+
activate: (event, linkText) => {
|
|
256
|
+
const parsed = this._parseFilePath(linkText);
|
|
257
|
+
this._handler(event, parsed.path, parsed.line, parsed.column);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
callback(links.length > 0 ? links : undefined);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Map a string index back to buffer positions.
|
|
266
|
+
* Returns buffer position as [lineIndex, columnIndex] 0-based,
|
|
267
|
+
* or [-1, -1] in case the lookup ran into a non-existing line.
|
|
268
|
+
* (Based on xterm.js WebLinkProvider implementation)
|
|
269
|
+
*/
|
|
270
|
+
_mapStrIdx(lineIndex, rowIndex, stringIndex) {
|
|
271
|
+
const buf = this._terminal.buffer.active;
|
|
272
|
+
const cell = buf.getNullCell();
|
|
273
|
+
let start = rowIndex;
|
|
274
|
+
while (stringIndex) {
|
|
275
|
+
const line = buf.getLine(lineIndex);
|
|
276
|
+
if (!line) {
|
|
277
|
+
return [-1, -1];
|
|
278
|
+
}
|
|
279
|
+
for (let i = start; i < line.length; ++i) {
|
|
280
|
+
line.getCell(i, cell);
|
|
281
|
+
const chars = cell.getChars();
|
|
282
|
+
const width = cell.getWidth();
|
|
283
|
+
if (width) {
|
|
284
|
+
stringIndex -= chars.length || 1;
|
|
285
|
+
}
|
|
286
|
+
if (stringIndex < 0) {
|
|
287
|
+
return [lineIndex, i];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
lineIndex++;
|
|
291
|
+
start = 0;
|
|
292
|
+
}
|
|
293
|
+
return [lineIndex, start];
|
|
294
|
+
}
|
|
295
|
+
_isLikelyFilePath(text) {
|
|
296
|
+
// 拡張子を持つか確認
|
|
297
|
+
return /\.[a-zA-Z0-9]+(?::\d+(?::\d+)?)?$/.test(text);
|
|
298
|
+
}
|
|
299
|
+
_isUrl(text) {
|
|
300
|
+
// URL形式を除外
|
|
301
|
+
return /^https?:\/\//.test(text) || /^file:\/\//.test(text);
|
|
302
|
+
}
|
|
303
|
+
_parseFilePath(text) {
|
|
304
|
+
const match = text.match(/^(.+?)(?::(\d+)(?::(\d+))?)?$/);
|
|
305
|
+
if (match) {
|
|
306
|
+
return {
|
|
307
|
+
path: match[1],
|
|
308
|
+
line: match[2] ? parseInt(match[2], 10) : undefined,
|
|
309
|
+
column: match[3] ? parseInt(match[3], 10) : undefined
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return { path: text };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Export helper functions for testing
|
|
316
|
+
const isLikelyFilePath = (text) => {
|
|
317
|
+
return /\.[a-zA-Z0-9]+(?::\d+(?::\d+)?)?$/.test(text) && !/^https?:\/\//.test(text);
|
|
318
|
+
};
|
|
319
|
+
const parseFilePath = (text) => {
|
|
320
|
+
const match = text.match(/^(.+?)(?::(\d+)(?::(\d+))?)?$/);
|
|
321
|
+
if (match) {
|
|
322
|
+
return {
|
|
323
|
+
path: match[1],
|
|
324
|
+
line: match[2] ? parseInt(match[2], 10) : undefined,
|
|
325
|
+
column: match[3] ? parseInt(match[3], 10) : undefined
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return { path: text };
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* xterm.js addon for detecting and handling file path links in terminal output.
|
|
333
|
+
* Similar to WebLinksAddon but for local file paths.
|
|
334
|
+
*
|
|
335
|
+
* Usage:
|
|
336
|
+
* ```typescript
|
|
337
|
+
* const fileLinksAddon = new FileLinksAddon((event, filePath, line, column) => {
|
|
338
|
+
* if (event.metaKey || event.ctrlKey) {
|
|
339
|
+
* // Open file in editor
|
|
340
|
+
* }
|
|
341
|
+
* });
|
|
342
|
+
* terminal.loadAddon(fileLinksAddon);
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
class FileLinksAddon {
|
|
346
|
+
_handler;
|
|
347
|
+
_terminal;
|
|
348
|
+
_linkProvider;
|
|
349
|
+
constructor(_handler) {
|
|
350
|
+
this._handler = _handler;
|
|
351
|
+
}
|
|
352
|
+
activate(terminal) {
|
|
353
|
+
this._terminal = terminal;
|
|
354
|
+
this._linkProvider = terminal.registerLinkProvider(new FileLinkProvider(terminal, this._handler));
|
|
355
|
+
}
|
|
356
|
+
dispose() {
|
|
357
|
+
this._linkProvider?.dispose();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
var XtermActionId;
|
|
362
|
+
(function (XtermActionId) {
|
|
363
|
+
XtermActionId["DATA_INPUT"] = "xterm:data-input";
|
|
364
|
+
XtermActionId["RESIZED"] = "xterm:resized";
|
|
365
|
+
XtermActionId["GITHUB_LINK_CLICK"] = "xterm:github-link-click";
|
|
366
|
+
XtermActionId["FILE_LINK_CLICK"] = "xterm:file-link-click";
|
|
367
|
+
})(XtermActionId || (XtermActionId = {}));
|
|
368
|
+
class XtermOrganismStore extends InjectableComponent {
|
|
369
|
+
static DI = makeDI(XtermOrganismStore, () => () => ({
|
|
370
|
+
data: '',
|
|
371
|
+
interactive: false,
|
|
372
|
+
queryResult: '',
|
|
373
|
+
useInteractiveTheme: false,
|
|
374
|
+
}), ['components', 'organisms', 'xterm']);
|
|
375
|
+
config = XtermOrganismStore.DI.injectConfig();
|
|
376
|
+
// Note: Using simple default values instead of _computed() to ensure
|
|
377
|
+
// input bindings from templates work correctly with Angular signal effects
|
|
378
|
+
data = input(undefined);
|
|
379
|
+
interactive = input(false);
|
|
380
|
+
queryResult = input('');
|
|
381
|
+
/** Use interactive theme even when not interactive (for display consistency) */
|
|
382
|
+
useInteractiveTheme = input(false);
|
|
383
|
+
/** Total bytes written to the buffer (for accurate diff detection when buffer overflows) */
|
|
384
|
+
totalWritten = input(0);
|
|
385
|
+
constructor() {
|
|
386
|
+
super();
|
|
387
|
+
XtermOrganismStore.DI.initialize(this);
|
|
388
|
+
}
|
|
389
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: XtermOrganismStore, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
390
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.0.5", type: XtermOrganismStore, isStandalone: true, selector: "organisms-xterm", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, queryResult: { classPropertyName: "queryResult", publicName: "queryResult", isSignal: true, isRequired: false, transformFunction: null }, useInteractiveTheme: { classPropertyName: "useInteractiveTheme", publicName: "useInteractiveTheme", isSignal: true, isRequired: false, transformFunction: null }, totalWritten: { classPropertyName: "totalWritten", publicName: "totalWritten", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0 });
|
|
391
|
+
}
|
|
392
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: XtermOrganismStore, decorators: [{
|
|
393
|
+
type: Directive,
|
|
394
|
+
args: [{ standalone: true, selector: 'organisms-xterm' }]
|
|
395
|
+
}], ctorParameters: () => [] });
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Minimum container size required for terminal initialization.
|
|
399
|
+
* Prevents issues with SR-only hidden containers (1x1 pixel) causing FitAddon problems.
|
|
400
|
+
*/
|
|
401
|
+
const MIN_CONTAINER_SIZE = 10;
|
|
402
|
+
/** Default display-only theme (for catalog/storybook) */
|
|
403
|
+
const DISPLAY_THEME = {
|
|
404
|
+
background: '#0a0a0a',
|
|
405
|
+
foreground: '#66d9ef',
|
|
406
|
+
cursor: '#ff79c6',
|
|
407
|
+
cyan: '#8be9fd',
|
|
408
|
+
brightCyan: '#50fa7b',
|
|
409
|
+
green: '#50fa7b',
|
|
410
|
+
brightGreen: '#50fa7b',
|
|
411
|
+
magenta: '#ff79c6',
|
|
412
|
+
brightMagenta: '#ff79c6',
|
|
413
|
+
yellow: '#f1fa8c',
|
|
414
|
+
brightYellow: '#ffb86c',
|
|
415
|
+
red: '#ff5555',
|
|
416
|
+
brightRed: '#ff5555'
|
|
417
|
+
};
|
|
418
|
+
/** Interactive session theme (matches original TerminalManager) */
|
|
419
|
+
const INTERACTIVE_THEME = {
|
|
420
|
+
background: '#0a0a0a',
|
|
421
|
+
foreground: '#e0e0e0',
|
|
422
|
+
cursor: '#e0e0e0',
|
|
423
|
+
black: '#000000',
|
|
424
|
+
red: '#cd3131',
|
|
425
|
+
green: '#0dbc79',
|
|
426
|
+
yellow: '#e5e510',
|
|
427
|
+
blue: '#2472c8',
|
|
428
|
+
magenta: '#bc3fbc',
|
|
429
|
+
cyan: '#11a8cd',
|
|
430
|
+
white: '#e5e5e5',
|
|
431
|
+
brightBlack: '#666666',
|
|
432
|
+
brightRed: '#f14c4c',
|
|
433
|
+
brightGreen: '#23d18b',
|
|
434
|
+
brightYellow: '#f5f543',
|
|
435
|
+
brightBlue: '#3b8eea',
|
|
436
|
+
brightMagenta: '#d670d6',
|
|
437
|
+
brightCyan: '#29b8db',
|
|
438
|
+
brightWhite: '#e5e5e5',
|
|
439
|
+
};
|
|
440
|
+
class XtermOrganism extends NgAtomicComponent {
|
|
101
441
|
container = viewChild.required('container');
|
|
442
|
+
store = inject(XtermOrganismStore);
|
|
443
|
+
hostElement = inject((ElementRef));
|
|
102
444
|
terminalReady = output();
|
|
103
445
|
githubLinkClick = output();
|
|
104
446
|
terminal;
|
|
105
447
|
fitAddon;
|
|
448
|
+
imageAddon;
|
|
106
449
|
githubLinksAddon;
|
|
450
|
+
fileLinksAddon;
|
|
107
451
|
resizeObserver;
|
|
452
|
+
lastDataLength = 0;
|
|
453
|
+
/** Tracks how many bytes have been processed (for totalWritten-based diff detection) */
|
|
454
|
+
lastProcessedLength = 0;
|
|
455
|
+
terminalInitialized = signal(false);
|
|
456
|
+
/** Tracks if initialization was skipped due to container being too small */
|
|
457
|
+
initSkippedDueToSize = false;
|
|
458
|
+
constructor() {
|
|
459
|
+
super();
|
|
460
|
+
// Watch for data input changes (with totalWritten support for buffer overflow handling)
|
|
461
|
+
effect(() => {
|
|
462
|
+
const isInitialized = this.terminalInitialized();
|
|
463
|
+
const rawData = this.store.data();
|
|
464
|
+
const totalWritten = this.store.totalWritten();
|
|
465
|
+
const dataIsFunction = typeof rawData === 'function';
|
|
466
|
+
const data = dataIsFunction ? rawData() : rawData;
|
|
467
|
+
if (!isInitialized)
|
|
468
|
+
return;
|
|
469
|
+
// Use totalWritten-based logic when available (handles buffer overflow correctly)
|
|
470
|
+
if (totalWritten > 0) {
|
|
471
|
+
this.writeDataWithTotalWritten(data ?? '', totalWritten);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
// Legacy fallback for backward compatibility
|
|
475
|
+
this.writeDataToTerminal(data);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
// Watch for interactive mode changes and update host class
|
|
479
|
+
// Note: Using effect with classList because @HostBinding doesn't work with OnPush + signals
|
|
480
|
+
effect(() => {
|
|
481
|
+
const isInteractive = this.store.interactive();
|
|
482
|
+
if (isInteractive) {
|
|
483
|
+
this.hostElement.nativeElement.classList.add('interactive');
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
this.hostElement.nativeElement.classList.remove('interactive');
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
// Watch for interactive mode changes and update cursor blink
|
|
490
|
+
effect(() => {
|
|
491
|
+
const isInitialized = this.terminalInitialized();
|
|
492
|
+
const isInteractive = this.store.interactive();
|
|
493
|
+
if (!isInitialized || !this.terminal)
|
|
494
|
+
return;
|
|
495
|
+
// Update cursor blink setting
|
|
496
|
+
this.terminal.options.cursorBlink = isInteractive;
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
writeDataToTerminal(data) {
|
|
500
|
+
// Guard against undefined/null/empty data
|
|
501
|
+
// Empty string is treated as "no data yet" (from startWith('') in streams)
|
|
502
|
+
// Explicit reset should use the public clear() or reset() methods
|
|
503
|
+
if (data === undefined || data === null || data === '') {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
// O(1) length comparison instead of O(n) startsWith
|
|
507
|
+
if (data.length >= this.lastDataLength) {
|
|
508
|
+
const newPart = data.slice(this.lastDataLength);
|
|
509
|
+
if (newPart) {
|
|
510
|
+
this.terminal.write(newPart);
|
|
511
|
+
}
|
|
512
|
+
this.lastDataLength = data.length;
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
// Data got shorter = reset occurred
|
|
516
|
+
this.terminal.reset();
|
|
517
|
+
this.terminal.write(data);
|
|
518
|
+
this.lastDataLength = data.length;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Write data using totalWritten for accurate diff detection.
|
|
523
|
+
* This method handles buffer overflow scenarios correctly.
|
|
524
|
+
* @param data The current buffer content
|
|
525
|
+
* @param totalWritten Total bytes written to the buffer (cumulative)
|
|
526
|
+
*/
|
|
527
|
+
writeDataWithTotalWritten(data, totalWritten) {
|
|
528
|
+
if (!data)
|
|
529
|
+
return;
|
|
530
|
+
// Calculate how many new bytes arrived
|
|
531
|
+
const newBytesCount = totalWritten - this.lastProcessedLength;
|
|
532
|
+
if (newBytesCount <= 0) {
|
|
533
|
+
// No new data or reset occurred
|
|
534
|
+
if (totalWritten < this.lastProcessedLength) {
|
|
535
|
+
// Reset detected
|
|
536
|
+
this.terminal.reset();
|
|
537
|
+
this.terminal.write(data);
|
|
538
|
+
this.lastProcessedLength = totalWritten;
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
// Extract new content from the end of buffer
|
|
543
|
+
const newContent = data.slice(-newBytesCount);
|
|
544
|
+
if (newContent) {
|
|
545
|
+
this.terminal.write(newContent);
|
|
546
|
+
}
|
|
547
|
+
this.lastProcessedLength = totalWritten;
|
|
548
|
+
}
|
|
108
549
|
ngAfterViewInit() {
|
|
550
|
+
const containerElement = this.container().nativeElement;
|
|
551
|
+
// Guard against initialization in very small containers (e.g., SR-only 1x1 pixel hidden elements)
|
|
552
|
+
// This prevents FitAddon issues and excessive processing in hidden query sections
|
|
553
|
+
if (containerElement.offsetWidth < MIN_CONTAINER_SIZE || containerElement.offsetHeight < MIN_CONTAINER_SIZE) {
|
|
554
|
+
this.initSkippedDueToSize = true;
|
|
555
|
+
// Retry initialization when container reaches minimum size (e.g., after CSS/layout settles)
|
|
556
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
557
|
+
if (containerElement.offsetWidth >= MIN_CONTAINER_SIZE && containerElement.offsetHeight >= MIN_CONTAINER_SIZE) {
|
|
558
|
+
this.resizeObserver?.disconnect();
|
|
559
|
+
this.resizeObserver = undefined;
|
|
560
|
+
this.initSkippedDueToSize = false;
|
|
561
|
+
this.initializeTerminal(containerElement);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
this.resizeObserver.observe(containerElement);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
this.initializeTerminal(containerElement);
|
|
568
|
+
}
|
|
569
|
+
initializeTerminal(containerElement) {
|
|
570
|
+
const isInteractive = this.store.interactive();
|
|
571
|
+
const useInteractiveTheme = this.store.useInteractiveTheme() || isInteractive;
|
|
109
572
|
this.terminal = new Terminal({
|
|
110
|
-
theme:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
cyan: '#8be9fd',
|
|
115
|
-
brightCyan: '#50fa7b',
|
|
116
|
-
green: '#50fa7b',
|
|
117
|
-
brightGreen: '#50fa7b',
|
|
118
|
-
magenta: '#ff79c6',
|
|
119
|
-
brightMagenta: '#ff79c6',
|
|
120
|
-
yellow: '#f1fa8c',
|
|
121
|
-
brightYellow: '#ffb86c',
|
|
122
|
-
red: '#ff5555',
|
|
123
|
-
brightRed: '#ff5555'
|
|
124
|
-
},
|
|
125
|
-
scrollback: 1000,
|
|
126
|
-
fontSize: 12,
|
|
573
|
+
theme: useInteractiveTheme ? INTERACTIVE_THEME : DISPLAY_THEME,
|
|
574
|
+
scrollback: isInteractive ? 10000 : 1000,
|
|
575
|
+
fontSize: useInteractiveTheme ? 14 : 12,
|
|
576
|
+
fontFamily: '"FiraCode Nerd Font", "Fira Code", "SF Mono", "Cascadia Code", "Consolas", "Courier New", monospace',
|
|
127
577
|
lineHeight: 1.2,
|
|
128
578
|
allowTransparency: false,
|
|
129
|
-
disableStdin:
|
|
130
|
-
cursorBlink:
|
|
579
|
+
disableStdin: !isInteractive,
|
|
580
|
+
cursorBlink: isInteractive,
|
|
131
581
|
cursorStyle: 'block',
|
|
132
582
|
convertEol: true,
|
|
133
583
|
rows: 24
|
|
@@ -135,34 +585,110 @@ class XtermOrganism {
|
|
|
135
585
|
this.fitAddon = new FitAddon();
|
|
136
586
|
this.terminal.loadAddon(this.fitAddon);
|
|
137
587
|
this.terminal.loadAddon(new WebLinksAddon());
|
|
588
|
+
// ImageAddon for displaying inline images (SIXEL and iTerm2 IIP support)
|
|
589
|
+
// Loaded dynamically to avoid browser compatibility issues
|
|
590
|
+
this.loadImageAddon();
|
|
138
591
|
this.githubLinksAddon = new GitHubLinksAddon((event, issueNumber) => {
|
|
592
|
+
this.dispatch({ id: XtermActionId.GITHUB_LINK_CLICK, payload: issueNumber });
|
|
593
|
+
});
|
|
594
|
+
this.terminal.loadAddon(this.githubLinksAddon);
|
|
595
|
+
this.fileLinksAddon = new FileLinksAddon((event, filePath, line, column) => {
|
|
139
596
|
if (event.metaKey || event.ctrlKey) {
|
|
140
|
-
this.
|
|
597
|
+
this.dispatch({ id: XtermActionId.FILE_LINK_CLICK, payload: { filePath, line, column } });
|
|
141
598
|
}
|
|
142
599
|
});
|
|
143
|
-
this.terminal.loadAddon(this.
|
|
144
|
-
const containerElement = this.container().nativeElement;
|
|
600
|
+
this.terminal.loadAddon(this.fileLinksAddon);
|
|
145
601
|
this.terminal.open(containerElement);
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
this.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.
|
|
152
|
-
|
|
602
|
+
// Setup event handlers for interactive mode
|
|
603
|
+
if (isInteractive) {
|
|
604
|
+
this.terminal.onData((data) => {
|
|
605
|
+
this.dispatch({ id: XtermActionId.DATA_INPUT, payload: data });
|
|
606
|
+
});
|
|
607
|
+
this.terminal.onResize(({ cols, rows }) => this.dispatch({ id: XtermActionId.RESIZED, payload: { cols, rows } }));
|
|
608
|
+
}
|
|
609
|
+
// Use ResizeObserver with debounce to wait for container size to stabilize
|
|
610
|
+
// (CSS transitions take 300-500ms, so fixed setTimeout(100ms) is too early)
|
|
611
|
+
let stableTimer = null;
|
|
612
|
+
let resizeTimer = null;
|
|
613
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
614
|
+
try {
|
|
615
|
+
if (!this.terminalInitialized()) {
|
|
153
616
|
this.fitAddon.fit();
|
|
617
|
+
// Debounce: wait for container to stop changing before initializing
|
|
618
|
+
if (stableTimer)
|
|
619
|
+
clearTimeout(stableTimer);
|
|
620
|
+
stableTimer = setTimeout(() => {
|
|
621
|
+
this.fitAddon.fit();
|
|
622
|
+
this.markInitialized(isInteractive);
|
|
623
|
+
}, 150);
|
|
154
624
|
}
|
|
155
|
-
|
|
156
|
-
|
|
625
|
+
else if (isInteractive) {
|
|
626
|
+
// Debounce resize to wait for CSS transitions to complete (300ms)
|
|
627
|
+
if (resizeTimer)
|
|
628
|
+
clearTimeout(resizeTimer);
|
|
629
|
+
resizeTimer = setTimeout(() => {
|
|
630
|
+
this.fitAddon.fit();
|
|
631
|
+
this.dispatch({ id: XtermActionId.RESIZED, payload: { cols: this.cols, rows: this.rows } });
|
|
632
|
+
}, 300);
|
|
157
633
|
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
634
|
+
}
|
|
635
|
+
catch (e) {
|
|
636
|
+
console.warn('Failed to fit terminal:', e);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
this.resizeObserver.observe(containerElement);
|
|
640
|
+
// Fallback: initialize after 500ms if ResizeObserver hasn't triggered stable state
|
|
641
|
+
setTimeout(() => {
|
|
642
|
+
if (!this.terminalInitialized()) {
|
|
643
|
+
this.fitAddon.fit();
|
|
644
|
+
this.markInitialized(isInteractive);
|
|
645
|
+
}
|
|
646
|
+
}, 500);
|
|
161
647
|
}
|
|
162
648
|
ngOnDestroy() {
|
|
163
649
|
this.resizeObserver?.disconnect();
|
|
650
|
+
// Remove terminal instance from window for E2E testing cleanup
|
|
651
|
+
if (typeof window !== 'undefined' && window.__XTERM_INSTANCES__) {
|
|
652
|
+
const instances = window.__XTERM_INSTANCES__;
|
|
653
|
+
const index = instances.indexOf(this.terminal);
|
|
654
|
+
if (index !== -1) {
|
|
655
|
+
instances.splice(index, 1);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
164
658
|
this.terminal?.dispose();
|
|
165
659
|
}
|
|
660
|
+
markInitialized(isInteractive) {
|
|
661
|
+
if (this.terminalInitialized())
|
|
662
|
+
return;
|
|
663
|
+
this.terminalInitialized.set(true);
|
|
664
|
+
this.terminalReady.emit(this.terminal);
|
|
665
|
+
// Expose terminal instance for E2E testing (only in development/test environments)
|
|
666
|
+
if (typeof window !== 'undefined') {
|
|
667
|
+
window.__XTERM_INSTANCES__ = window.__XTERM_INSTANCES__ || [];
|
|
668
|
+
window.__XTERM_INSTANCES__.push(this.terminal);
|
|
669
|
+
}
|
|
670
|
+
// Dispatch initial RESIZED for interactive mode
|
|
671
|
+
if (isInteractive) {
|
|
672
|
+
this.dispatch({ id: XtermActionId.RESIZED, payload: { cols: this.cols, rows: this.rows } });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
async loadImageAddon() {
|
|
676
|
+
try {
|
|
677
|
+
const { ImageAddon } = await import('@xterm/addon-image');
|
|
678
|
+
const imageAddonOptions = {
|
|
679
|
+
enableSizeReports: true,
|
|
680
|
+
sixelSupport: true,
|
|
681
|
+
sixelScrolling: true,
|
|
682
|
+
sixelPaletteLimit: 256,
|
|
683
|
+
};
|
|
684
|
+
this.imageAddon = new ImageAddon(imageAddonOptions);
|
|
685
|
+
this.terminal.loadAddon(this.imageAddon);
|
|
686
|
+
}
|
|
687
|
+
catch (e) {
|
|
688
|
+
// ImageAddon not available in browser environment - this is expected
|
|
689
|
+
console.debug('ImageAddon not available:', e);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
166
692
|
// Public API
|
|
167
693
|
write(text) {
|
|
168
694
|
this.terminal?.write(text);
|
|
@@ -182,17 +708,291 @@ class XtermOrganism {
|
|
|
182
708
|
scrollLines(lines) {
|
|
183
709
|
this.terminal?.scrollLines(lines);
|
|
184
710
|
}
|
|
711
|
+
fit() {
|
|
712
|
+
this.fitAddon?.fit();
|
|
713
|
+
// Only dispatch RESIZED after initialization to prevent sending wrong cols/rows
|
|
714
|
+
// during CSS transitions (SessionTemplateV2.ngAfterViewInit calls fit() before layout stabilizes)
|
|
715
|
+
if (this.terminalInitialized() && this.terminal) {
|
|
716
|
+
this.dispatch({ id: XtermActionId.RESIZED, payload: { cols: this.cols, rows: this.rows } });
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
get cols() {
|
|
720
|
+
return this.terminal?.cols ?? 80;
|
|
721
|
+
}
|
|
722
|
+
get rows() {
|
|
723
|
+
return this.terminal?.rows ?? 24;
|
|
724
|
+
}
|
|
725
|
+
getTerminal() {
|
|
726
|
+
// Only expose terminal after fitAddon.fit() has run (terminalInitialized === true)
|
|
727
|
+
return this.terminalInitialized() ? this.terminal : undefined;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Display an inline image using iTerm2's Inline Image Protocol
|
|
731
|
+
* @param base64Data Base64 encoded image data (PNG, JPEG, etc.)
|
|
732
|
+
* @param options Image display options
|
|
733
|
+
*/
|
|
734
|
+
displayImage(base64Data, options) {
|
|
735
|
+
if (!this.terminal)
|
|
736
|
+
return;
|
|
737
|
+
const args = ['inline=1'];
|
|
738
|
+
if (options?.width) {
|
|
739
|
+
args.push(`width=${options.width}`);
|
|
740
|
+
}
|
|
741
|
+
if (options?.height) {
|
|
742
|
+
args.push(`height=${options.height}`);
|
|
743
|
+
}
|
|
744
|
+
if (options?.preserveAspectRatio !== undefined) {
|
|
745
|
+
args.push(`preserveAspectRatio=${options.preserveAspectRatio ? 1 : 0}`);
|
|
746
|
+
}
|
|
747
|
+
if (options?.name) {
|
|
748
|
+
args.push(`name=${btoa(options.name)}`);
|
|
749
|
+
}
|
|
750
|
+
// iTerm2 Inline Image Protocol escape sequence
|
|
751
|
+
const escapeSequence = `\x1b]1337;File=${args.join(';')}:${base64Data}\x07`;
|
|
752
|
+
this.terminal.write(escapeSequence);
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Get the ImageAddon instance for advanced image operations
|
|
756
|
+
*/
|
|
757
|
+
getImageAddon() {
|
|
758
|
+
return this.imageAddon;
|
|
759
|
+
}
|
|
185
760
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: XtermOrganism, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
186
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.0.5", type: XtermOrganism, isStandalone: true, selector: "organisms-xterm", outputs: { terminalReady: "terminalReady", githubLinkClick: "githubLinkClick" }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }], ngImport: i0, template: `<div #container class="xterm-wrapper"></div>`, isInline: true, styles: ["", "@import\"https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css\";:host{display:block;width:100%;height:100%}.xterm-wrapper{width:100%;height:100%;background:#0a0a0a;position:relative;overflow:hidden}.xterm-wrapper ::ng-deep .xterm-cursor-layer{display:none!important}.xterm-wrapper ::ng-deep .xterm-cursor
|
|
761
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.0.5", type: XtermOrganism, isStandalone: true, selector: "organisms-xterm", outputs: { terminalReady: "terminalReady", githubLinkClick: "githubLinkClick" }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }], usesInheritance: true, hostDirectives: [{ directive: XtermOrganismStore, inputs: ["data", "data", "interactive", "interactive", "useInteractiveTheme", "useInteractiveTheme", "totalWritten", "totalWritten"] }], ngImport: i0, template: `<div #container class="xterm-wrapper"></div>`, isInline: true, styles: ["", "@import\"https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css\";:host{display:block;width:100%;height:100%}:host .xterm-wrapper{width:100%;height:100%;background:#0a0a0a;position:relative;overflow:hidden}:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor-layer{display:none!important}:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor,:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor-outline{opacity:0!important}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
187
762
|
}
|
|
188
763
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.5", ngImport: i0, type: XtermOrganism, decorators: [{
|
|
189
764
|
type: Component,
|
|
190
|
-
args: [{ selector: 'organisms-xterm', standalone: true, imports: [], template: `<div #container class="xterm-wrapper"></div>`, changeDetection: ChangeDetectionStrategy.OnPush,
|
|
191
|
-
|
|
765
|
+
args: [{ selector: 'organisms-xterm', standalone: true, imports: [], template: `<div #container class="xterm-wrapper"></div>`, changeDetection: ChangeDetectionStrategy.OnPush, hostDirectives: [{
|
|
766
|
+
directive: XtermOrganismStore,
|
|
767
|
+
inputs: ['data', 'interactive', 'useInteractiveTheme', 'totalWritten'],
|
|
768
|
+
}], styles: ["@import\"https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css\";:host{display:block;width:100%;height:100%}:host .xterm-wrapper{width:100%;height:100%;background:#0a0a0a;position:relative;overflow:hidden}:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor-layer{display:none!important}:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor,:host:not(.interactive) .xterm-wrapper ::ng-deep .xterm-cursor-outline{opacity:0!important}\n"] }]
|
|
769
|
+
}], ctorParameters: () => [] });
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Utility functions for displaying images in xterm using SIXEL format
|
|
773
|
+
*/
|
|
774
|
+
/**
|
|
775
|
+
* Generate ASCII art text using figlet
|
|
776
|
+
* Note: figlet is loaded dynamically as it's not available in browser environments
|
|
777
|
+
*/
|
|
778
|
+
async function generateAsciiArt(text, font = 'ANSI Shadow') {
|
|
779
|
+
try {
|
|
780
|
+
const figlet = await import('figlet');
|
|
781
|
+
return new Promise((resolve, reject) => {
|
|
782
|
+
figlet.default.text(text, { font: font }, (err, result) => {
|
|
783
|
+
if (err) {
|
|
784
|
+
reject(err);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
resolve(result || '');
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
catch (e) {
|
|
792
|
+
// figlet not available in browser - return simple text
|
|
793
|
+
console.debug('figlet not available:', e);
|
|
794
|
+
return text;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Convert ASCII art text to SVG with transparent background
|
|
799
|
+
*/
|
|
800
|
+
function asciiArtToSvg(asciiArt, options = {}) {
|
|
801
|
+
const { textColor = '#00FFFF', fontSize = 14 } = options;
|
|
802
|
+
const lines = asciiArt.split('\n');
|
|
803
|
+
const maxLineLength = Math.max(...lines.map(l => l.length));
|
|
804
|
+
// Calculate dimensions based on monospace font metrics (block chars need more width)
|
|
805
|
+
const charWidth = fontSize * 0.65;
|
|
806
|
+
const lineHeight = fontSize * 1.4;
|
|
807
|
+
const padding = 40;
|
|
808
|
+
const width = Math.ceil(maxLineLength * charWidth) + padding;
|
|
809
|
+
const height = Math.ceil(lines.length * lineHeight) + padding;
|
|
810
|
+
// Build SVG with text elements for each line
|
|
811
|
+
const textElements = lines.map((line, i) => {
|
|
812
|
+
// Escape special XML characters
|
|
813
|
+
const escapedLine = line
|
|
814
|
+
.replace(/&/g, '&')
|
|
815
|
+
.replace(/</g, '<')
|
|
816
|
+
.replace(/>/g, '>')
|
|
817
|
+
.replace(/"/g, '"');
|
|
818
|
+
return `<text x="20" y="${20 + (i + 1) * lineHeight}" fill="${textColor}" xml:space="preserve">${escapedLine}</text>`;
|
|
819
|
+
}).join('\n ');
|
|
820
|
+
return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
821
|
+
<style>text { font-family: 'Courier New', Consolas, monospace; font-size: ${fontSize}px; white-space: pre; }</style>
|
|
822
|
+
${textElements}
|
|
823
|
+
</svg>`;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Convert SVG to SIXEL format for xterm display
|
|
827
|
+
*/
|
|
828
|
+
async function svgToSixel(svgString, options = {}) {
|
|
829
|
+
const { width = 400, height = 200, backgroundColor } = options;
|
|
830
|
+
return new Promise((resolve, reject) => {
|
|
831
|
+
const canvas = document.createElement('canvas');
|
|
832
|
+
canvas.width = width;
|
|
833
|
+
canvas.height = height;
|
|
834
|
+
const ctx = canvas.getContext('2d');
|
|
835
|
+
if (!ctx) {
|
|
836
|
+
reject(new Error('Failed to get canvas context'));
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
// Fill background (transparent if not specified)
|
|
840
|
+
if (backgroundColor) {
|
|
841
|
+
ctx.fillStyle = backgroundColor;
|
|
842
|
+
ctx.fillRect(0, 0, width, height);
|
|
843
|
+
}
|
|
844
|
+
const img = new Image();
|
|
845
|
+
const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|
846
|
+
const url = URL.createObjectURL(blob);
|
|
847
|
+
img.onload = () => {
|
|
848
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
849
|
+
URL.revokeObjectURL(url);
|
|
850
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
851
|
+
const sixelData = imageDataToSixel(imageData);
|
|
852
|
+
resolve(sixelData);
|
|
853
|
+
};
|
|
854
|
+
img.onerror = (error) => {
|
|
855
|
+
URL.revokeObjectURL(url);
|
|
856
|
+
reject(error);
|
|
857
|
+
};
|
|
858
|
+
img.src = url;
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Convert ImageData to SIXEL escape sequence
|
|
863
|
+
*/
|
|
864
|
+
function imageDataToSixel(imageData) {
|
|
865
|
+
const { width, height, data } = imageData;
|
|
866
|
+
// Build color palette (quantize to 16 colors)
|
|
867
|
+
const palette = buildColorPalette(data, 16);
|
|
868
|
+
const colorMap = new Map();
|
|
869
|
+
palette.forEach((color, index) => {
|
|
870
|
+
colorMap.set(color, index);
|
|
871
|
+
});
|
|
872
|
+
// SIXEL header
|
|
873
|
+
let sixel = '\x1bPq';
|
|
874
|
+
// Define colors in palette
|
|
875
|
+
palette.forEach((color, index) => {
|
|
876
|
+
const [r, g, b] = hexToRgb(color);
|
|
877
|
+
const rPct = Math.round((r / 255) * 100);
|
|
878
|
+
const gPct = Math.round((g / 255) * 100);
|
|
879
|
+
const bPct = Math.round((b / 255) * 100);
|
|
880
|
+
sixel += `#${index};2;${rPct};${gPct};${bPct}`;
|
|
881
|
+
});
|
|
882
|
+
// Process image in bands of 6 rows
|
|
883
|
+
for (let bandY = 0; bandY < height; bandY += 6) {
|
|
884
|
+
for (let colorIdx = 0; colorIdx < palette.length; colorIdx++) {
|
|
885
|
+
const color = palette[colorIdx];
|
|
886
|
+
let rowData = '';
|
|
887
|
+
let hasPixels = false;
|
|
888
|
+
rowData += `#${colorIdx}`;
|
|
889
|
+
for (let x = 0; x < width; x++) {
|
|
890
|
+
let sixelValue = 0;
|
|
891
|
+
for (let dy = 0; dy < 6; dy++) {
|
|
892
|
+
const y = bandY + dy;
|
|
893
|
+
if (y >= height)
|
|
894
|
+
continue;
|
|
895
|
+
const pixelIndex = (y * width + x) * 4;
|
|
896
|
+
const r = data[pixelIndex];
|
|
897
|
+
const g = data[pixelIndex + 1];
|
|
898
|
+
const b = data[pixelIndex + 2];
|
|
899
|
+
const a = data[pixelIndex + 3];
|
|
900
|
+
if (a < 128)
|
|
901
|
+
continue;
|
|
902
|
+
const pixelColor = rgbToHex(r, g, b);
|
|
903
|
+
const nearestColor = findNearestColor(pixelColor, palette);
|
|
904
|
+
if (nearestColor === color) {
|
|
905
|
+
sixelValue |= (1 << dy);
|
|
906
|
+
hasPixels = true;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
rowData += String.fromCharCode(63 + sixelValue);
|
|
910
|
+
}
|
|
911
|
+
if (hasPixels) {
|
|
912
|
+
sixel += rowData;
|
|
913
|
+
sixel += '$';
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (bandY + 6 < height) {
|
|
917
|
+
sixel += '-';
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
sixel += '\x1b\\';
|
|
921
|
+
return sixel;
|
|
922
|
+
}
|
|
923
|
+
function buildColorPalette(data, maxColors) {
|
|
924
|
+
const colorCounts = new Map();
|
|
925
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
926
|
+
const r = data[i];
|
|
927
|
+
const g = data[i + 1];
|
|
928
|
+
const b = data[i + 2];
|
|
929
|
+
const a = data[i + 3];
|
|
930
|
+
if (a < 128)
|
|
931
|
+
continue;
|
|
932
|
+
const qr = Math.floor(r / 16) * 16;
|
|
933
|
+
const qg = Math.floor(g / 16) * 16;
|
|
934
|
+
const qb = Math.floor(b / 16) * 16;
|
|
935
|
+
const color = rgbToHex(qr, qg, qb);
|
|
936
|
+
colorCounts.set(color, (colorCounts.get(color) || 0) + 1);
|
|
937
|
+
}
|
|
938
|
+
return Array.from(colorCounts.entries())
|
|
939
|
+
.sort((a, b) => b[1] - a[1])
|
|
940
|
+
.slice(0, maxColors)
|
|
941
|
+
.map(([color]) => color);
|
|
942
|
+
}
|
|
943
|
+
function rgbToHex(r, g, b) {
|
|
944
|
+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
945
|
+
}
|
|
946
|
+
function hexToRgb(hex) {
|
|
947
|
+
const match = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
948
|
+
if (!match)
|
|
949
|
+
return [0, 0, 0];
|
|
950
|
+
return [parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16)];
|
|
951
|
+
}
|
|
952
|
+
function findNearestColor(targetHex, palette) {
|
|
953
|
+
const [tr, tg, tb] = hexToRgb(targetHex);
|
|
954
|
+
let nearest = palette[0];
|
|
955
|
+
let minDist = Infinity;
|
|
956
|
+
for (const color of palette) {
|
|
957
|
+
const [r, g, b] = hexToRgb(color);
|
|
958
|
+
const dist = (tr - r) ** 2 + (tg - g) ** 2 + (tb - b) ** 2;
|
|
959
|
+
if (dist < minDist) {
|
|
960
|
+
minDist = dist;
|
|
961
|
+
nearest = color;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return nearest;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Generate MACHINA ASCII art banner as SIXEL
|
|
968
|
+
*/
|
|
969
|
+
async function getMachinaAsciiArtSixel(options = {}) {
|
|
970
|
+
// Generate ASCII art for "MACHINA"
|
|
971
|
+
const machinaArt = await generateAsciiArt('MACHINA', 'ANSI Shadow');
|
|
972
|
+
// Convert to SVG with cyan color (terminal style)
|
|
973
|
+
const fontSize = 14;
|
|
974
|
+
const svg = asciiArtToSvg(machinaArt, {
|
|
975
|
+
textColor: options.textColor || '#00FFFF',
|
|
976
|
+
fontSize
|
|
977
|
+
});
|
|
978
|
+
// Calculate appropriate dimensions matching asciiArtToSvg calculation
|
|
979
|
+
const lines = machinaArt.split('\n');
|
|
980
|
+
const maxLineLength = Math.max(...lines.map(l => l.length));
|
|
981
|
+
const charWidth = fontSize * 0.65;
|
|
982
|
+
const lineHeight = fontSize * 1.4;
|
|
983
|
+
const padding = 40;
|
|
984
|
+
const defaultWidth = Math.ceil(maxLineLength * charWidth) + padding;
|
|
985
|
+
const defaultHeight = Math.ceil(lines.length * lineHeight) + padding;
|
|
986
|
+
return svgToSixel(svg, {
|
|
987
|
+
width: options.width || defaultWidth,
|
|
988
|
+
height: options.height || defaultHeight,
|
|
989
|
+
backgroundColor: options.backgroundColor || '#0a0a0a',
|
|
990
|
+
});
|
|
991
|
+
}
|
|
192
992
|
|
|
193
993
|
/**
|
|
194
994
|
* Generated bundle index. Do not edit.
|
|
195
995
|
*/
|
|
196
996
|
|
|
197
|
-
export { GitHubLinkProvider, GitHubLinksAddon, XtermOrganism };
|
|
997
|
+
export { GitHubLinkProvider, GitHubLinksAddon, WebLinksAddon, XtermActionId, XtermOrganism, XtermOrganismStore, asciiArtToSvg, generateAsciiArt, getMachinaAsciiArtSixel, svgToSixel };
|
|
198
998
|
//# sourceMappingURL=xxmachina-components-organisms-xterm.mjs.map
|