Skip to content

Commit 18d53e3

Browse files
committed
Migrate terminal preview to xterm.js
1 parent 4741e2d commit 18d53e3

10 files changed

Lines changed: 484 additions & 140 deletions

File tree

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"node": "^24.0.0"
44
},
55
"dependencies": {
6+
"@xterm/xterm": "^6.0.0",
67
"color": "^5.0.3",
78
"jquery": "^4.0.0",
89
"jquery-ui": "^1.14.2",
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import {
2+
TERMINAL_PREVIEW_COLUMNS,
3+
TERMINAL_PREVIEW_ROWS,
4+
TERMINAL_PREVIEW_SAMPLE_TEXT,
5+
terminalPreviewHeaderLines,
6+
terminalPreviewPromptCommand,
7+
} from '../../domain/terminal-preview/terminal-preview-model';
8+
9+
const LINE_BREAK = '\r\n';
10+
const ROW_LABEL_WIDTH = 6;
11+
const CELL_WIDTH = 7;
12+
const CELL_SEPARATOR = ' ';
13+
const ROW_LABEL_SEPARATOR = ' ';
14+
15+
function sgr(...codes) {
16+
if (codes.length === 0) {
17+
return '';
18+
}
19+
20+
return `\x1b[${codes.join(';')}m`;
21+
}
22+
23+
function reset() {
24+
return '\x1b[0m';
25+
}
26+
27+
function padRight(value, width) {
28+
return value.padEnd(width, ' ');
29+
}
30+
31+
function padLeft(value, width) {
32+
return value.padStart(width, ' ');
33+
}
34+
35+
function padCenter(value, width) {
36+
const availablePadding = Math.max(width - value.length, 0);
37+
const leftPadding = Math.floor(availablePadding / 2);
38+
const rightPadding = availablePadding - leftPadding;
39+
40+
return `${' '.repeat(leftPadding)}${value}${' '.repeat(rightPadding)}`;
41+
}
42+
43+
function styledText(text, codes) {
44+
return `${sgr(...codes)}${text}${reset()}`;
45+
}
46+
47+
function renderPrompt({ command = null } = {}) {
48+
const prompt = terminalPreviewPromptCommand();
49+
let out = '';
50+
51+
out += styledText(prompt.user, [36]);
52+
out += `@${prompt.host} `;
53+
out += styledText(prompt.directory, [36]);
54+
out += '> ';
55+
56+
if (command) {
57+
out += styledText(command, [34]);
58+
}
59+
60+
return out;
61+
}
62+
63+
function renderHelpLine() {
64+
return `Type ${styledText('help', [32])} for instructions on how to use fish`;
65+
}
66+
67+
function renderTableHeader() {
68+
const labels = TERMINAL_PREVIEW_COLUMNS.map((column) => padCenter(column.label, CELL_WIDTH));
69+
70+
return `${padRight('', ROW_LABEL_WIDTH)}${ROW_LABEL_SEPARATOR}${labels.join(CELL_SEPARATOR)}`;
71+
}
72+
73+
function renderTableRow(row) {
74+
const label = padLeft(row.label, ROW_LABEL_WIDTH);
75+
const cells = TERMINAL_PREVIEW_COLUMNS.map((column) => {
76+
const codes = [...row.sgr, ...column.sgr];
77+
const sample = padCenter(TERMINAL_PREVIEW_SAMPLE_TEXT, CELL_WIDTH);
78+
79+
return styledText(sample, codes);
80+
});
81+
82+
return `${label}${ROW_LABEL_SEPARATOR}${cells.join(CELL_SEPARATOR)}${reset()}`;
83+
}
84+
85+
function renderPreviewTable() {
86+
return [
87+
renderTableHeader(),
88+
...TERMINAL_PREVIEW_ROWS.map((row) => renderTableRow(row)),
89+
].join(LINE_BREAK);
90+
}
91+
92+
export function buildTerminalPreviewSequence() {
93+
return [
94+
terminalPreviewHeaderLines()[0],
95+
renderHelpLine(),
96+
renderPrompt({ command: terminalPreviewPromptCommand().command }),
97+
'',
98+
renderPreviewTable(),
99+
'',
100+
renderPrompt(),
101+
].join(LINE_BREAK);
102+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
export const TERMINAL_PREVIEW_COLUMNS = [
2+
{ label: ' ', colorName: 'background', sgr: [] },
3+
{ label: '40m', colorName: 'black', sgr: [40] },
4+
{ label: '41m', colorName: 'red', sgr: [41] },
5+
{ label: '42m', colorName: 'green', sgr: [42] },
6+
{ label: '43m', colorName: 'yellow', sgr: [43] },
7+
{ label: '44m', colorName: 'blue', sgr: [44] },
8+
{ label: '45m', colorName: 'magenta', sgr: [45] },
9+
{ label: '46m', colorName: 'cyan', sgr: [46] },
10+
{ label: '47m', colorName: 'white', sgr: [47] },
11+
];
12+
13+
export const TERMINAL_PREVIEW_ROWS = [
14+
{ label: 'm', colorName: 'foreground', sgr: [] },
15+
{ label: '1m', colorName: 'brightForeground', sgr: [1] },
16+
{ label: '30m', colorName: 'black', sgr: [30] },
17+
{ label: '1;30m', colorName: 'brightBlack', sgr: [1, 30] },
18+
{ label: '31m', colorName: 'red', sgr: [31] },
19+
{ label: '1;31m', colorName: 'brightRed', sgr: [1, 31] },
20+
{ label: '32m', colorName: 'green', sgr: [32] },
21+
{ label: '1;32m', colorName: 'brightGreen', sgr: [1, 32] },
22+
{ label: '33m', colorName: 'yellow', sgr: [33] },
23+
{ label: '1;33m', colorName: 'brightYellow', sgr: [1, 33] },
24+
{ label: '34m', colorName: 'blue', sgr: [34] },
25+
{ label: '1;34m', colorName: 'brightBlue', sgr: [1, 34] },
26+
{ label: '35m', colorName: 'magenta', sgr: [35] },
27+
{ label: '1;35m', colorName: 'brightMagenta', sgr: [1, 35] },
28+
{ label: '36m', colorName: 'cyan', sgr: [36] },
29+
{ label: '1;36m', colorName: 'brightCyan', sgr: [1, 36] },
30+
{ label: '37m', colorName: 'white', sgr: [37] },
31+
{ label: '1;37m', colorName: 'brightWhite', sgr: [1, 37] },
32+
];
33+
34+
export const TERMINAL_PREVIEW_SAMPLE_TEXT = 'gYw';
35+
36+
export function terminalPreviewHeaderLines() {
37+
return [
38+
'Welcome to fish, the friendly interactive shell',
39+
'Type help for instructions on how to use fish',
40+
];
41+
}
42+
43+
export function terminalPreviewPromptCommand() {
44+
return {
45+
user: 'ciembor',
46+
host: 'browser',
47+
directory: '~',
48+
command: './colors.sh',
49+
};
50+
}
51+
52+
export function terminalPreviewRows() {
53+
return TERMINAL_PREVIEW_ROWS;
54+
}
Lines changed: 30 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,165 +1,55 @@
11
<template>
22
<section id="terminal-display">
3-
<p>Welcome to fish, the friendly interactive shell</p>
4-
<p>
5-
Type <span class="green">help</span> for instructions on how to use fish
6-
</p>
7-
<p>
8-
<span class="cyan">ciembor</span>@browser
9-
<span class="cyan">~</span>>
10-
<span class="blue">./colors.sh</span>
11-
</p>
12-
<br />
13-
14-
<table id="colors">
15-
<tr>
16-
<th v-for="(th, index) in columnsTh" :key="`column-${index}`">{{ th }}</th>
17-
</tr>
18-
<tr v-for="(fontColorName, rowIndex) in fontColorNames" :key="`row-${rowIndex}`">
19-
<th class="row-th">{{ rowsTh[rowIndex] }}</th>
20-
<td
21-
v-for="(backgroundColorName, colIndex) in backgroundColorNames"
22-
:key="`cell-${rowIndex}-${colIndex}`"
23-
:class="getCellClass(fontColorName, backgroundColorName)"
24-
>
25-
gYw
26-
</td>
27-
</tr>
28-
</table>
29-
30-
<br />
31-
<p><span class="cyan">ciembor</span>@browser <span class="cyan">~</span>></p>
3+
<div ref="terminalElement" class="terminal-display__xterm" aria-label="Terminal color preview"></div>
324
</section>
335
</template>
346

357
<script>
8+
import { watch } from 'vue';
9+
import { buildTerminalPreviewSequence } from '../../../application/terminal-preview/build-terminal-preview-sequence';
10+
import { useCalculatedSchemeStore } from '../../shared/stores/calculated-scheme';
11+
import { createXtermTerminalPreview } from '../terminal-preview/xterm-terminal-preview';
12+
import { xtermThemeFromScheme } from '../terminal-preview/xterm-theme';
13+
3614
export default {
3715
name: 'TerminalDisplay',
38-
data() {
39-
return {
40-
columnsTh: [' ', ' ', '40m', '41m', '42m', '43m', '44m', '45m', '46m', '47m'],
41-
rowsTh: ['m', '1m', '30m', '1;30m', '31m', '1;31m', '32m', '1;32m', '33m', '1;33m', '34m', '1;34m', '35m', '1;35m', '36m', '1;36m', '37m', '1;37m'
42-
],
43-
backgroundColorNames: [
44-
'background',
45-
'black',
46-
'red',
47-
'green',
48-
'yellow',
49-
'blue',
50-
'magenta',
51-
'cyan',
52-
'white',
53-
],
54-
fontColorNames: [
55-
'foreground',
56-
'brightForeground',
57-
'black',
58-
'brightBlack',
59-
'red',
60-
'brightRed',
61-
'green',
62-
'brightGreen',
63-
'yellow',
64-
'brightYellow',
65-
'blue',
66-
'brightBlue',
67-
'magenta',
68-
'brightMagenta',
69-
'cyan',
70-
'brightCyan',
71-
'white',
72-
'brightWhite'
73-
]
74-
};
16+
setup() {
17+
const calculatedSchemeStore = useCalculatedSchemeStore();
18+
19+
return { calculatedSchemeStore };
20+
},
21+
mounted() {
22+
this.previewSequence = buildTerminalPreviewSequence();
23+
this.terminalPreview = createXtermTerminalPreview(this.$refs.terminalElement);
24+
this.stopThemeWatcher = watch(
25+
() => this.calculatedSchemeStore.calculatedScheme,
26+
(colors) => {
27+
this.renderTerminalPreview(colors);
28+
},
29+
{ immediate: true, deep: true }
30+
);
31+
},
32+
beforeUnmount() {
33+
this.stopThemeWatcher?.();
34+
this.terminalPreview?.dispose();
7535
},
7636
methods: {
77-
getCellClass(name, bgName) {
78-
let classes = '';
79-
if (name.startsWith('bright')) {
80-
classes += 'bold ';
81-
}
82-
classes += `${name} ${bgName}Bg`;
83-
return classes;
84-
}
85-
}
37+
renderTerminalPreview(colors) {
38+
this.terminalPreview?.render(this.previewSequence, xtermThemeFromScheme(colors));
39+
},
40+
},
8641
};
8742
</script>
8843
8944
<style lang="less" scoped>
9045
#terminal-display {
9146
visibility: visible;
9247
display: inline-block;
93-
font-family: Inconsolata;
94-
font-size: 20px;
9548
margin: 26px 0 0 20px;
9649
width: auto;
9750
height: auto;
9851
padding: 1px 2px;
9952
box-shadow: 0 0 10px #666;
100-
color: var(--color-foreground);
101-
background-color: var(--color-background);
102-
103-
table {
104-
border-collapse: separate;
105-
border-spacing: 0.5em 0;
106-
margin-right: 0.5em;
107-
}
108-
109-
td {
110-
margin-left: 1em;
111-
padding: 0 1em;
112-
}
113-
114-
.bold {
115-
font-weight: bold; // opera sux
116-
}
117-
118-
.row-th {
119-
text-align: right;
120-
}
121-
}
122-
123-
.foreground,
124-
.brightForeground {
125-
color: var(--color-foreground);
126-
}
127-
128-
.backgroundBg {
12953
background-color: var(--color-background);
13054
}
131-
132-
.black { color: var(--color-black); }
133-
.brightBlack { color: var(--color-bright-black); }
134-
.red { color: var(--color-red); }
135-
.brightRed { color: var(--color-bright-red); }
136-
.green { color: var(--color-green); }
137-
.brightGreen { color: var(--color-bright-green); }
138-
.yellow { color: var(--color-yellow); }
139-
.brightYellow { color: var(--color-bright-yellow); }
140-
.blue { color: var(--color-blue); }
141-
.brightBlue { color: var(--color-bright-blue); }
142-
.magenta { color: var(--color-magenta); }
143-
.brightMagenta { color: var(--color-bright-magenta); }
144-
.cyan { color: var(--color-cyan); }
145-
.brightCyan { color: var(--color-bright-cyan); }
146-
.white { color: var(--color-white); }
147-
.brightWhite { color: var(--color-bright-white); }
148-
149-
.blackBg { background-color: var(--color-black); }
150-
.brightBlackBg { background-color: var(--color-bright-black); }
151-
.redBg { background-color: var(--color-red); }
152-
.brightRedBg { background-color: var(--color-bright-red); }
153-
.greenBg { background-color: var(--color-green); }
154-
.brightGreenBg { background-color: var(--color-bright-green); }
155-
.yellowBg { background-color: var(--color-yellow); }
156-
.brightYellowBg { background-color: var(--color-bright-yellow); }
157-
.blueBg { background-color: var(--color-blue); }
158-
.brightBlueBg { background-color: var(--color-bright-blue); }
159-
.magentaBg { background-color: var(--color-magenta); }
160-
.brightMagentaBg { background-color: var(--color-bright-magenta); }
161-
.cyanBg { background-color: var(--color-cyan); }
162-
.brightCyanBg { background-color: var(--color-bright-cyan); }
163-
.whiteBg { background-color: var(--color-white); }
164-
.brightWhiteBg { background-color: var(--color-bright-white); }
16555
</style>

0 commit comments

Comments
 (0)