Skip to content

Commit eac7aa4

Browse files
committed
feat(hterm): hterm integration
1 parent 5f602b8 commit eac7aa4

17 files changed

+86
-1350
lines changed

package-lock.json

Lines changed: 6 additions & 900 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
@@ -132,6 +132,7 @@
132132
"file-loader": "^1.1.5",
133133
"fs-extra": "^4.0.2",
134134
"glob": "^7.1.2",
135+
"hterm-umdjs": "1.2.0",
135136
"html-loader": "^0.5.1",
136137
"html-webpack-plugin": "^2.30.1",
137138
"jasmine": "^2.8.0",

src/api/docker.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,15 @@ export function attachExec(id: string, cmd: any): Observable<any> {
6565
}
6666

6767
if (cmd.type === CommandType.store_cache) {
68-
observer.next({ type: 'data', data: chalk.yellow('==> saving cache ...') + '\r' });
68+
observer.next({
69+
type: 'data',
70+
data: chalk.yellow('==> saving cache ...') + '\r\n' });
6971
} else if (cmd.type === CommandType.restore_cache) {
70-
observer.next({ type: 'data', data: chalk.yellow('==> restoring cache ...') + '\r' });
72+
observer.next({
73+
type: 'data',
74+
data: chalk.yellow('==> restoring cache ...') + '\r\n' });
7175
} else {
72-
observer.next({ type: 'data', data: chalk.yellow('==> ' + command) + '\r' });
76+
observer.next({ type: 'data', data: chalk.yellow('==> ' + command) + '\r\n' });
7377
}
7478

7579
const container = docker.getContainer(id);
@@ -89,13 +93,12 @@ export function attachExec(id: string, cmd: any): Observable<any> {
8993

9094
ws.on('finish', () => {
9195
const duration = new Date().getTime() - startTime;
92-
observer.next({ type: 'data', data: `[exectime]: ${duration}` });
9396
observer.next({ type: 'exit', data: exitCode });
9497
observer.complete();
9598
});
9699

97100
ws._write = (chunk, enc, next) => {
98-
const str = chunk.toString();
101+
let str = chunk.toString('utf8');
99102

100103
if (str.includes('[error]')) {
101104
const splitted = str.split(' ');
@@ -104,7 +107,11 @@ export function attachExec(id: string, cmd: any): Observable<any> {
104107
} else if (str.includes('[success]')) {
105108
exitCode = 0;
106109
ws.end();
107-
} else if (!str.includes('/usr/bin/abstruse') && !str.startsWith('>')) {
110+
} else if (!str.includes('/usr/bin/abstruse \'' + cmd.command) && !str.startsWith('>')) {
111+
if (str.includes('//') && str.includes('@')) {
112+
str = str.replace(/\/\/(.*)@/, '//');
113+
}
114+
108115
observer.next({ type: 'data', data: str });
109116
}
110117

-112 KB
Binary file not shown.
-116 KB
Binary file not shown.
-112 KB
Binary file not shown.
-112 KB
Binary file not shown.

src/app/components/app-job/app-job.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export class AppJobComponent implements OnInit, OnDestroy {
106106
}
107107

108108
if (event.data === 'job started') {
109+
this.terminalInput = { clear: true };
109110
this.jobRun.status = 'running';
110111
this.jobRun.end_time = null;
111112
this.jobRun.start_time = event.additionalData;
@@ -187,7 +188,6 @@ export class AppJobComponent implements OnInit, OnDestroy {
187188
restartJob(e: MouseEvent): void {
188189
e.preventDefault();
189190
e.stopPropagation();
190-
this.terminalInput = { clear: true };
191191
this.processing = true;
192192
this.sshd = null;
193193
this.vnc = null;
Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1 @@
1-
<div class="window-terminal-container dracula-ansi-theme" [class.is-hidden]="noData" slimScroll [options]="scrollOptions" [scrollEvents]="scrollEvents">
2-
<div class="terminal" *ngFor="let cmd of commands; let i = index;" [id]="i">
3-
<div class="command-line" (click)="toggleCommand(i)" [class.is-opened]="cmd.visible">
4-
<span class="command" [innerHTML]="cmd.command"></span>
5-
<span class="time" *ngIf="cmd.time">{{ cmd.time }}</span>
6-
<span class="command-loader" *ngIf="!cmd.time">
7-
<img src="/images/icons/spinner.svg">
8-
</span>
9-
</div>
10-
<pre class="output" [class.is-hidden]="!cmd.visible" [innerHTML]="cmd.output"></pre>
11-
</div>
12-
</div>
1+
<div class="window-terminal-container"></div>

src/app/components/app-terminal/app-terminal.component.ts

Lines changed: 62 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ import {
88
Inject
99
} from '@angular/core';
1010
import { DOCUMENT } from '@angular/common';
11-
import * as AnsiUp from 'ansi_up';
12-
import { SlimScrollEvent, ISlimScrollOptions } from 'ngx-slimscroll';
11+
import * as hterm from 'hterm-umdjs';
12+
13+
const terminalColorPallete = ['rgb(40, 42, 54)', 'rgb(255, 85, 85)', 'rgb(80, 250, 123)',
14+
'rgb(243, 251, 151)', 'rgb(189, 147, 249)', 'rgb(255, 121, 198)', 'rgb(139, 233, 253)',
15+
'rgb(187, 187, 187)', 'rgb(85, 85, 85)', 'rgb(255, 85, 85)', 'rgb(80, 250, 123)',
16+
'rgb(243, 251, 151)', 'rgb(189, 147, 249)', 'rgb(255, 121, 198)', 'rgb(139, 233, 253)',
17+
'rgb(255, 255, 255)'];
1318

1419
@Component({
1520
selector: 'app-terminal',
@@ -18,183 +23,81 @@ import { SlimScrollEvent, ISlimScrollOptions } from 'ngx-slimscroll';
1823
export class AppTerminalComponent implements OnInit {
1924
@Input() data: any;
2025
@Input() options: { size: 'normal' | 'large' };
21-
au: any;
22-
commands: { command: string, visible: boolean, output: string, time: string }[];
23-
noData: boolean;
24-
initScroll: boolean;
25-
scrollOptions: ISlimScrollOptions;
26-
scrollEvents: EventEmitter<SlimScrollEvent>;
26+
hterm: hterm.Terminal;
27+
terminalReady: boolean;
28+
unwritenChanges: string;
2729

2830
constructor(
2931
private elementRef: ElementRef,
3032
@Inject(DOCUMENT) private document: any
3133
) {
32-
this.scrollOptions = {
33-
barBackground: '#666',
34-
gridBackground: '#000',
35-
barBorderRadius: '10',
36-
barWidth: '7',
37-
gridWidth: '7',
38-
barMargin: '2px 5px',
39-
gridMargin: '2px 5px',
40-
gridBorderRadius: '10',
41-
alwaysVisible: false
42-
};
43-
44-
this.scrollEvents = new EventEmitter<SlimScrollEvent>();
34+
hterm.hterm.defaultStorage = new hterm.lib.Storage.Local();
35+
this.hterm = new hterm.hterm.Terminal();
36+
this.terminalReady = false;
37+
this.unwritenChanges = '';
4538
}
4639

4740
ngOnInit() {
48-
this.au = new AnsiUp.default();
49-
this.au.use_classes = true;
50-
this.commands = [];
51-
this.noData = true;
41+
this.hterm.onVTKeystroke = () => {};
42+
this.hterm.showOverlay = () => {};
43+
this.hterm.onTerminalReady = () => {
44+
this.hterm.setWindowTitle = () => {};
45+
this.hterm.prefs_.set('cursor-color', 'transparent');
46+
this.hterm.prefs_.set('font-family', 'monaco, menlo, monospace');
47+
this.hterm.prefs_.set('font-size', 11);
48+
this.hterm.prefs_.set('audible-bell-sound', '');
49+
this.hterm.prefs_.set('font-smoothing', 'subpixel-antialiased');
50+
this.hterm.prefs_.set('enable-bold', false);
51+
this.hterm.prefs_.set('backspace-sends-backspace', true);
52+
this.hterm.prefs_.set('cursor-blink', false);
53+
this.hterm.prefs_.set('receive-encoding', 'raw');
54+
this.hterm.prefs_.set('send-encoding', 'raw');
55+
this.hterm.prefs_.set('alt-sends-what', 'browser-key');
56+
this.hterm.prefs_.set('scrollbar-visible', false);
57+
this.hterm.prefs_.set('enable-clipboard-notice', false);
58+
this.hterm.prefs_.set('background-color', '#000000');
59+
this.hterm.prefs_.set('foreground-color', '#f8f8f2');
60+
hterm.lib.colors.stockColorPalette.splice(0, terminalColorPallete.length)
61+
hterm.lib.colors.stockColorPalette = terminalColorPallete.concat(
62+
hterm.lib.colors.stockColorPalette);
63+
this.hterm.prefs_.set('color-palette-overrides', terminalColorPallete);
64+
65+
this.terminalReady = true;
66+
if (this.unwritenChanges) {
67+
this.printToTerminal(this.unwritenChanges);
68+
this.unwritenChanges = '';
69+
}
70+
};
71+
72+
this.hterm.decorate(this.document.querySelector('.window-terminal-container'));
73+
this.hterm.installKeyboard(null);
5274
}
5375

5476
ngOnChanges(changes: SimpleChange) {
5577
if (!this.data) {
5678
return;
5779
}
5880

59-
this.noData = false;
60-
6181
if (typeof this.data.clear !== 'undefined') {
62-
this.commands = [];
63-
} else {
64-
let output: string = this.au.ansi_to_html(this.data);
65-
const regex = /==[&gt;|>](.*)/g;
66-
let match;
67-
let commands: string[] = [];
68-
69-
if (output.match(regex)) {
70-
while (match = regex.exec(output)) {
71-
commands.push(match[0]);
72-
}
73-
74-
if (commands.length > 1) {
75-
this.commands = [];
76-
}
77-
78-
let retime = new RegExp('\\[exectime\\]: \\d*', 'igm');
79-
let times = [];
80-
while (match = retime.exec(output)) {
81-
let t = match[0].replace(/\[exectime\]: /igm, '');
82-
times.push((t / 10).toFixed(0));
83-
}
84-
85-
this.commands = commands.reduce((acc, curr, i) => {
86-
let next = commands[i + 1] || '';
87-
next = next.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
88-
const c = curr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
89-
let re = new RegExp('(' + c + ')(' + '[\\s\\S]+' + ')(' + next + ')');
90-
if (!output.match(re)) {
91-
re = new RegExp('(' + c + ')' + '[\\s\\S]+');
92-
}
93-
let time = times[i] ? Number(times[i]) : null;
94-
let out = output.match(re) && output.match(re)[2] ? output.match(re)[2].trim() : '';
95-
out = out.replace(retime, '');
96-
97-
out = out.replace(/(\[success\]: .*)/igm, '<span class="ansi-green-fg">$1</span>');
98-
out = out.replace(/(\[error\]: .*)/igm, '<span class="ansi-red-fg">$1</span>');
99-
if (output.includes('[exectime]: stopped')) {
100-
out = out.replace('stopped', '');
101-
}
102-
103-
return acc.concat({
104-
command: curr.replace('==&gt;', '').trim(),
105-
visible: i === commands.length - 1 ? true : false,
106-
output: out,
107-
time: time ? this.getDuration(time) : ''
108-
});
109-
}, this.commands);
110-
} else {
111-
if (output.includes('[exectime]')) {
112-
if (output !== '[exectime]: stopped') {
113-
let retime = new RegExp('\\[exectime\]: \\d*', 'igm');
114-
let match = output.match(retime);
115-
let time = Number((Number(match[0].replace('[exectime]: ', '')) / 10).toFixed(0));
116-
117-
if (this.commands[this.commands.length - 1]) {
118-
this.commands[this.commands.length - 1].time = time ? this.getDuration(time) : '0ms';
119-
}
120-
}
121-
} else {
122-
output = output.replace(/(\[success\]: .*)/igm, '<span class="ansi-green-fg">$1</span>');
123-
output = output.replace(/(\[error\]: .*)/igm, '<span class="ansi-red-fg">$1</span>');
124-
125-
if (this.commands[this.commands.length - 1]) {
126-
this.commands[this.commands.length - 1].output += output;
127-
}
128-
}
129-
}
130-
131-
if (output.includes('[exectime]: stopped')) {
132-
if (this.commands[this.commands.length - 1]) {
133-
this.commands[this.commands.length - 1].time = 'stopped';
134-
}
135-
136-
this.commands.push({
137-
command: 'Execution stopped, entered in debug mode.',
138-
visible: true,
139-
output: '',
140-
time: '...'
141-
});
142-
}
143-
144-
if (this.commands && this.commands.length) {
145-
this.commands = this.commands.map((cmd, i) => {
146-
const v = i === this.commands.length - 1 || cmd.visible;
147-
cmd.visible = v ? true : false;
148-
return cmd;
149-
});
150-
} else {
151-
this.commands.push({ command: output, visible: true, time: '.', output: '' });
152-
}
82+
this.hterm.keyboard.terminal.wipeContents();
83+
return;
15384
}
15485

155-
setTimeout(() => {
156-
const ev: SlimScrollEvent = {
157-
type: 'scrollToBottom',
158-
easing: 'linear',
159-
duration: 50
160-
};
161-
this.scrollEvents.emit(ev);
162-
}, 50);
163-
}
164-
165-
toggleCommand(index: number) {
166-
this.commands[index].visible = !this.commands[index].visible;
167-
setTimeout(() => this.recalculate());
168-
}
169-
170-
recalculate(): void {
171-
const event: SlimScrollEvent = {
172-
type: 'recalculate',
173-
easing: 'linear'
174-
};
86+
console.log(this.data);
17587

176-
this.scrollEvents.emit(event);
88+
if (!this.terminalReady) {
89+
this.unwritenChanges += this.data;
90+
} else {
91+
this.printToTerminal(this.data);
92+
}
17793
}
17894

179-
getDuration(millis: number): string {
180-
const dur = {};
181-
const units = [
182-
{label: 'ms', mod: 100 }, // millis
183-
{label: 'sec', mod: 60 },
184-
{label: 'min', mod: 60 },
185-
{label: 'h', mod: 24 },
186-
{label: 'd', mod: 31 }
187-
];
188-
units.forEach(u => millis = (millis - (dur[u.label] = (millis % u.mod))) / u.mod);
189-
const nonZero = (u) => { return dur[u.label]; };
190-
dur.toString = () => {
191-
return units
192-
.reverse()
193-
.filter(nonZero)
194-
.map(u => dur[u.label] + u.label)
195-
.join(', ');
196-
};
197-
198-
return dur.toString();
95+
printToTerminal(data: string) {
96+
this.hterm.io.print(this.data);
97+
if (this.hterm.keyboard.terminal
98+
&& this.hterm.keyboard.terminal.scrollPort_
99+
&& this.hterm.keyboard.terminal.scrollPort_.isScrolledEnd) {
100+
this.hterm.keyboard.terminal.scrollEnd();
101+
}
199102
}
200103
}

0 commit comments

Comments
 (0)