Skip to content

Commit c837c83

Browse files
committed
Render terminal block glyphs without row gaps
1 parent 84c19ac commit c837c83

7 files changed

Lines changed: 109 additions & 8 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ StandTerm vendors xterm.js runtime files under `static/` so the terminal works
512512
without a CDN:
513513

514514
- `@xterm/xterm` 6.0.0: `static/js/xterm.js`, `static/css/xterm.css`
515+
- `@xterm/addon-webgl` 0.19.0: `static/js/xterm-addon-webgl.js`
515516
- `@xterm/addon-fit` 0.11.0: `static/js/xterm-addon-fit.js`
516517
- `@xterm/addon-web-links` 0.12.0: `static/js/xterm-addon-web-links.js`
517518
- Powerline Symbols: `static/fonts/PowerlineSymbols.otf` (optional prompt-symbol fallback)

THIRD-PARTY-NOTICES.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ This project incorporates components from the following projects:
66
- **License**: MIT
77
- **Copyright**: (c) 2017-2026 The xterm.js authors
88
- **Project**: https://github.com/xtermjs/xterm.js
9-
- **Vendored files**: `static/js/xterm.js`, `static/js/xterm-addon-fit.js`, `static/js/xterm-addon-web-links.js`, `static/css/xterm.css`
10-
- **Versions**: `@xterm/xterm` 6.0.0, `@xterm/addon-fit` 0.11.0, `@xterm/addon-web-links` 0.12.0
11-
- **License text**: `static/licenses/xtermjs-MIT-LICENSE.txt`
9+
- **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`
10+
- **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
11+
- **License texts**: `static/licenses/xtermjs-MIT-LICENSE.txt`, `static/licenses/xterm-addon-webgl-MIT-LICENSE.txt`
1212

1313
## Socket.IO
1414
- **License**: MIT

static/js/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ assets from a CDN.
66
Current sources:
77

88
- `xterm.js`: `@xterm/xterm` 6.0.0
9+
- `xterm-addon-webgl.js`: `@xterm/addon-webgl` 0.19.0
910
- `xterm-addon-fit.js`: `@xterm/addon-fit` 0.11.0
1011
- `xterm-addon-web-links.js`: `@xterm/addon-web-links` 0.12.0
1112

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

21-
xterm.js and its addons are MIT licensed. Keep
22-
`../licenses/xtermjs-MIT-LICENSE.txt` and the xterm.js section in
22+
xterm.js and its addons are MIT licensed. Keep the matching files under
23+
`../licenses/` and the xterm.js section in
2324
`../../THIRD-PARTY-NOTICES.md` when releasing these files.

static/js/xterm-addon-webgl.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2018, The xterm.js authors (https://github.com/xtermjs/xterm.js)
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in
11+
all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

templates/index.html

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,11 +1247,13 @@ <h3>Session expired</h3>
12471247
async function initApp() {
12481248
try {
12491249
await loadScript('/static/js/xterm.js');
1250-
document.getElementById('progress-bar').style.width = '25%';
1250+
document.getElementById('progress-bar').style.width = '20%';
1251+
await loadScript('/static/js/xterm-addon-webgl.js');
1252+
document.getElementById('progress-bar').style.width = '40%';
12511253
await loadScript('/static/js/socket.io.min.js');
1252-
document.getElementById('progress-bar').style.width = '50%';
1254+
document.getElementById('progress-bar').style.width = '60%';
12531255
await loadScript('/static/js/xterm-addon-fit.js');
1254-
document.getElementById('progress-bar').style.width = '75%';
1256+
document.getElementById('progress-bar').style.width = '80%';
12551257
await loadScript('/static/js/xterm-addon-web-links.js');
12561258
document.getElementById('progress-bar').style.width = '100%';
12571259
} catch (e) {
@@ -2317,6 +2319,24 @@ <h3>Session expired</h3>
23172319
return term;
23182320
}
23192321

2322+
function enableWebglRenderer(state) {
2323+
if (!state || !state.term || typeof WebglAddon === 'undefined') return;
2324+
let addon = null;
2325+
try {
2326+
// Keep the drawing buffer readable for Agent viewport PNG capture.
2327+
addon = new WebglAddon.WebglAddon(true);
2328+
addon.onContextLoss(() => {
2329+
addon.dispose();
2330+
if (state.webglAddon === addon) state.webglAddon = null;
2331+
});
2332+
state.term.loadAddon(addon);
2333+
state.webglAddon = addon;
2334+
} catch (error) {
2335+
if (addon) addon.dispose();
2336+
console.warn('WebGL terminal renderer unavailable; using DOM renderer.', error);
2337+
}
2338+
}
2339+
23202340
function syncAgentTerminalMirrorSize(state) {
23212341
if (!state || !state.agentTerminalMirror || !state.term) return;
23222342
const cols = state.term.cols;
@@ -3496,6 +3516,7 @@ <h3>Session expired</h3>
34963516
titleEl,
34973517
term,
34983518
fitAddon,
3519+
webglAddon: null,
34993520
webLinksAddon: null,
35003521
inPip: false,
35013522
pipWindow: null,
@@ -3509,6 +3530,7 @@ <h3>Session expired</h3>
35093530
pipPagehideHandler: null,
35103531
pipResizeHandler: null
35113532
};
3533+
enableWebglRenderer(state);
35123534
terminals.set(terminalId, state);
35133535
configureWebLinks(state);
35143536
syncAgentTerminalMirrorSize(state);
@@ -3539,6 +3561,7 @@ <h3>Session expired</h3>
35393561
});
35403562
detachAgentFromTerminal(state, { notifyServer });
35413563
if (state.webLinksAddon) state.webLinksAddon.dispose();
3564+
if (state.webglAddon) state.webglAddon.dispose();
35423565
if (state.agentTerminalMirror) state.agentTerminalMirror.dispose();
35433566
state.term.dispose();
35443567
state.container.remove();
@@ -3729,6 +3752,7 @@ <h3>Session expired</h3>
37293752
fontSize: state.term.options.fontSize,
37303753
fontWeight: state.term.options.fontWeight,
37313754
colorScheme: prefs.colorScheme,
3755+
renderer: state.webglAddon ? 'webgl' : 'dom',
37323756
mirrorCursorStyle: state.agentTerminalMirror
37333757
? state.agentTerminalMirror.options.cursorStyle
37343758
: null,

tests/agent_browser_smoke.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,59 @@ def test_powerline_symbol_fallback_is_optional_and_applies_immediately(browser,
10351035
close_context(context)
10361036

10371037

1038+
def test_webgl_renderer_closes_block_glyph_row_gaps(browser, access_url):
1039+
context, page = new_page(browser, access_url)
1040+
try:
1041+
options = page.evaluate("() => window.terminalTest.getActiveTerminalOptions()")
1042+
check(options['renderer'] == 'webgl', 'terminal did not activate the WebGL renderer')
1043+
1044+
page.evaluate(
1045+
"""() => window.terminalTest.writeTerminalOutput(
1046+
'\\x1b[2J\\x1b[H\\x1b[38;2;215;135;135m'
1047+
+ '████████\\r\\n████████\\r\\n████████'
1048+
+ '\\x1b[0m'
1049+
)"""
1050+
)
1051+
page.wait_for_timeout(200)
1052+
pixels = page.evaluate(
1053+
"""() => {
1054+
const canvases = Array.from(document.querySelectorAll(
1055+
'.terminal-pane.active .xterm-screen canvas'
1056+
));
1057+
const canvas = canvases[canvases.length - 1];
1058+
if (!canvas) return { error: 'missing_canvas' };
1059+
const gl = canvas.getContext('webgl2');
1060+
if (!gl) return { error: 'missing_webgl_context' };
1061+
const rgba = new Uint8Array(canvas.width * canvas.height * 4);
1062+
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, rgba);
1063+
const coloredRows = [];
1064+
for (let y = 0; y < canvas.height; y += 1) {
1065+
let count = 0;
1066+
for (let x = 0; x < canvas.width; x += 1) {
1067+
const offset = (y * canvas.width + x) * 4;
1068+
const red = rgba[offset];
1069+
const green = rgba[offset + 1];
1070+
const blue = rgba[offset + 2];
1071+
if (red > 180 && green >= 100 && green <= 170 && blue >= 100 && blue <= 170) {
1072+
count += 1;
1073+
}
1074+
}
1075+
if (count >= 20) coloredRows.push(y);
1076+
}
1077+
let maxStep = 0;
1078+
for (let index = 1; index < coloredRows.length; index += 1) {
1079+
maxStep = Math.max(maxStep, coloredRows[index] - coloredRows[index - 1]);
1080+
}
1081+
return { coloredRowCount: coloredRows.length, maxStep };
1082+
}"""
1083+
)
1084+
check(not pixels.get('error'), f"could not inspect WebGL terminal pixels: {pixels.get('error')}")
1085+
check(pixels['coloredRowCount'] > 20, 'block glyph fixture did not render enough colored rows')
1086+
check(pixels['maxStep'] == 1, 'block glyphs retained a blank pixel row between terminal cells')
1087+
finally:
1088+
close_context(context)
1089+
1090+
10381091
def test_cursor_type_setting_updates_existing_and_new_terminals(browser, access_url):
10391092
context, page = new_page(browser, access_url)
10401093
try:
@@ -1508,6 +1561,7 @@ def main():
15081561
test_cjk_width_compatibility_defaults_off,
15091562
test_windows_font_fallback_defaults_and_migrates_legacy,
15101563
test_powerline_symbol_fallback_is_optional_and_applies_immediately,
1564+
test_webgl_renderer_closes_block_glyph_row_gaps,
15111565
test_cursor_type_setting_updates_existing_and_new_terminals,
15121566
test_settings_server_tab_loads_readonly_snapshot,
15131567
test_connection_diagnostics_are_session_scoped_and_redacted,

0 commit comments

Comments
 (0)