@celljs/ta-web 4.7.0 → 4.8.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/README.en.md +1 -1
- package/README.md +1 -1
- package/esm/browser/chat/chat-protocol.d.ts +4 -0
- package/esm/browser/chat/chat-view.js +4 -1
- package/esm/browser/chat/chat-view.js.map +1 -1
- package/esm/browser/chat/chat.d.ts +8 -1
- package/esm/browser/chat/chat.js +34 -15
- package/esm/browser/chat/chat.js.map +1 -1
- package/esm/browser/chat/draft-model.d.ts +15 -0
- package/esm/browser/chat/draft-model.js +53 -0
- package/esm/browser/chat/draft-model.js.map +1 -0
- package/esm/browser/components/app-view.js +10 -3
- package/esm/browser/components/app-view.js.map +1 -1
- package/esm/browser/components/composer-box.d.ts +6 -0
- package/esm/browser/components/composer-box.js +50 -13
- package/esm/browser/components/composer-box.js.map +1 -1
- package/esm/browser/components/composer.js +1 -1
- package/esm/browser/components/composer.js.map +1 -1
- package/esm/browser/components/dialog-view.d.ts +4 -1
- package/esm/browser/components/dialog-view.js +11 -7
- package/esm/browser/components/dialog-view.js.map +1 -1
- package/esm/browser/components/notice-center.js +3 -1
- package/esm/browser/components/notice-center.js.map +1 -1
- package/esm/browser/components/session-view.js +48 -4
- package/esm/browser/components/session-view.js.map +1 -1
- package/esm/browser/components/terminal-art.d.ts +7 -0
- package/esm/browser/components/terminal-art.js +12 -0
- package/esm/browser/components/terminal-art.js.map +1 -0
- package/esm/browser/components/tool-view.js +1 -1
- package/esm/browser/components/tool-view.js.map +1 -1
- package/esm/browser/components/web-composer.js +1 -1
- package/esm/browser/components/web-composer.js.map +1 -1
- package/esm/browser/extension/icons.js +4 -1
- package/esm/browser/extension/icons.js.map +1 -1
- package/esm/browser/extension/web-extension-api.d.ts +117 -1
- package/esm/browser/extension/web-extensions.d.ts +3 -0
- package/esm/browser/extension/web-extensions.js +22 -0
- package/esm/browser/extension/web-extensions.js.map +1 -1
- package/esm/browser/i18n/locales.d.ts +13 -1
- package/esm/browser/i18n/messages.d.ts +12 -0
- package/esm/browser/i18n/messages.js +24 -0
- package/esm/browser/i18n/messages.js.map +1 -1
- package/esm/browser/notices/notices-protocol.d.ts +3 -0
- package/esm/browser/notices/notices.d.ts +3 -0
- package/esm/browser/notices/notices.js +15 -0
- package/esm/browser/notices/notices.js.map +1 -1
- package/esm/browser/style/conversation.d.ts +1 -1
- package/esm/browser/style/conversation.js +16 -1
- package/esm/browser/style/conversation.js.map +1 -1
- package/esm/browser/style/layout.d.ts +1 -1
- package/esm/browser/style/layout.js +5 -0
- package/esm/browser/style/layout.js.map +1 -1
- package/esm/browser/style/shell.d.ts +1 -1
- package/esm/browser/style/shell.js +1 -0
- package/esm/browser/style/shell.js.map +1 -1
- package/esm/browser/terminal/canvas-font.d.ts +10 -0
- package/esm/browser/terminal/canvas-font.js +34 -0
- package/esm/browser/terminal/canvas-font.js.map +1 -0
- package/esm/browser/terminal/terminal-links.d.ts +18 -0
- package/esm/browser/terminal/terminal-links.js +72 -0
- package/esm/browser/terminal/terminal-links.js.map +1 -0
- package/esm/browser/terminal/terminal-theme.d.ts +8 -0
- package/esm/browser/terminal/terminal-theme.js +65 -0
- package/esm/browser/terminal/terminal-theme.js.map +1 -0
- package/esm/browser/terminal/web-terminal.d.ts +74 -0
- package/esm/browser/terminal/web-terminal.js +300 -0
- package/esm/browser/terminal/web-terminal.js.map +1 -0
- package/esm/browser/terminal/xterm.d.ts +5 -0
- package/esm/browser/terminal/xterm.js +9 -0
- package/esm/browser/terminal/xterm.js.map +1 -0
- package/esm/browser/util/desktop.d.ts +43 -0
- package/esm/browser/util/desktop.js +55 -0
- package/esm/browser/util/desktop.js.map +1 -0
- package/esm/browser/util/file-drag.d.ts +26 -0
- package/esm/browser/util/file-drag.js +73 -0
- package/esm/browser/util/file-drag.js.map +1 -0
- package/esm/browser/util/terminal-text.d.ts +33 -0
- package/esm/browser/util/terminal-text.js +209 -0
- package/esm/browser/util/terminal-text.js.map +1 -0
- package/esm/browser/util/tool.js +16 -0
- package/esm/browser/util/tool.js.map +1 -1
- package/lib/browser/chat/chat-protocol.d.ts +4 -0
- package/lib/browser/chat/chat-protocol.d.ts.map +1 -1
- package/lib/browser/chat/chat-view.d.ts.map +1 -1
- package/lib/browser/chat/chat-view.js +4 -1
- package/lib/browser/chat/chat-view.js.map +1 -1
- package/lib/browser/chat/chat.d.ts +8 -1
- package/lib/browser/chat/chat.d.ts.map +1 -1
- package/lib/browser/chat/chat.js +34 -15
- package/lib/browser/chat/chat.js.map +1 -1
- package/lib/browser/chat/chat.spec.js +30 -0
- package/lib/browser/chat/chat.spec.js.map +1 -1
- package/lib/browser/chat/draft-model.d.ts +16 -0
- package/lib/browser/chat/draft-model.d.ts.map +1 -0
- package/lib/browser/chat/draft-model.js +90 -0
- package/lib/browser/chat/draft-model.js.map +1 -0
- package/lib/browser/chat/draft-model.spec.d.ts +2 -0
- package/lib/browser/chat/draft-model.spec.d.ts.map +1 -0
- package/lib/browser/chat/draft-model.spec.js +31 -0
- package/lib/browser/chat/draft-model.spec.js.map +1 -0
- package/lib/browser/components/app-view.d.ts.map +1 -1
- package/lib/browser/components/app-view.js +10 -3
- package/lib/browser/components/app-view.js.map +1 -1
- package/lib/browser/components/composer-box.d.ts +6 -0
- package/lib/browser/components/composer-box.d.ts.map +1 -1
- package/lib/browser/components/composer-box.js +51 -14
- package/lib/browser/components/composer-box.js.map +1 -1
- package/lib/browser/components/composer.d.ts.map +1 -1
- package/lib/browser/components/composer.js +1 -1
- package/lib/browser/components/composer.js.map +1 -1
- package/lib/browser/components/dialog-view.d.ts +4 -1
- package/lib/browser/components/dialog-view.d.ts.map +1 -1
- package/lib/browser/components/dialog-view.js +11 -7
- package/lib/browser/components/dialog-view.js.map +1 -1
- package/lib/browser/components/notice-center.d.ts.map +1 -1
- package/lib/browser/components/notice-center.js +3 -1
- package/lib/browser/components/notice-center.js.map +1 -1
- package/lib/browser/components/plus-menu.spec.d.ts +2 -0
- package/lib/browser/components/plus-menu.spec.d.ts.map +1 -0
- package/lib/browser/components/plus-menu.spec.js +18 -0
- package/lib/browser/components/plus-menu.spec.js.map +1 -0
- package/lib/browser/components/session-view.d.ts.map +1 -1
- package/lib/browser/components/session-view.js +48 -4
- package/lib/browser/components/session-view.js.map +1 -1
- package/lib/browser/components/terminal-art.d.ts +8 -0
- package/lib/browser/components/terminal-art.d.ts.map +1 -0
- package/lib/browser/components/terminal-art.js +48 -0
- package/lib/browser/components/terminal-art.js.map +1 -0
- package/lib/browser/components/tool-view.js +1 -1
- package/lib/browser/components/tool-view.js.map +1 -1
- package/lib/browser/components/web-composer.d.ts.map +1 -1
- package/lib/browser/components/web-composer.js +1 -1
- package/lib/browser/components/web-composer.js.map +1 -1
- package/lib/browser/extension/icons.d.ts.map +1 -1
- package/lib/browser/extension/icons.js +3 -0
- package/lib/browser/extension/icons.js.map +1 -1
- package/lib/browser/extension/web-extension-api.d.ts +117 -1
- package/lib/browser/extension/web-extension-api.d.ts.map +1 -1
- package/lib/browser/extension/web-extensions.d.ts +3 -0
- package/lib/browser/extension/web-extensions.d.ts.map +1 -1
- package/lib/browser/extension/web-extensions.js +22 -0
- package/lib/browser/extension/web-extensions.js.map +1 -1
- package/lib/browser/i18n/locales.d.ts +13 -1
- package/lib/browser/i18n/locales.d.ts.map +1 -1
- package/lib/browser/i18n/messages.d.ts +12 -0
- package/lib/browser/i18n/messages.d.ts.map +1 -1
- package/lib/browser/i18n/messages.js +24 -0
- package/lib/browser/i18n/messages.js.map +1 -1
- package/lib/browser/notices/notices-protocol.d.ts +3 -0
- package/lib/browser/notices/notices-protocol.d.ts.map +1 -1
- package/lib/browser/notices/notices.d.ts +3 -0
- package/lib/browser/notices/notices.d.ts.map +1 -1
- package/lib/browser/notices/notices.js +15 -0
- package/lib/browser/notices/notices.js.map +1 -1
- package/lib/browser/notices/notices.spec.js +7 -2
- package/lib/browser/notices/notices.spec.js.map +1 -1
- package/lib/browser/style/conversation.d.ts +1 -1
- package/lib/browser/style/conversation.d.ts.map +1 -1
- package/lib/browser/style/conversation.js +16 -1
- package/lib/browser/style/conversation.js.map +1 -1
- package/lib/browser/style/layout.d.ts +1 -1
- package/lib/browser/style/layout.d.ts.map +1 -1
- package/lib/browser/style/layout.js +5 -0
- package/lib/browser/style/layout.js.map +1 -1
- package/lib/browser/style/shell.d.ts +1 -1
- package/lib/browser/style/shell.d.ts.map +1 -1
- package/lib/browser/style/shell.js +1 -0
- package/lib/browser/style/shell.js.map +1 -1
- package/lib/browser/terminal/canvas-font.d.ts +11 -0
- package/lib/browser/terminal/canvas-font.d.ts.map +1 -0
- package/lib/browser/terminal/canvas-font.js +37 -0
- package/lib/browser/terminal/canvas-font.js.map +1 -0
- package/lib/browser/terminal/canvas-font.spec.d.ts +2 -0
- package/lib/browser/terminal/canvas-font.spec.d.ts.map +1 -0
- package/lib/browser/terminal/canvas-font.spec.js +32 -0
- package/lib/browser/terminal/canvas-font.spec.js.map +1 -0
- package/lib/browser/terminal/terminal-links.d.ts +19 -0
- package/lib/browser/terminal/terminal-links.d.ts.map +1 -0
- package/lib/browser/terminal/terminal-links.js +75 -0
- package/lib/browser/terminal/terminal-links.js.map +1 -0
- package/lib/browser/terminal/terminal-links.spec.d.ts +2 -0
- package/lib/browser/terminal/terminal-links.spec.d.ts.map +1 -0
- package/lib/browser/terminal/terminal-links.spec.js +30 -0
- package/lib/browser/terminal/terminal-links.spec.js.map +1 -0
- package/lib/browser/terminal/terminal-theme.d.ts +9 -0
- package/lib/browser/terminal/terminal-theme.d.ts.map +1 -0
- package/lib/browser/terminal/terminal-theme.js +68 -0
- package/lib/browser/terminal/terminal-theme.js.map +1 -0
- package/lib/browser/terminal/terminal-theme.spec.d.ts +2 -0
- package/lib/browser/terminal/terminal-theme.spec.d.ts.map +1 -0
- package/lib/browser/terminal/terminal-theme.spec.js +24 -0
- package/lib/browser/terminal/terminal-theme.spec.js.map +1 -0
- package/lib/browser/terminal/web-terminal.d.ts +75 -0
- package/lib/browser/terminal/web-terminal.d.ts.map +1 -0
- package/lib/browser/terminal/web-terminal.js +304 -0
- package/lib/browser/terminal/web-terminal.js.map +1 -0
- package/lib/browser/terminal/xterm.d.ts +6 -0
- package/lib/browser/terminal/xterm.d.ts.map +1 -0
- package/lib/browser/terminal/xterm.js +16 -0
- package/lib/browser/terminal/xterm.js.map +1 -0
- package/lib/browser/util/desktop.d.ts +44 -0
- package/lib/browser/util/desktop.d.ts.map +1 -0
- package/lib/browser/util/desktop.js +58 -0
- package/lib/browser/util/desktop.js.map +1 -0
- package/lib/browser/util/desktop.spec.d.ts +2 -0
- package/lib/browser/util/desktop.spec.d.ts.map +1 -0
- package/lib/browser/util/desktop.spec.js +67 -0
- package/lib/browser/util/desktop.spec.js.map +1 -0
- package/lib/browser/util/file-drag.d.ts +27 -0
- package/lib/browser/util/file-drag.d.ts.map +1 -0
- package/lib/browser/util/file-drag.js +76 -0
- package/lib/browser/util/file-drag.js.map +1 -0
- package/lib/browser/util/file-drag.spec.d.ts +2 -0
- package/lib/browser/util/file-drag.spec.d.ts.map +1 -0
- package/lib/browser/util/file-drag.spec.js +46 -0
- package/lib/browser/util/file-drag.spec.js.map +1 -0
- package/lib/browser/util/terminal-text.d.ts +34 -0
- package/lib/browser/util/terminal-text.d.ts.map +1 -0
- package/lib/browser/util/terminal-text.js +212 -0
- package/lib/browser/util/terminal-text.js.map +1 -0
- package/lib/browser/util/terminal-text.spec.d.ts +2 -0
- package/lib/browser/util/terminal-text.spec.d.ts.map +1 -0
- package/lib/browser/util/terminal-text.spec.js +54 -0
- package/lib/browser/util/terminal-text.spec.js.map +1 -0
- package/lib/browser/util/tool.d.ts.map +1 -1
- package/lib/browser/util/tool.js +16 -0
- package/lib/browser/util/tool.js.map +1 -1
- package/lib/browser/util/tool.spec.js +12 -0
- package/lib/browser/util/tool.spec.js.map +1 -1
- package/package.json +11 -7
- package/src/browser/chat/chat-protocol.ts +4 -0
- package/src/browser/chat/chat-view.tsx +4 -0
- package/src/browser/chat/chat.spec.ts +31 -0
- package/src/browser/chat/chat.ts +36 -14
- package/src/browser/chat/draft-model.spec.tsx +31 -0
- package/src/browser/chat/draft-model.tsx +63 -0
- package/src/browser/components/app-view.tsx +10 -3
- package/src/browser/components/composer-box.tsx +52 -6
- package/src/browser/components/composer.tsx +1 -0
- package/src/browser/components/dialog-view.tsx +16 -7
- package/src/browser/components/notice-center.tsx +7 -1
- package/src/browser/components/plus-menu.spec.ts +16 -0
- package/src/browser/components/session-view.tsx +65 -4
- package/src/browser/components/terminal-art.tsx +21 -0
- package/src/browser/components/tool-view.tsx +1 -1
- package/src/browser/components/web-composer.tsx +1 -0
- package/src/browser/extension/icons.ts +6 -0
- package/src/browser/extension/web-extension-api.ts +131 -1
- package/src/browser/extension/web-extensions.ts +36 -1
- package/src/browser/i18n/messages.ts +24 -0
- package/src/browser/notices/notices-protocol.ts +3 -0
- package/src/browser/notices/notices.spec.ts +8 -2
- package/src/browser/notices/notices.ts +17 -1
- package/src/browser/style/conversation.ts +16 -1
- package/src/browser/style/layout.ts +5 -0
- package/src/browser/style/shell.ts +1 -0
- package/src/browser/terminal/canvas-font.spec.ts +29 -0
- package/src/browser/terminal/canvas-font.ts +32 -0
- package/src/browser/terminal/terminal-links.spec.ts +32 -0
- package/src/browser/terminal/terminal-links.ts +75 -0
- package/src/browser/terminal/terminal-theme.spec.ts +23 -0
- package/src/browser/terminal/terminal-theme.ts +66 -0
- package/src/browser/terminal/web-terminal.ts +330 -0
- package/src/browser/terminal/xterm.ts +10 -0
- package/src/browser/util/desktop.spec.ts +68 -0
- package/src/browser/util/desktop.ts +96 -0
- package/src/browser/util/file-drag.spec.ts +48 -0
- package/src/browser/util/file-drag.ts +78 -0
- package/src/browser/util/terminal-text.spec.ts +60 -0
- package/src/browser/util/terminal-text.ts +223 -0
- package/src/browser/util/tool.spec.ts +12 -0
- package/src/browser/util/tool.ts +17 -0
|
@@ -93,6 +93,8 @@ export const LAYOUT_STYLE = `
|
|
|
93
93
|
.ta-run-tokens { display: inline-flex; align-items: center; gap: 8px; flex: none; color: var(--ta-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
94
94
|
.ta-run-tokens > span { display: inline-flex; align-items: center; gap: 1px; }
|
|
95
95
|
.ta-run-line { display: flex; min-width: 0; min-height: 22px; padding-left: 6px; margin-top: -6px; }
|
|
96
|
+
/* Under the open steps of a turn the line is one more step: the same pitch, its dot on the string's line. */
|
|
97
|
+
.ta-thread-item:has(> .ta-message > .ta-process:last-child > .ta-process-steps) + .ta-run-line { margin-top: -18px; padding-left: 4.5px; }
|
|
96
98
|
.ta-status-waiting::before { background: var(--ta-yellow); }
|
|
97
99
|
/* On the head of a turn the face of ta already shows that it works. */
|
|
98
100
|
.ta-turn-head .ta-run-status > .ta-status { display: none; }
|
|
@@ -144,6 +146,9 @@ export const LAYOUT_STYLE = `
|
|
|
144
146
|
.ta-plus-line { height: 1px; margin: 5px 8px; border: 0; background: var(--ta-border-muted); flex: none; }
|
|
145
147
|
.ta-plus-back { display: flex; align-items: center; gap: 6px; width: 100%; height: 34px; margin-bottom: 4px; padding: 0 8px; border: 0; border-bottom: 1px solid var(--ta-border-muted); border-radius: 9px 9px 0 0; background: none; color: var(--ta-secondary); font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; }
|
|
146
148
|
.ta-plus-back:hover { color: var(--ta-text); }
|
|
149
|
+
.ta-plus-search { box-sizing: border-box; width: 100%; height: 32px; margin: 0 0 6px; padding: 0 10px; border: 1px solid var(--ta-border); border-radius: 9px; background: var(--ta-main); color: var(--ta-text); font: inherit; font-size: 13px; outline: none; }
|
|
150
|
+
.ta-plus-search:focus { border-color: var(--ta-border-strong, var(--ta-secondary)); }
|
|
151
|
+
.ta-plus-empty { padding: 10px 8px; color: var(--ta-secondary); font-size: 12.5px; }
|
|
147
152
|
@keyframes ta-plus-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
148
153
|
.ta-console-composer textarea { padding: 5px 2px; }
|
|
149
154
|
/* A long placeholder stays on one line, so the empty box keeps its height beside the controls of the +. */
|
|
@@ -133,6 +133,7 @@ export const SHELL_STYLE = `
|
|
|
133
133
|
.ta-toast-message { color: var(--ta-text); overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
134
134
|
.ta-toast-count { margin-left: 6px; padding: 0 5px; border-radius: 6px; background: var(--ta-chip); color: var(--ta-secondary); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
135
135
|
.ta-toast-detail { color: var(--ta-secondary); font-size: 12px; overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
136
|
+
.ta-toast-art { align-self: flex-start; width: 200px; height: auto; margin-top: 8px; border-radius: 10px; }
|
|
136
137
|
.ta-toast-action { align-self: flex-start; margin-top: 6px; height: 26px; padding: 0 10px; border-radius: 7px; border: 1px solid var(--ta-border-field); background: transparent; font-size: 12px; font-weight: 500; transition: background 120ms; }
|
|
137
138
|
.ta-toast-action:hover { background: var(--ta-hover); }
|
|
138
139
|
.ta-toast .ta-toast-close { width: 24px; height: 24px; margin-top: -1px; color: var(--ta-muted); }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { CanvasFontFix } from './canvas-font';
|
|
3
|
+
|
|
4
|
+
/** A context that, as Chromium 156, keeps its font when the new one starts with a space. */
|
|
5
|
+
class StrictContext {
|
|
6
|
+
protected value = '10px sans-serif';
|
|
7
|
+
get font(): string {
|
|
8
|
+
return this.value;
|
|
9
|
+
}
|
|
10
|
+
set font(font: string) {
|
|
11
|
+
if (!/^\s/.test(font)) {
|
|
12
|
+
this.value = font.replace(/^(normal )+/, '');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('CanvasFontFix', () => {
|
|
18
|
+
it('sets the font xterm.js meant, and leaves a valid one as it is', () => {
|
|
19
|
+
const context = new StrictContext();
|
|
20
|
+
context.font = ' normal 24px Menlo';
|
|
21
|
+
expect(context.font).toBe('10px sans-serif');
|
|
22
|
+
CanvasFontFix.install([StrictContext.prototype, undefined]);
|
|
23
|
+
CanvasFontFix.install([StrictContext.prototype]);
|
|
24
|
+
context.font = ' normal 24px Menlo';
|
|
25
|
+
expect(context.font).toBe('24px Menlo');
|
|
26
|
+
context.font = 'italic bold 12px monospace';
|
|
27
|
+
expect(context.font).toBe('italic bold 12px monospace');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const INSTALLED = Symbol.for('ta.canvasFontTrim');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* xterm.js's WebGL renderer sets the font of its canvases as `${italic} ${weight} ${size}px ${family}`,
|
|
5
|
+
* which starts with a space when the text is not italic. Chromium 156 (Electron 45, the desktop app)
|
|
6
|
+
* rejects a font with leading spaces and keeps `10px sans-serif`: the glyphs come out tiny in cells of
|
|
7
|
+
* the right size. Setting `font` without the leading spaces makes it the font xterm.js meant; a font
|
|
8
|
+
* that was valid is unchanged. Remove when @xterm/addon-webgl writes `normal` there.
|
|
9
|
+
*/
|
|
10
|
+
export namespace CanvasFontFix {
|
|
11
|
+
export function install(prototypes: readonly (object | undefined)[] = defaults()): void {
|
|
12
|
+
for (const prototype of prototypes) {
|
|
13
|
+
const target = prototype as Record<PropertyKey, unknown> | undefined;
|
|
14
|
+
const font = target && Object.getOwnPropertyDescriptor(target, 'font');
|
|
15
|
+
if (!target || !font?.set || target[INSTALLED]) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const set = font.set;
|
|
19
|
+
Object.defineProperty(target, 'font', {
|
|
20
|
+
...font,
|
|
21
|
+
set(this: object, value: unknown) {
|
|
22
|
+
set.call(this, typeof value === 'string' ? value.trimStart() : value);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
Object.defineProperty(target, INSTALLED, { value: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function defaults(): (object | undefined)[] {
|
|
30
|
+
return [globalThis.CanvasRenderingContext2D?.prototype, globalThis.OffscreenCanvasRenderingContext2D?.prototype];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { TerminalLinkUtil } from './terminal-links';
|
|
3
|
+
|
|
4
|
+
const texts = (line: string) => TerminalLinkUtil.find(line).map(link => line.slice(link.start, link.end));
|
|
5
|
+
|
|
6
|
+
describe('TerminalLinkUtil', () => {
|
|
7
|
+
it('finds web pages, without the punctuation after them', () => {
|
|
8
|
+
expect(texts('Local: http://localhost:5173/, docs at https://example.com/a?b=1.')).toEqual(['http://localhost:5173/', 'https://example.com/a?b=1']);
|
|
9
|
+
expect(TerminalLinkUtil.find('see (https://x.dev/a)')[0].target).toEqual({ type: 'url', url: 'https://x.dev/a' });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('finds paths with the place compilers and grep print', () => {
|
|
13
|
+
const line = "src/app.ts:12:5 - error TS2304: Cannot find name 'x'.";
|
|
14
|
+
expect(texts(line)).toEqual(['src/app.ts:12:5']);
|
|
15
|
+
expect(TerminalLinkUtil.find(line)[0].target).toEqual({ type: 'path', path: 'src/app.ts', selection: { startLine: 12, startColumn: 5 } });
|
|
16
|
+
expect(texts('src/a.ts(10,5): error')).toEqual(['src/a.ts(10,5)']);
|
|
17
|
+
expect(TerminalLinkUtil.find('src/a.ts(10,5): error')[0].target).toMatchObject({ path: 'src/a.ts', selection: { startLine: 10, startColumn: 5 } });
|
|
18
|
+
expect(texts('lib/x.js:3:const a = 1')).toEqual(['lib/x.js:3']);
|
|
19
|
+
expect(texts('modified: ./docs/README.md and /etc/hosts, AGENTS.md.')).toEqual(['./docs/README.md', '/etc/hosts', 'AGENTS.md']);
|
|
20
|
+
expect(texts('cd docs/ ; ls')).toEqual(['docs/']);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('leaves out what is no file', () => {
|
|
24
|
+
expect(texts('v4.6.1 1.2.3 e.g. console.log ~/x deepseek-flash <inline:ta> Node.js /help and/or')).toEqual([]);
|
|
25
|
+
expect(texts('see https://example.com/src/a.ts')).toEqual(['https://example.com/src/a.ts']);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('reads the directory of OSC 7', () => {
|
|
29
|
+
expect(TerminalLinkUtil.cwdOf('file://mac.local/Users/me/my%20app')).toBe('/Users/me/my app');
|
|
30
|
+
expect(TerminalLinkUtil.cwdOf('nonsense')).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type LinkTarget, LinkUtil } from '../util/link';
|
|
2
|
+
|
|
3
|
+
/** A link in a line of a terminal: `text.slice(start, end)`. */
|
|
4
|
+
export interface TerminalLink {
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
target: LinkTarget;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const URL_START = /\b(?:https?|file):\/\//gi;
|
|
11
|
+
/** What a path may be in: anything up to a space, a quote or a bracket. */
|
|
12
|
+
const WORD = /[^\s'"`<>()[\]{}|;,]+/g;
|
|
13
|
+
/** A path at the start of a word, with the line and column a compiler or grep prints after it (`:10`, `:10:5`). */
|
|
14
|
+
const PATH = /^(?:\.{1,2}\/|\/)?(?:[\w@+.-]+\/)*[\w@+.-]+\/?(?::\d+(?::\d+)?)?/;
|
|
15
|
+
/** A path no one mistakes for a word, with or without an extension: `./x`, `../x`, `/etc/hosts` (not `/help`). */
|
|
16
|
+
const EXPLICIT = /^(?:\.{1,2}\/[^/]|\/[^/]+\/[^/])/;
|
|
17
|
+
/** TypeScript's and MSBuild's `(10,5)` right after a path. */
|
|
18
|
+
const PARENS = /^\((\d+)(?:,(\d+))?\)/;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The links of a line of a terminal, as VS Code finds them: http(s) and file URLs, and paths of files
|
|
22
|
+
* (`src/a.ts:10:5`, `./b.md`, `/etc/hosts`, `docs/`) with the place a compiler or grep prints after
|
|
23
|
+
* them. A name alone is a file only with an extension files have (LinkUtil.codeReference); a path
|
|
24
|
+
* starting with `./`, `../` or two names under `/` is one with any name.
|
|
25
|
+
*/
|
|
26
|
+
export namespace TerminalLinkUtil {
|
|
27
|
+
export function find(text: string): TerminalLink[] {
|
|
28
|
+
const links: TerminalLink[] = [];
|
|
29
|
+
for (const found of text.matchAll(URL_START)) {
|
|
30
|
+
const start = found.index;
|
|
31
|
+
const url = /^file:/i.test(found[0]) ? /^file:\/\/[^\s'"`<>]+/i.exec(text.slice(start))?.[0] : LinkUtil.bareUrl(text.slice(start));
|
|
32
|
+
const target = url && LinkUtil.parse(url);
|
|
33
|
+
if (target) {
|
|
34
|
+
links.push({ start, end: start + url.length, target });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const inUrl = (index: number) => links.some(link => index >= link.start && index < link.end);
|
|
38
|
+
const urls = links.length;
|
|
39
|
+
for (const word of text.matchAll(WORD)) {
|
|
40
|
+
if (inUrl(word.index)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const path = PATH.exec(word[0])?.[0].replace(/[.:]+$/, '');
|
|
44
|
+
if (!path) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const start = word.index;
|
|
48
|
+
let end = start + path.length;
|
|
49
|
+
let reference = path;
|
|
50
|
+
const parens = path.length === word[0].length ? PARENS.exec(text.slice(end)) : null;
|
|
51
|
+
if (parens && !/:\d+$/.test(path)) {
|
|
52
|
+
reference = `${path}:${parens[1]}${parens[2] ? `:${parens[2]}` : ''}`;
|
|
53
|
+
end += parens[0].length;
|
|
54
|
+
}
|
|
55
|
+
const target = LinkUtil.codeReference(reference) ?? (EXPLICIT.test(reference) ? LinkUtil.parse(reference) : undefined);
|
|
56
|
+
if (target) {
|
|
57
|
+
links.push({ start, end, target });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return urls === links.length ? links : links.sort((a, b) => a.start - b.start);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `file://host/path` of OSC 7, the directory a shell says it is in. */
|
|
64
|
+
export function cwdOf(osc7: string): string | undefined {
|
|
65
|
+
const found = /^file:\/\/[^/]*(\/.*)$/i.exec(osc7.trim());
|
|
66
|
+
if (!found) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return decodeURIComponent(found[1]);
|
|
71
|
+
} catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { TerminalTheme } from './terminal-theme';
|
|
3
|
+
|
|
4
|
+
describe('TerminalTheme', () => {
|
|
5
|
+
it("takes the page's background and text, and VS Code's colours for the theme", () => {
|
|
6
|
+
const page: Record<string, string> = { '--ta-code': ' #161618', '--ta-text': '#e5e5e5', '--ta-mono': " 'SF Mono', monospace" };
|
|
7
|
+
const dark = TerminalTheme.of('dark', name => page[name] ?? '');
|
|
8
|
+
expect(dark.background).toBe('#161618');
|
|
9
|
+
expect(dark.foreground).toBe('#e5e5e5');
|
|
10
|
+
expect(dark.cursor).toBe('#e5e5e5');
|
|
11
|
+
expect(dark.cursorAccent).toBe('#161618');
|
|
12
|
+
expect(dark.green).toBe('#0dbc79');
|
|
13
|
+
expect(TerminalTheme.fontFamily(name => page[name] ?? '')).toBe("'SF Mono', monospace");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('has colours of its own when the page has none yet', () => {
|
|
17
|
+
const light = TerminalTheme.of('light', () => '');
|
|
18
|
+
expect(light.background).toBe('#f7f8fa');
|
|
19
|
+
expect(light.foreground).toBe('#1a1a1a');
|
|
20
|
+
expect(light.blue).toBe('#0451a5');
|
|
21
|
+
expect(TerminalTheme.fontFamily(() => '')).toBe('monospace');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ITheme } from '@xterm/xterm';
|
|
2
|
+
import type { Theme } from '../settings/settings-protocol';
|
|
3
|
+
|
|
4
|
+
/** The sixteen colours of VS Code's terminal, for a dark and a light page. */
|
|
5
|
+
const ANSI: Record<Theme, ITheme> = {
|
|
6
|
+
dark: {
|
|
7
|
+
black: '#000000',
|
|
8
|
+
red: '#cd3131',
|
|
9
|
+
green: '#0dbc79',
|
|
10
|
+
yellow: '#e5e510',
|
|
11
|
+
blue: '#2472c8',
|
|
12
|
+
magenta: '#bc3fbc',
|
|
13
|
+
cyan: '#11a8cd',
|
|
14
|
+
white: '#e5e5e5',
|
|
15
|
+
brightBlack: '#666666',
|
|
16
|
+
brightRed: '#f14c4c',
|
|
17
|
+
brightGreen: '#23d18b',
|
|
18
|
+
brightYellow: '#f5f543',
|
|
19
|
+
brightBlue: '#3b8eea',
|
|
20
|
+
brightMagenta: '#d670d6',
|
|
21
|
+
brightCyan: '#29b8db',
|
|
22
|
+
brightWhite: '#e5e5e5',
|
|
23
|
+
selectionBackground: '#264f78',
|
|
24
|
+
selectionInactiveBackground: '#3a3d41',
|
|
25
|
+
scrollbarSliderBackground: 'rgba(121, 121, 121, 0.4)',
|
|
26
|
+
scrollbarSliderHoverBackground: 'rgba(100, 100, 100, 0.7)',
|
|
27
|
+
scrollbarSliderActiveBackground: 'rgba(191, 191, 191, 0.4)',
|
|
28
|
+
},
|
|
29
|
+
light: {
|
|
30
|
+
black: '#000000',
|
|
31
|
+
red: '#cd3131',
|
|
32
|
+
green: '#00bc00',
|
|
33
|
+
yellow: '#949800',
|
|
34
|
+
blue: '#0451a5',
|
|
35
|
+
magenta: '#bc05bc',
|
|
36
|
+
cyan: '#0598bc',
|
|
37
|
+
white: '#555555',
|
|
38
|
+
brightBlack: '#666666',
|
|
39
|
+
brightRed: '#cd3131',
|
|
40
|
+
brightGreen: '#14ce14',
|
|
41
|
+
brightYellow: '#b5ba00',
|
|
42
|
+
brightBlue: '#0451a5',
|
|
43
|
+
brightMagenta: '#bc05bc',
|
|
44
|
+
brightCyan: '#0598bc',
|
|
45
|
+
brightWhite: '#a5a5a5',
|
|
46
|
+
selectionBackground: '#add6ff',
|
|
47
|
+
selectionInactiveBackground: '#e5ebf1',
|
|
48
|
+
scrollbarSliderBackground: 'rgba(100, 100, 100, 0.4)',
|
|
49
|
+
scrollbarSliderHoverBackground: 'rgba(100, 100, 100, 0.7)',
|
|
50
|
+
scrollbarSliderActiveBackground: 'rgba(0, 0, 0, 0.6)',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export namespace TerminalTheme {
|
|
55
|
+
/** The colours of a terminal on the page: its background and text are the page's (`--ta-*`), the rest VS Code's. */
|
|
56
|
+
export function of(theme: Theme, variable: (name: string) => string): ITheme {
|
|
57
|
+
const background = variable('--ta-code').trim() || (theme === 'dark' ? '#161618' : '#f7f8fa');
|
|
58
|
+
const foreground = variable('--ta-text').trim() || (theme === 'dark' ? '#e5e5e5' : '#1a1a1a');
|
|
59
|
+
return { ...ANSI[theme], background, foreground, cursor: foreground, cursorAccent: background };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The font of the page's code: `--ta-mono`. */
|
|
63
|
+
export function fontFamily(variable: (name: string) => string): string {
|
|
64
|
+
return variable('--ta-mono').trim() || 'monospace';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import type { FitAddon } from '@xterm/addon-fit';
|
|
2
|
+
import type { IBufferCellPosition, ILink, Terminal } from '@xterm/xterm';
|
|
3
|
+
import type { WebTerminal, WebTerminalOptions } from '../extension/web-extension-api';
|
|
4
|
+
import { message } from '../i18n/messages';
|
|
5
|
+
import type { OpenLinkOptions } from '../links/links-protocol';
|
|
6
|
+
import { ShortcutUtil } from '../settings/agent-settings';
|
|
7
|
+
import type { TaSettings } from '../settings/settings-protocol';
|
|
8
|
+
import type { LinkTarget } from '../util/link';
|
|
9
|
+
import { TerminalLinkUtil } from './terminal-links';
|
|
10
|
+
import { TerminalTheme } from './terminal-theme';
|
|
11
|
+
|
|
12
|
+
type XtermModule = typeof import('./xterm.js');
|
|
13
|
+
|
|
14
|
+
/** Loads xterm.js the first time a terminal is made, not with the page. */
|
|
15
|
+
export namespace XtermLoader {
|
|
16
|
+
let loading: Promise<XtermModule> | undefined;
|
|
17
|
+
|
|
18
|
+
export function load(): Promise<XtermModule> {
|
|
19
|
+
loading ??= import('./xterm.js').catch(error => {
|
|
20
|
+
loading = undefined;
|
|
21
|
+
throw error;
|
|
22
|
+
});
|
|
23
|
+
return loading;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_COLS = 80;
|
|
28
|
+
const DEFAULT_ROWS = 24;
|
|
29
|
+
const SCROLLBACK = 5000;
|
|
30
|
+
/** Rows of one line that wraps, at most, read for its links. */
|
|
31
|
+
const MAX_WRAPPED_ROWS = 50;
|
|
32
|
+
/** A view being dragged to a new size fits once it stops. */
|
|
33
|
+
const FIT_MS = 50;
|
|
34
|
+
|
|
35
|
+
/** Opens what a link points to, as a link of a reply (TaLinks.open). */
|
|
36
|
+
export type TerminalLinkOpener = (target: LinkTarget, options: OpenLinkOptions) => void;
|
|
37
|
+
|
|
38
|
+
function cssVariable(name: string): string {
|
|
39
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A terminal as VS Code's: xterm.js on the GPU (its WebGL renderer; the DOM one when the browser has
|
|
44
|
+
* no WebGL or loses it), in the theme of the page, which it follows. It opens in an element of its own
|
|
45
|
+
* that moves from view to view, so that it keeps its screen and scrollback while no view shows it.
|
|
46
|
+
* URLs and paths of files are links, opened with Cmd or Ctrl + click, from the directory the shell says
|
|
47
|
+
* it is in (OSC 7), else the one it started in.
|
|
48
|
+
*/
|
|
49
|
+
export class DefaultWebTerminal implements WebTerminal {
|
|
50
|
+
cols: number;
|
|
51
|
+
rows: number;
|
|
52
|
+
/** Where paths of its links are read from. */
|
|
53
|
+
protected cwd?: string;
|
|
54
|
+
protected term?: Terminal;
|
|
55
|
+
protected fit?: FitAddon;
|
|
56
|
+
protected module?: XtermModule;
|
|
57
|
+
protected opened = false;
|
|
58
|
+
protected disposed = false;
|
|
59
|
+
/** Focus asked for before xterm.js opened: it takes it once it does. */
|
|
60
|
+
protected wantsFocus = false;
|
|
61
|
+
/** Output that came before xterm.js loaded. */
|
|
62
|
+
protected pending: string[] = [];
|
|
63
|
+
protected readonly dataListeners = new Set<(data: string) => void>();
|
|
64
|
+
protected readonly resizeListeners = new Set<(size: { cols: number; rows: number }) => void>();
|
|
65
|
+
protected readonly host = document.createElement('div');
|
|
66
|
+
protected parent?: HTMLElement;
|
|
67
|
+
protected observer?: ResizeObserver;
|
|
68
|
+
protected fitTimer?: ReturnType<typeof setTimeout>;
|
|
69
|
+
protected unsubscribe?: () => void;
|
|
70
|
+
|
|
71
|
+
constructor(
|
|
72
|
+
protected readonly settings: TaSettings,
|
|
73
|
+
protected readonly openLink: TerminalLinkOpener,
|
|
74
|
+
options: WebTerminalOptions = {},
|
|
75
|
+
) {
|
|
76
|
+
this.cols = options.cols ?? DEFAULT_COLS;
|
|
77
|
+
this.rows = options.rows ?? DEFAULT_ROWS;
|
|
78
|
+
this.cwd = options.cwd;
|
|
79
|
+
this.host.className = 'ta-terminal';
|
|
80
|
+
XtermLoader.load().then(
|
|
81
|
+
module => this.start(module),
|
|
82
|
+
error => console.error('The terminal did not load:', error),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
write(data: string): void {
|
|
87
|
+
if (this.term) {
|
|
88
|
+
this.term.write(data);
|
|
89
|
+
} else if (!this.disposed) {
|
|
90
|
+
this.pending.push(data);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onData(listener: (data: string) => void): () => void {
|
|
95
|
+
this.dataListeners.add(listener);
|
|
96
|
+
return () => this.dataListeners.delete(listener);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
onResize(listener: (size: { cols: number; rows: number }) => void): () => void {
|
|
100
|
+
this.resizeListeners.add(listener);
|
|
101
|
+
return () => this.resizeListeners.delete(listener);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
attach(element: HTMLElement): () => void {
|
|
105
|
+
this.detach();
|
|
106
|
+
this.parent = element;
|
|
107
|
+
element.appendChild(this.host);
|
|
108
|
+
this.observer = new ResizeObserver(() => this.layout());
|
|
109
|
+
this.observer.observe(element);
|
|
110
|
+
this.open();
|
|
111
|
+
this.layout();
|
|
112
|
+
return () => {
|
|
113
|
+
if (this.parent === element) {
|
|
114
|
+
this.detach();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
focus(): void {
|
|
120
|
+
if (this.opened) {
|
|
121
|
+
this.term?.focus();
|
|
122
|
+
} else {
|
|
123
|
+
this.wantsFocus = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
dispose(): void {
|
|
128
|
+
this.disposed = true;
|
|
129
|
+
this.detach();
|
|
130
|
+
this.unsubscribe?.();
|
|
131
|
+
this.term?.dispose();
|
|
132
|
+
this.pending = [];
|
|
133
|
+
this.dataListeners.clear();
|
|
134
|
+
this.resizeListeners.clear();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
protected start(module: XtermModule): void {
|
|
138
|
+
if (this.disposed) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.module = module;
|
|
142
|
+
const term = new module.Terminal({
|
|
143
|
+
cols: this.cols,
|
|
144
|
+
rows: this.rows,
|
|
145
|
+
// The Unicode 11 widths of emoji and CJK, as programs reckon them.
|
|
146
|
+
allowProposedApi: true,
|
|
147
|
+
fontFamily: TerminalTheme.fontFamily(cssVariable),
|
|
148
|
+
fontSize: 12,
|
|
149
|
+
lineHeight: 1.2,
|
|
150
|
+
scrollback: SCROLLBACK,
|
|
151
|
+
cursorBlink: true,
|
|
152
|
+
cursorInactiveStyle: 'outline',
|
|
153
|
+
minimumContrastRatio: 4.5,
|
|
154
|
+
drawBoldTextInBrightColors: true,
|
|
155
|
+
rescaleOverlappingGlyphs: true,
|
|
156
|
+
theme: TerminalTheme.of(this.settings.state.theme, cssVariable),
|
|
157
|
+
});
|
|
158
|
+
const unicode = new module.Unicode11Addon();
|
|
159
|
+
term.loadAddon(unicode);
|
|
160
|
+
term.unicode.activeVersion = '11';
|
|
161
|
+
this.fit = new module.FitAddon();
|
|
162
|
+
term.loadAddon(this.fit);
|
|
163
|
+
// Shift+Enter as the kitty keyboard protocol writes it: a new line in the editor of pi (ta), not a send.
|
|
164
|
+
term.attachCustomKeyEventHandler(event => {
|
|
165
|
+
if (event.key === 'Enter' && event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
|
166
|
+
if (event.type === 'keydown') {
|
|
167
|
+
this.emitData('\x1b[13;2u');
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
});
|
|
173
|
+
term.onData(data => this.emitData(data));
|
|
174
|
+
// Mouse reports in the old encoding, bytes over 127: as characters, which the server sends as they are.
|
|
175
|
+
term.onBinary(data => this.emitData(data));
|
|
176
|
+
term.onResize(size => {
|
|
177
|
+
this.cols = size.cols;
|
|
178
|
+
this.rows = size.rows;
|
|
179
|
+
for (const listener of this.resizeListeners) {
|
|
180
|
+
listener(size);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
term.parser.registerOscHandler(7, data => {
|
|
184
|
+
this.cwd = TerminalLinkUtil.cwdOf(data) ?? this.cwd;
|
|
185
|
+
return true;
|
|
186
|
+
});
|
|
187
|
+
term.registerLinkProvider({ provideLinks: (y, callback) => callback(this.linksAt(term, y)) });
|
|
188
|
+
for (const data of this.pending) {
|
|
189
|
+
term.write(data);
|
|
190
|
+
}
|
|
191
|
+
this.pending = [];
|
|
192
|
+
this.term = term;
|
|
193
|
+
this.unsubscribe = this.settings.subscribe(state => {
|
|
194
|
+
term.options.theme = TerminalTheme.of(state.theme, cssVariable);
|
|
195
|
+
});
|
|
196
|
+
this.open();
|
|
197
|
+
this.layout();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Opens xterm.js where it can measure its font: once, the first time it is in the page with a size
|
|
202
|
+
* (a hidden tab has none; ResizeObserver calls again when it shows).
|
|
203
|
+
*/
|
|
204
|
+
protected open(): void {
|
|
205
|
+
const { term, module, parent } = this;
|
|
206
|
+
if (!term || !module || this.opened || !parent?.clientWidth || !parent.clientHeight) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.opened = true;
|
|
210
|
+
term.open(this.host);
|
|
211
|
+
const input = this.host.querySelector('.xterm-helper-textarea');
|
|
212
|
+
for (const name of ['autocomplete', 'autocorrect', 'autocapitalize']) {
|
|
213
|
+
input?.setAttribute(name, 'off');
|
|
214
|
+
}
|
|
215
|
+
input?.setAttribute('spellcheck', 'false');
|
|
216
|
+
try {
|
|
217
|
+
const webgl = new module.WebglAddon();
|
|
218
|
+
webgl.onContextLoss(() => webgl.dispose());
|
|
219
|
+
term.loadAddon(webgl);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.warn('No WebGL for the terminal; it draws with the DOM:', error);
|
|
222
|
+
}
|
|
223
|
+
// After the renderer: WebGL measures characters otherwise than the DOM, so the size it fits differs.
|
|
224
|
+
this.fit?.fit();
|
|
225
|
+
if (this.wantsFocus) {
|
|
226
|
+
this.wantsFocus = false;
|
|
227
|
+
term.focus();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Opens it the first time its view has a size, and fits it to the view after that, once the size
|
|
233
|
+
* settled; a hidden view (no size) keeps the size it had. Timers, not frames: a window in the
|
|
234
|
+
* background gets no frames, and the terminal would stay unopened there.
|
|
235
|
+
*/
|
|
236
|
+
protected layout(): void {
|
|
237
|
+
if (!this.parent?.clientWidth || !this.parent.clientHeight) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!this.opened) {
|
|
241
|
+
this.open();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
clearTimeout(this.fitTimer);
|
|
245
|
+
this.fitTimer = setTimeout(() => {
|
|
246
|
+
const size = this.parent?.clientWidth && this.parent.clientHeight ? this.fit?.proposeDimensions() : undefined;
|
|
247
|
+
if (size && (size.cols !== this.term?.cols || size.rows !== this.term?.rows)) {
|
|
248
|
+
this.fit?.fit();
|
|
249
|
+
}
|
|
250
|
+
}, FIT_MS);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The links of the line (with the rows it wraps onto) that crosses row `y` (from 1). */
|
|
254
|
+
protected linksAt(term: Terminal, y: number): ILink[] | undefined {
|
|
255
|
+
const buffer = term.buffer.active;
|
|
256
|
+
let first = y - 1;
|
|
257
|
+
while (first > 0 && y - 1 - first < MAX_WRAPPED_ROWS && buffer.getLine(first)?.isWrapped) {
|
|
258
|
+
first--;
|
|
259
|
+
}
|
|
260
|
+
// The text of the line, and where each of its characters (UTF-16 units) is on the screen.
|
|
261
|
+
let text = '';
|
|
262
|
+
const places: (IBufferCellPosition & { width: number })[] = [];
|
|
263
|
+
for (let row = first; row - first < MAX_WRAPPED_ROWS; row++) {
|
|
264
|
+
const line = buffer.getLine(row);
|
|
265
|
+
if (!line || (row > first && !line.isWrapped)) {
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
for (let x = 0; x < line.length; x++) {
|
|
269
|
+
const cell = line.getCell(x);
|
|
270
|
+
const width = cell?.getWidth() ?? 1;
|
|
271
|
+
if (!cell || width === 0) {
|
|
272
|
+
// The second half of a wide character.
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const chars = cell.getChars() || ' ';
|
|
276
|
+
for (let unit = 0; unit < chars.length; unit++) {
|
|
277
|
+
places.push({ x: x + 1, y: row + 1, width });
|
|
278
|
+
}
|
|
279
|
+
text += chars;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const links = TerminalLinkUtil.find(text).filter(link => places[link.start].y <= y && places[link.end - 1].y >= y);
|
|
283
|
+
if (!links.length) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
const mac = ShortcutUtil.isMac();
|
|
287
|
+
return links.map(link => {
|
|
288
|
+
const start = places[link.start];
|
|
289
|
+
const last = places[link.end - 1];
|
|
290
|
+
const hint =
|
|
291
|
+
link.target.type === 'path'
|
|
292
|
+
? mac
|
|
293
|
+
? 'ta.terminal.openLinkMac'
|
|
294
|
+
: 'ta.terminal.openLink'
|
|
295
|
+
: mac
|
|
296
|
+
? 'ta.terminal.openUrlMac'
|
|
297
|
+
: 'ta.terminal.openUrl';
|
|
298
|
+
return {
|
|
299
|
+
range: { start: { x: start.x, y: start.y }, end: { x: last.x + last.width - 1, y: last.y } },
|
|
300
|
+
text: text.slice(link.start, link.end),
|
|
301
|
+
decorations: { underline: true, pointerCursor: true },
|
|
302
|
+
activate: (event: MouseEvent) => {
|
|
303
|
+
if (mac ? event.metaKey : event.ctrlKey) {
|
|
304
|
+
this.openLink(link.target, { cwd: this.cwd, external: event.shiftKey && link.target.type !== 'path' });
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
hover: () => {
|
|
308
|
+
this.host.title = message(this.settings.state.language, hint);
|
|
309
|
+
},
|
|
310
|
+
leave: () => {
|
|
311
|
+
this.host.title = '';
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
protected detach(): void {
|
|
318
|
+
this.observer?.disconnect();
|
|
319
|
+
this.observer = undefined;
|
|
320
|
+
clearTimeout(this.fitTimer);
|
|
321
|
+
this.host.remove();
|
|
322
|
+
this.parent = undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
protected emitData(data: string): void {
|
|
326
|
+
for (const listener of this.dataListeners) {
|
|
327
|
+
listener(data);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// What a terminal loads the first time one is made: xterm.js, its WebGL renderer and its styles.
|
|
2
|
+
import '@xterm/xterm/css/xterm.css';
|
|
3
|
+
import { CanvasFontFix } from './canvas-font';
|
|
4
|
+
|
|
5
|
+
CanvasFontFix.install();
|
|
6
|
+
|
|
7
|
+
export { FitAddon } from '@xterm/addon-fit';
|
|
8
|
+
export { Unicode11Addon } from '@xterm/addon-unicode11';
|
|
9
|
+
export { WebglAddon } from '@xterm/addon-webgl';
|
|
10
|
+
export { Terminal } from '@xterm/xterm';
|