feature(user-menu) add (#221)

This commit is contained in:
coderaiser 2019-05-05 17:40:05 +03:00
parent f60af287d3
commit eb4f7c0d7c
34 changed files with 562 additions and 41 deletions

25
.cloudcmd.menu.js Normal file
View file

@ -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();
},
};

View file

@ -21,6 +21,7 @@ const cssNames = [
'view', 'view',
'config', 'config',
'terminal', 'terminal',
'user-menu',
...getCSSList('columns'), ...getCSSList('columns'),
]; ];
@ -35,7 +36,7 @@ const plugins = clean([
const rules = [{ const rules = [{
test: /\.css$/, test: /\.css$/,
exclude: /css\/(nojs|view|config|terminal|columns.*)\.css/, exclude: /css\/(nojs|view|config|terminal|user-menu|columns.*)\.css/,
use: extractMain.extract([ use: extractMain.extract([
'css-loader', 'css-loader',
]), ]),

View file

@ -32,7 +32,6 @@ const babelDev = {
babelrc: false, babelrc: false,
plugins: [ plugins: [
'module:babel-plugin-macros', 'module:babel-plugin-macros',
'@babel/plugin-proposal-object-rest-spread',
], ],
}; };
@ -43,7 +42,7 @@ const rules = clean([
loader: 'babel-loader', loader: 'babel-loader',
}, },
isDev && { isDev && {
test: /sw\.js$/, test: /\.js$/,
exclude: /node_modules/, exclude: /node_modules/,
loader: 'babel-loader', loader: 'babel-loader',
options: babelDev, options: babelDev,
@ -90,7 +89,9 @@ module.exports = {
[modules + '/operation']: `${dirModules}/operation/index.js`, [modules + '/operation']: `${dirModules}/operation/index.js`,
[modules + '/konsole']: `${dirModules}/konsole.js`, [modules + '/konsole']: `${dirModules}/konsole.js`,
[modules + '/terminal']: `${dirModules}/terminal.js`, [modules + '/terminal']: `${dirModules}/terminal.js`,
[modules + '/terminal-run']: `${dirModules}/terminal-run.js`,
[modules + '/cloud']: `${dirModules}/cloud.js`, [modules + '/cloud']: `${dirModules}/cloud.js`,
[modules + '/user-menu']: `${dirModules}/user-menu/index.js`,
[modules + '/polyfill']: `${dirModules}/polyfill.js`, [modules + '/polyfill']: `${dirModules}/polyfill.js`,
}, },
output: { output: {

View file

@ -10,6 +10,7 @@
".md" ".md"
], ],
"dictionary":[ "dictionary":[
"CloudCmd",
"Dev", "Dev",
"Dropbox", "Dropbox",
"Deepword", "Deepword",
@ -27,12 +28,14 @@
"Zalitok", "Zalitok",
"WebSocket", "WebSocket",
"auth", "auth",
"binded",
"cd", "cd",
"cloudcmd", "cloudcmd",
"coderaiser", "coderaiser",
"com", "com",
"dev",
"deepword", "deepword",
"dev",
"destructuring",
"dropbox", "dropbox",
"dword", "dword",
"edward", "edward",

53
HELP.md
View file

@ -109,6 +109,7 @@ Cloud Commander supports the following command-line parameters:
| `--dropbox` | enable dropbox integration | `--dropbox` | enable dropbox integration
| `--dropbox-token` | set dropbox token | `--dropbox-token` | set dropbox token
| `--log` | enable logging | `--log` | enable logging
| `--user-menu` | enable user menu
| `--no-show-config` | do not show config values | `--no-show-config` | do not show config values
| `--no-server` | do not start server | `--no-server` | do not start server
| `--no-auth` | disable authorization | `--no-auth` | disable authorization
@ -137,6 +138,7 @@ Cloud Commander supports the following command-line parameters:
| `--no-dropbox` | disable dropbox integration | `--no-dropbox` | disable dropbox integration
| `--no-dropbox-token` | unset dropbox token | `--no-dropbox-token` | unset dropbox token
| `--no-log` | disable logging | `--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. 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 |Key |Operation
|:----------------------|:-------------------------------------------- |:----------------------|:--------------------------------------------
| `F1` | help | `F1` | help
| `F2` | rename | `F2` | rename or show `user menu`
| `F3` | view, change directory | `F3` | view, change directory
| `Shift + F3` | view as markdown | `Shift + F3` | view as markdown
| `F4` | edit | `F4` | edit
@ -421,7 +423,8 @@ Here's a description of all options:
"importListen" : false, // listen on config updates "importListen" : false, // listen on config updates
"dropbox" : false, // disable dropbox integration "dropbox" : false, // disable dropbox integration
"dropboxToken" : "", // unset dropbox token "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_TOKEN` - authorization token used to connect to export server
- `CLOUDCMD_IMPORT_URL` - url of an import server - `CLOUDCMD_IMPORT_URL` - url of an import server
- `CLOUDCMD_IMPORT_LISTEN`- enable listen on config updates from 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 ### Distribute

View file

@ -73,6 +73,7 @@ const args = require('minimist')(argv.slice(2), {
'import-listen', 'import-listen',
'log', 'log',
'dropbox', 'dropbox',
'user-menu',
], ],
default: { default: {
server : true, server : true,
@ -111,6 +112,7 @@ const args = require('minimist')(argv.slice(2), {
'one-file-panel': choose(env.bool('one_file_panel'), config('oneFilePanel')), 'one-file-panel': choose(env.bool('one_file_panel'), config('oneFilePanel')),
'confirm-copy': choose(env.bool('confirm_copy'), config('confirmCopy')), 'confirm-copy': choose(env.bool('confirm_copy'), config('confirmCopy')),
'confirm-move': choose(env.bool('confirm_move'), config('confirmMove')), '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'), 'keys-panel': env.bool('keys_panel') || config('keysPanel'),
'import-token': env('import_token') || config('importToken'), 'import-token': env('import_token') || config('importToken'),
'export-token': env('export_token') || config('exportToken'), 'export-token': env('export_token') || config('exportToken'),
@ -180,6 +182,7 @@ function main() {
config('importToken', args['import-token']); config('importToken', args['import-token']);
config('importListen', args['import-listen']); config('importListen', args['import-listen']);
config('importUrl', args['import-url']); config('importUrl', args['import-url']);
config('userMenu', args['user-menu']);
config('dropbox', args['dropbox']); config('dropbox', args['dropbox']);
config('dropboxToken', args['dropbox-token'] || ''); config('dropboxToken', args['dropbox-token'] || '');

View file

@ -5,6 +5,8 @@
const itype = require('itype/legacy'); const itype = require('itype/legacy');
const exec = require('execon'); const exec = require('execon');
const jonny = require('jonny/legacy'); const jonny = require('jonny/legacy');
const tryToCatch = require('try-to-catch/legacy');
const Util = require('../../common/util'); const Util = require('../../common/util');
const Images = require('./images'); const Images = require('./images');
@ -746,7 +748,7 @@ function CmdProto() {
* *
* @currentFile * @currentFile
*/ */
this.renameCurrent = (current) => { this.renameCurrent = async (current) => {
const {Dialog} = DOM; const {Dialog} = DOM;
if (!DOM.isCurrentFile(current)) if (!DOM.isCurrentFile(current))
@ -757,30 +759,30 @@ function CmdProto() {
if (from === '..') if (from === '..')
return Dialog.alert.noFiles(); return Dialog.alert.noFiles();
const cancel = false; const [e, to] = await tryToCatch(Dialog.prompt, 'Rename', from);
Dialog.prompt('Rename', from, {cancel}).then((to) => { if (e)
const isExist = !!DOM.getCurrentByName(to); return;
const dirPath = DOM.getCurrentDirPath(); const isExist = !!DOM.getCurrentByName(to);
const dirPath = DOM.getCurrentDirPath();
if (from === to)
if (from === to)
return;
const files = {
from : dirPath + from,
to : dirPath + to,
};
RESTful.mv(files, (error) => {
if (error)
return; return;
const files = { DOM.setCurrentName(to, current);
from : dirPath + from, Storage.remove(dirPath);
to : dirPath + to,
};
RESTful.mv(files, (error) => { if (isExist)
if (error) CloudCmd.refresh();
return;
DOM.setCurrentName(to, current);
Storage.remove(dirPath);
if (isExist)
CloudCmd.refresh();
});
}); });
}; };
@ -940,7 +942,7 @@ function CmdProto() {
const info = DOM.CurrentInfo; const info = DOM.CurrentInfo;
const current = currentFile || DOM.getCurrentFile(); const current = currentFile || DOM.getCurrentFile();
const files = current.parentElement; const files = current.parentElement;
const panel = files.parentElement; const panel = files.parentElement || DOM.getPanel();
const panelPassive = DOM.getPanel({ const panelPassive = DOM.getPanel({
active: false, active: false,

View file

@ -186,6 +186,9 @@ function KeyProto() {
break; break;
case Key.F2: case Key.F2:
if (CloudCmd.config('userMenu'))
return CloudCmd.UserMenu.show();
DOM.renameCurrent(current); DOM.renameCurrent(current);
break; break;

View file

@ -123,7 +123,7 @@ module.exports.initKeysPanel = () => {
const clickFuncs = { const clickFuncs = {
'f1' : CloudCmd.Help.show, 'f1' : CloudCmd.Help.show,
'f2' : DOM.renameCurrent, 'f2' : initF2,
'f3' : CloudCmd.View.show, 'f3' : CloudCmd.View.show,
'f4' : CloudCmd.EditFile.show, 'f4' : CloudCmd.EditFile.show,
'f5' : operation('copy'), '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) => { const getPanel = (side) => {
if (!itype.string(side)) if (!itype.string(side))
return side; return side;

View file

@ -1,7 +1,6 @@
'use strict'; 'use strict';
/* global DOM */ /* global DOM */
/* global CloudCmd */
const { const {
Dialog, Dialog,

View file

@ -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();
},
});
}

View file

@ -24,6 +24,7 @@ CloudCmd.Terminal = exports;
let Loaded; let Loaded;
let Terminal; let Terminal;
let Socket;
const loadAll = async () => { const loadAll = async () => {
const {prefix} = CloudCmd; const {prefix} = CloudCmd;
@ -84,17 +85,19 @@ function create() {
socketPath: CloudCmd.prefix, socketPath: CloudCmd.prefix,
fontFamily: 'Droid Sans Mono', fontFamily: 'Droid Sans Mono',
}; };
const {socket, terminal} = gritty(document.body, options); const {socket, terminal} = gritty(document.body, options);
Socket = socket;
Terminal = terminal; Terminal = terminal;
terminal.on('key', (char, {keyCode, shiftKey}) => { Terminal.on('key', (char, {keyCode, shiftKey}) => {
if (shiftKey && keyCode === Key.ESC) { if (shiftKey && keyCode === Key.ESC) {
hide(); hide();
} }
}); });
socket.on('connect', exec.with(authCheck, socket)); Socket.on('connect', exec.with(authCheck, socket));
} }
function authCheck(spawn) { function authCheck(spawn) {
@ -105,7 +108,7 @@ function authCheck(spawn) {
}); });
} }
function show(callback) { function show() {
if (!Loaded) if (!Loaded)
return; return;
@ -114,10 +117,7 @@ function show(callback) {
CloudCmd.View.show(Terminal.element, { CloudCmd.View.show(Terminal.element, {
afterShow: () => { afterShow: () => {
if (Terminal) Terminal.focus();
Terminal.focus();
exec(callback);
}, },
}); });
} }

View file

@ -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;
};

View file

@ -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();
});

View file

@ -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(`<option>${option}</option>`);
}
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,
});
};

View file

@ -132,3 +132,8 @@
font-family : 'Fontello'; font-family : 'Fontello';
content : '\e81b '; content : '\e81b ';
} }
.icon-user-menu::before {
font-family : 'Fontello';
content : '\e81c ';
}

14
css/user-menu.css Normal file
View file

@ -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;
}

Binary file not shown.

View file

@ -174,6 +174,12 @@
"code": 59413, "code": 59413,
"src": "entypo" "src": "entypo"
}, },
{
"uid": "f805bb95d40c7ef2bc51b3d50d4f2e5c",
"css": "th-list",
"code": 59420,
"src": "fontawesome"
},
{ {
"uid": "60617c8adc1e7eb3c444a5491dd13f57", "uid": "60617c8adc1e7eb3c444a5491dd13f57",
"css": "attention-circled-1", "css": "attention-circled-1",

View file

@ -1,7 +1,7 @@
<?xml version="1.0" standalone="no"?> <?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2018 by original authors @ fontello.com</metadata> <metadata>Copyright (C) 2019 by original authors @ fontello.com</metadata>
<defs> <defs>
<font id="fontello" horiz-adv-x="1000" > <font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" /> <font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
@ -61,6 +61,8 @@
<glyph glyph-name="logout" unicode="&#xe81a;" d="M502 0l0 100 98 0 0-100q0-40-29-70t-71-30l-400 0q-40 0-70 30t-30 70l0 700q0 42 30 71t70 29l400 0q42 0 71-29t29-71l0-150-98 0 0 150-402 0 0-700 402 0z m398 326l-198-196 0 120-450 0 0 150 450 0 0 120z" horiz-adv-x="900" /> <glyph glyph-name="logout" unicode="&#xe81a;" d="M502 0l0 100 98 0 0-100q0-40-29-70t-71-30l-400 0q-40 0-70 30t-30 70l0 700q0 42 30 71t70 29l400 0q42 0 71-29t29-71l0-150-98 0 0 150-402 0 0-700 402 0z m398 326l-198-196 0 120-450 0 0 150 450 0 0 120z" horiz-adv-x="900" />
<glyph glyph-name="terminal-1" unicode="&#xe81b;" d="M1360 849v-1000h-1360v1000h1360z m-838-600h318v77h-318v-77z m-362 77l317 135v96l-317 134v-99l209-84-209-83v-99z" horiz-adv-x="1360" /> <glyph glyph-name="terminal-1" unicode="&#xe81b;" d="M1360 849v-1000h-1360v1000h1360z m-838-600h318v77h-318v-77z m-362 77l317 135v96l-317 134v-99l209-84-209-83v-99z" horiz-adv-x="1360" />
<glyph glyph-name="th-list" unicode="&#xe81c;" d="M286 154v-108q0-22-16-37t-38-16h-178q-23 0-38 16t-16 37v108q0 22 16 38t38 15h178q23 0 38-15t16-38z m0 285v-107q0-22-16-38t-38-15h-178q-23 0-38 15t-16 38v107q0 23 16 38t38 16h178q23 0 38-16t16-38z m714-285v-108q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v108q0 22 16 38t38 15h535q23 0 38-15t16-38z m-714 571v-107q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v107q0 22 16 38t38 16h178q23 0 38-16t16-38z m714-286v-107q0-22-16-38t-38-15h-535q-23 0-38 15t-16 38v107q0 23 16 38t38 16h535q23 0 38-16t16-38z m0 286v-107q0-22-16-38t-38-16h-535q-23 0-38 16t-16 38v107q0 22 16 38t38 16h535q23 0 38-16t16-38z" horiz-adv-x="1000" />
</font> </font>
</defs> </defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -21,6 +21,7 @@
<div id="js-keyspanel" class="keyspanel"> <div id="js-keyspanel" class="keyspanel">
<button id=f1 class="cmd-button reduce-text icon-help" title="Help" >F1</button> <button id=f1 class="cmd-button reduce-text icon-help" title="Help" >F1</button>
<button id=f2 class="cmd-button reduce-text icon-rename" title="Rename" >F2</button> <button id=f2 class="cmd-button reduce-text icon-rename" title="Rename" >F2</button>
<button id=f2 class="cmd-button reduce-text icon-user-menu" title="User Menu" >F2</button>
<button id=f3 class="cmd-button reduce-text icon-view" title="View" >F3</button> <button id=f3 class="cmd-button reduce-text icon-view" title="View" >F3</button>
<button id=f4 class="cmd-button reduce-text icon-edit " title="Edit" >F4</button> <button id=f4 class="cmd-button reduce-text icon-edit " title="Edit" >F4</button>
<button id=f5 class="cmd-button reduce-text icon-copy" title="Copy" >F5</button> <button id=f5 class="cmd-button reduce-text icon-copy" title="Copy" >F5</button>

View file

@ -43,6 +43,7 @@
"importListen": false, "importListen": false,
"log": true, "log": true,
"dropbox": false, "dropbox": false,
"dropboxToken": "" "dropboxToken": "",
"userMenu": false
} }

View file

@ -42,6 +42,7 @@
"--dropbox ": "enable dropbox integration", "--dropbox ": "enable dropbox integration",
"--dropbox-token ": "set dropbox token", "--dropbox-token ": "set dropbox token",
"--log ": "enable logging", "--log ": "enable logging",
"--user-menu ": "enable user menu",
"--no-show-config ": "do not show config values", "--no-show-config ": "do not show config values",
"--no-server ": "do not start server", "--no-server ": "do not start server",
"--no-auth ": "disable authorization", "--no-auth ": "disable authorization",
@ -70,5 +71,6 @@
"--no-show-file-name ": "do not show file name in view and edit", "--no-show-file-name ": "do not show file name in view and edit",
"--no-dropbox ": "disable dropbox integration", "--no-dropbox ": "disable dropbox integration",
"--no-dropbox-token ": "unset dropbox token", "--no-dropbox-token ": "unset dropbox token",
"--no-log ": "disable logging" "--no-log ": "disable logging",
"--no-user-menu ": "disable user menu"
} }

View file

@ -15,7 +15,9 @@
"operation", "operation",
"konsole", "konsole",
"terminal", "terminal",
"cloud" "terminal-run",
"cloud",
"user-menu"
], ],
"remote": [{ "remote": [{
"name": "socket", "name": "socket",

View file

@ -65,6 +65,7 @@ programs in browser from any computer, mobile or tablet device.
--dropbox enable dropbox integration --dropbox enable dropbox integration
--dropbox-token set dropbox token --dropbox-token set dropbox token
--log enable logging --log enable logging
--user-menu enable user menu
--no-show-config do not show config values --no-show-config do not show config values
--no-server do not start server --no-server do not start server
--no-auth disable authorization --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 disable dropbox integration
--no-dropbox-token unset dropbox token --no-dropbox-token unset dropbox token
--no-log disable logging --no-log disable logging
--no-user-menu disable user menu
.SH RESOURCES AND DOCUMENTATION .SH RESOURCES AND DOCUMENTATION

View file

@ -102,6 +102,7 @@
"execon": "^1.2.0", "execon": "^1.2.0",
"express": "^4.13.0", "express": "^4.13.0",
"files-io": "^3.0.0", "files-io": "^3.0.0",
"find-up": "^4.0.0",
"flop": "^6.0.0", "flop": "^6.0.0",
"for-each-key": "^1.0.1", "for-each-key": "^1.0.1",
"format-io": "^1.0.0", "format-io": "^1.0.0",

View file

@ -11,6 +11,7 @@ const cloudfunc = require(DIR_COMMON + 'cloudfunc');
const authentication = require(DIR + 'auth'); const authentication = require(DIR + 'auth');
const config = require(DIR + 'config'); const config = require(DIR + 'config');
const modulas = require(DIR + 'modulas'); const modulas = require(DIR + 'modulas');
const userMenu = require(DIR + 'user-menu');
const rest = require(DIR + 'rest'); const rest = require(DIR + 'rest');
const route = require(DIR + 'route'); const route = require(DIR + 'route');
const validate = require(DIR + 'validate'); const validate = require(DIR + 'validate');
@ -223,6 +224,7 @@ function cloudcmd(prefix, plugins, modules) {
root, root,
}), }),
userMenu,
rest, rest,
route({ route({
html: defaultHtml, html: defaultHtml,

View file

@ -111,6 +111,7 @@ function indexProcessing(options) {
const noConfig = !config('configDialog'); const noConfig = !config('configDialog');
const noConsole = !config('console'); const noConsole = !config('console');
const noTerminal = !config('terminal'); const noTerminal = !config('terminal');
const noUserMenu = !config('userMenu');
const {panel} = options; const {panel} = options;
let {data} = options; let {data} = options;
@ -139,6 +140,13 @@ function indexProcessing(options) {
data = data data = data
.replace('icon-terminal', 'icon-terminal none'); .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, { const left = rendy(Template.panel, {
side : 'left', side : 'left',
content : panel, content : panel,

View file

@ -264,6 +264,7 @@ test('cloudcmd: route: realpath: error', async (t) => {
options, options,
}); });
/*eslint require-atomic-updates:0*/
fs.realpath = realpath; fs.realpath = realpath;
t.ok(/^ENOENT/.test(body), 'should return error'); t.ok(/^ENOENT/.test(body), 'should return error');

53
server/user-menu.js Normal file
View file

@ -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('');
}

21
server/user-menu.spec.js Normal file
View file

@ -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();
});