diff --git a/HELP.md b/HELP.md index f88af817..56c26969 100644 --- a/HELP.md +++ b/HELP.md @@ -80,6 +80,8 @@ Cloud Commander supports command line parameters: | `--one-panel-mode` | set one panel mode `--config-dialog` | enable config dialog `--console` | enable console + `--terminal` | enable terminal + `--terminal-path` | set terminal path | `--no-server` | do not start server | `--no-auth` | disable authorization | `--no-online` | load scripts from local server @@ -90,6 +92,7 @@ Cloud Commander supports command line parameters: | `--no-one-panel-mode` | unset one panel mode | `--no-config-dialog` | disable config dialog | `--no-console` | disable console +| `--no-terminal` | disable terminal If no parameters given Cloud Commander reads information from `~/.cloudcmd.json` and use port from it (`8000` default). if port variables `PORT` or `VCAP_APP_PORT` isn't exist. @@ -203,9 +206,62 @@ Console For more details see [console hot keys](https://github.com/cloudcmd/console#hot-keys "Console Hot Keys"). -### Environment Variables +Terminal +--------------- +![Terminal](/img/screen/terminal.png "Terminal") -Every program executed in `console` has these `environment` variables: +### Install + +`Terminal` disabled and not installed by default. To use it you should install [gritty](https://github.com/cloudcmd/gritty "Gritty") with: + +```sh +npm i gritty -g +``` + +And then set the path of a terminal with: + +```sh +cloudcmd --terminal --terminal-path `gritty --path` --save +``` + +### Windows + +On Windows you need to install `windows-build-tools` before: + +```sh +npm install --global windows-build-tools +``` + +Then get path of a `gritty` with: + +```sh +gritty --path +``` +It will returns something like: + +```sh +C:\Users\coderaiser\AppData\Roaming\npm\node_modules\gritty +``` + +Set this path as `--terminal-path` with: + +```sh +cloudcmd --save --terminal --terminal-path "C:\Users\coderaiser\AppData\Roaming\npm\node_modules\gritty" +``` + +After that you can use `terminal` in the same way as a `console`. + +### Hot keys + +|Key |Operation +|:----------------------|:-------------------------------------------- +| `Shift` + `~` | open +| `Shift` + `Esc` | close + +Environment Variables +--------------- + +Every program executed in `console` or `terminal` has these `environment` variables: - `ACTIVE_DIR` - directory that contains cursor - `PASSIVE_DIR` - directory with no cursor @@ -260,7 +316,9 @@ Here is description of options: "htmlDialogs" : true, /* use html dialogs */ "onePanelMode" : false, /* set one panel mode */ "configDialog" : true, /* enable config dialog */ - "console" : true /* enable console */ + "console" : true, /* enable console */ + "terminal" : false, /* disable terminal */ + "terminalPath" : '', /* path of a terminal */ } ``` diff --git a/bin/cloudcmd.js b/bin/cloudcmd.js index 9094d2f4..cfd2a6aa 100755 --- a/bin/cloudcmd.js +++ b/bin/cloudcmd.js @@ -18,7 +18,8 @@ const args = require('minimist')(argv.slice(2), { 'editor', 'packer', 'root', - 'prefix' + 'prefix', + 'terminal-path', ], boolean: [ 'auth', @@ -31,6 +32,7 @@ const args = require('minimist')(argv.slice(2), { 'progress', 'config-dialog', 'console', + 'terminal', 'one-panel-mode', 'html-dialogs' ], @@ -49,7 +51,9 @@ const args = require('minimist')(argv.slice(2), { prefix : config('prefix') || '', progress : config('progress'), console : config('console'), + terminal : config('terminal'), + 'terminal-path': config('terminalPath'), 'config-dialog': config('configDialog'), 'one-panel-mode': config('onePanelMode'), 'html-dialogs': config('htmlDialogs') @@ -88,6 +92,8 @@ if (args.version) { config('username', args.username); config('progress', args.progress); config('console', args.console); + config('terminal', args.terminal); + config('terminalPath', args.terminalPath); config('editor', args.editor); config('prefix', args.prefix); config('root', args.root); diff --git a/client/dom/files.js b/client/dom/files.js index 8d87d9df..44e9f7e1 100644 --- a/client/dom/files.js +++ b/client/dom/files.js @@ -98,12 +98,12 @@ function showError(name) { throw(error); } -function getSystemFile(url, callback) { +function getSystemFile(file, callback) { const prefix = CloudCmd.PREFIX; - if (!Promises[url]) - Promises[url] = new Promise((success, error) => { - url = prefix + url; + if (!Promises[file]) + Promises[file] = new Promise((success, error) => { + const url = prefix + file; load.ajax({ url, @@ -112,10 +112,10 @@ function getSystemFile(url, callback) { }); }); - Promises[url].then((data) => { + Promises[file].then((data) => { callback(null, data); }, (error) => { - Promises[url] = null; + Promises[file] = null; callback(error); }); } diff --git a/client/key.js b/client/key.js index 64e65d2c..6a0721de 100644 --- a/client/key.js +++ b/client/key.js @@ -347,6 +347,9 @@ function KeyProto() { break; case Key.TRA: + if (shift) + return CloudCmd.Terminal.show(); + CloudCmd.Konsole.show(); event.preventDefault(); break; diff --git a/client/modules/terminal.js b/client/modules/terminal.js new file mode 100644 index 00000000..b3148c01 --- /dev/null +++ b/client/modules/terminal.js @@ -0,0 +1,129 @@ +'use strict'; + +/* global CloudCmd, gritty */ + +const exec = require('execon'); +const load = require('../dom/load'); +const DOM = require('../dom'); +const Images = require('../dom/images'); +const {Dialog} = DOM; + +const TITLE = 'Terminal'; + +CloudCmd.Terminal = TerminalProto; + +const {Key} = CloudCmd; + +let Element; +let Loaded; +let Terminal; + +const {config} = CloudCmd; + +function TerminalProto() { + if (!config('terminal')) + return; + + Images.show.load('top'); + + exec.series([ + CloudCmd.View, + loadAll, + create, + show, + ]); + + Element = load({ + name: 'div', + style: 'height: 99%', + className : 'terminal', + }); + + return module.exports; +} + +module.exports.show = show; + +module.exports.hide = hide; + +function hide () { + CloudCmd.View.hide(); +} + +function getPrefix() { + return CloudCmd.PREFIX + '/gritty'; +} + +function getEnv() { + return { + ACTIVE_DIR: DOM.getCurrentDirPath, + PASSIVE_DIR: DOM.getNotCurrentDirPath, + CURRENT_NAME: DOM.getCurrentName, + CURRENT_PATH: DOM.getCurrentPath + }; +} + +function create(callback) { + const options = { + env: getEnv(), + prefix: getPrefix(), + socketPath: CloudCmd.PREFIX, + }; + + const {socket, terminal} = gritty(Element, options); + + terminal.focus(); + + Terminal = terminal; + + terminal.on('key', (char, {keyCode, shiftKey}) => { + if (shiftKey && keyCode === Key.ESC) { + hide(); + } + }); + + socket.on('connect', exec.with(authCheck, socket)); + + exec(callback); +} + +function authCheck(spawn) { + if (!config('auth')) + return; + + spawn.emit('auth', config('username'), config('password')); + + spawn.on('reject', () => { + Dialog.alert(TITLE, 'Wrong credentials!'); + }); +} + +function show(callback) { + if (!Loaded) + return; + + CloudCmd.View.show(Element, { + afterShow: () => { + if (Terminal) { + Terminal.fit(); // lines corrupt without + Terminal.focus(); + } + + exec(callback); + } + }); +} + +function loadAll(callback) { + const prefix = getPrefix(); + const url = prefix + '/gritty.js'; + + DOM.load.js(url, (error) => { + if (error) + return Dialog.alert(TITLE, error.message); + + Loaded = true; + exec(callback); + }); +} + diff --git a/css/view.css b/css/view.css index 0bd628e9..93cd8c4b 100644 --- a/css/view.css +++ b/css/view.css @@ -1,4 +1,5 @@ .view { + height: 100%; font-size: 16px; white-space: pre; } diff --git a/img/screen/terminal.png b/img/screen/terminal.png new file mode 100644 index 00000000..911a9d08 Binary files /dev/null and b/img/screen/terminal.png differ diff --git a/json/config.json b/json/config.json index f18437c7..38638b36 100644 --- a/json/config.json +++ b/json/config.json @@ -24,6 +24,8 @@ "configDialog": true, "onePanelMode": false, "configDialog": true, - "console": true + "console": true, + "terminal": false, + "terminalPath": "" } diff --git a/json/help.json b/json/help.json index bdce15b2..cbb6e27b 100644 --- a/json/help.json +++ b/json/help.json @@ -18,6 +18,8 @@ "--one-panel-mode ": "set one panel mode", "--config-dialog ": "enable config dialog", "--console ": "enable console", + "--terminal ": "enable terminal", + "--terminal-path ": "set terminal path", "--open ": "open web browser when server started", "--no-server ": "do not start server", "--no-auth ": "disable authorization", @@ -28,5 +30,6 @@ "--no-html-dialogs ": "do not use html dialogs", "--no-one-panel-mode ": "unset one panel mode", "--no-config-dialog ": "disable config dialog", - "--no-console ": "disable console" + "--no-console ": "disable console", + "--no-terminal ": "disable terminal" } diff --git a/json/modules.json b/json/modules.json index 85c5b2d8..31ed77a8 100644 --- a/json/modules.json +++ b/json/modules.json @@ -11,6 +11,7 @@ "upload", "operation", "konsole", + "terminal", "cloud", [{ "name": "remote", "data": [{ diff --git a/man/cloudcmd.1 b/man/cloudcmd.1 index 45efcf3b..0cceb1d9 100644 --- a/man/cloudcmd.1 +++ b/man/cloudcmd.1 @@ -42,6 +42,8 @@ programs in browser from any computer, mobile or tablet device. --one-panel-mode set one panel mode --config-dialog enable config dialog --console enable console + --terminal enable terminal + --terminal-path set terminal path --no-auth disable authorization --no-server do not start server --no-online load scripts from local server @@ -52,6 +54,7 @@ programs in browser from any computer, mobile or tablet device. --no-one-panel-mode unset one panel mode --no-config-dialog disable config dialog --no-console disable console + --no-terminal disable terminal .SH RESOURCES AND DOCUMENTATION diff --git a/server/cloudcmd.js b/server/cloudcmd.js index 36ec7fda..1515cc49 100644 --- a/server/cloudcmd.js +++ b/server/cloudcmd.js @@ -13,6 +13,7 @@ const route = require(DIR + 'route'); const validate = require(DIR + 'validate'); const prefixer = require(DIR + 'prefixer'); const pluginer = require(DIR + 'plugins'); +const terminal = require(DIR + 'terminal'); const apart = require('apart'); const join = require('join-io'); @@ -32,8 +33,9 @@ const omnes = require('omnes/legacy'); const criton = require('criton'); const root = () => config('root'); -const emptyFunc = (req, res, next) => next(); -emptyFunc.middle = () => emptyFunc; + +const notEmpty = (a) => a; +const clean = (a) => a.filter(notEmpty); const isDev = process.env.NODE_ENV === 'development'; @@ -44,7 +46,7 @@ function getPrefix(prefix) { return prefix || ''; } -module.exports = function(params) { +module.exports = (params) => { const p = params || {}; const options = p.config || {}; const plugins = p.plugins; @@ -54,7 +56,7 @@ module.exports = function(params) { checkPlugins(plugins); - keys.forEach(function(name) { + keys.forEach((name) => { let value = options[name]; switch(name) { @@ -176,6 +178,11 @@ function listen(prefix, socket) { authCheck, prefix: prefix + '/console', }); + + config('terminal') && terminal.listen(socket, { + authCheck, + prefix: prefix + '/gritty', + }); } function cloudcmd(prefix, plugins) { @@ -191,13 +198,17 @@ function cloudcmd(prefix, plugins) { const ponseStatic = ponse.static(DIR_ROOT, {cache}); - const funcs = [ - konsole({ + const funcs = clean([ + config('console') && konsole({ prefix: prefix + '/console', minify, online, }), + config('terminal') && terminal({ + prefix: prefix + '/gritty', + }), + edward({ prefix : prefix + '/edward', minify, @@ -277,7 +288,7 @@ function cloudcmd(prefix, plugins) { pluginer(plugins), ponseStatic - ]; + ]); return funcs; } diff --git a/server/terminal.js b/server/terminal.js new file mode 100644 index 00000000..bbd6e740 --- /dev/null +++ b/server/terminal.js @@ -0,0 +1,29 @@ +'use strict'; + +const tryCatch = require('try-catch'); +const config = require('./config'); + +const noop = () => {}; +noop.listen = noop; + +module.exports = getTerminal(config('terminal')); + +function getTerminal(term) { + if (!term) + return noop; + + let result; + + const e = tryCatch(() => { + result = require(config('terminalPath')); + }); + + if (!e) + return result; + + config('terminal', false); + console.log(`cloudcmd --terminal: ${e.message}`); + + return noop; +} + diff --git a/webpack.config.js b/webpack.config.js index 13497bae..bcfcf106 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -52,6 +52,7 @@ module.exports = { [modules + '/upload']: `${dirModules}/upload.js`, [modules + '/operation']: `${dirModules}/operation.js`, [modules + '/konsole']: `${dirModules}/konsole.js`, + [modules + '/terminal']: `${dirModules}/terminal.js`, [modules + '/cloud']: `${dirModules}/cloud.js`, [modules + '/polyfill']: `${dirModules}/polyfill.js`, },