Skip to content
This repository has been archived by the owner on Sep 20, 2023. It is now read-only.

[WIP] improve shell - waits for alpha1 Backend #258

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions src/app/desktop/windows/terminal/terminal-api.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,64 @@
import { SafeHtml } from '@angular/platform-browser';
import {Device} from 'src/app/api/devices/device';

export interface TerminalAPI {
/**
* Outputs html to the terminal (followed by a line break)
* @param html HTML string
*/
output(html: string);
output(html: string): void;

/**
* Outputs html without a line break to the terminal
* @param html HTML string
*/
outputRaw(html: string);
outputRaw(html: string): void;

/**
* Outputs text to the terminal
* @param text Raw text
*/
outputText(text: string);
outputText(text: string): void;

/**
* Outputs a html node to the terminal
* @param node `Node`
*/
outputNode(node: Node);
outputNode(node: Node): void;

/**
* Closes the terminal window
*/
closeTerminal();
closeTerminal(): void;

/**
* Clears the complete terminal
*/
clear();
clear(): void;

changePrompt(prompt: string | SafeHtml, trust?: boolean);
changePrompt(prompt: string | SafeHtml, trust?: boolean): void;

pushState(state: TerminalState);
pushState(state: TerminalState): void;

popState(): TerminalState;

/**
* Shutdowns the current device
*
* @returns: if the shutdown was successful
*/
shutdown(): Promise<boolean>;

getOwnerDevice(): Device;
}


export interface TerminalState {
execute(command: string);
execute(command: string): void;

autocomplete(content: string): string;
autocomplete(content: string): Promise<string>;

getHistory(): string[];

refreshPrompt();
refreshPrompt(): void;
}
117 changes: 111 additions & 6 deletions src/app/desktop/windows/terminal/terminal-states.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import { DomSanitizer } from '@angular/platform-browser';
import { SecurityContext } from '@angular/core';
import { SettingsService } from '../settings/settings.service';
import { FileService } from '../../../api/files/file.service';
import { DeviceService } from '../../../api/devices/device.service';
import { Path } from '../../../api/files/path';
import { of } from 'rxjs';
import { Device } from '../../../api/devices/device';
import { WindowDelegate } from '../../window/window-delegate';
import { File } from '../../../api/files/file';
import { Shell } from '../../../shell/shell';
import { ShellApi } from '../../../shell/shellapi';


function escapeHtml(html) {
Expand Down Expand Up @@ -52,7 +55,7 @@ export abstract class CommandTerminalState implements TerminalState {

abstract commandNotFound(command: string);

autocomplete(content: string): string {
async autocomplete(content: string): Promise<string> {
return content
? Object.entries(this.commands)
.filter(command => !command[1].hidden)
Expand Down Expand Up @@ -178,6 +181,10 @@ export class DefaultTerminalState extends CommandTerminalState {
executor: this.info.bind(this),
description: 'shows info of the current device'
},
'msh': {
executor: this.msh.bind(this),
description: 'creates a new shell'
},

// easter egg
'chaozz': {
Expand All @@ -191,8 +198,8 @@ export class DefaultTerminalState extends CommandTerminalState {
working_dir: string = Path.ROOT; // UUID of the working directory

constructor(protected websocket: WebsocketService, private settings: SettingsService, private fileService: FileService,
private domSanitizer: DomSanitizer, protected windowDelegate: WindowDelegate, protected activeDevice: Device,
protected terminal: TerminalAPI, public promptColor: string = null) {
private deviceService: DeviceService, private domSanitizer: DomSanitizer, protected windowDelegate: WindowDelegate,
protected activeDevice: Device, protected terminal: TerminalAPI, public promptColor: string = null) {
super();
}

Expand Down Expand Up @@ -1181,8 +1188,8 @@ export class DefaultTerminalState extends CommandTerminalState {
this.websocket.ms('device', ['device', 'info'], { device_uuid: args[0] }).subscribe(infoData => {
this.websocket.ms('service', ['part_owner'], { device_uuid: args[0] }).subscribe(partOwnerData => {
if (infoData['owner'] === this.websocket.account.uuid || partOwnerData['ok'] === true) {
this.terminal.pushState(new DefaultTerminalState(this.websocket, this.settings, this.fileService, this.domSanitizer,
this.windowDelegate, infoData, this.terminal, '#DD2C00'));
this.terminal.pushState(new DefaultTerminalState(this.websocket, this.settings, this.fileService, this.deviceService,
this.domSanitizer, this.windowDelegate, infoData, this.terminal, '#DD2C00'));
} else {
this.terminal.outputText('Access denied');
}
Expand Down Expand Up @@ -1554,6 +1561,15 @@ export class DefaultTerminalState extends CommandTerminalState {

DefaultTerminalState.registerPromptAppenders(element);
}

msh() {
this.terminal.pushState(
new ShellTerminal(
this.websocket, this.settings, this.fileService, this.deviceService, this.domSanitizer, this.windowDelegate,
this.activeDevice, this.terminal, this.promptColor
)
)
}
}


Expand All @@ -1579,7 +1595,7 @@ export abstract class ChoiceTerminalState implements TerminalState {
this.terminal.outputText('\'' + choice + '\' is not one of the following: ' + Object.keys(this.choices).join(', '));
}

autocomplete(content: string): string {
async autocomplete(content: string): Promise<string> {
return content ? Object.keys(this.choices).sort().find(choice => choice.startsWith(content)) : '';
}

Expand Down Expand Up @@ -1650,3 +1666,92 @@ export class BruteforceTerminalState extends ChoiceTerminalState {
this.terminal.changePrompt(`<span style="color: gold">${prompt}</span>`, true);
}
}


class DefaultStdin implements TerminalState {
private callback: (stdin: string) => void;

constructor(private terminal: TerminalAPI) {}

read(callback: (stdin: string) => void) {
this.callback = callback;
this.terminal.pushState(this);
}

execute(command: string) {
const input = command ? command : '';
this.terminal.popState();
this.callback(input);
}

async autocomplete(i: string): Promise<string> {
return i;
}

getHistory(): string[] {
return [];
}

refreshPrompt() {
this.terminal.changePrompt('>');
}
}


class ShellTerminal implements TerminalState {
private shell: Shell;

constructor(private websocket: WebsocketService, private settings: SettingsService, private fileService: FileService,
private deviceService: DeviceService, private domSanitizer: DomSanitizer, windowDelegate: WindowDelegate,
private activeDevice: Device, private terminal: TerminalAPI, private promptColor: string = null
) {
const shellApi = new ShellApi(
this.websocket, this.settings, this.fileService, this.deviceService,
this.domSanitizer, windowDelegate, this.activeDevice,
terminal, this.promptColor, this.refreshPrompt.bind(this),
Path.ROOT
);
this.shell = new Shell(
this.terminal.output.bind(this.terminal),
// TODO use this
// this.terminal.outputText.bind(this.terminal),
this.stdinHandler.bind(this),
this.terminal.outputText.bind(this.terminal),
shellApi,
);
}

/** default implementaion for stdin: reading from console */
stdinHandler(callback: (input: string) => void) {
return new DefaultStdin(this.terminal).read(callback);
}
execute(command: string) {
this.shell.execute(command);
}

async autocomplete(content: string): Promise<string> {
return await this.shell.autocomplete(content);
}

getHistory(): string[] {
return this.shell.getHistory();
}

refreshPrompt() {
this.fileService.getAbsolutePath(this.activeDevice['uuid'], this.shell.api.working_dir).subscribe(path => {
// const color = this.domSanitizer.sanitize(SecurityContext.STYLE, this.promptColor || this.settings.getTPC());
// TODO undo this
const color = 'yellow';
const prompt = this.domSanitizer.bypassSecurityTrustHtml(
`<span style="color: ${color}">` +
`${escapeHtml(this.websocket.account.name)}@${escapeHtml(this.activeDevice['name'])}` +
`<span style="color: white">:</span>` +
`<span style="color: #0089ff;">/${path.join('/')}</span>$` +
`</span>`
);
+ this.terminal.changePrompt(prompt);
});

}

}
55 changes: 38 additions & 17 deletions src/app/desktop/windows/terminal/terminal.component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { SettingsService } from '../settings/settings.service';
import { Component, ElementRef, OnInit, SecurityContext, Type, ViewChild } from '@angular/core';
import { WindowComponent, WindowDelegate } from '../../window/window-delegate';
import { TerminalAPI, TerminalState } from './terminal-api';
import { DefaultTerminalState } from './terminal-states';
import { WebsocketService } from '../../../websocket.service';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { FileService } from '../../../api/files/file.service';
import { WindowManager } from '../../window-manager/window-manager';
import {SettingsService} from '../settings/settings.service';
import {Component, ElementRef, OnInit, SecurityContext, Type, ViewChild} from '@angular/core';
import {WindowComponent, WindowDelegate} from '../../window/window-delegate';
import {TerminalAPI, TerminalState} from './terminal-api';
import {DefaultTerminalState} from './terminal-states';
import {WebsocketService} from '../../../websocket.service';
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
import {DeviceService} from '../../../api/devices/device.service';
import {FileService} from '../../../api/files/file.service';
import {WindowManager} from '../../window-manager/window-manager';
import {Router} from '@angular/router';
import {Device} from 'src/app/api/devices/device';

// noinspection AngularMissingOrInvalidDeclarationInModule
@Component({
Expand All @@ -15,9 +18,9 @@ import { WindowManager } from '../../window-manager/window-manager';
styleUrls: ['./terminal.component.scss']
})
export class TerminalComponent extends WindowComponent implements OnInit, TerminalAPI {
@ViewChild('history', { static: true }) history: ElementRef;
@ViewChild('prompt', { static: true }) prompt: ElementRef;
@ViewChild('cmdLine', { static: true }) cmdLine: ElementRef;
@ViewChild('history', {static: true}) history: ElementRef;
@ViewChild('prompt', {static: true}) prompt: ElementRef;
@ViewChild('cmdLine', {static: true}) cmdLine: ElementRef;

currentState: TerminalState[] = [];
promptHtml: SafeHtml;
Expand All @@ -28,8 +31,10 @@ export class TerminalComponent extends WindowComponent implements OnInit, Termin
private websocket: WebsocketService,
private settings: SettingsService,
private fileService: FileService,
private deviceService: DeviceService,
private windowManager: WindowManager,
private domSanitizer: DomSanitizer
private domSanitizer: DomSanitizer,
private router: Router,
) {
super();
}
Expand All @@ -40,6 +45,7 @@ export class TerminalComponent extends WindowComponent implements OnInit, Termin
this.websocket,
this.settings,
this.fileService,
this.deviceService,
this.domSanitizer,
this.delegate,
this.delegate.device,
Expand Down Expand Up @@ -107,10 +113,11 @@ export class TerminalComponent extends WindowComponent implements OnInit, Termin
}

autocomplete(content: string) {
const completed = this.getState().autocomplete(content);
if (completed) {
this.cmdLine.nativeElement.value = completed;
}
this.getState().autocomplete(content).then((completed) => {
if (completed) {
this.cmdLine.nativeElement.value = completed;
}
});
}

previousFromHistory() {
Expand Down Expand Up @@ -167,6 +174,20 @@ export class TerminalComponent extends WindowComponent implements OnInit, Termin
clear() {
this.history.nativeElement.value = '';
}

async shutdown(): Promise<boolean> {
const uuid = this.delegate.device['uuid'];
try {
await this.deviceService.togglePower(uuid).toPromise();
} catch {
return false;
}
return await this.router.navigate(['device'], {queryParams: {device: uuid}});
}

getOwnerDevice(): Device {
return this.delegate.device;
}
}

export class TerminalWindowDelegate extends WindowDelegate {
Expand Down
14 changes: 14 additions & 0 deletions src/app/shell/builtins/builtins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Status} from './status';
import {Hostname} from './hostname';
import {Miner} from './miner';
import {Cd} from './cd';
import {Ls} from './ls';
import {Clear} from './clear';
import {Exit} from './exit';
import {Dl} from './dl';
import {Shutdown} from './shutdown';
import {Ping} from './ping';
import {Credits} from './credits';

export const BUILTINS = [Status, Hostname, Miner, Cd, Ls, Clear, Exit, Dl, Shutdown, Ping, Credits];

Loading