Skip to content

Commit

Permalink
multiple shell improvements
Browse files Browse the repository at this point in the history
refactored first commands
added autocomplete after first word (cryptic-game#39)
added some argparsing
improved help command
added aliases
  • Loading branch information
Akida31 committed May 29, 2021
1 parent 9ad7c58 commit cf3cc5d
Show file tree
Hide file tree
Showing 16 changed files with 782 additions and 21 deletions.
2 changes: 1 addition & 1 deletion src/app/desktop/windows/terminal/terminal-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export interface TerminalAPI {
export interface TerminalState {
execute(command: string);

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

getHistory(): string[];

Expand Down
84 changes: 80 additions & 4 deletions src/app/desktop/windows/terminal/terminal-states.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ 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 'src/app/shell/shell';
import {ShellApi} from 'src/app/shell/shellapi';


function escapeHtml(html: string): string {
Expand All @@ -33,7 +35,7 @@ export abstract class CommandTerminalState implements TerminalState {
executeCommand(command: string, args: string[], io: IOHandler = null) {
const iohandler = io ? io : {
stdout: this.stdoutHandler.bind(this),
stdin: this.stdinHandler,
stdin: this.stdinHandler.bind(this),
stderr: this.stderrHandler.bind(this),
args: args
};
Expand Down Expand Up @@ -170,7 +172,7 @@ export abstract class CommandTerminalState implements TerminalState {

abstract commandNotFound(command: string, iohandler: IOHandler): void;

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 @@ -312,6 +314,10 @@ export class DefaultTerminalState extends CommandTerminalState {
executor: this.read.bind(this),
description: 'read input of user'
},
'msh': {
executor: this.msh.bind(this),
description: 'create a new shell'
},

// easter egg
'chaozz': {
Expand Down Expand Up @@ -1947,6 +1953,16 @@ export class DefaultTerminalState extends CommandTerminalState {
this.setExitCode(0);
});
}

msh(_: IOHandler) {
this.terminal.pushState(
new ShellTerminal(
this.websocket, this.settings, this.fileService,
this.domSanitizer, this.windowDelegate, this.activeDevice,
this.terminal, this.promptColor
)
);
}
}


Expand All @@ -1972,7 +1988,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 @@ -2060,7 +2076,7 @@ class DefaultStdin implements TerminalState {
this.callback(input);
}

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

Expand Down Expand Up @@ -2144,3 +2160,63 @@ enum OutputType {
TEXT,
NODE,
}

class ShellTerminal implements TerminalState {
private shell: Shell;

constructor(private websocket: WebsocketService, private settings: SettingsService, private fileService: FileService,
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.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);
});

}

}

33 changes: 17 additions & 16 deletions src/app/desktop/windows/terminal/terminal.component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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 {FileService} from '../../../api/files/file.service';
import {WindowManager} from '../../window-manager/window-manager';

// noinspection AngularMissingOrInvalidDeclarationInModule
@Component({
Expand All @@ -15,9 +15,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 Down Expand Up @@ -107,10 +107,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
10 changes: 10 additions & 0 deletions src/app/shell/builtins/builtins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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';

export const BUILTINS = [Status, Hostname, Miner, Cd, Ls, Clear, Exit];

43 changes: 43 additions & 0 deletions src/app/shell/builtins/cd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {Command, IOHandler, ArgType} from '../command';
import {ShellApi} from '../shellapi';
import {Path} from 'src/app/api/files/path';

export class Cd extends Command {
constructor(shellApi: ShellApi) {
super('cd', shellApi);
this.addDescription('changes the working directory');
this.addPositionalArgument({name: 'directory', optional: true, argType: ArgType.DIRECTORY});
}

async run(iohandler: IOHandler): Promise<number> {
const args = iohandler.positionalArgs;
const input = args.length === 1 ? args[0] : '/';
let path: Path;
try {
path = Path.fromString(input, this.shellApi.working_dir);
} catch {
iohandler.stderr('The specified path is not valid');
return 1;
}
let file: any;
try {
file = await this.shellApi.fileService.getFromPath(this.shellApi.activeDevice['uuid'], path).toPromise();
} catch (error) {
if (error.message === 'file_not_found') {
iohandler.stderr('That directory does not exist');
return 2;
} else {
this.reportError(error);
return 1;
}
}
if (file.is_directory) {
this.shellApi.working_dir = file.uuid;
this.shellApi.refreshPrompt();
return 0;
} else {
iohandler.stderr('That is not a directory');
return 1;
}
}
}
14 changes: 14 additions & 0 deletions src/app/shell/builtins/clear.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Command, IOHandler} from '../command';
import {ShellApi} from '../shellapi';

export class Clear extends Command {
constructor(shellApi: ShellApi) {
super('clear', shellApi);
this.addDescription('clears the terminal');
}

async run(_: IOHandler): Promise<number> {
this.shellApi.terminal.clear();
return 0;
}
}
14 changes: 14 additions & 0 deletions src/app/shell/builtins/exit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Command, IOHandler} from '../command';
import {ShellApi} from '../shellapi';

export class Exit extends Command {
constructor(shellApi: ShellApi) {
super('exit', shellApi);
this.addDescription('closes the terminal or leaves another device');
}

async run(_: IOHandler): Promise<number> {
this.shellApi.terminal.popState();
return 0;
}
}
51 changes: 51 additions & 0 deletions src/app/shell/builtins/hostname.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {Command, IOHandler} from '../command';
import {Device} from '../../api/devices/device';
import {ShellApi} from '../shellapi';

export class Hostname extends Command {
constructor(shellApi: ShellApi) {
super('hostname', shellApi);
this.addDescription('changes the name of the device');
this.addPositionalArgument({name: 'name', optional: true});
}

async run(iohandler: IOHandler): Promise<number> {
const args = iohandler.positionalArgs;
if (args.length === 1) {
const hostname = args[0];
let newDevice: Device;
try {
newDevice = await this.shellApi.websocket.ms('device', ['device', 'change_name'], {
device_uuid: this.shellApi.activeDevice['uuid'],
name: hostname
}).toPromise();
} catch {
iohandler.stderr('The hostname couldn\'t be changed');
return 1;
}
this.shellApi.activeDevice = newDevice;
this.shellApi.refreshPrompt();

if (this.shellApi.activeDevice.uuid === this.shellApi.windowDelegate.device.uuid) {
Object.assign(this.shellApi.windowDelegate.device, newDevice);
}
} else {
let device: Device;
try {
device = await this.shellApi.websocket.ms(
'device',
['device', 'info'],
{device_uuid: this.shellApi.activeDevice['uuid']}
).toPromise();
} catch {
iohandler.stdout(this.shellApi.activeDevice['name']);
}
if (device['name'] !== this.shellApi.activeDevice['name']) {
this.shellApi.activeDevice = device;
this.shellApi.refreshPrompt();
}
iohandler.stdout(device['name']);
}
return 0;
}
}
54 changes: 54 additions & 0 deletions src/app/shell/builtins/ls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {Command, IOHandler, ArgType} from '../command';
import {ShellApi} from '../shellapi';
import {Path} from 'src/app/api/files/path';
import {File} from 'src/app/api/files/file';

export class Ls extends Command {
constructor(shellApi: ShellApi) {
super('ls', shellApi);
this.addDescription('shows files of the current working directory');
this.addPositionalArgument({name: 'path', optional: true, argType: ArgType.PATH});
}

async run(iohandler: IOHandler): Promise<number> {
const args = iohandler.positionalArgs;
let files: File[];
if (args.length === 0) {
files = await this.shellApi.listFilesOfWorkingDir();
} else {
let path: Path;
try {
path = Path.fromString(args[0], this.shellApi.working_dir);
} catch {
iohandler.stderr('The specified path is not valid');
return 1;
}
try {
const target = await this.shellApi.fileService.getFromPath(this.shellApi.activeDevice['uuid'], path).toPromise();
if (target.is_directory) {
files = await this.shellApi.fileService.getFiles(this.shellApi.activeDevice['uuid'], target.uuid).toPromise();
} else {
files = [target];
}
} catch (error) {
if (error.message === 'file_not_found') {
iohandler.stderr('That directory does not exist');
return 2;
} else {
this.reportError(error);
return 1;
}
}
}

files.filter((file) => file.is_directory).sort().forEach(folder => {
// TODO use escape codes
iohandler.stdout(`<span style="color: ${this.shellApi.settings.getLSFC()};">${(this.shellApi.settings.getLSPrefix()) ? '[Folder] ' : ''}${folder.filename}</span>`);
});

files.filter((file) => !file.is_directory).sort().forEach(file => {
iohandler.stdout(`${(this.shellApi.settings.getLSPrefix() ? '[File] ' : '')}${file.filename}`);
});
return 0;
}
}
Loading

0 comments on commit cf3cc5d

Please sign in to comment.