Skip to content

Commit

Permalink
feat(hterm): hterm integration
Browse files Browse the repository at this point in the history
  • Loading branch information
Izak88 committed Nov 8, 2017
1 parent 5f602b8 commit eac7aa4
Show file tree
Hide file tree
Showing 17 changed files with 86 additions and 1,350 deletions.
906 changes: 6 additions & 900 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"file-loader": "^1.1.5",
"fs-extra": "^4.0.2",
"glob": "^7.1.2",
"hterm-umdjs": "1.2.0",
"html-loader": "^0.5.1",
"html-webpack-plugin": "^2.30.1",
"jasmine": "^2.8.0",
Expand Down
19 changes: 13 additions & 6 deletions src/api/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,15 @@ export function attachExec(id: string, cmd: any): Observable<any> {
}

if (cmd.type === CommandType.store_cache) {
observer.next({ type: 'data', data: chalk.yellow('==> saving cache ...') + '\r' });
observer.next({
type: 'data',
data: chalk.yellow('==> saving cache ...') + '\r\n' });
} else if (cmd.type === CommandType.restore_cache) {
observer.next({ type: 'data', data: chalk.yellow('==> restoring cache ...') + '\r' });
observer.next({
type: 'data',
data: chalk.yellow('==> restoring cache ...') + '\r\n' });
} else {
observer.next({ type: 'data', data: chalk.yellow('==> ' + command) + '\r' });
observer.next({ type: 'data', data: chalk.yellow('==> ' + command) + '\r\n' });
}

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

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

ws._write = (chunk, enc, next) => {
const str = chunk.toString();
let str = chunk.toString('utf8');

if (str.includes('[error]')) {
const splitted = str.split(' ');
Expand All @@ -104,7 +107,11 @@ export function attachExec(id: string, cmd: any): Observable<any> {
} else if (str.includes('[success]')) {
exitCode = 0;
ws.end();
} else if (!str.includes('/usr/bin/abstruse') && !str.startsWith('>')) {
} else if (!str.includes('/usr/bin/abstruse \'' + cmd.command) && !str.startsWith('>')) {
if (str.includes('//') && str.includes('@')) {
str = str.replace(/\/\/(.*)@/, '//');
}

observer.next({ type: 'data', data: str });
}

Expand Down
Binary file removed src/app/assets/public/fonts/RobotoMono-Bold.ttf
Binary file not shown.
Binary file removed src/app/assets/public/fonts/RobotoMono-Light.ttf
Binary file not shown.
Binary file removed src/app/assets/public/fonts/RobotoMono-Medium.ttf
Binary file not shown.
Binary file removed src/app/assets/public/fonts/RobotoMono-Regular.ttf
Binary file not shown.
2 changes: 1 addition & 1 deletion src/app/components/app-job/app-job.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export class AppJobComponent implements OnInit, OnDestroy {
}

if (event.data === 'job started') {
this.terminalInput = { clear: true };
this.jobRun.status = 'running';
this.jobRun.end_time = null;
this.jobRun.start_time = event.additionalData;
Expand Down Expand Up @@ -187,7 +188,6 @@ export class AppJobComponent implements OnInit, OnDestroy {
restartJob(e: MouseEvent): void {
e.preventDefault();
e.stopPropagation();
this.terminalInput = { clear: true };
this.processing = true;
this.sshd = null;
this.vnc = null;
Expand Down
13 changes: 1 addition & 12 deletions src/app/components/app-terminal/app-terminal.component.html
Original file line number Diff line number Diff line change
@@ -1,12 +1 @@
<div class="window-terminal-container dracula-ansi-theme" [class.is-hidden]="noData" slimScroll [options]="scrollOptions" [scrollEvents]="scrollEvents">
<div class="terminal" *ngFor="let cmd of commands; let i = index;" [id]="i">
<div class="command-line" (click)="toggleCommand(i)" [class.is-opened]="cmd.visible">
<span class="command" [innerHTML]="cmd.command"></span>
<span class="time" *ngIf="cmd.time">{{ cmd.time }}</span>
<span class="command-loader" *ngIf="!cmd.time">
<img src="/images/icons/spinner.svg">
</span>
</div>
<pre class="output" [class.is-hidden]="!cmd.visible" [innerHTML]="cmd.output"></pre>
</div>
</div>
<div class="window-terminal-container"></div>
221 changes: 62 additions & 159 deletions src/app/components/app-terminal/app-terminal.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ import {
Inject
} from '@angular/core';
import { DOCUMENT } from '@angular/common';
import * as AnsiUp from 'ansi_up';
import { SlimScrollEvent, ISlimScrollOptions } from 'ngx-slimscroll';
import * as hterm from 'hterm-umdjs';

const terminalColorPallete = ['rgb(40, 42, 54)', 'rgb(255, 85, 85)', 'rgb(80, 250, 123)',
'rgb(243, 251, 151)', 'rgb(189, 147, 249)', 'rgb(255, 121, 198)', 'rgb(139, 233, 253)',
'rgb(187, 187, 187)', 'rgb(85, 85, 85)', 'rgb(255, 85, 85)', 'rgb(80, 250, 123)',
'rgb(243, 251, 151)', 'rgb(189, 147, 249)', 'rgb(255, 121, 198)', 'rgb(139, 233, 253)',
'rgb(255, 255, 255)'];

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

constructor(
private elementRef: ElementRef,
@Inject(DOCUMENT) private document: any
) {
this.scrollOptions = {
barBackground: '#666',
gridBackground: '#000',
barBorderRadius: '10',
barWidth: '7',
gridWidth: '7',
barMargin: '2px 5px',
gridMargin: '2px 5px',
gridBorderRadius: '10',
alwaysVisible: false
};

this.scrollEvents = new EventEmitter<SlimScrollEvent>();
hterm.hterm.defaultStorage = new hterm.lib.Storage.Local();
this.hterm = new hterm.hterm.Terminal();
this.terminalReady = false;
this.unwritenChanges = '';
}

ngOnInit() {
this.au = new AnsiUp.default();
this.au.use_classes = true;
this.commands = [];
this.noData = true;
this.hterm.onVTKeystroke = () => {};
this.hterm.showOverlay = () => {};
this.hterm.onTerminalReady = () => {
this.hterm.setWindowTitle = () => {};
this.hterm.prefs_.set('cursor-color', 'transparent');
this.hterm.prefs_.set('font-family', 'monaco, menlo, monospace');
this.hterm.prefs_.set('font-size', 11);
this.hterm.prefs_.set('audible-bell-sound', '');
this.hterm.prefs_.set('font-smoothing', 'subpixel-antialiased');
this.hterm.prefs_.set('enable-bold', false);
this.hterm.prefs_.set('backspace-sends-backspace', true);
this.hterm.prefs_.set('cursor-blink', false);
this.hterm.prefs_.set('receive-encoding', 'raw');
this.hterm.prefs_.set('send-encoding', 'raw');
this.hterm.prefs_.set('alt-sends-what', 'browser-key');
this.hterm.prefs_.set('scrollbar-visible', false);
this.hterm.prefs_.set('enable-clipboard-notice', false);
this.hterm.prefs_.set('background-color', '#000000');
this.hterm.prefs_.set('foreground-color', '#f8f8f2');
hterm.lib.colors.stockColorPalette.splice(0, terminalColorPallete.length)
hterm.lib.colors.stockColorPalette = terminalColorPallete.concat(
hterm.lib.colors.stockColorPalette);
this.hterm.prefs_.set('color-palette-overrides', terminalColorPallete);

this.terminalReady = true;
if (this.unwritenChanges) {
this.printToTerminal(this.unwritenChanges);
this.unwritenChanges = '';
}
};

this.hterm.decorate(this.document.querySelector('.window-terminal-container'));
this.hterm.installKeyboard(null);
}

ngOnChanges(changes: SimpleChange) {
if (!this.data) {
return;
}

this.noData = false;

if (typeof this.data.clear !== 'undefined') {
this.commands = [];
} else {
let output: string = this.au.ansi_to_html(this.data);
const regex = /==[&gt;|>](.*)/g;
let match;
let commands: string[] = [];

if (output.match(regex)) {
while (match = regex.exec(output)) {
commands.push(match[0]);
}

if (commands.length > 1) {
this.commands = [];
}

let retime = new RegExp('\\[exectime\\]: \\d*', 'igm');
let times = [];
while (match = retime.exec(output)) {
let t = match[0].replace(/\[exectime\]: /igm, '');
times.push((t / 10).toFixed(0));
}

this.commands = commands.reduce((acc, curr, i) => {
let next = commands[i + 1] || '';
next = next.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
const c = curr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
let re = new RegExp('(' + c + ')(' + '[\\s\\S]+' + ')(' + next + ')');
if (!output.match(re)) {
re = new RegExp('(' + c + ')' + '[\\s\\S]+');
}
let time = times[i] ? Number(times[i]) : null;
let out = output.match(re) && output.match(re)[2] ? output.match(re)[2].trim() : '';
out = out.replace(retime, '');

out = out.replace(/(\[success\]: .*)/igm, '<span class="ansi-green-fg">$1</span>');
out = out.replace(/(\[error\]: .*)/igm, '<span class="ansi-red-fg">$1</span>');
if (output.includes('[exectime]: stopped')) {
out = out.replace('stopped', '');
}

return acc.concat({
command: curr.replace('==&gt;', '').trim(),
visible: i === commands.length - 1 ? true : false,
output: out,
time: time ? this.getDuration(time) : ''
});
}, this.commands);
} else {
if (output.includes('[exectime]')) {
if (output !== '[exectime]: stopped') {
let retime = new RegExp('\\[exectime\]: \\d*', 'igm');
let match = output.match(retime);
let time = Number((Number(match[0].replace('[exectime]: ', '')) / 10).toFixed(0));

if (this.commands[this.commands.length - 1]) {
this.commands[this.commands.length - 1].time = time ? this.getDuration(time) : '0ms';
}
}
} else {
output = output.replace(/(\[success\]: .*)/igm, '<span class="ansi-green-fg">$1</span>');
output = output.replace(/(\[error\]: .*)/igm, '<span class="ansi-red-fg">$1</span>');

if (this.commands[this.commands.length - 1]) {
this.commands[this.commands.length - 1].output += output;
}
}
}

if (output.includes('[exectime]: stopped')) {
if (this.commands[this.commands.length - 1]) {
this.commands[this.commands.length - 1].time = 'stopped';
}

this.commands.push({
command: 'Execution stopped, entered in debug mode.',
visible: true,
output: '',
time: '...'
});
}

if (this.commands && this.commands.length) {
this.commands = this.commands.map((cmd, i) => {
const v = i === this.commands.length - 1 || cmd.visible;
cmd.visible = v ? true : false;
return cmd;
});
} else {
this.commands.push({ command: output, visible: true, time: '.', output: '' });
}
this.hterm.keyboard.terminal.wipeContents();
return;
}

setTimeout(() => {
const ev: SlimScrollEvent = {
type: 'scrollToBottom',
easing: 'linear',
duration: 50
};
this.scrollEvents.emit(ev);
}, 50);
}

toggleCommand(index: number) {
this.commands[index].visible = !this.commands[index].visible;
setTimeout(() => this.recalculate());
}

recalculate(): void {
const event: SlimScrollEvent = {
type: 'recalculate',
easing: 'linear'
};
console.log(this.data);

this.scrollEvents.emit(event);
if (!this.terminalReady) {
this.unwritenChanges += this.data;
} else {
this.printToTerminal(this.data);
}
}

getDuration(millis: number): string {
const dur = {};
const units = [
{label: 'ms', mod: 100 }, // millis
{label: 'sec', mod: 60 },
{label: 'min', mod: 60 },
{label: 'h', mod: 24 },
{label: 'd', mod: 31 }
];
units.forEach(u => millis = (millis - (dur[u.label] = (millis % u.mod))) / u.mod);
const nonZero = (u) => { return dur[u.label]; };
dur.toString = () => {
return units
.reverse()
.filter(nonZero)
.map(u => dur[u.label] + u.label)
.join(', ');
};

return dur.toString();
printToTerminal(data: string) {
this.hterm.io.print(this.data);
if (this.hterm.keyboard.terminal
&& this.hterm.keyboard.terminal.scrollPort_
&& this.hterm.keyboard.terminal.scrollPort_.isScrolledEnd) {
this.hterm.keyboard.terminal.scrollEnd();
}
}
}
7 changes: 0 additions & 7 deletions src/app/styles/fonts.sass
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,3 @@ $font-family-regular: 'Montserrat-Medium', sans-serif
$font-family-semibold: 'Montserrat-SemiBold', sans-serif
$font-family-bold: 'Montserrat-Bold', sans-serif
$font-family-extrabold: 'Montserrat-ExtraBold', sans-serif

+font-face('RobotoMono', '/fonts/RobotoMono-Light', 300, 'normal')
+font-face('RobotoMono', '/fonts/RobotoMono-Regular', 400, 'normal')
+font-face('RobotoMono', '/fonts/RobotoMono-Medium', 500, 'normal')
+font-face('RobotoMono', '/fonts/RobotoMono-Bold', 700, 'normal')

$font-family-roboto-mono: 'RobotoMono', monospace
1 change: 0 additions & 1 deletion src/app/styles/forms.sass
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,4 @@ textarea
&.form-input
background: $white
font-size: 12px
font-family: $font-family-roboto-mono
resize: none
1 change: 0 additions & 1 deletion src/app/styles/images.sass
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

.image-build-log
background: $black
font-family: $font-family-roboto-mono
color: #f8f8f2
font-size: 12px
margin: 0
Expand Down
Loading

0 comments on commit eac7aa4

Please sign in to comment.