From eb4f7c0d7cba6df68807005d49b085b4edbf5e68 Mon Sep 17 00:00:00 2001 From: coderaiser Date: Sun, 5 May 2019 17:40:05 +0300 Subject: [PATCH] feature(user-menu) add (#221) --- .cloudcmd.menu.js | 25 +++ .webpack/css.js | 3 +- .webpack/js.js | 5 +- .yaspellerrc | 5 +- HELP.md | 53 ++++++- bin/cloudcmd.js | 3 + client/dom/index.js | 46 +++--- client/key/index.js | 3 + client/listeners/index.js | 9 +- client/modules/operation/set-listeners.js | 1 - client/modules/terminal-run.js | 146 ++++++++++++++++++ client/modules/terminal.js | 14 +- client/modules/user-menu/get-user-menu.js | 20 +++ .../modules/user-menu/get-user-menu.spec.js | 29 ++++ client/modules/user-menu/index.js | 112 ++++++++++++++ css/icons.css | 5 + css/user-menu.css | 14 ++ font/fontello.eot | Bin 12648 -> 13012 bytes font/fontello.json | 6 + font/fontello.svg | 4 +- font/fontello.ttf | Bin 12480 -> 12844 bytes font/fontello.woff | Bin 7876 -> 8048 bytes font/fontello.woff2 | Bin 6572 -> 6704 bytes html/index.html | 1 + json/config.json | 3 +- json/help.json | 4 +- json/modules.json | 4 +- man/cloudcmd.1 | 2 + package.json | 1 + server/cloudcmd.js | 2 + server/route.js | 8 + server/route.spec.js | 1 + server/user-menu.js | 53 +++++++ server/user-menu.spec.js | 21 +++ 34 files changed, 562 insertions(+), 41 deletions(-) create mode 100644 .cloudcmd.menu.js create mode 100644 client/modules/terminal-run.js create mode 100644 client/modules/user-menu/get-user-menu.js create mode 100644 client/modules/user-menu/get-user-menu.spec.js create mode 100644 client/modules/user-menu/index.js create mode 100644 css/user-menu.css create mode 100644 server/user-menu.js create mode 100644 server/user-menu.spec.js diff --git a/.cloudcmd.menu.js b/.cloudcmd.menu.js new file mode 100644 index 00000000..010b6615 --- /dev/null +++ b/.cloudcmd.menu.js @@ -0,0 +1,25 @@ +'use strict'; + +module.exports = { + 'F2 - Rename file': async ({DOM}) => { + await DOM.renameCurrent(); + }, + 'D - Build Dev': async ({CloudCmd}) => { + await CloudCmd.TerminalRun.show({ + command: 'npm run build:client:dev', + autoClose: false, + closeMessage: 'Press any button to close Terminal', + }); + + CloudCmd.refresh(); + }, + 'P - Build Prod': async ({CloudCmd}) => { + await CloudCmd.TerminalRun.show({ + command: 'npm run build:client', + autoClose: true, + }); + + CloudCmd.refresh(); + }, +}; + diff --git a/.webpack/css.js b/.webpack/css.js index f28fa100..2338b53d 100644 --- a/.webpack/css.js +++ b/.webpack/css.js @@ -21,6 +21,7 @@ const cssNames = [ 'view', 'config', 'terminal', + 'user-menu', ...getCSSList('columns'), ]; @@ -35,7 +36,7 @@ const plugins = clean([ const rules = [{ test: /\.css$/, - exclude: /css\/(nojs|view|config|terminal|columns.*)\.css/, + exclude: /css\/(nojs|view|config|terminal|user-menu|columns.*)\.css/, use: extractMain.extract([ 'css-loader', ]), diff --git a/.webpack/js.js b/.webpack/js.js index 7fd45044..e156fbd1 100644 --- a/.webpack/js.js +++ b/.webpack/js.js @@ -32,7 +32,6 @@ const babelDev = { babelrc: false, plugins: [ 'module:babel-plugin-macros', - '@babel/plugin-proposal-object-rest-spread', ], }; @@ -43,7 +42,7 @@ const rules = clean([ loader: 'babel-loader', }, isDev && { - test: /sw\.js$/, + test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader', options: babelDev, @@ -90,7 +89,9 @@ module.exports = { [modules + '/operation']: `${dirModules}/operation/index.js`, [modules + '/konsole']: `${dirModules}/konsole.js`, [modules + '/terminal']: `${dirModules}/terminal.js`, + [modules + '/terminal-run']: `${dirModules}/terminal-run.js`, [modules + '/cloud']: `${dirModules}/cloud.js`, + [modules + '/user-menu']: `${dirModules}/user-menu/index.js`, [modules + '/polyfill']: `${dirModules}/polyfill.js`, }, output: { diff --git a/.yaspellerrc b/.yaspellerrc index c6a7ec3b..ad0eddc1 100644 --- a/.yaspellerrc +++ b/.yaspellerrc @@ -10,6 +10,7 @@ ".md" ], "dictionary":[ + "CloudCmd", "Dev", "Dropbox", "Deepword", @@ -27,12 +28,14 @@ "Zalitok", "WebSocket", "auth", + "binded", "cd", "cloudcmd", "coderaiser", "com", - "dev", "deepword", + "dev", + "destructuring", "dropbox", "dword", "edward", diff --git a/HELP.md b/HELP.md index 5540f599..2ec743a8 100644 --- a/HELP.md +++ b/HELP.md @@ -109,6 +109,7 @@ Cloud Commander supports the following command-line parameters: | `--dropbox` | enable dropbox integration | `--dropbox-token` | set dropbox token | `--log` | enable logging +| `--user-menu` | enable user menu | `--no-show-config` | do not show config values | `--no-server` | do not start server | `--no-auth` | disable authorization @@ -137,6 +138,7 @@ Cloud Commander supports the following command-line parameters: | `--no-dropbox` | disable dropbox integration | `--no-dropbox-token` | unset dropbox token | `--no-log` | disable logging +| `--no-user-menu` | disable user menu For options not specified by command-line parameters, Cloud Commander then reads configuration data from `~/.cloudcmd.json`. It uses port `8000` by default. @@ -162,7 +164,7 @@ Hot keys |Key |Operation |:----------------------|:-------------------------------------------- | `F1` | help -| `F2` | rename +| `F2` | rename or show `user menu` | `F3` | view, change directory | `Shift + F3` | view as markdown | `F4` | edit @@ -421,7 +423,8 @@ Here's a description of all options: "importListen" : false, // listen on config updates "dropbox" : false, // disable dropbox integration "dropboxToken" : "", // unset dropbox token - "log" : true // logging + "log" : true, // logging + "userMenu" : false // do not show user menu } ``` @@ -458,6 +461,52 @@ Some config options can be overridden with environment variables, such as: - `CLOUDCMD_IMPORT_TOKEN` - authorization token used to connect to export server - `CLOUDCMD_IMPORT_URL` - url of an import server - `CLOUDCMD_IMPORT_LISTEN`- enable listen on config updates from import server +- `CLOUDCMD_USER_MENU`- enable `user menu` + +### User Menu + +You can enable `user menu` with help of a flag `--user-menu` (consider that file rename using `F2` will be disabled). +When you press `F2` Cloud Commander will a file `.cloudcmd.menu.js` by walking up parent directories, if can't read it will try to read `~/.cloudcmd.menu.js`. +Let's consider example `user menu` works file: + +```js +module.exports = { + 'F2 - Rename file': async ({DOM}) => { + await DOM.renameCurrent(); + }, + 'D - Build Dev': async ({CloudCmd}) => { + await CloudCmd.TerminalRun.show({ + command: 'npm run build:client:dev', + autoClose: false, + closeMessage: 'Press any button to close Terminal', + }); + + CloudCmd.refresh(); + }, + 'P - Build Prod': async ({CloudCmd}) => { + await CloudCmd.TerminalRun.show({ + command: 'npm run build:client', + autoClose: true, + }); + + CloudCmd.refresh(); + }, +}; +``` + +You will have ability to run one of this 3 commands with help of double click, enter, or binded key (`F2`, `D` or `P` in this example). Also you can run commands in terminal, or execute any built-in function of `Cloud Commander` extended it's interface. + +#### User Menu API + +Here you can find `API` that can be used in **User Menu**. **DOM** and **CloudCmd** to main objects you receive in arguments list using destructuring. + +**DOM** contains all base functions of `Cloud Commander` (rename, remove, download etc); + +- `renameCurrent` - shows renames current file dialog, and does renaming. + +**CloudCmd** contains all modules (`Terminal`, `View`, `Edit`, `Config`, `Console` etc); + +- `TerminalRun` - module that shows `Terminal` with a `command` from options and closes terminal when everything is done. ### Distribute diff --git a/bin/cloudcmd.js b/bin/cloudcmd.js index 95da4136..1e4e3ac5 100755 --- a/bin/cloudcmd.js +++ b/bin/cloudcmd.js @@ -73,6 +73,7 @@ const args = require('minimist')(argv.slice(2), { 'import-listen', 'log', 'dropbox', + 'user-menu', ], default: { server : true, @@ -111,6 +112,7 @@ const args = require('minimist')(argv.slice(2), { 'one-file-panel': choose(env.bool('one_file_panel'), config('oneFilePanel')), 'confirm-copy': choose(env.bool('confirm_copy'), config('confirmCopy')), 'confirm-move': choose(env.bool('confirm_move'), config('confirmMove')), + 'user-menu': choose(env.bool('user_menu'), config('userMenu')), 'keys-panel': env.bool('keys_panel') || config('keysPanel'), 'import-token': env('import_token') || config('importToken'), 'export-token': env('export_token') || config('exportToken'), @@ -180,6 +182,7 @@ function main() { config('importToken', args['import-token']); config('importListen', args['import-listen']); config('importUrl', args['import-url']); + config('userMenu', args['user-menu']); config('dropbox', args['dropbox']); config('dropboxToken', args['dropbox-token'] || ''); diff --git a/client/dom/index.js b/client/dom/index.js index e7623e09..271b563d 100644 --- a/client/dom/index.js +++ b/client/dom/index.js @@ -5,6 +5,8 @@ const itype = require('itype/legacy'); const exec = require('execon'); const jonny = require('jonny/legacy'); +const tryToCatch = require('try-to-catch/legacy'); + const Util = require('../../common/util'); const Images = require('./images'); @@ -746,7 +748,7 @@ function CmdProto() { * * @currentFile */ - this.renameCurrent = (current) => { + this.renameCurrent = async (current) => { const {Dialog} = DOM; if (!DOM.isCurrentFile(current)) @@ -757,30 +759,30 @@ function CmdProto() { if (from === '..') return Dialog.alert.noFiles(); - const cancel = false; + const [e, to] = await tryToCatch(Dialog.prompt, 'Rename', from); - Dialog.prompt('Rename', from, {cancel}).then((to) => { - const isExist = !!DOM.getCurrentByName(to); - const dirPath = DOM.getCurrentDirPath(); - - if (from === to) + if (e) + return; + const isExist = !!DOM.getCurrentByName(to); + const dirPath = DOM.getCurrentDirPath(); + + if (from === to) + return; + + const files = { + from : dirPath + from, + to : dirPath + to, + }; + + RESTful.mv(files, (error) => { + if (error) return; - const files = { - from : dirPath + from, - to : dirPath + to, - }; + DOM.setCurrentName(to, current); + Storage.remove(dirPath); - RESTful.mv(files, (error) => { - if (error) - return; - - DOM.setCurrentName(to, current); - Storage.remove(dirPath); - - if (isExist) - CloudCmd.refresh(); - }); + if (isExist) + CloudCmd.refresh(); }); }; @@ -940,7 +942,7 @@ function CmdProto() { const info = DOM.CurrentInfo; const current = currentFile || DOM.getCurrentFile(); const files = current.parentElement; - const panel = files.parentElement; + const panel = files.parentElement || DOM.getPanel(); const panelPassive = DOM.getPanel({ active: false, diff --git a/client/key/index.js b/client/key/index.js index 6cb11e4d..627400a4 100644 --- a/client/key/index.js +++ b/client/key/index.js @@ -186,6 +186,9 @@ function KeyProto() { break; case Key.F2: + if (CloudCmd.config('userMenu')) + return CloudCmd.UserMenu.show(); + DOM.renameCurrent(current); break; diff --git a/client/listeners/index.js b/client/listeners/index.js index ffc4d8c5..2f8c6771 100644 --- a/client/listeners/index.js +++ b/client/listeners/index.js @@ -123,7 +123,7 @@ module.exports.initKeysPanel = () => { const clickFuncs = { 'f1' : CloudCmd.Help.show, - 'f2' : DOM.renameCurrent, + 'f2' : initF2, 'f3' : CloudCmd.View.show, 'f4' : CloudCmd.EditFile.show, 'f5' : operation('copy'), @@ -141,6 +141,13 @@ module.exports.initKeysPanel = () => { }); }; +function initF2() { + if (CloudCmd.config('userMenu')) + return CloudCmd.UserMenu.show(); + + return DOM.renameCurrent(); +} + const getPanel = (side) => { if (!itype.string(side)) return side; diff --git a/client/modules/operation/set-listeners.js b/client/modules/operation/set-listeners.js index f3414772..71d7b64e 100644 --- a/client/modules/operation/set-listeners.js +++ b/client/modules/operation/set-listeners.js @@ -1,7 +1,6 @@ 'use strict'; /* global DOM */ -/* global CloudCmd */ const { Dialog, diff --git a/client/modules/terminal-run.js b/client/modules/terminal-run.js new file mode 100644 index 00000000..02ce4f28 --- /dev/null +++ b/client/modules/terminal-run.js @@ -0,0 +1,146 @@ +'use strict'; + +/* global CloudCmd, gritty */ + +const {promisify} = require('es6-promisify'); +const tryToCatch = require('try-to-catch/legacy'); + +require('../../css/terminal.css'); + +const exec = require('execon'); +const load = require('load.js'); +const DOM = require('../dom'); +const Images = require('../dom/images'); + +const loadParallel = promisify(load.parallel); + +const {Dialog} = DOM; +const { + Key, + config, +} = CloudCmd; + +CloudCmd.TerminalRun = exports; + +let Loaded; +let Terminal; +let Socket; + +const loadAll = async () => { + const {prefix} = CloudCmd; + + const prefixGritty = getPrefix(); + const js = `${prefixGritty}/gritty.js`; + const css = `${prefix}/dist/terminal.css`; + + const [e] = await tryToCatch(loadParallel, [js, css]); + + if (e) { + const src = e.target.src.replace(window.location.href, ''); + return Dialog.alert(`file ${src} could not be loaded`); + } + + Loaded = true; +}; + +module.exports.init = async () => { + if (!config('terminal')) + return; + + Images.show.load('top'); + + await CloudCmd.View(); + await loadAll(); +}; + +module.exports.show = show; +module.exports.hide = hide; + +function hide () { + CloudCmd.View.hide(); +} + +function getPrefix() { + return CloudCmd.prefix + '/gritty'; +} + +function getPrefixSocket() { + return CloudCmd.prefixSocket + '/gritty'; +} + +function getEnv() { + return { + ACTIVE_DIR: DOM.getCurrentDirPath, + PASSIVE_DIR: DOM.getNotCurrentDirPath, + CURRENT_NAME: DOM.getCurrentName, + CURRENT_PATH: DOM.getCurrentPath, + }; +} + +function create(createOptions) { + const { + command, + autoClose, + closeMessage = 'Press any key to close Terminal...', + } = createOptions; + + const options = { + env: getEnv(), + prefix: getPrefixSocket(), + socketPath: CloudCmd.prefix, + fontFamily: 'Droid Sans Mono', + command, + autoRestart: false, + }; + + let commandExit = false; + + const {socket, terminal} = gritty(document.body, options); + + Socket = socket; + Terminal = terminal; + + Terminal.on('key', (char, {keyCode, shiftKey}) => { + if (commandExit) + hide(); + + if (shiftKey && keyCode === Key.ESC) { + hide(); + } + }); + + Socket.on('exit', () => { + if (autoClose) + return hide(); + + terminal.write(`\n${closeMessage}`); + commandExit = true; + }); + + Socket.on('connect', exec.with(authCheck, socket)); +} + +function authCheck(spawn) { + spawn.emit('auth', config('username'), config('password')); + + spawn.on('reject', () => { + Dialog.alert('Wrong credentials!'); + }); +} + +async function show(options = {}) { + if (!Loaded) + return; + + if (!config('terminal')) + return; + + await create(options); + + CloudCmd.View.show(Terminal.element, { + afterShow: () => { + Terminal.focus(); + }, + }); +} + diff --git a/client/modules/terminal.js b/client/modules/terminal.js index ddb7cd37..ee33f4ae 100644 --- a/client/modules/terminal.js +++ b/client/modules/terminal.js @@ -24,6 +24,7 @@ CloudCmd.Terminal = exports; let Loaded; let Terminal; +let Socket; const loadAll = async () => { const {prefix} = CloudCmd; @@ -84,17 +85,19 @@ function create() { socketPath: CloudCmd.prefix, fontFamily: 'Droid Sans Mono', }; + const {socket, terminal} = gritty(document.body, options); + Socket = socket; Terminal = terminal; - terminal.on('key', (char, {keyCode, shiftKey}) => { + Terminal.on('key', (char, {keyCode, shiftKey}) => { if (shiftKey && keyCode === Key.ESC) { hide(); } }); - socket.on('connect', exec.with(authCheck, socket)); + Socket.on('connect', exec.with(authCheck, socket)); } function authCheck(spawn) { @@ -105,7 +108,7 @@ function authCheck(spawn) { }); } -function show(callback) { +function show() { if (!Loaded) return; @@ -114,10 +117,7 @@ function show(callback) { CloudCmd.View.show(Terminal.element, { afterShow: () => { - if (Terminal) - Terminal.focus(); - - exec(callback); + Terminal.focus(); }, }); } diff --git a/client/modules/user-menu/get-user-menu.js b/client/modules/user-menu/get-user-menu.js new file mode 100644 index 00000000..4b3d8923 --- /dev/null +++ b/client/modules/user-menu/get-user-menu.js @@ -0,0 +1,20 @@ +'use strict'; + +const defaultUserMenu = { + 'F2 - Rename file': async ({DOM}) => { + DOM.renameCurrent(); + }, +}; + +module.exports = (menuFn) => { + if (!menuFn) + return defaultUserMenu; + + const module = {}; + const fn = Function('module', menuFn); + + fn(module); + + return module.exports; +}; + diff --git a/client/modules/user-menu/get-user-menu.spec.js b/client/modules/user-menu/get-user-menu.spec.js new file mode 100644 index 00000000..77ee4fea --- /dev/null +++ b/client/modules/user-menu/get-user-menu.spec.js @@ -0,0 +1,29 @@ +'use strict'; + +const test = require('supertape'); +const getUserMenu = require('./get-user-menu'); + +test('user-menu: getUserMenu', (t) => { + const menu = `module.exports = { + 'F2 - Rename file': ({DOM}) => { + const {element} = DOM.CurrentInfo; + DOM.renameCurrent(element); + } + }`; + + const result = getUserMenu(menu); + + const [key] = Object.keys(result); + + t.equal(key, 'F2 - Rename file', 'should equal'); + t.end(); +}); + +test('user-menu: getUserMenu: no args', (t) => { + const result = getUserMenu(); + const [key] = Object.keys(result); + + t.equal(key, 'F2 - Rename file', 'should equal'); + t.end(); +}); + diff --git a/client/modules/user-menu/index.js b/client/modules/user-menu/index.js new file mode 100644 index 00000000..78e1f5dc --- /dev/null +++ b/client/modules/user-menu/index.js @@ -0,0 +1,112 @@ +'use strict'; + +/* global CloudCmd, DOM */ + +require('../../../css/user-menu.css'); + +const currify = require('currify/legacy'); +const {promisify} = require('es6-promisify'); +const load = require('load.js'); +const createElement = require('@cloudcmd/create-element'); + +const Images = require('../../dom/images'); +const getUserMenu = require('./get-user-menu'); + +const loadCSS = promisify(load.css); + +const Name = 'UserMenu'; +CloudCmd[Name] = module.exports; + +const {Key} = CloudCmd; + +module.exports.init = async () => { + await Promise.all([ + loadCSS(`${CloudCmd.prefix}/dist/user-menu.css`), + CloudCmd.View(), + ]); +}; + +module.exports.show = show; +module.exports.hide = hide; + +const getKey = (a) => a.split(' - ')[0]; +const beginWith = (a) => (b) => !b.indexOf(a); + +const {CurrentInfo} = DOM; + +async function show() { + Images.show.load('top'); + + const {dirPath} = CurrentInfo; + const res = await fetch(`/api/v1/user-menu?dir=${dirPath}`); + const userMenu = getUserMenu(await res.text()); + const options = Object.keys(userMenu); + + const el = createElement('select', { + className: 'cloudcmd-user-menu', + innerHTML: fillTemplate(options), + size: 10, + }); + + const keys = options.map(getKey); + el.addEventListener('keydown', onKeyDown(keys, options, userMenu)); + el.addEventListener('dblclick', onDblClick(options, userMenu)); + + const afterShow = () => el.focus(); + const autoSize = true; + + Images.hide(); + + CloudCmd.View.show(el, { + autoSize, + afterShow, + }); +} + +function fillTemplate(options) { + const result = []; + + for (const option of options) { + result.push(``); + } + + return result.join(''); +} + +function hide() { + CloudCmd.View.hide(); +} + +const onDblClick = currify(async (options, userMenu, e) => { + const {value} = e.target; + await runUserMenu(value, options, userMenu); +}); + +const onKeyDown = currify(async (keys, options, userMenu, e) => { + const {keyCode} = e; + const key = e.key.toUpperCase(); + + let value; + + if (keyCode === Key.ENTER) + ({value} = e.target); + else if (keys.includes(key)) + value = options.find(beginWith(key)); + else + return; + + e.preventDefault(); + e.stopPropagation(); + + await runUserMenu(value, options, userMenu); +}); + +const runUserMenu = async (value, options, userMenu) => { + hide(); + + await userMenu[value]({ + DOM, + CloudCmd, + }); +}; + diff --git a/css/icons.css b/css/icons.css index 4f979db7..253b3593 100644 --- a/css/icons.css +++ b/css/icons.css @@ -132,3 +132,8 @@ font-family : 'Fontello'; content : '\e81b '; } + +.icon-user-menu::before { + font-family : 'Fontello'; + content : '\e81c '; +} diff --git a/css/user-menu.css b/css/user-menu.css new file mode 100644 index 00000000..37f07c14 --- /dev/null +++ b/css/user-menu.css @@ -0,0 +1,14 @@ +.cloudcmd-user-menu { + font-size: 16px; + font-family: 'Droid Sans Mono', 'Ubuntu Mono', 'Consolas', monospace; + width: 400px; +} + +.cloudcmd-user-menu:focus { + outline: 0; +} + +.cloudcmd-user-menu > option:checked { + box-shadow: 20px -20px 0 2px rgba(49, 123, 249) inset; +} + diff --git a/font/fontello.eot b/font/fontello.eot index 4274ddd2908829e0efc110032eaaa475d96df979..c886783f811e722665b18fb0c993c2d45f84d749 100644 GIT binary patch delta 809 zcmZWlO-vI(6n<}ZyZt3K5w?lKGNpxvP-5h#1gV0F9Q=tQlB$qc3KmLeVPiL@UhKid z5al4*n^EN8!HbEphJ$+YDkPfJ#H$CAg_9Q}5}>|mt06j@owwi5`)2l86UC%b>;MRN zEGZFrd1Ay^8hDtG1KRxT#*+@M7b9a;0(?brc z-dtp7Z4IO-<~O!uerHav_&vb1*l9HswxnTnS@ptQ#!=hLBY6d&^h>fNH+Z2DjzK?M zf;dD6dfjMup(5FpaVJU+WGJz58`4=gLIvTPjCAsa8)LC67teQBSEt9;(AHd6tJnig zEurqNh_AClJxn-M#XW>k=>CeH_xUSC8&-%utl&`9&_lWi2vl*W@M~Os?bGnToqgo$ z-ey^0KA*PNbHMSBM$N*Rrz)(E(<aW<}8c`T>OnN#Uy4_SX)W(D9e2 z`Ot9bHAW_u`G~>5kRxR{}sQ q>~JSyuPHaJVsGJH(>Wxo5ei0Fuh7H zD^Xy(`k#q`K?TTXP)I9C&+VExArike*mv02CJh@)rPUj`W<$G_EXmM+OF# zDGUr!t1?m(Q-sck0QJf{0OidxfC3!zSuOzi6+pg9Ms7((wprN!mq39zK!K8+{N%)h zt~4ND0ca?LO>SaE0iz3#CXnv~{`2|SHbz_be7(F>d(VS6s@&d(@$r4J|?4e-0WjDtuaWXPmOwLf=#%Qrw aUd4+Ms3uF*YV$HxW=3FuZ{Db;%mV;&(rBFk diff --git a/font/fontello.json b/font/fontello.json index 61c8d00e..354b93cc 100644 --- a/font/fontello.json +++ b/font/fontello.json @@ -174,6 +174,12 @@ "code": 59413, "src": "entypo" }, + { + "uid": "f805bb95d40c7ef2bc51b3d50d4f2e5c", + "css": "th-list", + "code": 59420, + "src": "fontawesome" + }, { "uid": "60617c8adc1e7eb3c444a5491dd13f57", "css": "attention-circled-1", diff --git a/font/fontello.svg b/font/fontello.svg index afc5a844..ac6e533b 100644 --- a/font/fontello.svg +++ b/font/fontello.svg @@ -1,7 +1,7 @@ -Copyright (C) 2018 by original authors @ fontello.com +Copyright (C) 2019 by original authors @ fontello.com @@ -61,6 +61,8 @@ + + \ No newline at end of file diff --git a/font/fontello.ttf b/font/fontello.ttf index 1065b0d3a9035ab21a8d53fd0533384405dae35c..57d3d33173116cfd592e11de175917715a215633 100644 GIT binary patch delta 790 zcmZWl%}W$v7=NCb+1b@yQ&4B1bavL+Ttp?zFE%YJ=+FmcCR=H^PP*&L&a6WYVVCw0 zgoFoWJVa=D=+L1cn&=Q6qH78T1^odDtM_P&-0gYYEu`LI-e-Ov&+mQbd8*o3KD+kJQ55q<)|D$qjsIbk)Un~C++V2JPsy5NR2Gi#f-M^wsMqf4i)8wqRkMC%)X zj>k*+QGDqP;{Y}|%NS`{0Hg?jyTI>$wNb64rUejSgm{4-a^xDEuXx(iNQ&roa2(bE z`!_&??X()*ll%}?<{FrH7cUFX_Gd+a;txK@`=J_wa0mwA0>q*Jc+`*HN)&moI9`Fg z4>{zyaSzf-I7|iMihy+Pxv|mMrSoUI%d0csX>2`MUnhFCrsha@SC8CzSgIu)DdS$k z$n;Q2&&r_^(TXLa4@)>wHuRFt0<<#jWPU}JUdRgmw{!1RX=iI!;bd9a>D}Y_N26xu z%u;362Zls>^@gOPf?}E#In9C!8+#NkOo}S8t+2kwgucH#&4-3duRVPIVZ6Hk8~FJg zoBx;Z7pwl%;?Lr=XOJHPa5BMmRGgKd&G9*BpL*0Et16TNKyioE3dE>$E%XYbZf(;U bB`~^UoBR!`wjVp`Yk%56_3Il`Gs*Kdc6ruAWK)vz~ zKzXwapa92wmJ2|B1(2_jky}!cZ5HD-Fn302<0*lbcvk!05uG z3FP|#`3iZ7xv6L7$|(SuIY0-VEyyn}sV+Uen}I>u2I%O-f}+%dr;gft7#IvJCjN+K z+%!3a(VkIy@)SmO%`OH;pb-oVEey@)=DXZ3;|+08@r--ew(ih+{`aPO0FAow7}@e2NcX1Wheho zD4ATKXw4o9woZ2QHbqWGMvKV@l(sQiY%W*!Vg#x=q++%Cp9(W0Fo-vEswwjT0O0#$ A2mk;8 diff --git a/font/fontello.woff b/font/fontello.woff index 9a4a595269b97a02fca49f8f7720d3023dc1f65f..fd4d9d12c41f37dd2611bcc2872deeab13cd143d 100644 GIT binary patch delta 6248 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 Bw-x{Z delta 6019 zcmV-}7kudOKEyo~cTYw}009610017u01p5F001z+krY3FTw`rvZ~y=S*Z=?k%K!iZ z>aN}HV`Fx7AOHXYqyPW_6951JAO_X{0%mY$Z2$lQxBvhJkN^M+aEg(TS!ZE$Z~y=Z zNB{r;2mk;82mk;85NB+8W&i*PPyhfDg#Z8=stRfcL1<-RWB>pf*Z=?kG5`PoHWJTN zsAyyd03ZMW03-;X1kh-0ba(&&8!P|-08sz{0Ca0M|LJURV_^UQ8-M@+ z06YKy06budX6bEVcyIs!8@vDj03ZMW03ZQG3@L75ZDjxe8{7Z@0e1iZ0?o{w9w2aU zb94Xz9aI1S0P_F<0bzS}%e!!LWpDrh9!LNH0D1tEQ2|K-#FLu=Cx2!R`k%!R#T>*i z9VpBQk^=zx<_r4(c%1Fh*=++c6hqNFvFs$?vv{8pT#!FgOukYnyRi9WmJtC*fCOnU z5D83yiCwM1@W()nU+)fmf5N=)H*Wn*t9@PWchA)9b?fts&o+N?#w%~U``&^jE7ok- z^1&xxe6wTEfg>l*Tw)k~amN2x)-?QNHP+ic_16x+Fp5e|QSYy)L={c~g)>3nbWk`a z6qU5X*`cVvQ#el)P8NkTM&YzkICm6o019^jgkN32||bi1SddXAqy-~TgX^o0~Talf@xwWcFYhn z<5n&uv~@fk@Yunf)H8tFxG;+UVU|m^ ze5jHa7Zf=h(v1~Rs=;fA-hS+{x3B;3fc%xq|D-AVRSj>mU(rC5x%yR=%bMnTSk;*O z(3KCa9CC`ep?#{Rsr$sxe;>dOg>lXl`#Z9#5<=!}+KD##NSzFlp~kvS;LHUS2yGIH zGAgkdg4Qs;pD2V%3KjMd;ADz`EW;Q9kjIG(@^IMd3K_XvTIKPs?1C+AF#qJCN)gg% zNjn(jQhfkQMp%~>FPP!^{z`2X3@~$XDR5QbBTd~eo*awWf%|)(e{H&2HcGOKTiIm= zBbM$5rz;YSYnJZ$$>OSfbm?oOi_;^;YqC0a1XFIm9F2F3 zetBV)4qYMA?OV8k&(McRfas(^YGkCbSqB#f+*(Swi}ESra)ZiTD()=-g~24Q2~oy~ zqL6V-qSzW<(q)Fre?48bCAEAu)gF(A3uYl038p+e(v>yjbZRN2(eNVFL$#90%6Ukp za`jL;l^cM1vScW%Tr#|n#S4`hx`Cm~RPw~Fq&NFX)>X)RwW}?Ip2M?4Y>oW`%L?7R z8cxu-3Cr))?V-J4%L?x`EbEolQro6Yx;NAjhBL94{U5XTf9aY9!-;|g%i{LyR=0K4 zDkQ+_j-y57_9yH*eG=_Ykuq7|SnH-#qAft!-M)a%P4=S)Py+jjj4mOgO91DNEAVl` zfe)KTMh`~}g~z%emlu0ogleTOD;6YbP{}DN88^OEt5$00=v-H1DUr$ND)m}7NT$Kg zY`XxxevfuE#*H8Nm|6q^X5C87oVt;J^%>HSBz=DG+P>9CAA9y$*nnfF*SYj91^QQC(~0n6wD%hN zzKq_#oD7g5P#WRJzzRR3pb)77A_!K41nW1Q(?-S{e?3HD5>uoZ^b_pcjcXwhCQYf> zkU7XZ38$2g5zgs2p_~rKHl1_hyv~L33j7`u%>UO9HG1!!A`zJRopUB8CK?7IgKJmz z_ZB;IR?G;lA?E|xSOv8Z?qC}Iv>K??&`C`Tup^(tOR^F`v`Ut0 zE1`k|e+_0rx(Z6ZUMpd&>+svgwni1gF4q-T5I>p8wRX?i$;q|wZrXA&C8laE9=j)7 zfm#OM%~p6ujnvQB$IsA%VIDZm+?K#ar0_uy6hbYbL=fh2*#@u}XH~ z9_tmng9@{!?c=ARSUFSi`L_AHoxR@2uCr0ne~!MLMept>k2dzEacSDXMe+%%aMtu7 zg1b0zP2zY(;>r}Rpr%UN6j2FvtLWyk%*GMWJwq+vb_-aC7T8d!0IBp>`d2KkReP2e zyE{9wnN%`rdOa?gw1ee!aNaNjh!%juo;o zlna@mxGsPoD=bTj0wFLboTX&60U{pL(V0@Yl@1UJL=eUc?_t+9bq;qxRxWV`|R8Q%08ese~6b@ zWO~!N7L2wuhKa-;5;h7@o55W@fN)2pF(OIyK14XWDb`5gD0c7*2Te2rK-!|F9`NJN zd!a?1-+6_30YfFvaZlClsi z$Mg(*6p7gVk!T67L?Q<|j_UE0f0cdX=8hbK)=U=WShp?u-oYK*EXz_x_@GD3A>37NE@o0Ofoxf1j2W9u{K) z^VpIPq=gfR(YQi&WWIL50xV6<2tvnV_S5R0Xv%z5(S~e~s#18Ds;YhbW=!JeWeNUM zQ)>&TP!;3;ChzJtE|>*Z{xwuWZ`l7RF2ZV2eooz>u93*L)#0V1MYpnKOQcnbthvV=?X7MPk2d2{YizRYg6y7? z)AW(&_rX+diB#M%f8Dvv>MYS>m$fW^|JGt}Z=wJ_`R=LC{$!!FtQcL^IlZLVqjZ)w z?Adc-58bzD|0nDW-HIwOL#mCE3&p#IBFMnNXH6mH52A8L6-4LNkdXL-QyCr3b;N@b z3O$Uv1(3_nBb_O@aAEU=kBiWUrv2aCE5~K|z4v4}&i#m>f1GYZ%)2g}kVeo^i#UYG zWv~3+&*z8Qx47TFl@hI#v@!+aUIcCnb9_sWe4W7l}qX zm6sKKp$Fw^IeaoCA-W2eIbRu|Wt83Q`mtX>n=c&rvsgy+BDPcJky>;`_ba~fX&56$z&`FZ)Ob8pD4iJFaCBYELHAOClTZsY+g(Gn`9FIpLy3Qp+;_dNv zt1S|X#GLnf29lvJ`he;#pmB3@@S#2uV8yR z`ujWJBijSV?CTK0Pi4mr_)TA7Zcy~l%Z0vMzoeJv2I+m8}&{I-K zfg%%He@KT`f+LF^ET|(C%y0FSr z1c(Y*TW3au}GA_?{NL zsae|1fJ-xH4eu<1_H1O@=S-x!Z?W>+>!|1w;n$7Ca01`ZVft^n-8cPa_@;yWHy5yN zANxC82M_M@&c^+Kd<0EIGoVa}8U-^Se@ygcs-S8Dxy4Ogm~&4q2s+{!LTu;TH4o*y zg>PtSA*3bJ62TzUL&1RG=f(Sa0{9p7^46|!pq>n%mJN$fzFMx9F{em})78sV*>2B% z2VQ#S>gjjhIem(izGuG-(u;G` z;Q*URN4&^Uj&>Fz5&;fZ8^H~!M9A#+#Z_;EdJT1jA^U)>h*vXSozc1rk7CTw6nr($|MVI;0K7Z>;P`!Q8l{jbHciRfk9slk&yt)!6ZQuTR z9nN}RWTT8B{`HWa#*#uNhRorUf9G`p2FyXhBnO44Jb1?}J-oax!f^qGL1>6_7nwL( z3aJ}RV_shg#f%os=Ueca5qEFBh*Z*$<%=V3+;#MpS3a%kmbUdG(#a|r`mjx1TQ70m zzzaAcZWypv;g_76*XbR(h`oH?Z_cwF4mR|V-bS&by%otJ=wb8dfRJ`je-I#A1ME{n z`gAi|cMMs0V6i(G8e&5h9yqLvjr(5~ihY7t_{9-T_c^W`caK~`_G@tXTRnl#B#Sz~ zwAJZ1fGhIFt(xWdI0l0U2MGJ#yVGdkee`jdLOy-mMa=mPp>EU>x5TAQjl>PAc9rj;zKbkq6IY4jV zRn_X~u;|C}!l}~q^t7WfFb}!HYV>0i;cX<_NVUbyfF>a$qI`}(m;Q)o-hW6umvZ|i?(+wzB~-Ez)#)DchvX@8jC{3m z!@?Fu8-y;$YaU52p zHIs{|l~ehwBBu>UHlmuziCh8c!xX`u%Zp1WW^y&>nuSiMv_n?7ngt542`oT4=e2y1B=)3_Y@kza0n)Xe^T^yi21pHNC_$S*9Rn~ zQmM9d^7OLr{BvOncg#+CPM zj%SHlCdXXJPO>Yv5{IH>6 zRIygnz_hvPoXa`6WRfDW-MGV?lifH$LY@%EQ ze@qv?vOMeLjLB|v4_U){U4ljRk{EG0C=2ZU}Rum0Af2E zv#@x6o39Mq%rAf<4A+f0T4415U;i^%1DJz=Tn+{%kSG8kI||>Esv8^z3jmw#26&Uq z8%hTZ3;+;ZBWaTz96kqC0096103DNN93Fq&YQiuWhTqJeZ3^4iV5iH`P{!&H3`7tF zhn{RFGCUo%*0xd`Nu%Ix>>hS8yNO-PUfVc^vX(T@_vFi$uK{q4ZwTbi6GzShXN+<_ z!6|x{3-r&Hi{ux}B~f<=~gPLC~;%yu1%kc0$uix{&VGAOvEgZL zTEkttE_K$G#PFbo2rir0C=2jj!SaFFc5!4-C!GG z3<>%F$PKjMNJ1L0Dr3nd$z_t$#}uKmp?9xqrn|FAc2<>L*9H?zk)uF~83g92ut1F^ zR=B|}*0{qx9`J}KJmUqgc*8qBu)$}xRw<4w<1w!ty4VHz8&o(#y6ShnCiY`uZ+~} zd$TfraQ`SSI6GZC8ia~tkd0q;plE+c##S*e{$SrT&o$d_=T$J33c(LiYCVXkZ5-#v xqB{kY=gN}(!S|`UOq1Iz`vqrwU@ri8oMZ6b!EhodVk0ACU*rbHog9+~A8BgCTMGaH diff --git a/font/fontello.woff2 b/font/fontello.woff2 index b8c98b96e3f9b8b9ca4bf97340e974851696ffcc..dae85710040bf58bfd75af9887060d7171d950a2 100644 GIT binary patch literal 6704 zcmV-08qei-Pew8T0RR9102(j=4*&oF05U8902$Z-0RR9100000000000000000000 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 Gf(sZKC(cv= literal 6572 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 diff --git a/html/index.html b/html/index.html index e0fd8595..7f6edac5 100644 --- a/html/index.html +++ b/html/index.html @@ -21,6 +21,7 @@
+ diff --git a/json/config.json b/json/config.json index eec170c8..406a4421 100644 --- a/json/config.json +++ b/json/config.json @@ -43,6 +43,7 @@ "importListen": false, "log": true, "dropbox": false, - "dropboxToken": "" + "dropboxToken": "", + "userMenu": false } diff --git a/json/help.json b/json/help.json index e9455868..af6adc1a 100644 --- a/json/help.json +++ b/json/help.json @@ -42,6 +42,7 @@ "--dropbox ": "enable dropbox integration", "--dropbox-token ": "set dropbox token", "--log ": "enable logging", + "--user-menu ": "enable user menu", "--no-show-config ": "do not show config values", "--no-server ": "do not start server", "--no-auth ": "disable authorization", @@ -70,5 +71,6 @@ "--no-show-file-name ": "do not show file name in view and edit", "--no-dropbox ": "disable dropbox integration", "--no-dropbox-token ": "unset dropbox token", - "--no-log ": "disable logging" + "--no-log ": "disable logging", + "--no-user-menu ": "disable user menu" } diff --git a/json/modules.json b/json/modules.json index ad52b824..d8df33c9 100644 --- a/json/modules.json +++ b/json/modules.json @@ -15,7 +15,9 @@ "operation", "konsole", "terminal", - "cloud" + "terminal-run", + "cloud", + "user-menu" ], "remote": [{ "name": "socket", diff --git a/man/cloudcmd.1 b/man/cloudcmd.1 index 9db5e7d1..bed5672a 100644 --- a/man/cloudcmd.1 +++ b/man/cloudcmd.1 @@ -65,6 +65,7 @@ programs in browser from any computer, mobile or tablet device. --dropbox enable dropbox integration --dropbox-token set dropbox token --log enable logging + --user-menu enable user menu --no-show-config do not show config values --no-server do not start server --no-auth disable authorization @@ -94,6 +95,7 @@ programs in browser from any computer, mobile or tablet device. --no-dropbox disable dropbox integration --no-dropbox-token unset dropbox token --no-log disable logging + --no-user-menu disable user menu .SH RESOURCES AND DOCUMENTATION diff --git a/package.json b/package.json index cc715043..3a7cb3ad 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "execon": "^1.2.0", "express": "^4.13.0", "files-io": "^3.0.0", + "find-up": "^4.0.0", "flop": "^6.0.0", "for-each-key": "^1.0.1", "format-io": "^1.0.0", diff --git a/server/cloudcmd.js b/server/cloudcmd.js index b6193626..5909f6db 100644 --- a/server/cloudcmd.js +++ b/server/cloudcmd.js @@ -11,6 +11,7 @@ const cloudfunc = require(DIR_COMMON + 'cloudfunc'); const authentication = require(DIR + 'auth'); const config = require(DIR + 'config'); const modulas = require(DIR + 'modulas'); +const userMenu = require(DIR + 'user-menu'); const rest = require(DIR + 'rest'); const route = require(DIR + 'route'); const validate = require(DIR + 'validate'); @@ -223,6 +224,7 @@ function cloudcmd(prefix, plugins, modules) { root, }), + userMenu, rest, route({ html: defaultHtml, diff --git a/server/route.js b/server/route.js index e22aeaeb..fcafb8be 100644 --- a/server/route.js +++ b/server/route.js @@ -111,6 +111,7 @@ function indexProcessing(options) { const noConfig = !config('configDialog'); const noConsole = !config('console'); const noTerminal = !config('terminal'); + const noUserMenu = !config('userMenu'); const {panel} = options; let {data} = options; @@ -139,6 +140,13 @@ function indexProcessing(options) { data = data .replace('icon-terminal', 'icon-terminal none'); + if (noUserMenu) + data = data + .replace('icon-user-menu', 'icon-user-menu none'); + else + data = data + .replace('icon-rename', 'icon-rename none'); + const left = rendy(Template.panel, { side : 'left', content : panel, diff --git a/server/route.spec.js b/server/route.spec.js index 2d167d1d..04c62a3a 100644 --- a/server/route.spec.js +++ b/server/route.spec.js @@ -264,6 +264,7 @@ test('cloudcmd: route: realpath: error', async (t) => { options, }); + /*eslint require-atomic-updates:0*/ fs.realpath = realpath; t.ok(/^ENOENT/.test(body), 'should return error'); diff --git a/server/user-menu.js b/server/user-menu.js new file mode 100644 index 00000000..162e6474 --- /dev/null +++ b/server/user-menu.js @@ -0,0 +1,53 @@ +'use strict'; + +const {homedir} = require('os'); +const fs = require('fs'); +const {join} = require('path'); +const {promisify} = require('util'); + +const tryToCatch = require('try-to-catch'); +const findUp = require('find-up'); + +const readFile = promisify(fs.readFile); +const menuName = '.cloudcmd.menu.js'; +const homeMenuPath = join(homedir(), menuName); + +module.exports = async (req, res, next) => { + if (req.url.indexOf('/api/v1/user-menu')) + return next(); + + const {method} = req; + + if (method === 'GET') + return onGET(req.query, res); + + next(); +}; + +async function onGET({dir}, res) { + const [errorFind, currentMenuPath] = await tryToCatch(findUp, [ + menuName, + ], {cwd: dir}); + + if (errorFind && errorFind.code !== 'ENOENT') + return res + .status(404) + .send(e.message); + + if (errorFind && errorFind.code === 'ENOENT') + return res.send(''); + + const menuPath = currentMenuPath || homeMenuPath; + const [e, data] = await tryToCatch(readFile, menuPath, 'utf8'); + + if (!e) + return res.send(data); + + if (e.code !== 'ENOENT') + return res + .status(404) + .send(e.message); + + return res.send(''); +} + diff --git a/server/user-menu.spec.js b/server/user-menu.spec.js new file mode 100644 index 00000000..b350f284 --- /dev/null +++ b/server/user-menu.spec.js @@ -0,0 +1,21 @@ +'use strict'; + +const fs = require('fs'); +const {join} = require('path'); + +const test = require('supertape'); +const serveOnce = require('serve-once'); + +const userMenu = require('./user-menu'); +const {request} = serveOnce(() => userMenu); + +const userMenuPath = join(__dirname, '..', '.cloudcmd.menu.js'); +const userMenuFile = fs.readFileSync(userMenuPath, 'utf8'); + +test('cloudcmd: user menu', async (t) => { + const {body} = await request.get(`/api/v1/user-menu?dir=${__dirname}`); + + t.equal(userMenuFile, body, 'should equal'); + t.end(); +}); +