Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ StandTerm vendors xterm.js runtime files under `static/` so the terminal works
without a CDN:

- `@xterm/xterm` 6.0.0: `static/js/xterm.js`, `static/css/xterm.css`
- `@xterm/addon-webgl` 0.19.0: `static/js/xterm-addon-webgl.js`
- `@xterm/addon-fit` 0.11.0: `static/js/xterm-addon-fit.js`
- `@xterm/addon-web-links` 0.12.0: `static/js/xterm-addon-web-links.js`
- Powerline Symbols: `static/fonts/PowerlineSymbols.otf` (optional prompt-symbol fallback)
Expand Down
6 changes: 3 additions & 3 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ This project incorporates components from the following projects:
- **License**: MIT
- **Copyright**: (c) 2017-2026 The xterm.js authors
- **Project**: https://github.com/xtermjs/xterm.js
- **Vendored files**: `static/js/xterm.js`, `static/js/xterm-addon-fit.js`, `static/js/xterm-addon-web-links.js`, `static/css/xterm.css`
- **Versions**: `@xterm/xterm` 6.0.0, `@xterm/addon-fit` 0.11.0, `@xterm/addon-web-links` 0.12.0
- **License text**: `static/licenses/xtermjs-MIT-LICENSE.txt`
- **Vendored files**: `static/js/xterm.js`, `static/js/xterm-addon-webgl.js`, `static/js/xterm-addon-fit.js`, `static/js/xterm-addon-web-links.js`, `static/css/xterm.css`
- **Versions**: `@xterm/xterm` 6.0.0, `@xterm/addon-webgl` 0.19.0, `@xterm/addon-fit` 0.11.0, `@xterm/addon-web-links` 0.12.0
- **License texts**: `static/licenses/xtermjs-MIT-LICENSE.txt`, `static/licenses/xterm-addon-webgl-MIT-LICENSE.txt`

## Socket.IO
- **License**: MIT
Expand Down
5 changes: 3 additions & 2 deletions static/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ assets from a CDN.
Current sources:

- `xterm.js`: `@xterm/xterm` 6.0.0
- `xterm-addon-webgl.js`: `@xterm/addon-webgl` 0.19.0
- `xterm-addon-fit.js`: `@xterm/addon-fit` 0.11.0
- `xterm-addon-web-links.js`: `@xterm/addon-web-links` 0.12.0

Expand All @@ -18,6 +19,6 @@ Source checkout:
The JavaScript bundles are copied from the npm package `lib/` output. The
matching stylesheet is copied to `../css/xterm.css` from `@xterm/xterm`.

xterm.js and its addons are MIT licensed. Keep
`../licenses/xtermjs-MIT-LICENSE.txt` and the xterm.js section in
xterm.js and its addons are MIT licensed. Keep the matching files under
`../licenses/` and the xterm.js section in
`../../THIRD-PARTY-NOTICES.md` when releasing these files.
2 changes: 2 additions & 0 deletions static/js/xterm-addon-webgl.js

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions static/licenses/xterm-addon-webgl-MIT-LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) 2018, The xterm.js authors (https://github.com/xtermjs/xterm.js)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
30 changes: 27 additions & 3 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1247,11 +1247,13 @@ <h3>Session expired</h3>
async function initApp() {
try {
await loadScript('/static/js/xterm.js');
document.getElementById('progress-bar').style.width = '25%';
document.getElementById('progress-bar').style.width = '20%';
await loadScript('/static/js/xterm-addon-webgl.js');
document.getElementById('progress-bar').style.width = '40%';
await loadScript('/static/js/socket.io.min.js');
document.getElementById('progress-bar').style.width = '50%';
document.getElementById('progress-bar').style.width = '60%';
await loadScript('/static/js/xterm-addon-fit.js');
document.getElementById('progress-bar').style.width = '75%';
document.getElementById('progress-bar').style.width = '80%';
await loadScript('/static/js/xterm-addon-web-links.js');
document.getElementById('progress-bar').style.width = '100%';
} catch (e) {
Expand Down Expand Up @@ -2317,6 +2319,24 @@ <h3>Session expired</h3>
return term;
}

function enableWebglRenderer(state) {
if (!state || !state.term || typeof WebglAddon === 'undefined') return;
let addon = null;
try {
// Keep the drawing buffer readable for Agent viewport PNG capture.
addon = new WebglAddon.WebglAddon(true);
addon.onContextLoss(() => {
addon.dispose();
if (state.webglAddon === addon) state.webglAddon = null;
});
state.term.loadAddon(addon);
state.webglAddon = addon;
} catch (error) {
if (addon) addon.dispose();
console.warn('WebGL terminal renderer unavailable; using DOM renderer.', error);
}
}

function syncAgentTerminalMirrorSize(state) {
if (!state || !state.agentTerminalMirror || !state.term) return;
const cols = state.term.cols;
Expand Down Expand Up @@ -3496,6 +3516,7 @@ <h3>Session expired</h3>
titleEl,
term,
fitAddon,
webglAddon: null,
webLinksAddon: null,
inPip: false,
pipWindow: null,
Expand All @@ -3509,6 +3530,7 @@ <h3>Session expired</h3>
pipPagehideHandler: null,
pipResizeHandler: null
};
enableWebglRenderer(state);
terminals.set(terminalId, state);
configureWebLinks(state);
syncAgentTerminalMirrorSize(state);
Expand Down Expand Up @@ -3539,6 +3561,7 @@ <h3>Session expired</h3>
});
detachAgentFromTerminal(state, { notifyServer });
if (state.webLinksAddon) state.webLinksAddon.dispose();
if (state.webglAddon) state.webglAddon.dispose();
if (state.agentTerminalMirror) state.agentTerminalMirror.dispose();
state.term.dispose();
state.container.remove();
Expand Down Expand Up @@ -3729,6 +3752,7 @@ <h3>Session expired</h3>
fontSize: state.term.options.fontSize,
fontWeight: state.term.options.fontWeight,
colorScheme: prefs.colorScheme,
renderer: state.webglAddon ? 'webgl' : 'dom',
mirrorCursorStyle: state.agentTerminalMirror
? state.agentTerminalMirror.options.cursorStyle
: null,
Expand Down
54 changes: 54 additions & 0 deletions tests/agent_browser_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,59 @@ def test_powerline_symbol_fallback_is_optional_and_applies_immediately(browser,
close_context(context)


def test_webgl_renderer_closes_block_glyph_row_gaps(browser, access_url):
context, page = new_page(browser, access_url)
try:
options = page.evaluate("() => window.terminalTest.getActiveTerminalOptions()")
check(options['renderer'] == 'webgl', 'terminal did not activate the WebGL renderer')

page.evaluate(
"""() => window.terminalTest.writeTerminalOutput(
'\\x1b[2J\\x1b[H\\x1b[38;2;215;135;135m'
+ '████████\\r\\n████████\\r\\n████████'
+ '\\x1b[0m'
)"""
)
page.wait_for_timeout(200)
pixels = page.evaluate(
"""() => {
const canvases = Array.from(document.querySelectorAll(
'.terminal-pane.active .xterm-screen canvas'
));
const canvas = canvases[canvases.length - 1];
if (!canvas) return { error: 'missing_canvas' };
const gl = canvas.getContext('webgl2');
if (!gl) return { error: 'missing_webgl_context' };
const rgba = new Uint8Array(canvas.width * canvas.height * 4);
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, rgba);
const coloredRows = [];
for (let y = 0; y < canvas.height; y += 1) {
let count = 0;
for (let x = 0; x < canvas.width; x += 1) {
const offset = (y * canvas.width + x) * 4;
const red = rgba[offset];
const green = rgba[offset + 1];
const blue = rgba[offset + 2];
if (red > 180 && green >= 100 && green <= 170 && blue >= 100 && blue <= 170) {
count += 1;
}
}
if (count >= 20) coloredRows.push(y);
}
let maxStep = 0;
for (let index = 1; index < coloredRows.length; index += 1) {
maxStep = Math.max(maxStep, coloredRows[index] - coloredRows[index - 1]);
}
return { coloredRowCount: coloredRows.length, maxStep };
}"""
)
check(not pixels.get('error'), f"could not inspect WebGL terminal pixels: {pixels.get('error')}")
check(pixels['coloredRowCount'] > 20, 'block glyph fixture did not render enough colored rows')
check(pixels['maxStep'] == 1, 'block glyphs retained a blank pixel row between terminal cells')
finally:
close_context(context)


def test_cursor_type_setting_updates_existing_and_new_terminals(browser, access_url):
context, page = new_page(browser, access_url)
try:
Expand Down Expand Up @@ -1508,6 +1561,7 @@ def main():
test_cjk_width_compatibility_defaults_off,
test_windows_font_fallback_defaults_and_migrates_legacy,
test_powerline_symbol_fallback_is_optional_and_applies_immediately,
test_webgl_renderer_closes_block_glyph_row_gaps,
test_cursor_type_setting_updates_existing_and_new_terminals,
test_settings_server_tab_loads_readonly_snapshot,
test_connection_diagnostics_are_session_scoped_and_redacted,
Expand Down
Loading