Skip to content

Commit 84c19ac

Browse files
authored
Merge pull request #6 from askac/fix/windows-font-fallback
Improve terminal symbol font fallbacks
2 parents 72173da + f5b720a commit 84c19ac

7 files changed

Lines changed: 201 additions & 8 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -514,15 +514,16 @@ without a CDN:
514514
- `@xterm/xterm` 6.0.0: `static/js/xterm.js`, `static/css/xterm.css`
515515
- `@xterm/addon-fit` 0.11.0: `static/js/xterm-addon-fit.js`
516516
- `@xterm/addon-web-links` 0.12.0: `static/js/xterm-addon-web-links.js`
517+
- Powerline Symbols: `static/fonts/PowerlineSymbols.otf` (optional prompt-symbol fallback)
517518

518519
The browser bundles are copied from official npm release packages. A matching
519520
source checkout is kept at `/mnt/d/workspace/github/xterm.js`, tag `6.0.0` /
520521
commit `f447274f430fd22513f6adbf9862d19524471c04`, for auditing and future
521522
upgrades.
522523

523-
xterm.js and these addons are MIT licensed. Keep `THIRD-PARTY-NOTICES.md`,
524-
`static/licenses/xtermjs-MIT-LICENSE.txt`, and `static/js/README.md` when
525-
publishing releases that include the vendored files.
524+
xterm.js, these addons, and Powerline Symbols are MIT licensed. Keep
525+
`THIRD-PARTY-NOTICES.md`, the matching files under `static/licenses/`, and the
526+
asset README files when publishing releases that include the vendored files.
526527

527528
## Security Notes
528529

THIRD-PARTY-NOTICES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ This project incorporates components from the following projects:
1515
- **Copyright**: (c) 2014-2023 Guillermo Rauch
1616
- **Project**: https://github.com/socketio/socket.io
1717

18+
## Powerline Symbols
19+
- **License**: MIT
20+
- **Copyright**: Copyright 2013 Kim Silkebækken and other contributors
21+
- **Project**: https://github.com/powerline/powerline
22+
- **Vendored file**: `static/fonts/PowerlineSymbols.otf`
23+
- **Upstream commit**: `11808cbe5c16e4621edcaefa916b5add81eab799`
24+
- **License text**: `static/licenses/powerline-MIT-LICENSE.txt`
25+
1826
## Microsoft Windows Terminal (Color Schemes)
1927
- **License**: MIT
2028
- **Copyright**: (c) Microsoft Corporation

static/fonts/PowerlineSymbols.otf

2.21 KB
Binary file not shown.

static/fonts/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Vendored Fonts
2+
3+
`PowerlineSymbols.otf` is copied from the official Powerline repository at
4+
commit `11808cbe5c16e4621edcaefa916b5add81eab799`:
5+
6+
https://github.com/powerline/powerline/blob/11808cbe5c16e4621edcaefa916b5add81eab799/font/PowerlineSymbols.otf
7+
8+
The file is 2,264 bytes and has this SHA-256 digest:
9+
10+
```text
11+
4a2496a009b1649878ce067a7ec2aed9f79656c90136971e1dba00766515f7a1
12+
```
13+
14+
It is used only when the optional Powerline symbol fallback preference is
15+
enabled. The browser font face is limited to `U+E0A0-E0A2` and
16+
`U+E0B0-E0B3`.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Copyright 2013 Kim Silkebækken and other contributors
2+
https://github.com/powerline/powerline
3+
4+
Permission is hereby granted, free of charge, to any person obtaining
5+
a copy of this software and associated documentation files (the
6+
"Software"), to deal in the Software without restriction, including
7+
without limitation the rights to use, copy, modify, merge, publish,
8+
distribute, sublicense, and/or sell copies of the Software, and to
9+
permit persons to whom the Software is furnished to do so, subject to
10+
the following conditions:
11+
12+
The above copyright notice and this permission notice shall be
13+
included in all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

templates/index.html

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55
<!-- Local Fallback Assets -->
66
<link rel="stylesheet" href="/static/css/xterm.css" />
77
<style>
8+
@font-face {
9+
font-family: "StandTerm Powerline Symbols";
10+
src: url("/static/fonts/PowerlineSymbols.otf") format("opentype");
11+
font-display: block;
12+
font-style: normal;
13+
font-weight: 400;
14+
unicode-range: U+E0A0-E0A2, U+E0B0-E0B3;
15+
}
816
:root { --status-bar-height: 26px; --tab-bar-height: 34px; }
917
body { margin: 0; background: #000; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
1018
#terminal-tabs { position: absolute; top: 0; left: 0; right: 0; height: var(--tab-bar-height); box-sizing: border-box; background: #151515; border-bottom: 1px solid #2a2a2a; display: none; align-items: center; gap: 4px; padding: 4px 8px; z-index: 40; overflow: hidden; }
@@ -610,6 +618,7 @@ <h3>Settings</h3>
610618
</select>
611619
</div>
612620
<div class="settings-row"><label for="pref-fontFace">Font face</label><input type="text" id="pref-fontFace"></div>
621+
<div class="settings-row"><label for="pref-powerlineSymbols" title="Use the bundled Powerline symbols font as a fallback for prompt glyphs such as the branch symbol.">Enable Powerline symbol fallback</label><input type="checkbox" id="pref-powerlineSymbols"></div>
613622
<div class="settings-row"><label for="pref-fontSize">Font size</label><input type="number" id="pref-fontSize" min="8" max="32"></div>
614623
<div class="settings-row"><label for="pref-fontWeight" title="Heavier weights can improve perceived brightness of thin glyphs, especially with macOS grayscale antialiasing.">Font weight</label>
615624
<select id="pref-fontWeight"><option value="normal">Normal (400)</option><option value="500">Medium (500)</option><option value="600">Semibold (600)</option><option value="bold">Bold (700)</option></select>
@@ -930,14 +939,15 @@ <h3>Session expired</h3>
930939
// --- Preferences ---
931940
const PREF_KEY = 'terminal.pref.v1';
932941
const LEGACY_DEFAULT_FONT_FACE = 'Consolas, "Courier New", monospace';
942+
const WINDOWS_TERMINAL_FONT_FACE = 'Consolas, "Cascadia Mono", "Courier New", monospace';
933943
const LEGACY_MAC_DEFAULT_FONT_FACE = 'Menlo, Monaco, "Noto Sans Mono CJK TC", "PingFang TC", "Apple LiGothic", monospace';
934944
const MAC_TERMINAL_FONT_FACE = 'Menlo, Monaco, "SF Mono", "Noto Sans Mono CJK TC", "Sarasa Mono TC", "Biaodian Pro Sans CNS", monospace';
935945
function isApplePlatform() {
936946
return /Mac|iPhone|iPad|iPod/i.test(`${navigator.platform || ''} ${navigator.userAgent || ''}`);
937947
}
938948
function getDefaultTerminalFontFace() {
939949
if (isApplePlatform()) return MAC_TERMINAL_FONT_FACE;
940-
return LEGACY_DEFAULT_FONT_FACE;
950+
return WINDOWS_TERMINAL_FONT_FACE;
941951
}
942952
const PREF_DEFAULTS = {
943953
useCustomMenu: true,
@@ -946,6 +956,7 @@ <h3>Session expired</h3>
946956
detectUrls: true,
947957
urlClickAction: 'overlay',
948958
fontFace: getDefaultTerminalFontFace(),
959+
powerlineSymbols: false,
949960
fontSize: 14,
950961
// macOS grayscale antialiasing renders thin bright glyphs dimmer
951962
// than Windows ClearType, so default to a slightly heavier weight
@@ -975,6 +986,10 @@ <h3>Session expired</h3>
975986
}
976987
}
977988
function savePrefs(p) { localStorage.setItem(PREF_KEY, JSON.stringify(p)); }
989+
function getTerminalFontFace() {
990+
if (!prefs.powerlineSymbols) return prefs.fontFace;
991+
return '"StandTerm Powerline Symbols", ' + prefs.fontFace;
992+
}
978993
function normalizeFontSize(value, fallback = PREF_DEFAULTS.fontSize) {
979994
const text = String(value).trim();
980995
if (!/^\d+$/.test(text)) return fallback;
@@ -2292,7 +2307,7 @@ <h3>Session expired</h3>
22922307
cursorBlink: false,
22932308
disableStdin: true,
22942309
cursorStyle: prefs.cursorStyle,
2295-
fontFamily: prefs.fontFace,
2310+
fontFamily: getTerminalFontFace(),
22962311
fontSize: prefs.fontSize,
22972312
fontWeight: prefs.fontWeight,
22982313
scrollback: 1000,
@@ -3448,7 +3463,7 @@ <h3>Session expired</h3>
34483463
allowProposedApi: true,
34493464
cursorBlink: true,
34503465
cursorStyle: prefs.cursorStyle,
3451-
fontFamily: prefs.fontFace,
3466+
fontFamily: getTerminalFontFace(),
34523467
fontSize: prefs.fontSize,
34533468
fontWeight: prefs.fontWeight,
34543469
theme: SCHEMES[prefs.colorScheme] || SCHEMES.vintage
@@ -3716,6 +3731,9 @@ <h3>Session expired</h3>
37163731
colorScheme: prefs.colorScheme,
37173732
mirrorCursorStyle: state.agentTerminalMirror
37183733
? state.agentTerminalMirror.options.cursorStyle
3734+
: null,
3735+
mirrorFontFamily: state.agentTerminalMirror
3736+
? state.agentTerminalMirror.options.fontFamily
37193737
: null
37203738
});
37213739
},
@@ -5401,6 +5419,7 @@ <h3>Session expired</h3>
54015419
document.getElementById('pref-urlClickAction').value = prefs.urlClickAction;
54025420
document.getElementById('pref-colorScheme').value = prefs.colorScheme;
54035421
document.getElementById('pref-fontFace').value = prefs.fontFace;
5422+
document.getElementById('pref-powerlineSymbols').checked = prefs.powerlineSymbols;
54045423
document.getElementById('pref-fontSize').value = prefs.fontSize;
54055424
document.getElementById('pref-fontWeight').value = prefs.fontWeight;
54065425
document.getElementById('pref-cursorStyle').value = prefs.cursorStyle;
@@ -5486,20 +5505,21 @@ <h3>Session expired</h3>
54865505
prefs.urlClickAction = document.getElementById('pref-urlClickAction').value;
54875506
prefs.colorScheme = document.getElementById('pref-colorScheme').value;
54885507
prefs.fontFace = document.getElementById('pref-fontFace').value;
5508+
prefs.powerlineSymbols = document.getElementById('pref-powerlineSymbols').checked;
54895509
prefs.fontSize = normalizeFontSize(document.getElementById('pref-fontSize').value, prefs.fontSize);
54905510
prefs.fontWeight = normalizeFontWeight(document.getElementById('pref-fontWeight').value, prefs.fontWeight);
54915511
prefs.cursorStyle = normalizeCursorStyle(document.getElementById('pref-cursorStyle').value, prefs.cursorStyle);
54925512
savePrefs(prefs);
54935513
terminals.forEach(state => {
54945514
state.term.options.cursorStyle = prefs.cursorStyle;
5495-
state.term.options.fontFamily = prefs.fontFace;
5515+
state.term.options.fontFamily = getTerminalFontFace();
54965516
state.term.options.fontSize = prefs.fontSize;
54975517
state.term.options.fontWeight = prefs.fontWeight;
54985518
state.term.options.theme = SCHEMES[prefs.colorScheme] || SCHEMES.vintage;
54995519
applyCjkWidthPreference(state.term);
55005520
if (state.agentTerminalMirror) {
55015521
state.agentTerminalMirror.options.cursorStyle = prefs.cursorStyle;
5502-
state.agentTerminalMirror.options.fontFamily = prefs.fontFace;
5522+
state.agentTerminalMirror.options.fontFamily = getTerminalFontFace();
55035523
state.agentTerminalMirror.options.fontSize = prefs.fontSize;
55045524
state.agentTerminalMirror.options.fontWeight = prefs.fontWeight;
55055525
state.agentTerminalMirror.options.theme = SCHEMES[prefs.colorScheme] || SCHEMES.vintage;

tests/agent_browser_smoke.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,131 @@ def test_cjk_width_compatibility_defaults_off(browser, access_url):
910910
close_context(context)
911911

912912

913+
def test_windows_font_fallback_defaults_and_migrates_legacy(browser, access_url):
914+
expected = 'Consolas, "Cascadia Mono", "Courier New", monospace'
915+
legacy = 'Consolas, "Courier New", monospace'
916+
custom = 'Custom Mono, monospace'
917+
context, page = new_page(browser, access_url)
918+
try:
919+
initial = page.evaluate("() => window.terminalTest.getActiveTerminalOptions().fontFamily")
920+
check(initial == expected, 'terminal font fallback did not default to Cascadia Mono')
921+
922+
page.evaluate(
923+
"fontFace => localStorage.setItem('terminal.pref.v1', JSON.stringify({ fontFace }))",
924+
legacy,
925+
)
926+
page.reload(wait_until='domcontentloaded')
927+
page.wait_for_function('() => !!window.terminalTest', timeout=10000)
928+
page.wait_for_function(
929+
"() => window.terminalTest.getSocketState().connected === true",
930+
timeout=10000,
931+
)
932+
page.wait_for_function(
933+
'() => window.terminalTest.getActiveTerminalOptions() !== null',
934+
timeout=10000,
935+
)
936+
migrated = page.evaluate("() => window.terminalTest.getActiveTerminalOptions().fontFamily")
937+
check(migrated == expected, 'legacy terminal font fallback was not migrated')
938+
939+
page.evaluate(
940+
"fontFace => localStorage.setItem('terminal.pref.v1', JSON.stringify({ fontFace }))",
941+
custom,
942+
)
943+
page.reload(wait_until='domcontentloaded')
944+
page.wait_for_function('() => !!window.terminalTest', timeout=10000)
945+
page.wait_for_function(
946+
'() => window.terminalTest.getActiveTerminalOptions() !== null',
947+
timeout=10000,
948+
)
949+
preserved = page.evaluate("() => window.terminalTest.getActiveTerminalOptions().fontFamily")
950+
check(preserved == custom, 'custom terminal font face was overwritten by default migration')
951+
finally:
952+
close_context(context)
953+
954+
955+
def test_powerline_symbol_fallback_is_optional_and_applies_immediately(browser, access_url):
956+
context, page = new_page(browser, access_url)
957+
try:
958+
initial = page.evaluate("() => window.terminalTest.getActiveTerminalOptions()")
959+
check(
960+
not initial['fontFamily'].startswith('"StandTerm Powerline Symbols"'),
961+
'Powerline symbol fallback defaulted on',
962+
)
963+
964+
page.click('#quick-settings')
965+
page.wait_for_selector('#settings-modal.open', timeout=5000)
966+
page.click('.settings-nav-item[data-tab="appearance"]')
967+
check(
968+
page.locator('#pref-powerlineSymbols').is_checked() is False,
969+
'Powerline symbol fallback checkbox defaulted on',
970+
)
971+
page.check('#pref-powerlineSymbols')
972+
page.click('#settings-save')
973+
page.wait_for_function(
974+
"() => window.terminalTest.getActiveTerminalOptions().fontFamily.startsWith('\\\"StandTerm Powerline Symbols\\\"')",
975+
timeout=5000,
976+
)
977+
enabled = page.evaluate(
978+
"""() => ({
979+
options: window.terminalTest.getActiveTerminalOptions(),
980+
stored: JSON.parse(localStorage.getItem('terminal.pref.v1')).powerlineSymbols
981+
})"""
982+
)
983+
check(enabled['stored'] is True, 'Powerline symbol fallback preference was not saved')
984+
check(
985+
enabled['options']['mirrorFontFamily'] == enabled['options']['fontFamily'],
986+
'Powerline symbol fallback did not update the agent mirror',
987+
)
988+
loaded = page.evaluate(
989+
"""async () => {
990+
const fonts = await document.fonts.load(
991+
'14px "StandTerm Powerline Symbols"',
992+
'\ue0a0'
993+
);
994+
return fonts.length;
995+
}"""
996+
)
997+
check(loaded > 0, 'bundled Powerline symbols font did not load')
998+
999+
page.click('#new-tab-btn')
1000+
page.wait_for_function(
1001+
"() => window.terminalTest.getTerminalTabsState().tabs.length === 2",
1002+
timeout=5000,
1003+
)
1004+
new_tab = page.evaluate("() => window.terminalTest.getActiveTerminalOptions()")
1005+
check(
1006+
new_tab['fontFamily'] == enabled['options']['fontFamily'],
1007+
'new terminal did not use the Powerline symbol fallback',
1008+
)
1009+
check(
1010+
new_tab['mirrorFontFamily'] == enabled['options']['fontFamily'],
1011+
'new agent mirror did not use the Powerline symbol fallback',
1012+
)
1013+
1014+
page.click('#quick-settings')
1015+
page.wait_for_selector('#settings-modal.open', timeout=5000)
1016+
page.click('.settings-nav-item[data-tab="appearance"]')
1017+
page.uncheck('#pref-powerlineSymbols')
1018+
page.click('#settings-save')
1019+
page.wait_for_function(
1020+
"() => !window.terminalTest.getActiveTerminalOptions().fontFamily.startsWith('\\\"StandTerm Powerline Symbols\\\"')",
1021+
timeout=5000,
1022+
)
1023+
disabled = page.evaluate(
1024+
"""() => ({
1025+
options: window.terminalTest.getActiveTerminalOptions(),
1026+
stored: JSON.parse(localStorage.getItem('terminal.pref.v1')).powerlineSymbols
1027+
})"""
1028+
)
1029+
check(disabled['stored'] is False, 'Powerline symbol fallback disable was not saved')
1030+
check(
1031+
disabled['options']['mirrorFontFamily'] == disabled['options']['fontFamily'],
1032+
'disabling Powerline symbol fallback did not update the agent mirror',
1033+
)
1034+
finally:
1035+
close_context(context)
1036+
1037+
9131038
def test_cursor_type_setting_updates_existing_and_new_terminals(browser, access_url):
9141039
context, page = new_page(browser, access_url)
9151040
try:
@@ -1381,6 +1506,8 @@ def main():
13811506
test_paste_review_approve_and_cancel,
13821507
test_approval_payload_and_stale_rejections,
13831508
test_cjk_width_compatibility_defaults_off,
1509+
test_windows_font_fallback_defaults_and_migrates_legacy,
1510+
test_powerline_symbol_fallback_is_optional_and_applies_immediately,
13841511
test_cursor_type_setting_updates_existing_and_new_terminals,
13851512
test_settings_server_tab_loads_readonly_snapshot,
13861513
test_connection_diagnostics_are_session_scoped_and_redacted,

0 commit comments

Comments
 (0)