Initial commit
This commit is contained in:
720
ui/commands/commands.js
Normal file
720
ui/commands/commands.js
Normal file
@@ -0,0 +1,720 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import Exception from "./exception.js";
|
||||
import * as stream from "../stream/streams.js";
|
||||
import * as subscribe from "../stream/subscribe.js";
|
||||
|
||||
export const NEXT_PROMPT = 1;
|
||||
export const NEXT_WAIT = 2;
|
||||
export const NEXT_DONE = 3;
|
||||
|
||||
export class Result {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {string} name Result type
|
||||
* @param {Info} info Result info
|
||||
* @param {object} control Result controller
|
||||
*/
|
||||
constructor(name, info, control) {
|
||||
this.name = name;
|
||||
this.info = info;
|
||||
this.control = control;
|
||||
}
|
||||
}
|
||||
|
||||
class Done {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {object} data Step data
|
||||
*
|
||||
*/
|
||||
constructor(data) {
|
||||
this.s = !!data.success;
|
||||
this.d = data.successData;
|
||||
this.errorTitle = data.errorTitle;
|
||||
this.errorMessage = data.errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the error of current Done
|
||||
*
|
||||
* @returns {string} title
|
||||
*
|
||||
*/
|
||||
error() {
|
||||
return this.errorTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the error message of current Done
|
||||
*
|
||||
* @returns {string} message
|
||||
*
|
||||
*/
|
||||
message() {
|
||||
return this.errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not current Done is representing a success
|
||||
*
|
||||
* @returns {boolean} True when success, false otherwise
|
||||
*/
|
||||
success() {
|
||||
return this.s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns final data
|
||||
*
|
||||
* @returns {Result} Successful result
|
||||
*/
|
||||
data() {
|
||||
return this.d;
|
||||
}
|
||||
}
|
||||
|
||||
class Wait {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {object} data Step data
|
||||
*
|
||||
*/
|
||||
constructor(data) {
|
||||
this.t = data.title;
|
||||
this.m = data.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the title of current Wait
|
||||
*
|
||||
* @returns {string} title
|
||||
*
|
||||
*/
|
||||
title() {
|
||||
return this.t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the message of current Wait
|
||||
*
|
||||
* @returns {string} message
|
||||
*
|
||||
*/
|
||||
message() {
|
||||
return this.m;
|
||||
}
|
||||
}
|
||||
|
||||
const defField = {
|
||||
name: "",
|
||||
description: "",
|
||||
type: "",
|
||||
value: "",
|
||||
example: "",
|
||||
verify(v) {
|
||||
return "OK";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a Prompt field
|
||||
*
|
||||
* @param {object} def Field default value
|
||||
* @param {object} f Field value
|
||||
*
|
||||
* @returns {object} Field data
|
||||
*
|
||||
* @throws {Exception} When input field is invalid
|
||||
*
|
||||
*/
|
||||
export function field(def, f) {
|
||||
let n = {};
|
||||
|
||||
for (let i in def) {
|
||||
n[i] = def[i];
|
||||
}
|
||||
|
||||
for (let i in f) {
|
||||
if (typeof n[i] !== typeof f[i]) {
|
||||
throw new Exception(
|
||||
'Field data type for "' +
|
||||
i +
|
||||
'" was not unmatched. Expecting "' +
|
||||
typeof def[i] +
|
||||
'", got "' +
|
||||
typeof f[i] +
|
||||
'" instead'
|
||||
);
|
||||
}
|
||||
|
||||
n[i] = f[i];
|
||||
}
|
||||
|
||||
if (!n["name"]) {
|
||||
throw new Exception('Field "name" must be specified');
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a group of field value
|
||||
*
|
||||
* @param {object} definitions Definition of a group of fields
|
||||
* @param {array<object>} fs Data of the field group
|
||||
*
|
||||
* @returns {array<object>} Result fields
|
||||
*
|
||||
* @throws {Exception} When input field is invalid
|
||||
*
|
||||
*/
|
||||
export function fields(definitions, fs) {
|
||||
let fss = [];
|
||||
|
||||
for (let i in fs) {
|
||||
if (!fs[i]["name"]) {
|
||||
throw new Exception('Field "name" must be specified');
|
||||
}
|
||||
|
||||
if (!definitions[fs[i].name]) {
|
||||
throw new Exception('Undefined field "' + fs[i].name + '"');
|
||||
}
|
||||
|
||||
fss.push(field(definitions[fs[i].name], fs[i]));
|
||||
}
|
||||
|
||||
return fss;
|
||||
}
|
||||
|
||||
class Prompt {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {object} data Step data
|
||||
*
|
||||
* @throws {Exception} If the field verify is not a function while
|
||||
* not null
|
||||
*/
|
||||
constructor(data) {
|
||||
this.t = data.title;
|
||||
this.m = data.message;
|
||||
this.a = data.actionText;
|
||||
this.r = data.respond;
|
||||
this.c = data.cancel;
|
||||
|
||||
this.i = [];
|
||||
this.f = {};
|
||||
|
||||
for (let i in data.inputs) {
|
||||
let f = field(defField, data.inputs[i]);
|
||||
|
||||
this.i.push(f);
|
||||
|
||||
this.f[data.inputs[i].name.toLowerCase()] = {
|
||||
value: f.value,
|
||||
verify: f.verify
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the title of current Prompt
|
||||
*
|
||||
* @returns {string} title
|
||||
*
|
||||
*/
|
||||
title() {
|
||||
return this.t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the message of current Prompt
|
||||
*
|
||||
* @returns {string} message
|
||||
*
|
||||
*/
|
||||
message() {
|
||||
return this.m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the input field of current prompt
|
||||
*
|
||||
* @returns {array} Input fields
|
||||
*
|
||||
*/
|
||||
inputs() {
|
||||
let inputs = [];
|
||||
|
||||
for (let i in this.i) {
|
||||
inputs.push(this.i[i]);
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the action
|
||||
*
|
||||
* @returns {string} Action name
|
||||
*
|
||||
*/
|
||||
actionText() {
|
||||
return this.a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive the submit of current prompt
|
||||
*
|
||||
* @param {object} inputs Input value
|
||||
*
|
||||
* @returns {any} The result of the step responder
|
||||
*
|
||||
* @throws {Exception} When the field is undefined or invalid
|
||||
*
|
||||
*/
|
||||
submit(inputs) {
|
||||
let fields = {};
|
||||
|
||||
for (let i in this.f) {
|
||||
fields[i] = this.f[i].value;
|
||||
}
|
||||
|
||||
for (let i in inputs) {
|
||||
let k = i.toLowerCase();
|
||||
|
||||
if (typeof fields[k] === "undefined") {
|
||||
throw new Exception('Field "' + k + '" is undefined');
|
||||
}
|
||||
|
||||
try {
|
||||
this.f[k].verify(inputs[i]);
|
||||
} catch (e) {
|
||||
throw new Exception('Field "' + k + '" is invalid: ' + e);
|
||||
}
|
||||
|
||||
fields[k] = inputs[i];
|
||||
}
|
||||
|
||||
return this.r(fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel current wait operation
|
||||
*
|
||||
*/
|
||||
cancel() {
|
||||
return this.c();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Wizard step
|
||||
*
|
||||
* @param {string} type Step type
|
||||
* @param {object} data Step data
|
||||
*
|
||||
* @returns {object} Step data
|
||||
*
|
||||
*/
|
||||
function next(type, data) {
|
||||
return {
|
||||
type() {
|
||||
return type;
|
||||
},
|
||||
data() {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create data for a Done step of the wizard
|
||||
*
|
||||
* @param {boolean} success
|
||||
* @param {Success} successData
|
||||
* @param {string} errorTitle
|
||||
* @param {string} errorMessage
|
||||
*
|
||||
* @returns {object} Done step data
|
||||
*
|
||||
*/
|
||||
export function done(success, successData, errorTitle, errorMessage) {
|
||||
return next(NEXT_DONE, {
|
||||
success: success,
|
||||
successData: successData,
|
||||
errorTitle: errorTitle,
|
||||
errorMessage: errorMessage
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create data for a Wait step of the wizard
|
||||
*
|
||||
* @param {string} title Waiter title
|
||||
* @param {message} message Waiter message
|
||||
*
|
||||
* @returns {object} Done step data
|
||||
*
|
||||
*/
|
||||
export function wait(title, message) {
|
||||
return next(NEXT_WAIT, {
|
||||
title: title,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create data for a Prompt step of the wizard
|
||||
*
|
||||
* @param {string} title Title of the prompt
|
||||
* @param {string} message Message of the prompt
|
||||
* @param {string} actionText Text of the action (button)
|
||||
* @param {function} respond Respond callback
|
||||
* @param {function} cancel cancel handler
|
||||
* @param {object} inputs Input field objects
|
||||
*
|
||||
* @returns {object} Prompt step data
|
||||
*
|
||||
*/
|
||||
export function prompt(title, message, actionText, respond, cancel, inputs) {
|
||||
return next(NEXT_PROMPT, {
|
||||
title: title,
|
||||
message: message,
|
||||
actionText: actionText,
|
||||
inputs: inputs,
|
||||
respond: respond,
|
||||
cancel: cancel
|
||||
});
|
||||
}
|
||||
|
||||
class Next {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {object} data Step data
|
||||
*/
|
||||
constructor(data) {
|
||||
this.t = data.type();
|
||||
this.d = data.data();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return step type
|
||||
*
|
||||
* @returns {string} Step type
|
||||
*/
|
||||
type() {
|
||||
return this.t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return step data
|
||||
*
|
||||
* @returns {Done|Prompt} Step data
|
||||
*
|
||||
* @throws {Exception} When the step type is unknown
|
||||
*
|
||||
*/
|
||||
data() {
|
||||
switch (this.type()) {
|
||||
case NEXT_PROMPT:
|
||||
return new Prompt(this.d);
|
||||
|
||||
case NEXT_WAIT:
|
||||
return new Wait(this.d);
|
||||
|
||||
case NEXT_DONE:
|
||||
return new Done(this.d);
|
||||
|
||||
default:
|
||||
throw new Exception("Unknown data type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Wizard {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {function} builder Command builder
|
||||
* @param {subscribe.Subscribe} subs Wizard step subscriber
|
||||
*
|
||||
*/
|
||||
constructor(built, subs) {
|
||||
this.built = built;
|
||||
this.subs = subs;
|
||||
this.closed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Next step
|
||||
*
|
||||
* @returns {Next} Next step
|
||||
*
|
||||
* @throws {Exception} When wizard is closed
|
||||
*
|
||||
*/
|
||||
async next() {
|
||||
if (this.closed) {
|
||||
throw new Exception("Wizard already closed, no next step is available");
|
||||
}
|
||||
|
||||
let n = await this.subs.subscribe();
|
||||
|
||||
if (n.type() === NEXT_DONE) {
|
||||
this.close();
|
||||
}
|
||||
|
||||
return new Next(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether or not the command is started
|
||||
*
|
||||
* @returns {boolean} True when the command already started, false otherwise
|
||||
*
|
||||
*/
|
||||
started() {
|
||||
return this.built.started();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close current wizard
|
||||
*
|
||||
* @returns {any} Close result
|
||||
*
|
||||
*/
|
||||
close() {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closed = true;
|
||||
|
||||
return this.built.close();
|
||||
}
|
||||
}
|
||||
|
||||
export class Info {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {Builder} info Builder info
|
||||
*
|
||||
*/
|
||||
constructor(info) {
|
||||
this.type = info.name();
|
||||
this.info = info.description();
|
||||
this.tcolor = info.color();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return command name
|
||||
*
|
||||
* @returns {string} Command name
|
||||
*
|
||||
*/
|
||||
name() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return command description
|
||||
*
|
||||
* @returns {string} Command description
|
||||
*
|
||||
*/
|
||||
description() {
|
||||
return this.info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the theme color of the command
|
||||
*
|
||||
* @returns {string} Command name
|
||||
*
|
||||
*/
|
||||
color() {
|
||||
return this.tcolor;
|
||||
}
|
||||
}
|
||||
|
||||
class Builder {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {object} command Command builder
|
||||
*
|
||||
*/
|
||||
constructor(command) {
|
||||
this.cid = command.id();
|
||||
this.builder = (n, i, r, u, y, x) => {
|
||||
return command.builder(n, i, r, u, y, x);
|
||||
};
|
||||
this.launchCmd = (n, i, r, u, y, x) => {
|
||||
return command.launch(n, i, r, u, y, x);
|
||||
};
|
||||
this.launcherCmd = c => {
|
||||
return command.launcher(c);
|
||||
};
|
||||
this.type = command.name();
|
||||
this.info = command.description();
|
||||
this.tcolor = command.color();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the command ID
|
||||
*
|
||||
* @returns {number} Command ID
|
||||
*
|
||||
*/
|
||||
id() {
|
||||
return this.cid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return command name
|
||||
*
|
||||
* @returns {string} Command name
|
||||
*
|
||||
*/
|
||||
name() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return command description
|
||||
*
|
||||
* @returns {string} Command description
|
||||
*
|
||||
*/
|
||||
description() {
|
||||
return this.info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the theme color of the command
|
||||
*
|
||||
* @returns {string} Command name
|
||||
*
|
||||
*/
|
||||
color() {
|
||||
return this.tcolor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build command wizard
|
||||
*
|
||||
* @param {stream.Streams} streams
|
||||
* @param {controls.Controls} controls
|
||||
* @param {history.History} history
|
||||
* @param {object} config
|
||||
*
|
||||
* @returns {Wizard} Command wizard
|
||||
*
|
||||
*/
|
||||
build(streams, controls, history, config) {
|
||||
let subs = new subscribe.Subscribe();
|
||||
|
||||
return new Wizard(
|
||||
this.builder(new Info(this), config, streams, subs, controls, history),
|
||||
subs
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch command wizard out of given launcher string
|
||||
*
|
||||
* @param {stream.Streams} streams
|
||||
* @param {controls.Controls} controls
|
||||
* @param {history.History} history
|
||||
* @param {string} launcher Launcher format
|
||||
*
|
||||
* @returns {Wizard} Command wizard
|
||||
*
|
||||
*/
|
||||
launch(streams, controls, history, launcher) {
|
||||
let subs = new subscribe.Subscribe();
|
||||
|
||||
return new Wizard(
|
||||
this.launchCmd(
|
||||
new Info(this),
|
||||
launcher,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history
|
||||
),
|
||||
subs
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build launcher string out of given config
|
||||
*
|
||||
* @param {object} config Configuration object
|
||||
*
|
||||
* @return {string} Launcher string
|
||||
*/
|
||||
launcher(config) {
|
||||
return this.name() + ":" + this.launcherCmd(config);
|
||||
}
|
||||
}
|
||||
|
||||
export class Commands {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {[]object} commands Command array
|
||||
*
|
||||
*/
|
||||
constructor(commands) {
|
||||
this.commands = [];
|
||||
|
||||
for (let i in commands) {
|
||||
this.commands.push(new Builder(commands[i]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all commands
|
||||
*
|
||||
* @returns {[]Builder} A group of command
|
||||
*
|
||||
*/
|
||||
all() {
|
||||
return this.commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select one command
|
||||
*
|
||||
* @param {number} id Command ID
|
||||
*
|
||||
* @returns {Builder} Command builder
|
||||
*
|
||||
*/
|
||||
select(id) {
|
||||
return this.commands[id];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user