Skip to content

Commit 39b1734

Browse files
committed
feat: image, ligature support and multiple fixes
- fixed fonts related issue - add image support - add ligature support - improve terminal stuffs
1 parent 0334417 commit 39b1734

File tree

9 files changed

+308
-25
lines changed

9 files changed

+308
-25
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
"@ungap/custom-elements": "^1.3.0",
100100
"@xterm/addon-attach": "^0.11.0",
101101
"@xterm/addon-fit": "^0.10.0",
102+
"@xterm/addon-image": "^0.8.0",
102103
"@xterm/addon-search": "^0.15.0",
103104
"@xterm/addon-unicode11": "^0.8.0",
104105
"@xterm/addon-web-links": "^0.11.0",
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// pretty basic ligature implementation for webview
2+
export default class LigaturesAddon {
3+
constructor(options = {}) {
4+
// fallback ligatures if a font does not support ligatures natively
5+
this._fallbackLigatures =
6+
options.fallbackLigatures ||
7+
[
8+
"<--",
9+
"<---",
10+
"<<-",
11+
"<-",
12+
"->",
13+
"->>",
14+
"-->",
15+
"--->",
16+
"<==",
17+
"<===",
18+
"<<=",
19+
"<=",
20+
"=>",
21+
"=>>",
22+
"==>",
23+
"===>",
24+
">=",
25+
">>=",
26+
"<->",
27+
"<-->",
28+
"<--->",
29+
"<---->",
30+
"<=>",
31+
"<==>",
32+
"<===>",
33+
"<====>",
34+
"<~~",
35+
"<~",
36+
"~>",
37+
"~~>",
38+
"::",
39+
":::",
40+
"==",
41+
"!=",
42+
"===",
43+
"!==",
44+
":=",
45+
":-",
46+
":+",
47+
"<*",
48+
"<*>",
49+
"*>",
50+
"<|",
51+
"<|>",
52+
"|>",
53+
"+:",
54+
"-:",
55+
"=:",
56+
":>",
57+
"++",
58+
"+++",
59+
"<!--",
60+
"<!---",
61+
"<***>",
62+
].sort((a, b) => b.length - a.length);
63+
this._characterJoinerId = undefined;
64+
this._terminal = undefined;
65+
}
66+
67+
activate(terminal) {
68+
this._terminal = terminal;
69+
this._characterJoinerId = terminal.registerCharacterJoiner(
70+
this._joinCharacters.bind(this),
71+
);
72+
terminal.element.style.fontFeatureSettings = `"liga" on, "calt" on`;
73+
}
74+
75+
dispose() {
76+
if (this._characterJoinerId !== undefined) {
77+
this._terminal?.deregisterCharacterJoiner(this._characterJoinerId);
78+
this._characterJoinerId = undefined;
79+
}
80+
if (this._terminal?.element) {
81+
this._terminal.element.style.fontFeatureSettings = "";
82+
}
83+
}
84+
85+
_joinCharacters(text) {
86+
return this._findLigatureRanges(text, this._fallbackLigatures);
87+
}
88+
89+
_findLigatureRanges(text, ligatures) {
90+
const ranges = [];
91+
for (let i = 0; i < text.length; i++) {
92+
for (const ligature of ligatures) {
93+
if (text.startsWith(ligature, i)) {
94+
ranges.push([i, i + ligature.length]);
95+
i += ligature.length - 1;
96+
break;
97+
}
98+
}
99+
}
100+
return ranges;
101+
}
102+
}

src/components/terminal/terminal.js

Lines changed: 128 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55

66
import { AttachAddon } from "@xterm/addon-attach";
77
import { FitAddon } from "@xterm/addon-fit";
8+
import { ImageAddon } from "@xterm/addon-image";
89
import { SearchAddon } from "@xterm/addon-search";
910
import { Unicode11Addon } from "@xterm/addon-unicode11";
1011
import { WebLinksAddon } from "@xterm/addon-web-links";
1112
import { WebglAddon } from "@xterm/addon-webgl";
1213
import { Terminal as Xterm } from "@xterm/xterm";
1314
import confirm from "dialogs/confirm";
15+
import fonts from "lib/fonts";
1416
import appSettings from "lib/settings";
17+
import LigaturesAddon from "./ligatures";
1518
import { getTerminalSettings } from "./terminalDefaults";
1619
import TerminalThemeManager from "./terminalThemeManager";
1720

@@ -45,6 +48,8 @@ export default class TerminalComponent {
4548
this.unicode11Addon = null;
4649
this.searchAddon = null;
4750
this.webLinksAddon = null;
51+
this.imageAddon = null;
52+
this.ligaturesAddon = null;
4853
this.container = null;
4954
this.websocket = null;
5055
this.pid = null;
@@ -77,13 +82,18 @@ export default class TerminalComponent {
7782
this.terminal.loadAddon(this.unicode11Addon);
7883
this.terminal.loadAddon(this.searchAddon);
7984
this.terminal.loadAddon(this.webLinksAddon);
80-
try {
81-
this.terminal.loadAddon(this.webglAddon);
82-
} catch (error) {
83-
console.error("Failed to load WebglAddon:", error);
84-
this.webglAddon.dispose();
85+
86+
// Load conditional addons based on settings
87+
const terminalSettings = getTerminalSettings();
88+
89+
// Load image addon if enabled
90+
if (terminalSettings.imageSupport) {
91+
this.loadImageAddon();
8592
}
8693

94+
// Load font if specified
95+
this.loadTerminalFont();
96+
8797
// Set up terminal event handlers
8898
this.setupEventHandlers();
8999
}
@@ -138,12 +148,23 @@ export default class TerminalComponent {
138148
this.container = container;
139149

140150
try {
141-
// Ensure container has proper dimensions
142-
if (container.offsetHeight === 0) {
143-
container.style.height = "400px";
151+
try {
152+
this.terminal.loadAddon(this.webglAddon);
153+
this.terminal.open(container);
154+
} catch (error) {
155+
console.error("Failed to load WebglAddon:", error);
156+
this.webglAddon.dispose();
144157
}
145158

146-
this.terminal.open(container);
159+
if (!this.terminal.element) {
160+
// webgl loading failed for some reason, attach with DOM renderer
161+
this.terminal.open(container);
162+
}
163+
const terminalSettings = getTerminalSettings();
164+
// Load ligatures addon if enabled
165+
if (terminalSettings.fontLigatures) {
166+
this.loadLigaturesAddon();
167+
}
147168

148169
// Wait for terminal to render then fit
149170
setTimeout(() => {
@@ -354,6 +375,100 @@ export default class TerminalComponent {
354375
});
355376
}
356377

378+
/**
379+
* Load image addon
380+
*/
381+
loadImageAddon() {
382+
if (!this.imageAddon) {
383+
try {
384+
this.imageAddon = new ImageAddon();
385+
this.terminal.loadAddon(this.imageAddon);
386+
} catch (error) {
387+
console.error("Failed to load ImageAddon:", error);
388+
}
389+
}
390+
}
391+
392+
/**
393+
* Dispose image addon
394+
*/
395+
disposeImageAddon() {
396+
if (this.imageAddon) {
397+
try {
398+
this.imageAddon.dispose();
399+
this.imageAddon = null;
400+
} catch (error) {
401+
console.error("Failed to dispose ImageAddon:", error);
402+
}
403+
}
404+
}
405+
406+
/**
407+
* Update image support setting
408+
* @param {boolean} enabled - Whether to enable image support
409+
*/
410+
updateImageSupport(enabled) {
411+
if (enabled) {
412+
this.loadImageAddon();
413+
} else {
414+
this.disposeImageAddon();
415+
}
416+
}
417+
418+
/**
419+
* Load ligatures addon
420+
*/
421+
loadLigaturesAddon() {
422+
if (!this.ligaturesAddon) {
423+
try {
424+
this.ligaturesAddon = new LigaturesAddon();
425+
this.terminal.loadAddon(this.ligaturesAddon);
426+
} catch (error) {
427+
console.error("Failed to load LigaturesAddon:", error);
428+
}
429+
}
430+
}
431+
432+
/**
433+
* Dispose ligatures addon
434+
*/
435+
disposeLigaturesAddon() {
436+
if (this.ligaturesAddon) {
437+
try {
438+
this.ligaturesAddon.dispose();
439+
this.ligaturesAddon = null;
440+
} catch (error) {
441+
console.error("Failed to dispose LigaturesAddon:", error);
442+
}
443+
}
444+
}
445+
446+
/**
447+
* Update font ligatures setting
448+
* @param {boolean} enabled - Whether to enable font ligatures
449+
*/
450+
updateFontLigatures(enabled) {
451+
if (enabled) {
452+
this.loadLigaturesAddon();
453+
} else {
454+
this.disposeLigaturesAddon();
455+
}
456+
}
457+
458+
/**
459+
* Load terminal font if it's not already loaded
460+
*/
461+
async loadTerminalFont() {
462+
const fontFamily = this.options.fontFamily;
463+
if (fontFamily && fonts.get(fontFamily)) {
464+
try {
465+
await fonts.loadFont(fontFamily);
466+
} catch (error) {
467+
console.warn(`Failed to load terminal font ${fontFamily}:`, error);
468+
}
469+
}
470+
}
471+
357472
/**
358473
* Terminate terminal session
359474
*/
@@ -379,6 +494,10 @@ export default class TerminalComponent {
379494
dispose() {
380495
this.terminate();
381496

497+
// Dispose addons
498+
this.disposeImageAddon();
499+
this.disposeLigaturesAddon();
500+
382501
if (this.terminal) {
383502
this.terminal.dispose();
384503
}

src/components/terminal/terminalDefaults.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,14 @@ export const DEFAULT_TERMINAL_SETTINGS = {
1212
tabStopWidth: 4,
1313
convertEol: true,
1414
letterSpacing: 0,
15+
imageSupport: false,
16+
fontLigatures: false,
1517
};
1618

1719
export function getTerminalSettings() {
1820
const settings = appSettings.value.terminalSettings || {};
1921
return {
2022
...DEFAULT_TERMINAL_SETTINGS,
21-
fontFamily:
22-
settings.fontFamily ||
23-
DEFAULT_TERMINAL_SETTINGS.fontFamily ||
24-
appSettings.value.fontFamily,
2523
...settings,
2624
};
2725
}

src/components/terminal/terminalManager.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,22 +67,25 @@ class TerminalManager {
6767
);
6868
}
6969

70+
// Use PID as unique ID if available, otherwise fall back to terminalId
71+
const uniqueId = terminalComponent.pid || terminalId;
72+
7073
// Setup event handlers
7174
this.setupTerminalHandlers(
7275
terminalFile,
7376
terminalComponent,
74-
terminalId,
77+
uniqueId,
7578
);
7679

7780
const instance = {
78-
id: terminalId,
81+
id: uniqueId,
7982
name: terminalName,
8083
component: terminalComponent,
8184
file: terminalFile,
8285
container: terminalContainer,
8386
};
8487

85-
this.terminals.set(terminalId, instance);
88+
this.terminals.set(uniqueId, instance);
8689
resolve(instance);
8790
} catch (error) {
8891
console.error("Failed to initialize terminal:", error);
@@ -143,12 +146,15 @@ class TerminalManager {
143146

144147
terminalComponent.onTitleChange = (title) => {
145148
if (title) {
146-
terminalFile.filename = title;
149+
// Format terminal title as "Terminal ! - title"
150+
const formattedTitle = `Terminal ${this.terminalCounter} - ${title}`;
151+
terminalFile.filename = formattedTitle;
147152
}
148153
};
149154

150155
// Store references for cleanup
151156
terminalFile._terminalId = terminalId;
157+
terminalFile.terminalComponent = terminalComponent;
152158
//terminalFile._resizeObserver = resizeObserver;
153159
}
154160

0 commit comments

Comments
 (0)