Compare commits

..

No commits in common. "master" and "v11.7.0" have entirely different histories.

298 changed files with 10361 additions and 18354 deletions

17
.appveyor.yml Normal file
View file

@ -0,0 +1,17 @@
platform: x64
install:
- ps: Install-Product node $env:nodejs_version $env:platform
build: off
build_script:
- node --version
- npm --version
- npm i now nupdate -g
- nupdate -a gritty
- now rm cloudcmd -y -t %NOW_TOKEN% || true
- now --public -t %NOW_TOKEN%
- now alias -t %NOW_TOKEN%

21
.babelrc Normal file
View file

@ -0,0 +1,21 @@
{
"presets": [
["@babel/preset-env", {
"exclude": [
"transform-regenerator"
]
}],
],
"plugins": [
"@babel/plugin-transform-object-assign",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-syntax-dynamic-import",
["module:fast-async", {
"compiler": {
"generators": false,
"runtimePatter": ".js"
}
}],
"module:babel-plugin-macros",
]
}

View file

@ -1,9 +0,0 @@
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"module:babel-plugin-macros",
"@babel/plugin-transform-optional-chaining"
]
}

View file

@ -1,4 +0,0 @@
last 2 Chrome versions
last 2 Safari versions
Firefox ESR
not dead

View file

@ -1,31 +0,0 @@
export default {
'F2 - Rename File': renameCurrent,
'L - Lint': run('npm run lint'),
'F - Fix Lint': run('npm run fix:lint'),
'T - Test': run('npm run test'),
'C - Coverage': run('npm run coverage'),
'D - Build Dev': run('npm run build:client:dev'),
'P - Build Prod': run('npm run build:client'),
};
async function renameCurrent(DOM) {
await DOM.renameCurrent();
}
function run(command) {
return async ({CloudCmd, DOM}) => {
const {TerminalRun, config} = CloudCmd;
const {CurrentInfo} = DOM;
const {dirPath} = CurrentInfo;
const cwd = config('root') + dirPath;
return await TerminalRun.show({
cwd,
command,
closeMessage: 'Press any key to close Terminal',
autoClose: false,
});
};
}

View file

@ -11,7 +11,8 @@ yarn-error.log
yarn.lock yarn.lock
now.json now.json
docker Dockerfile
Dockerfile.alpine
webpack.config.js webpack.config.js
cssnano.config.js cssnano.config.js
@ -19,5 +20,6 @@ cssnano.config.js
bin/release.js bin/release.js
client client
legacy
server_ server_

View file

@ -9,7 +9,7 @@ root = true
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = false trim_trailing_whitespace = true
indent_style = space indent_style = space
indent_size = 4 indent_size = 4

19
.eslintrc Normal file
View file

@ -0,0 +1,19 @@
{
"env": {
"es6": true,
"node": true,
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
},
"rules": {
"indent": ["error", 4],
"semi": "error",
"no-console": 0,
"no-use-before-define": 0,
},
"extends": [
"eslint:recommended"
]
}

15
.eslintrc.server Normal file
View file

@ -0,0 +1,15 @@
{
"env": {
"node": true
},
"rules": {
"no-process-exit": 0
},
"extends": [
".eslintrc",
"plugin:node/recommended"
],
"plugins": [
"node"
]
}

3
.github/FUNDING.yml vendored
View file

@ -1,3 +0,0 @@
github: coderaiser
open_collective: cloudcmd
ko_fi: coderaiser

12
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,12 @@
<!--
Thank you for reporting an issue. Please fill in the template below. If unsure
about something, just do as best as you're able.
-->
* **Version** (`cloudcmd -v`):
* **Node Version** `node -v`:
* **OS** (`uname -a` on Linux):
* **Browser name/version**:
* **Used Command Line Parameters**:
* **Changed Config**:

View file

@ -1,45 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: needs clarification
assignees: coderaiser
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- **Version** (`cloudcmd -v`):
- **Node Version** `node -v`:
- **OS** (`uname -a` on Linux):
- **Browser name/version**:
- **Used Command Line Parameters**:
- **Changed Config**:
```json
{}
```
- [ ] 🎁 **I'm ready to donate on https://opencollective.com/cloudcmd**
- [ ] 🎁 **I'm ready to donate on https://ko-fi.com/coderaiser**
- [ ] 💪 **I'm willing to work on this issue**
**Additional context**
Add any other context about the problem here.

View file

@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Stack Overflow
url: https://stackoverflow.com/search?q=cloudcmd
about: Please ask and answer questions here.

View file

@ -1,21 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,24 +0,0 @@
***
name: Tracking issue
about: Create an issue with bug report or feature request.
title: ""
labels: needs triage
assignees: coderaiser
***
- **Version** (`cloudcmd -v`):
- **Node Version** `node -v`:
- **OS** (`uname -a` on Linux):
- **Browser name/version**:
- **Used Command Line Parameters**:
- **Changed Config**:
```json
{}
```
- [ ] 🎁 **I'm ready to donate on https://opencollective.com/cloudcmd**
- [ ] 🎁 **I'm ready to donate on https://ko-fi.com/coderaiser**
- [ ] 💪 **I'm willing to work on this issue**

View file

@ -4,5 +4,6 @@ about something, just do as best as you're able.
--> -->
- [ ] commit message named according to [Contributing Guide](https://github.com/coderaiser/cloudcmd/blob/master/CONTRIBUTING.md "Contributting Guide") - [ ] commit message named according to [Contributing Guide](https://github.com/coderaiser/cloudcmd/blob/master/CONTRIBUTING.md "Contributting Guide")
- [ ] `npm run fix:lint` is OK - [ ] `npm run codestyle` is OK
- [ ] `npm test` is OK - [ ] `npm test` is OK

View file

@ -1,60 +0,0 @@
name: Docker IO
permissions:
contents: write
on:
- push
jobs:
buildx:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Use Node.js 24.x
uses: actions/setup-node@v6
with:
node-version: 24.x
- name: Install Redrun
run: bun i redrun -g --no-save
- name: NPM Install
run: bun i --no-save
- name: Lint
run: redrun lint
- name: Build
id: build
run: |
redrun build
VERSION=$(grep '"version":' package.json -m1 | cut -d\" -f4)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push io-image
uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile.io
platforms: linux/amd64,linux/arm64
push: true
tags: |
coderaiser/cloudcmd:io
coderaiser/cloudcmd:${{ steps.build.outputs.version }}-io
ghcr.io/${{ github.repository }}-io
ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-io

View file

@ -1,88 +0,0 @@
name: Docker CI
permissions:
contents: write
on:
push:
tags:
- "*"
jobs:
buildx:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Use Node.js 22.x
uses: actions/setup-node@v6
with:
node-version: 22.x
- name: Install Redrun
run: bun i redrun -g --no-save
- name: NPM Install
run: bun i --no-save
- name: Lint
run: redrun lint
- name: Build
id: build
run: |
redrun build
VERSION=$(grep '"version":' package.json -m1 | cut -d\" -f4)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push base-image
uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
coderaiser/cloudcmd:latest
coderaiser/cloudcmd:${{ steps.build.outputs.version }}
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}
- name: Build and push alpine-image
uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile.alpine
platforms: linux/amd64,linux/arm64
push: true
tags: |
coderaiser/cloudcmd:alpine
coderaiser/cloudcmd:latest-alpine
coderaiser/cloudcmd:${{ steps.build.outputs.version }}-alpine
ghcr.io/${{ github.repository }}:latest-alpine
ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-alpine
- name: Build and push slim-image
uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile.slim
platforms: linux/amd64,linux/arm64
push: true
tags: |
coderaiser/cloudcmd:slim
coderaiser/cloudcmd:latest-slim
coderaiser/cloudcmd:${{ steps.build.outputs.version }}-slim
ghcr.io/${{ github.repository }}-slim
ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-slim

View file

@ -1,53 +0,0 @@
name: Node CI
on:
- push
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
env:
NAME: cloudcmd
strategy:
matrix:
node-version:
- 22.x
- 24.x
- 26.x
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- name: Install Redrun
run: bun i redrun madrun -g --no-save
- name: Install
run: bun i --no-save
- name: Init Madrun
run: madrun --init
- name: Lint
run: redrun fix:lint
- name: Typos
uses: coderaiser/typos.ai@v1.1.8
with:
key: ${{ secrets.TYPOS_AI_KEY }}
- name: Commit fixes
uses: EndBug/add-and-commit@v10
continue-on-error: true
with:
message: "chore: ${{ env.NAME }}: actions: lint ☘️"
- name: Build
run: redrun build
- name: Test
run: redrun test
- name: Coverage
run: redrun coverage coverage:report
- name: Coveralls
uses: coverallsapp/github-action@v2
continue-on-error: true
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

20
.gitignore vendored
View file

@ -1,17 +1,17 @@
*.swp
*.log
*.lock
.nyc_output
.DS_Store
.idea
package-lock.json package-lock.json
npm-debug.log* yarn.lock
yarn-error.log
node_modules node_modules
npm-debug.log*
coverage coverage
modules/execon modules/execon
modules/emitify modules/emitify
.nyc_output
*.swp
dist dist
dist-dev dist-dev

View file

@ -1,62 +0,0 @@
import {run, cutEnv} from 'madrun';
import {defineEnv} from 'supertape/env';
const testEnv = defineEnv({
timeout: 7000,
css: true,
});
const buildEnv = {
NODE_ENV: 'production',
};
export default {
'start': () => 'node bin/cloudcmd.js',
'start:dev': async () => await run('start', null, {
NODE_ENV: 'development',
}),
'build:start': () => run(['build:client', 'start']),
'build:start:dev': () => run([
'build:client:dev',
'start:dev',
]),
'lint:all': () => run('lint:progress'),
'lint': () => 'redlint fix; putout .',
'lint:progress': () => run('lint', '-f progress'),
'watch:lint': () => 'nodemon -w client -w server -w test -w common -w .webpack -x "putout -s"',
'fresh:lint': () => run('lint', '--fresh'),
'lint:fresh': () => run('lint', '--fresh'),
'fix:lint': async () => `putout --fix . && redlint fix`,
'lint:stream': () => run('lint', '-f stream'),
'test': () => [testEnv, `tape '{test}/**/*.js' '{bin,client,static,common,server}/**/*.spec.js' -f fail`],
'test:e2e': () => `tape 'test-e2e/**/*.js'`,
'test:client': () => `tape 'test/client/**/*.js'`,
'test:server': () => `tape 'test/**/*.js' 'server/**/*.spec.js' 'common/**/*.spec.js'`,
'wisdom': async () => await run(['lint:all', 'build', 'test'], null, {
CI: 1,
}),
'wisdom:type': () => 'bin/release.js',
'coverage': async () => [testEnv, `c8 ${await cutEnv('test')}`],
'coverage:report': () => 'c8 report --reporter=lcov',
'report': () => 'c8 report --reporter=lcov',
'6to5': () => [buildEnv, 'rspack build --config rspack.config.js'],
'6to5:client': () => run('6to5', '--mode production'),
'6to5:client:dev': async () => await run('6to5', '--mode development', {
NODE_ENV: 'development',
}),
'pre6to5:client': () => 'rimraf dist',
'pre6to5:client:dev': () => 'rimraf dist-dev',
'watch:client': () => run('6to5:client', '--watch'),
'watch:client:dev': () => run('6to5:client:dev', '--watch'),
'watch:server': () => 'nodemon bin/cloudcmd.js',
'watch:test': async () => [testEnv, `nodemon -w client -w server -w test -w common -x ${await cutEnv('test')}`],
'watch:test:client': async () => `nodemon -w client -w test/client -x ${await run('test:client')}`,
'watch:test:server': async () => `nodemon -w client -w test/client -x ${await run('test:server')}`,
'watch:coverage': async () => [testEnv, `nodemon -w server -w test -w common -x ${await cutEnv('coverage')}`],
'watch:fix:lint': async () => `nodemon -w client -w server -w test -w common -x '${await run('fix:lint')}'`,
'build': async () => run('6to5:*'),
'build:dev': async () => run('build:client:dev'),
'build:client': () => run('6to5:client'),
'build:client:dev': () => run('6to5:client:dev'),
'heroku-postbuild': () => run('6to5:client'),
};

View file

@ -1,29 +1,30 @@
*.spec.*
*.config.*
*.fixture.js*
*.ai
*.cdr
*.eps
*.log
*.lock
.* .*
*.spec.js
*.fixture.js
manifest.yml manifest.yml
Dockerfile*
docker-compose.yml docker-compose.yml
now.json test
app.json
bower.json
deno.json
bin/release.*
img/logo/cloudcmd-hq.png
webpack.config.js
docker
test*
fixture fixture
fixture-*
coverage coverage
css css
html html
yarn-error.log
yarn.lock
now.json
cssnano.config.js
app.json
bower.json
manifest.yml
bin/release.js
client client
webpack.config.js
*.ai
*.cdr
*.eps

View file

@ -1,15 +0,0 @@
{
"checkCoverage": false,
"all": false,
"exclude": [
"**/*.spec.js",
"**/*.*.js",
"**/*.config.*",
"**/fixture",
"**/test/**"
],
"branches": 100,
"lines": 100,
"functions": 100,
"statements": 100
}

View file

@ -1,60 +0,0 @@
{
"plugins": ["cloudcmd"],
"ignore": [
"*.md",
"app.json",
"fontello.json",
"html",
"fixture*"
],
"rules": {
"package-json/add-type": "off"
},
"match": {
".filesystem.json": {
"nodejs/rename-file-cjs-to-js": "off"
},
"base64": {
"types/convert-typeof-to-is-type": "off"
},
"*.md": {
"nodejs/convert-commonjs-to-esm": "on"
},
".webpack": {
"webpack": "on"
},
"server": {
"nodejs/remove-process-exit": "on"
},
"server/{server,exit}.js": {
"nodejs/remove-process-exit": "off"
},
"server/{server,exit,terminal,distribute/log}.{js,mjs}": {
"remove-console": "off"
},
"client/{client,cloudcmd,load-module}.{js,mjs}": {
"remove-console": "off"
},
"client": {
"nodejs": "off"
},
"client/sw": {
"remove-console": "off"
},
"test/common/cloudfunc.js": {
"remove-console": "off"
},
"storage.js": {
"promises/remove-useless-async": "off"
},
"docker.yml": {
"github/set-node-versions": "off"
},
"vim.js": {
"merge-duplicate-functions": "off"
},
"common": {
"nodejs/declare": "off"
}
}
}

View file

@ -1,38 +0,0 @@
import {env} from 'node:process';
import {rspack} from '@rspack/core';
const {CssExtractRspackPlugin} = rspack;
const isDev = env.NODE_ENV === 'development';
const plugins = [
new CssExtractRspackPlugin({
filename: '[name].css',
}),
];
const rules = [{
test: /\.css$/i,
use: [CssExtractRspackPlugin.loader, {
loader: 'css-loader',
options: {
url: true,
},
}],
}, {
test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/,
type: 'asset/inline',
}];
export default {
mode: isDev ? 'development' : 'production',
plugins,
module: {
rules,
},
optimization: {
minimize: !isDev,
minimizer: [
new rspack.LightningCssMinimizerRspackPlugin(),
],
},
};

View file

@ -1,36 +0,0 @@
import {env} from 'node:process';
import HtmlWebpackPlugin from 'html-webpack-plugin';
const isDev = env.NODE_ENV === 'development';
export const plugins = [
new HtmlWebpackPlugin({
inject: false,
template: 'html/index.html',
minify: !isDev && getMinifyHtmlOptions(),
}),
];
function getMinifyHtmlOptions() {
return {
removeComments: true,
removeCommentsFromCDATA: true,
removeCDATASectionsFromCDATA: true,
collapseWhitespace: true,
collapseBooleanAttributes: true,
removeAttributeQuotes: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
/* оставляем, поскольку у нас
* в элемент fm генерируеться
* таблица файлов
*/
removeEmptyElements: false,
removeOptionalTags: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
minifyJS: true,
};
}

View file

@ -1,173 +0,0 @@
import {resolve, sep} from 'node:path';
import {fileURLToPath} from 'node:url';
import {env} from 'node:process';
import {rspack} from '@rspack/core';
const resolveModule = (a) => fileURLToPath(import.meta.resolve(a));
const {
EnvironmentPlugin,
NormalModuleReplacementPlugin,
ProvidePlugin,
} = rspack;
const modules = './modules';
const dirModules = './client/modules';
const dirCss = './css';
const dirThemes = `${dirCss}/themes`;
const dirColumns = `${dirCss}/columns`;
const dir = './client';
const {NODE_ENV} = env;
const isDev = NODE_ENV === 'development';
const rootDir = new URL('..', import.meta.url).pathname;
const dist = resolve(rootDir, 'dist');
const distDev = resolve(rootDir, 'dist-dev');
const devtool = isDev ? 'eval' : 'source-map';
const noParse = (a) => a.endsWith('.spec.js');
// codegen.macro is a babel-macro (build-time codegen), not supported by
// Rspack's native SWC transform, so client/sw/sw.js (the only file that
// uses it) keeps going through babel-loader. Everything else uses
// Rspack's built-in SWC loader, which is the main source of the speedup.
const rules = [{
test: /sw\/sw\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
}, {
test: /\.[mc]?js$/,
exclude: [/node_modules/, /sw\/sw\.js$/],
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: {
syntax: 'ecmascript',
},
},
env: {
targets: 'defaults',
},
},
}];
const plugins = [
new NormalModuleReplacementPlugin(/^node:/, (resource) => {
resource.request = resource.request.replace(/^node:/, '');
}),
new NormalModuleReplacementPlugin(/^putout$/, '@putout/bundle'),
new EnvironmentPlugin({
NODE_ENV,
}),
new ProvidePlugin({
process: 'process/browser',
}),
];
const splitChunks = {
chunks: 'all',
cacheGroups: {
abcCommon: {
name: 'cloudcmd.common',
chunks: (chunk) => {
const lazyChunks = [
'sw',
'nojs',
'view',
'edit',
'terminal',
'config',
'user-menu',
'help',
'themes/dark',
'themes/light',
'columns/name-size',
'columns/name-size-date',
'columns/name-size-time',
'columns/name-size-date-time',
];
return !lazyChunks.includes(chunk.name);
},
minChunks: 1,
enforce: true,
priority: -1,
reuseExistingChunk: true,
},
},
};
export default {
resolve: {
symlinks: false,
alias: {
'node:process': 'process',
'node:path': 'path',
},
fallback: {
path: resolveModule('path-browserify'),
process: resolveModule('process/browser'),
util: resolveModule('util'),
},
},
devtool,
optimization: {
splitChunks,
},
entry: {
'themes/dark': `${dirThemes}/dark.css`,
'themes/light': `${dirThemes}/light.css`,
'columns/name-size': `${dirColumns}/name-size.css`,
'columns/name-size-date': `${dirColumns}/name-size-date.css`,
'columns/name-size-date-time': `${dirColumns}/name-size-date-time.css`,
'nojs': `${dirCss}/nojs.css`,
'help': `${dirCss}/help.css`,
'view': `${dirCss}/view.css`,
'config': `${dirCss}/config.css`,
'terminal': `${dirCss}/terminal.css`,
'user-menu': `${dirCss}/user-menu.css`,
'sw': `${dir}/sw/sw.js`,
'cloudcmd': `${dir}/cloudcmd.js`,
[`${modules}/edit`]: `${dirModules}/edit.js`,
[`${modules}/edit-file`]: `${dirModules}/edit-file.js`,
[`${modules}/edit-file-vim`]: `${dirModules}/edit-file-vim.js`,
[`${modules}/edit-names`]: `${dirModules}/edit-names.js`,
[`${modules}/edit-names-vim`]: `${dirModules}/edit-names-vim.js`,
[`${modules}/menu`]: `${dirModules}/menu/index.js`,
[`${modules}/view`]: `${dirModules}/view/index.js`,
[`${modules}/help`]: `${dirModules}/help.js`,
[`${modules}/markdown`]: `${dirModules}/markdown.js`,
[`${modules}/config`]: `${dirModules}/config/index.js`,
[`${modules}/contact`]: `${dirModules}/contact.js`,
[`${modules}/upload`]: `${dirModules}/upload.js`,
[`${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`,
[`${modules}/command-line`]: `${dirModules}/command-line.js`,
},
output: {
filename: '[name].js',
path: isDev ? distDev : dist,
pathinfo: isDev,
devtoolModuleFilenameTemplate,
publicPath: '/dist/',
},
module: {
rules,
noParse,
},
plugins,
performance: {
maxEntrypointSize: 800_000,
maxAssetSize: 600_000,
},
};
function devtoolModuleFilenameTemplate(info) {
const resource = info.absoluteResourcePath.replace(rootDir + sep, '');
return `file://cloudcmd/${resource}`;
}

17
.stylelintrc.yml Normal file
View file

@ -0,0 +1,17 @@
extends: stylelint-config-standard
rules:
indentation: 4
declaration-block-trailing-semicolon: always
declaration-colon-space-before: null
selector-list-comma-newline-after: null
comment-empty-line-before: null
number-leading-zero: null
number-no-trailing-zeros: null
string-quotes: single
function-url-quotes: never
no-eol-whitespace: null
font-family-name-quotes: always-unless-keyword
font-family-no-missing-generic-family-keyword: null
rule-empty-line-before: null
max-empty-lines: 2

38
.travis.yml Normal file
View file

@ -0,0 +1,38 @@
language: node_js
node_js:
- 11
- 10
- 8
matrix:
allow_failures:
- os: windows
os:
- linux
- osx
- windows
script:
- npm run lint && npm run build && npm run coverage && npm run report
notifications:
email:
on_success: never
on_failure: change
sudo: required
services:
- docker
deploy:
provider: script
script: npm run docker-ci
skip_cleanup: true
on:
node: '8'
condition: $TRAVIS_OS_NAME = linux
tags: true
all_branches: false

View file

@ -1,2 +0,0 @@
[files]
extend-exclude = ["ChangeLog"]

75
.webpack/css.js Normal file
View file

@ -0,0 +1,75 @@
'use strict';
const fs = require('fs');
const {
basename,
extname,
join,
} = require('path');
const {env} = process;
const isDev = env.NODE_ENV === 'development';
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const extractCSS = (a) => new ExtractTextPlugin(`${a}.css`);
const extractMain = extractCSS('[name]');
const cssNames = [
'nojs',
'view',
'config',
'terminal',
...getCSSList('columns'),
];
const cssPlugins = cssNames.map(extractCSS);
const clean = (a) => a.filter(Boolean);
const plugins = clean([
...cssPlugins,
extractMain,
!isDev && new OptimizeCssAssetsPlugin(),
]);
const rules = [{
test: /\.css$/,
exclude: /css\/(nojs|view|config|terminal|columns.*)\.css/,
use: extractMain.extract([
'css-loader',
]),
},
...cssPlugins.map(extract), {
test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/,
loader: 'url-loader?limit=100000',
}];
module.exports = {
plugins,
module: {
rules,
},
};
function getCSSList(dir) {
const base = (a) => basename(a, extname(a));
const addDir = (name) => `${dir}/${name}`;
const rootDir = join(__dirname, '..');
return fs.readdirSync(`${rootDir}/css/${dir}`)
.map(base)
.map(addDir);
}
function extract(extractPlugin) {
const {filename} = extractPlugin;
return {
test: RegExp(`css/${filename}`),
use: extractPlugin.extract([
'css-loader',
])
};
}

43
.webpack/html.js Normal file
View file

@ -0,0 +1,43 @@
'use strict';
const {env} = process;
const isDev = env.NODE_ENV === 'development';
const HtmlWebpackPlugin = require('html-webpack-plugin');
const plugins = [
new HtmlWebpackPlugin({
inject: false,
template: 'html/index.html',
minify: !isDev && getMinifyHtmlOptions(),
}),
];
module.exports = {
plugins,
};
function getMinifyHtmlOptions() {
return {
removeComments: true,
removeCommentsFromCDATA: true,
removeCDATASectionsFromCDATA: true,
collapseWhitespace: true,
collapseBooleanAttributes: true,
removeAttributeQuotes: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
/* оставляем, поскольку у нас
* в элемент fm генерируеться
* таблица файлов
*/
removeEmptyElements: false,
removeOptionalTags: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
minifyJS: true,
};
}

129
.webpack/js.js Normal file
View file

@ -0,0 +1,129 @@
'use strict';
const {
resolve,
sep,
join,
} = require('path');
const {
EnvironmentPlugin,
} = require('webpack');
const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin');
const dir = './client';
const dirModules = './client/modules';
const modules = './modules';
const {env} = process;
const isDev = env.NODE_ENV === 'development';
const rootDir = join(__dirname, '..');
const dist = resolve(rootDir, 'dist');
const distDev = resolve(rootDir, 'dist-dev');
const devtool = isDev ? 'eval' : 'source-map';
const notEmpty = (a) => a;
const clean = (array) => array.filter(notEmpty);
const noParse = (a) => /\.spec\.js$/.test(a);
const babelDev = {
babelrc: false,
plugins: [
'module:babel-plugin-macros',
'@babel/plugin-proposal-object-rest-spread',
]
};
const rules = clean([
!isDev && {
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
isDev && {
test: /sw\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: babelDev
}]);
const plugins = [
new EnvironmentPlugin(['NODE_ENV']),
new ServiceWorkerWebpackPlugin({
entry: join(__dirname, '..', 'client', 'sw', 'sw.js'),
excludes: ['*'],
}),
];
const splitChunks = {
name: 'cloudcmd.common',
chunks: 'all',
};
module.exports = {
resolve: {
symlinks: false
},
devtool,
optimization: {
splitChunks,
},
entry: {
cloudcmd: `${dir}/cloudcmd.js`,
[modules + '/edit']: `${dirModules}/edit.js`,
[modules + '/edit-file']: `${dirModules}/edit-file.js`,
[modules + '/edit-file-vim']: `${dirModules}/edit-file-vim.js`,
[modules + '/edit-names']: `${dirModules}/edit-names.js`,
[modules + '/edit-names-vim']: `${dirModules}/edit-names-vim.js`,
[modules + '/menu']: `${dirModules}/menu.js`,
[modules + '/view']: `${dirModules}/view.js`,
[modules + '/help']: `${dirModules}/help.js`,
[modules + '/markdown']: `${dirModules}/markdown.js`,
[modules + '/config']: `${dirModules}/config.js`,
[modules + '/contact']: `${dirModules}/contact.js`,
[modules + '/upload']: `${dirModules}/upload.js`,
[modules + '/operation']: `${dirModules}/operation/index.js`,
[modules + '/konsole']: `${dirModules}/konsole.js`,
[modules + '/terminal']: `${dirModules}/terminal.js`,
[modules + '/cloud']: `${dirModules}/cloud.js`,
[modules + '/polyfill']: `${dirModules}/polyfill.js`,
},
output: {
filename: '[name].js',
path: isDev ? distDev : dist,
pathinfo: isDev,
devtoolModuleFilenameTemplate,
publicPath: '/dist/',
},
externals: [
externals
],
module: {
rules,
noParse,
},
plugins,
};
function externals(context, request, fn) {
if (!isDev)
return fn();
const list = [
'es6-promise',
];
if (list.includes(request))
return fn(null, request);
fn();
}
function devtoolModuleFilenameTemplate(info) {
const resource = info.absoluteResourcePath.replace(rootDir + sep, '');
return `file://cloudcmd/${resource}`;
}

View file

@ -10,7 +10,6 @@
".md" ".md"
], ],
"dictionary":[ "dictionary":[
"CloudCmd",
"Dev", "Dev",
"Dropbox", "Dropbox",
"Deepword", "Deepword",
@ -22,23 +21,18 @@
"Iptables", "Iptables",
"JitSu", "JitSu",
"Node", "Node",
"IO",
"Olena", "Olena",
"TarZak", "TarZak",
"Termux", "Termux",
"Zalitok", "Zalitok",
"WebSocket", "WebSocket",
"auth", "auth",
"autostart",
"binded",
"cd", "cd",
"cloudcmd", "cloudcmd",
"coderaiser", "coderaiser",
"com", "com",
"deepword",
"dev", "dev",
"destructuring", "deepword",
"dropbox",
"dword", "dword",
"edward", "edward",
"favicon", "favicon",
@ -46,18 +40,13 @@
"gz", "gz",
"io", "io",
"js", "js",
"linux",
"maintainers", "maintainers",
"markdown",
"microservice", "microservice",
"minification", "minification",
"mouseup", "mouseup",
"named", "named",
"nginx", "nginx",
"npm",
"or io", "or io",
"patreon",
"rc",
"refactor", "refactor",
"sexualized", "sexualized",
"sslPort", "sslPort",
@ -66,7 +55,6 @@
"v0", "v0",
"v1", "v1",
"v2", "v2",
"yml", "yml"
"systemd"
] ]
} }

View file

@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo
Examples of behavior that contributes to creating a positive environment include: Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language * Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences * Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism * Gracefully accepting constructive criticism
- Focusing on what is best for the community * Focusing on what is best for the community
- Showing empathy towards other community members * Showing empathy towards other community members
Examples of unacceptable behavior by participants include: Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances * The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks * Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment * Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission * Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting * Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities ## Our Responsibilities

View file

@ -1,22 +1,20 @@
## Commit Commit
---------------
Format of the commit message: **type(scope) subject** Format of the commit message: **type(scope) subject**
**Type**: **Type**:
- feature(scope) subject
- feature: scope: subject - fix(scope) subject
- fix: scope: subject - docs(scope) subject
- docs: scope: subject - refactor(scope) subject
- refactor: scope: subject - test(scope) subject
- test: scope: subject - chore(scope) subject
- chore: scope: subject
**Scope**: **Scope**:
Scope could be anything specifying place of the commit change. Scope could be anything specifying place of the commit change.
For example util, console, view, edit, style etc... For example util, console, view, edit, style etc...
**Subject text**: **Subject text**:
- use imperative, present tense: “change” not “changed” nor “changes” - use imperative, present tense: “change” not “changed” nor “changes”
- don't capitalize first letter - don't capitalize first letter
- no dot (.) at the end - no dot (.) at the end
@ -25,6 +23,5 @@ For example util, console, view, edit, style etc...
- includes motivation for the change and contrasts with previous behavior - includes motivation for the change and contrasts with previous behavior
**Examples**: **Examples**:
- [fix(style) .name{width}: 37% -> 35%](https://github.com/coderaiser/cloudcmd/commit/94b0642e3990c17b3a0ee3efeb75f343e1e7c050)
- [fix: style: .name{width}: 37% -> 35%](https://github.com/coderaiser/cloudcmd/commit/94b0642e3990c17b3a0ee3efeb75f343e1e7c050) - [fix(console) dispatch: focus -> mouseup](https://github.com/coderaiser/cloudcmd/commit/f41ec5058d1411e86a881f8e8077e0572e0409ec)
- [fix: console: dispatch: focus -> mouseup](https://github.com/coderaiser/cloudcmd/commit/f41ec5058d1411e86a881f8e8077e0572e0409ec)

2529
ChangeLog

File diff suppressed because it is too large Load diff

25
Dockerfile Normal file
View file

@ -0,0 +1,25 @@
FROM node:10
MAINTAINER Coderaiser
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY package.json /usr/src/app/
RUN npm config set package-lock false && \
npm install --production && \
npm i gritty && \
npm cache clean --force
COPY . /usr/src/app
WORKDIR /root
ENV cloudcmd_terminal true
ENV cloudcmd_terminal_path gritty
ENV cloudcmd_open false
EXPOSE 8000
ENTRYPOINT ["/usr/src/app/bin/cloudcmd.js"]

29
Dockerfile.alpine Normal file
View file

@ -0,0 +1,29 @@
FROM node:alpine
MAINTAINER Coderaiser
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY package.json /usr/src/app/
RUN npm config set package-lock false && \
npm install --production && \
apk update && \
apk add --no-cache bash make g++ python && \
npm i gritty && \
npm cache clean --force && \
apk del make g++ python && \
rm -rf /usr/include /tmp/* /var/cache/apk/*
COPY . /usr/src/app
WORKDIR /root
ENV cloudcmd_terminal true
ENV cloudcmd_terminal_path gritty
ENV cloudcmd_open false
EXPOSE 8000
ENTRYPOINT ["/usr/src/app/bin/cloudcmd.js"]

1147
HELP.md

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
(The MIT License) (The MIT License)
Copyright (c) 2012-2025 Coderaiser <mnemonic.enemy@gmail.com> Copyright (c) 2012-2018 Coderaiser <mnemonic.enemy@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

158
README.md
View file

@ -1,22 +1,26 @@
# Cloud Commander v19.19.1 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![Deploy on InstaPods][DeployInstaPodsIMG]][DeployInstaPodsURL] # Cloud Commander v11.7.0 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Now status][BuildAppveyorIMGURL]][BuildAppveyorURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![OpenCollective](https://opencollective.com/cloudcmd/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/cloudcmd/sponsors/badge.svg)](#sponsors)
### [Main][MainURL] [Blog][BlogURL] [Support][SupportURL] [Demo][DemoURL] ### [Main][MainURL] [Blog][BlogURL] Live([Heroku][HerokuURL], [Now][NowURL])
[MainURL]: https://cloudcmd.io "Main"
[BlogURL]: https://blog.cloudcmd.io "Blog"
[SupportURL]: https://patreon.com/coderaiser "Patreon"
[DemoURL]: https://cloudcmd-zdp6.onrender.com
[NPM_INFO_IMG]: https://nodei.co/npm/cloudcmd.png [NPM_INFO_IMG]: https://nodei.co/npm/cloudcmd.png
[BuildStatusURL]: https://github.com/coderaiser/cloudcmd/actions/workflows/nodejs.yml "Build Status" [MainURL]: http://cloudcmd.io "Main"
[BuildStatusIMGURL]: https://github.com/coderaiser/cloudcmd/actions/workflows/nodejs.yml/badge.svg [BlogURL]: http://blog.cloudcmd.io "Blog"
[HerokuURL]: https://cloudcmd.herokuapp.com/ "Heroku"
[NowURL]: https://cloudcmd.now.sh/ "Now"
[BuildStatusURL]: https://travis-ci.org/coderaiser/cloudcmd "Build Status"
[BuildStatusIMGURL]: https://img.shields.io/travis/coderaiser/cloudcmd.svg?style=flat-squere&longCache=true
[BuildAppveyorURL]: https://ci.appveyor.com/project/coderaiser/cloudcmd
[BuildAppveyorIMGURL]: https://ci.appveyor.com/api/projects/status/tse6sc8dxrqxqehi?svg=true
[CodacyURL]: https://www.codacy.com/app/coderaiser/cloudcmd [CodacyURL]: https://www.codacy.com/app/coderaiser/cloudcmd
[CodacyIMG]: https://api.codacy.com/project/badge/Grade/ddda78be780549ce8754f8d47a8c0e36 [CodacyIMG]: https://api.codacy.com/project/badge/Grade/ddda78be780549ce8754f8d47a8c0e36
[GitterURL]: https://gitter.im/cloudcmd/hello [GitterURL]: https://gitter.im/cloudcmd/hello
[GitterIMGURL]: https://img.shields.io/gitter/room/coderaiser/cloudcmd.js.svg [GitterIMGURL]: https://img.shields.io/gitter/room/coderaiser/cloudcmd.js.svg
[DeployURL]: https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy" [DeployURL]: https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy"
[DeployIMG]: https://www.herokucdn.com/deploy/button.png [DeployIMG]: https://www.herokucdn.com/deploy/button.png
[DeployInstaPodsIMG]: https://img.shields.io/badge/deploy%20on-InstaPods-blue
[DeployInstaPodsURL]: https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd
**Cloud Commander** a file manager for the web with console and editor. **Cloud Commander** a file manager for the web with console and editor.
@ -27,7 +31,6 @@
``` ```
npm i cloudcmd -g npm i cloudcmd -g
``` ```
## Start ## Start
For starting just type in console: For starting just type in console:
@ -45,8 +48,8 @@ Open url `http://localhost:8000` in browser.
You will see something similar to this. You will see something similar to this.
![View](https://cloudcmd.io/img/screen/view.png "View") ![View](https://cloudcmd.io/img/screen/view.png "View")
## Deploy
## Deploy
`Cloud Commander` could be easily deployed to [Heroku][DeployURL]. `Cloud Commander` could be easily deployed to [Heroku][DeployURL].
[![Deploy][DeployIMG]][DeployURL] [![Deploy][DeployIMG]][DeployURL]
@ -70,31 +73,33 @@ npm i cloudcmd express socket.io -S
And create `index.js`: And create `index.js`:
```js ```js
import http from 'node:http'; const http = require('http');
import {cloudcmd} from 'cloudcmd'; const cloudcmd = require('cloudcmd');
import {Server} from 'socket.io'; const io = require('socket.io');
import express from 'express'; const app = require('express')();
const app = express();
const port = 1337; const port = 1337;
const prefix = '/'; const prefix = '/cloudcmd';
const server = http.createServer(app); const server = http.createServer(app);
const socket = new Server(server, { const socket = io.listen(server, {
path: `${prefix}socket.io`, path: `${prefix}/socket.io`
}); });
const config = { const config = {
name: 'cloudcmd :)', prefix // base URL or function which returns base URL (optional)
}; };
const plugins = [
__dirname + '/plugin.js'
];
const filePicker = { const filePicker = {
data: { data: {
FilePicker: { FilePicker: {
key: 'key', key: 'key',
}, }
}, }
}; };
// override option from json/modules.json // override option from json/modules.json
@ -102,40 +107,22 @@ const modules = {
filePicker, filePicker,
}; };
const { app.use(cloudcmd({
createConfigManager,
configPath,
} = cloudcmd;
const configManager = createConfigManager({
configPath,
});
app.use(prefix, cloudcmd({
socket, // used by Config, Edit (optional) and Console (required) socket, // used by Config, Edit (optional) and Console (required)
config, // config data (optional) config, // config data (optional)
plugins, // optional
modules, // optional modules, // optional
configManager, // optional
})); }));
server.listen(port); server.listen(port);
``` ```
## Docker Docker
---------------
The docker images are provided for multiple architectures and types. The following list shows all existing images:
| Architecture | Type |
|----------------|--------------|
| amd64 | linux |
| arm64 (arm/v8) | linux |
| amd64 | linux-alpine |
| arm64 (arm/v8) | linux-alpine |
`Cloud Commander` could be used as a [docker container](https://hub.docker.com/r/coderaiser/cloudcmd/ "Docker container") this way: `Cloud Commander` could be used as a [docker container](https://hub.docker.com/r/coderaiser/cloudcmd/ "Docker container") this way:
```sh ```sh
docker run -it --rm -v ~:/root -v /:/mnt/fs -w=/root -p 8000:8000 coderaiser/cloudcmd docker run -t --rm -v ~:/root -v /:/mnt/fs -p 8000:8000 coderaiser/cloudcmd
``` ```
Config would be read from home directory, hosts root file system would be mount to `/mnt/fs`, Config would be read from home directory, hosts root file system would be mount to `/mnt/fs`,
@ -161,19 +148,84 @@ When you create this file run:
docker-compose up docker-compose up
``` ```
## Documentation Get involved
---------------
More documentation you can find on https://cloudcmd.io/.
## Get involved
There is a lot ways to be involved in `Cloud Commander` development: There is a lot ways to be involved in `Cloud Commander` development:
- support project on patreon: https://patreon.com/coderaiser;
- if you find a bug or got idea to share [create an issue](https://github.com/coderaiser/cloudcmd/issues/new "Create issue"); - if you find a bug or got idea to share [create an issue](https://github.com/coderaiser/cloudcmd/issues/new "Create issue");
- if you fixed a bug, typo or implemented new feature [create pull request](https://github.com/coderaiser/cloudcmd/compare "Create pull request"); - if you fixed a bug, typo or implemented new feature [create pull request](https://github.com/coderaiser/cloudcmd/compare "Create pull request");
- if you know languages you can help with [site translations](https://github.com/coderaiser/cloudcmd/wiki "Cloud Commander community wiki"); - if you know languages you can help with [site translations](https://github.com/coderaiser/cloudcmd/wiki "Cloud Commander community wiki");
## Backers
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/cloudcmd#backer)]
[![](https://opencollective.com/cloudcmd/backer/0/avatar.svg)](https://opencollective.com/cloudcmd/backer/0/website)
[![](https://opencollective.com/cloudcmd/backer/1/avatar.svg)](https://opencollective.com/cloudcmd/backer/1/website)
[![](https://opencollective.com/cloudcmd/backer/2/avatar.svg)](https://opencollective.com/cloudcmd/backer/2/website)
[![](https://opencollective.com/cloudcmd/backer/3/avatar.svg)](https://opencollective.com/cloudcmd/backer/3/website)
[![](https://opencollective.com/cloudcmd/backer/4/avatar.svg)](https://opencollective.com/cloudcmd/backer/4/website)
[![](https://opencollective.com/cloudcmd/backer/5/avatar.svg)](https://opencollective.com/cloudcmd/backer/5/website)
[![](https://opencollective.com/cloudcmd/backer/6/avatar.svg)](https://opencollective.com/cloudcmd/backer/6/website)
[![](https://opencollective.com/cloudcmd/backer/7/avatar.svg)](https://opencollective.com/cloudcmd/backer/7/website)
[![](https://opencollective.com/cloudcmd/backer/8/avatar.svg)](https://opencollective.com/cloudcmd/backer/8/website)
[![](https://opencollective.com/cloudcmd/backer/9/avatar.svg)](https://opencollective.com/cloudcmd/backer/9/website)
[![](https://opencollective.com/cloudcmd/backer/10/avatar.svg)](https://opencollective.com/cloudcmd/backer/10/website)
[![](https://opencollective.com/cloudcmd/backer/11/avatar.svg)](https://opencollective.com/cloudcmd/backer/11/website)
[![](https://opencollective.com/cloudcmd/backer/12/avatar.svg)](https://opencollective.com/cloudcmd/backer/12/website)
[![](https://opencollective.com/cloudcmd/backer/13/avatar.svg)](https://opencollective.com/cloudcmd/backer/13/website)
[![](https://opencollective.com/cloudcmd/backer/14/avatar.svg)](https://opencollective.com/cloudcmd/backer/14/website)
[![](https://opencollective.com/cloudcmd/backer/15/avatar.svg)](https://opencollective.com/cloudcmd/backer/15/website)
[![](https://opencollective.com/cloudcmd/backer/16/avatar.svg)](https://opencollective.com/cloudcmd/backer/16/website)
[![](https://opencollective.com/cloudcmd/backer/17/avatar.svg)](https://opencollective.com/cloudcmd/backer/17/website)
[![](https://opencollective.com/cloudcmd/backer/18/avatar.svg)](https://opencollective.com/cloudcmd/backer/18/website)
[![](https://opencollective.com/cloudcmd/backer/19/avatar.svg)](https://opencollective.com/cloudcmd/backer/19/website)
[![](https://opencollective.com/cloudcmd/backer/20/avatar.svg)](https://opencollective.com/cloudcmd/backer/20/website)
[![](https://opencollective.com/cloudcmd/backer/21/avatar.svg)](https://opencollective.com/cloudcmd/backer/21/website)
[![](https://opencollective.com/cloudcmd/backer/22/avatar.svg)](https://opencollective.com/cloudcmd/backer/22/website)
[![](https://opencollective.com/cloudcmd/backer/23/avatar.svg)](https://opencollective.com/cloudcmd/backer/23/website)
[![](https://opencollective.com/cloudcmd/backer/24/avatar.svg)](https://opencollective.com/cloudcmd/backer/24/website)
[![](https://opencollective.com/cloudcmd/backer/25/avatar.svg)](https://opencollective.com/cloudcmd/backer/25/website)
[![](https://opencollective.com/cloudcmd/backer/26/avatar.svg)](https://opencollective.com/cloudcmd/backer/26/website)
[![](https://opencollective.com/cloudcmd/backer/27/avatar.svg)](https://opencollective.com/cloudcmd/backer/27/website)
[![](https://opencollective.com/cloudcmd/backer/28/avatar.svg)](https://opencollective.com/cloudcmd/backer/28/website)
[![](https://opencollective.com/cloudcmd/backer/29/avatar.svg)](https://opencollective.com/cloudcmd/backer/29/website)
## Sponsors
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/cloudcmd#sponsor)]
[![](https://opencollective.com/cloudcmd/sponsor/0/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/0/website)
[![](https://opencollective.com/cloudcmd/sponsor/1/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/1/website)
[![](https://opencollective.com/cloudcmd/sponsor/2/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/2/website)
[![](https://opencollective.com/cloudcmd/sponsor/3/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/3/website)
[![](https://opencollective.com/cloudcmd/sponsor/4/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/4/website)
[![](https://opencollective.com/cloudcmd/sponsor/5/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/5/website)
[![](https://opencollective.com/cloudcmd/sponsor/6/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/6/website)
[![](https://opencollective.com/cloudcmd/sponsor/7/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/7/website)
[![](https://opencollective.com/cloudcmd/sponsor/8/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/8/website)
[![](https://opencollective.com/cloudcmd/sponsor/9/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/9/website)
[![](https://opencollective.com/cloudcmd/sponsor/10/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/10/website)
[![](https://opencollective.com/cloudcmd/sponsor/11/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/11/website)
[![](https://opencollective.com/cloudcmd/sponsor/12/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/12/website)
[![](https://opencollective.com/cloudcmd/sponsor/13/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/13/website)
[![](https://opencollective.com/cloudcmd/sponsor/14/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/14/website)
[![](https://opencollective.com/cloudcmd/sponsor/15/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/15/website)
[![](https://opencollective.com/cloudcmd/sponsor/16/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/16/website)
[![](https://opencollective.com/cloudcmd/sponsor/17/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/17/website)
[![](https://opencollective.com/cloudcmd/sponsor/18/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/18/website)
[![](https://opencollective.com/cloudcmd/sponsor/19/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/19/website)
[![](https://opencollective.com/cloudcmd/sponsor/20/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/20/website)
[![](https://opencollective.com/cloudcmd/sponsor/21/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/21/website)
[![](https://opencollective.com/cloudcmd/sponsor/22/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/22/website)
[![](https://opencollective.com/cloudcmd/sponsor/23/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/23/website)
[![](https://opencollective.com/cloudcmd/sponsor/24/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/24/website)
[![](https://opencollective.com/cloudcmd/sponsor/25/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/25/website)
[![](https://opencollective.com/cloudcmd/sponsor/26/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/26/website)
[![](https://opencollective.com/cloudcmd/sponsor/27/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/27/website)
[![](https://opencollective.com/cloudcmd/sponsor/28/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/28/website)
[![](https://opencollective.com/cloudcmd/sponsor/29/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/29/website)
## License ## License
MIT MIT

View file

@ -11,7 +11,6 @@
"folder", "folder",
"orthodox" "orthodox"
], ],
"stack": "heroku-18",
"env": { "env": {
"NPM_CONFIG_PRODUCTION": { "NPM_CONFIG_PRODUCTION": {
"description": "Keep false to install devDependencies and build frontend", "description": "Keep false to install devDependencies and build frontend",
@ -63,6 +62,11 @@
"value": "", "value": "",
"required": false "required": false
}, },
"CLOUDCMD_CONFIG_DIALOG": {
"description": "show config dialog",
"value": "false",
"required": false
},
"CLOUDCMD_ONE_FILE_PANEL": { "CLOUDCMD_ONE_FILE_PANEL": {
"description": "show one file panel", "description": "show one file panel",
"value": "false", "value": "false",
@ -159,4 +163,3 @@
"required": false "required": false
} }
} }
}

View file

@ -1,48 +1,31 @@
#!/usr/bin/env node #!/usr/bin/env node
import process from 'node:process'; 'use strict';
import {promisify} from 'node:util';
import {tryToCatch} from 'try-to-catch';
import parse from 'yargs-parser';
import exit from '../server/exit.js';
import {createConfig, configPath} from '../server/config.js';
import * as env from '../server/env.js';
import prefixer from '../server/prefixer.js';
import * as validate from '../server/validate.js';
import Info from '../package.json' with {
type: 'json',
};
process.on('unhandledRejection', exit); const Info = require('../package');
const DIR_SERVER = '../server/';
const isUndefined = (a) => typeof a === 'undefined'; const {promisify} = require('util');
const wraptile = require('wraptile');
const exit = require(DIR_SERVER + 'exit');
const config = require(DIR_SERVER + 'config');
const env = require(DIR_SERVER + 'env');
const prefixer = require(DIR_SERVER + '/prefixer');
const noop = () => {};
const choose = (a, b) => { const choose = (a, b) => {
if (isUndefined(a)) if (a === undefined)
return b; return b;
return a; return a;
}; };
const config = createConfig({ process.on('unhandledRejection', exit);
configPath,
});
const maybeRoot = (a) => { const argv = process.argv;
if (a === '.') const args = require('minimist')(argv.slice(2), {
return process.cwd();
return a;
};
const yargsOptions = {
configuration: {
'strip-aliased': true,
'strip-dashed': true,
},
coerce: {
root: maybeRoot,
},
string: [ string: [
'name', 'name',
'port', 'port',
@ -57,12 +40,9 @@ const yargsOptions = {
'terminal-path', 'terminal-path',
'terminal-command', 'terminal-command',
'columns', 'columns',
'menu',
'theme',
'import-url', 'import-url',
'import-token', 'import-token',
'export-token', 'export-token',
'dropbox-token',
], ],
boolean: [ boolean: [
'auth', 'auth',
@ -71,9 +51,9 @@ const yargsOptions = {
'server', 'server',
'online', 'online',
'open', 'open',
'progress',
'config-dialog', 'config-dialog',
'config-auth', 'config-auth',
'config-port',
'console', 'console',
'sync-console-path', 'sync-console-path',
'contact', 'contact',
@ -83,7 +63,6 @@ const yargsOptions = {
'confirm-copy', 'confirm-copy',
'confirm-move', 'confirm-move',
'show-config', 'show-config',
'show-dot-files',
'show-file-name', 'show-file-name',
'vim', 'vim',
'keys-panel', 'keys-panel',
@ -92,70 +71,62 @@ const yargsOptions = {
'import', 'import',
'import-listen', 'import-listen',
'log', 'log',
'zip',
'dropbox',
], ],
default: { default: {
'server': true, server : true,
'name': choose(env.parse('name'), config('name')), name : choose(env('name'), config('name')),
'auth': choose(env.bool('auth'), config('auth')), auth : choose(env.bool('auth'), config('auth')),
'port': config('port'), port : config('port'),
'online': config('online'), online : config('online'),
'open': choose(env.bool('open'), config('open')), open : choose(env.bool('open'), config('open')),
'editor': env.parse('editor') || config('editor'), editor : env('editor') || config('editor'),
'menu': env.parse('menu') || config('menu'), packer : config('packer') || 'tar',
'packer': config('packer') || 'tar', zip : config('zip'),
'zip': config('zip'), username : env('username') || config('username'),
'username': env.parse('username') || config('username'), root : choose(env('root'), config('root')),
'root': choose(env.parse('root'), config('root')), prefix : config('prefix'),
'prefix': choose(env.parse('prefix'), config('prefix')), progress : config('progress'),
'console': choose(env.bool('console'), config('console')), console : choose(env.bool('console'), config('console')),
'contact': choose(env.bool('contact'), config('contact')), contact : choose(env.bool('contact'), config('contact')),
'terminal': choose(env.bool('terminal'), config('terminal')), terminal : choose(env.bool('terminal'), config('terminal')),
'columns': env.parse('columns') || config('columns') || '', columns : env('columns') || config('columns') || '',
'theme': env.parse('theme') || config('theme') || '', vim : choose(env.bool('vim'), config('vim')),
'vim': choose(env.bool('vim'), config('vim')), log : config('log'),
'log': choose(env.bool('log'), config('log')),
'import-url': env.parse('import_url') || config('importUrl'), 'import-url': env('import_url') || config('importUrl'),
'import-listen': choose(env.bool('import_listen'), config('importListen')), 'import-listen': choose(env.bool('import_listen'), config('importListen')),
'import': choose(env.bool('import'), config('import')), import : choose(env.bool('import'), config('import')),
'export': choose(env.bool('export'), config('export')), export : choose(env.bool('export'), config('export')),
'prefix-socket': config('prefixSocket'), 'prefix-socket': config('prefixSocket'),
'show-dot-files': choose(env.bool('show_dot_files'), config('showDotFiles')),
'show-file-name': choose(env.bool('show_file_name'), config('showFileName')), 'show-file-name': choose(env.bool('show_file_name'), config('showFileName')),
'sync-console-path': choose(env.bool('sync_console_path'), config('syncConsolePath')), 'sync-console-path': choose(env.bool('sync_console_path'), config('syncConsolePath')),
'config-dialog': choose(env.bool('config_dialog'), config('configDialog')), 'config-dialog': choose(env.bool('config_dialog'), config('configDialog')),
'config-auth': choose(env.bool('config_auth'), config('configAuth')), 'config-auth': choose(env.bool('config_auth'), config('configAuth')),
'config-port': choose(env.bool('config_port'), config('configPort')), 'terminal-path': env('terminal_path') || config('terminalPath'),
'terminal-path': env.parse('terminal_path') || config('terminalPath'), 'terminal-command': env('terminal_command') || config('terminalCommand'),
'terminal-command': env.parse('terminal_command') || config('terminalCommand'),
'terminal-auto-restart': choose(env.bool('terminal_auto_restart'), config('terminalAutoRestart')), 'terminal-auto-restart': choose(env.bool('terminal_auto_restart'), config('terminalAutoRestart')),
'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')),
'keys-panel': env.bool('keys_panel') || config('keysPanel'), 'keys-panel': env.bool('keys_panel') || config('keysPanel'),
'import-token': env.parse('import_token') || config('importToken'), 'import-token': env('import_token') || config('importToken'),
'export-token': env.parse('export_token') || config('exportToken'), 'export-token': env('export_token') || config('exportToken'),
'dropbox': config('dropbox'),
'dropbox-token': config('dropboxToken') || '',
}, },
alias: { alias: {
version: 'v', v: 'version',
help: 'h', h: 'help',
password: 'p', p: 'password',
online: 'o', o: 'online',
username: 'u', u: 'username',
save: 's', s: 'save',
auth: 'a', a: 'auth',
config: 'c', c: 'config'
}, },
}; unknown: (cmd) => {
exit('\'%s\' is not a cloudcmd option. See \'cloudcmd --help\'.', cmd);
const {argv} = process; }
const args = parse(argv.slice(2), yargsOptions); });
if (args.version) if (args.version)
version(); version();
@ -164,23 +135,11 @@ else if (args.help)
else else
main(); main();
async function main() { function main() {
const {validateArgs} = await import('@putout/cli-validate-args');
const error = await validateArgs(args, [
...yargsOptions.boolean,
...yargsOptions.string,
]);
if (error)
return exit(error);
if (args.repl) if (args.repl)
await repl(); repl();
validate.columns(args.columns);
validate.theme(args.theme);
checkUpdate();
port(args.port); port(args.port);
config('name', args.name); config('name', args.name);
@ -188,88 +147,74 @@ async function main() {
config('online', args.online); config('online', args.online);
config('open', args.open); config('open', args.open);
config('username', args.username); config('username', args.username);
config('progress', args.progress);
config('console', args.console); config('console', args.console);
config('syncConsolePath', args.syncConsolePath); config('syncConsolePath', args['sync-console-path']);
config('showDotFiles', args.showDotFiles); config('showFileName', args['show-file-name']);
config('showFileName', args.showFileName);
config('contact', args.contact); config('contact', args.contact);
config('terminal', args.terminal); config('terminal', args.terminal);
config('terminalPath', args.terminalPath); config('terminalPath', args['terminal-path']);
config('terminalCommand', args.terminalCommand); config('terminalCommand', args['terminal-command']);
config('terminalAutoRestart', args.terminalAutoRestart); config('terminalAutoRestart', args['terminal-auto-restart']);
config('editor', args.editor); config('editor', args.editor);
config('menu', args.menu);
config('prefix', prefixer(args.prefix)); config('prefix', prefixer(args.prefix));
config('prefixSocket', prefixer(args.prefixSocket)); // MAJOR: remove condition on v12
config('root', args.root || '/'); config('prefixSocket', prefixer(args['prefix-socket']) || config('prefix'));
config('root', args.root);
config('vim', args.vim); config('vim', args.vim);
config('theme', args.theme);
config('columns', args.columns); config('columns', args.columns);
config('log', args.log); config('log', args.log);
config('confirmCopy', args.confirmCopy); config('confirmCopy', args['confirm-copy']);
config('confirmMove', args.confirmMove); config('confirmMove', args['confirm-move']);
config('oneFilePanel', args.oneFilePanel); config('oneFilePanel', args['one-file-panel']);
config('configDialog', args.configDialog); config('configDialog', args['config-dialog']);
config('configAuth', args.configAuth); config('configAuth', args['config-auth']);
config('configPort', args.configPort); config('keysPanel', args['keys-panel']);
config('keysPanel', args.keysPanel);
config('export', args.export); config('export', args.export);
config('exportToken', args.exportToken); config('exportToken', args['export-token']);
config('import', args.import); config('import', args.import);
config('importToken', args.importToken); config('importToken', args['import-token']);
config('importListen', args.importListen); config('importListen', args['import-listen']);
config('importUrl', args.importUrl); config('importUrl', args['import-url']);
config('dropbox', args.dropbox); readConfig(args.config);
config('dropboxToken', args.dropboxToken);
await readConfig(args.config);
const options = { const options = {
root: config('root'), root: args.root || '/', // --no-root
editor: config('editor'), editor: args.editor,
packer: config('packer'), packer: args.packer,
prefix: config('prefix'), prefix: args.prefix,
prefixSocket: config('prefixSocket'), prefixSocket: args['prefix-socket'] || args.prefix, // MAJOR: remove condition on v12
columns: config('columns'), columns: args.columns,
theme: config('theme'),
menu: config('menu'),
}; };
const password = env.parse('password') || args.password; const password = env('password') || args.password;
if (password) if (password)
config('password', await getPassword(password)); config('password', getPassword(password));
validateRoot(options.root, config); validateRoot(options.root);
if (args.showConfig) if (args['show-config'])
await showConfig(); showConfig();
const {distributeImport} = await import('../server/distribute/import.js'); const startWraped = wraptile(start, options);
const importConfig = promisify(distributeImport); const distribute = require('../server/distribute');
const importConfig = promisify(distribute.import);
const caller = (fn) => fn();
await start(options, config); importConfig()
.then(args.save ? caller(config.save) : noop)
if (args.save) .then(startWraped(options));
config.write();
await tryToCatch(checkUpdate);
await importConfig(config);
} }
function validateRoot(root, config) { function validateRoot(root) {
validate.root(root, config); const validate = require(DIR_SERVER + 'validate');
validate.root(root, console.log);
if (root === '/')
return;
if (config('log'))
console.log(`root: ${root}`);
} }
async function getPassword(password) { function getPassword(password) {
const {default: criton} = await import('criton'); const criton = require('criton');
return criton(password, config('algo')); return criton(password, config('algo'));
} }
@ -277,12 +222,14 @@ function version() {
console.log('v' + Info.version); console.log('v' + Info.version);
} }
async function start(options, config) { function start(config) {
const SERVER = DIR_SERVER + 'server';
if (!args.server) if (!args.server)
return; return;
const {default: server} = await import('../server/server.js'); const server = require(SERVER);
server(options, config); server(config);
} }
function port(arg) { function port(arg) {
@ -294,38 +241,38 @@ function port(arg) {
exit('cloudcmd --port: should be a number'); exit('cloudcmd --port: should be a number');
} }
async function showConfig() { function showConfig() {
const {showConfig} = await import('../server/show-config.js'); const show = require('../server/show-config');
const data = showConfig(config('*')); const data = show(config('*'));
console.log(data); console.log(data);
} }
async function readConfig(name) { function readConfig(name) {
if (!name) if (!name)
return; return;
const {default: forEachKey} = await import('for-each-key'); const fs = require('fs');
const tryCatch = require('try-catch');
const jju = require('jju');
const forEachKey = require('for-each-key');
const data = await import(name, { const readjsonSync = (name) => jju.parse(fs.readFileSync(name, 'utf8'), {
with: { mode: 'json'
type: 'json',
},
}); });
const [error, data] = tryCatch(readjsonSync, name);
if (error)
return exit(error.message);
forEachKey(config, data); forEachKey(config, data);
} }
async function help() { function help() {
const {default: bin} = await import('../json/help.json', { const bin = require('../json/help');
with: { const forEachKey = require('for-each-key');
type: 'json', const currify = require('currify');
},
});
const {default: forEachKey} = await import('for-each-key');
const {default: currify} = await import('currify');
const usage = 'Usage: cloudcmd [options]'; const usage = 'Usage: cloudcmd [options]';
const url = Info.homepage; const url = Info.homepage;
const log = currify((a, b, c) => console.log(a, b, c)); const log = currify((a, b, c) => console.log(a, b, c));
@ -336,27 +283,31 @@ async function help() {
console.log('\nGeneral help using Cloud Commander: <%s>', url); console.log('\nGeneral help using Cloud Commander: <%s>', url);
} }
async function repl() { function repl() {
console.log('REPL mode enabled (telnet localhost 1337)'); console.log('REPL mode enabled (telnet localhost 1337)');
await import('../server/repl.js'); require(DIR_SERVER + 'repl');
} }
async function checkUpdate() { function checkUpdate() {
const {default: load} = await import('package-json'); const load = require('package-json');
const {version} = await load(Info.name, 'latest');
await showUpdateInfo(version); load(Info.name, 'latest')
.then(showUpdateInfo)
.catch(noop);
} }
async function showUpdateInfo(version) { function showUpdateInfo(data) {
const version = data.version;
if (version === Info.version) if (version === Info.version)
return; return;
const {default: chalk} = await import('chalk'); const chalk = require('chalk');
const latestVersion = chalk.green.bold(`v${version}`); const latestVersion = chalk.green.bold('v' + version);
const latest = `update available: ${latestVersion}`; const latest = `update available: ${latestVersion}`;
const current = chalk.dim(`(current: v${Info.version})`); const current = chalk.dim(`(current: v${Info.version})`);
console.log('%s %s', latest, current); console.log('%s %s', latest, current);
} }

View file

@ -1,26 +0,0 @@
import {spawnSync} from 'node:child_process';
import {test} from 'supertape';
import info from '../package.json' with {
type: 'json',
};
const cliPath = new URL('cloudcmd.js', import.meta.url).pathname;
test('cloudcmd: bin: cli: -h', (t) => {
const {stdout} = spawnSync(cliPath, ['-h'], {
encoding: 'utf8',
});
t.match(stdout, `Options`);
t.end();
});
test('cloudcmd: bin: cli: -v', (t) => {
const {version} = info;
const {stdout} = spawnSync(cliPath, ['-v'], {
encoding: 'utf8',
});
t.match(stdout, `v${version}`);
t.end();
});

View file

@ -1,72 +1,67 @@
#!/usr/bin/env node #!/usr/bin/env node
import {promisify} from 'node:util'; 'use strict';
import process from 'node:process';
import {tryToCatch} from 'try-to-catch';
import minor from 'minor';
import _place from 'place';
import {rendy} from 'rendy';
import shortdate from 'shortdate';
import Info from '../package.json' with {
type: 'json',
};
const place = promisify(_place); const DIR = '../';
const Info = require(DIR + 'package');
await main(); const minor = require('minor');
const place = require('place');
const rendy = require('rendy');
const shortdate = require('shortdate');
async function main() { const ERROR = Error('ERROR: version is missing. release --patch|--minor|--major');
const history = '## Version history\n\n';
const link = '//github.com/coderaiser/cloudcmd/releases/tag/';
const template = '- ' +
'*{{ date }}*, ' +
'**[v{{ version }}]' +
'(' +
link +
'v{{ version }})**\n';
const {version} = Info;
const [error, versionNew] = await tryToCatch(cl);
main((error) => {
if (error) if (error)
return console.error(error); console.error(error.message);
await replaceVersion('README.md', version, versionNew);
await replaceVersion('HELP.md', version, versionNew);
const historyNew = history +
rendy(template, {
date: shortdate(),
version: versionNew,
}); });
await replaceVersion('HELP.md', history, historyNew); function main(callback) {
} const history = 'Version history\n---------------\n';
const link = '//github.com/coderaiser/cloudcmd/releases/tag/';
const template = '- *{{ date }}*, ' +
'**[v{{ version }}]' +
'(' + link + 'v{{ version }})**\n';
async function replaceVersion(name, version, versionNew) { const version = Info.version;
const [error] = await tryToCatch(place, name, version, versionNew);
cl((error, versionNew) => {
if (error) if (error)
return console.error(error); return callback(error);
console.log(`done: ${name}`); replaceVersion('README.md', version, versionNew, callback);
replaceVersion('HELP.md', version, versionNew, function() {
const historyNew = history + rendy(template, {
date : shortdate(),
version : versionNew
});
replaceVersion('HELP.md', history, historyNew, callback);
});
});
} }
async function cl() { function replaceVersion(name, version, versionNew, callback) {
const {argv} = process; place(name, version, versionNew, (error) => {
if (error)
return callback(error);
callback(null, 'done: ' + name);
});
}
function cl(callback) {
const argv = process.argv;
const length = argv.length - 1; const length = argv.length - 1;
const last = process.argv[length]; const last = process.argv[length];
const regExp = /^--(major|minor|patch)$/; const regExp = /^--(major|minor|patch)$/;
const [, match] = last.match(regExp) || []; const [, match] = last.match(regExp) || [];
console.log(last);
if (!regExp.test(last)) if (!regExp.test(last))
throw Error('ERROR: version is missing. release --patch|--minor|--major'); return callback(ERROR);
return getVersionNew(last, match); callback(null, getVersionNew(last, match));
} }
function getVersionNew(last, match) { function getVersionNew(last, match) {
@ -75,3 +70,4 @@ function getVersionNew(last, match) {
return last.substr(3); return last.substr(3);
} }

View file

@ -1,59 +1,76 @@
import process from 'node:process'; 'use strict';
import Emitify from 'emitify';
import inherits from 'inherits'; /* global DOM */
import {rendy} from 'rendy';
import load from 'load.js'; const Emitify = require('emitify/legacy');
import {tryToCatch} from 'try-to-catch'; const inherits = require('inherits');
import {addSlashToEnd} from 'format-io'; const rendy = require('rendy/legacy');
import pascalCase from 'just-pascal-case'; const exec = require('execon');
import currify from 'currify'; const load = require('load.js');
import {
const {kebabToCamelCase} = require('../common/util');
const isDev = process.env.NODE_ENV === 'development';
const Images = require('./dom/images');
const {
unregisterSW,
} = require('./sw/register');
const jonny = require('jonny/legacy');
const currify = require('currify/legacy');
const bind = (f, ...a) => () => f(...a);
const noop = () => {};
const {
apiURL, apiURL,
formatMsg, formatMsg,
buildFromJSON, buildFromJSON,
} from '#common/cloudfunc'; } = require('../common/cloudfunc');
import * as Images from '#dom/images';
import {unregisterSW} from './sw/register.js';
import {getJsonFromFileTable} from './get-json-from-file-table.js';
import {Key} from './key/index.js';
import {loadModule} from './load-module.js';
const noJS = (a) => a.replace(/.js$/, ''); const loadModule = require('./load-module');
const isDev = process.env.NODE_ENV === 'development';
inherits(CloudCmdProto, Emitify); inherits(CloudCmdProto, Emitify);
export const createCloudCmd = ({DOM, Listeners}) => { module.exports = new CloudCmdProto(DOM);
return new CloudCmdProto({
DOM,
Listeners,
});
};
load.addErrorListener((e, src) => { load.addErrorListener((e, src) => {
const msg = `file ${src} could not be loaded`; const msg = `file ${src} could not be loaded`;
Images.show.error(msg); Images.show.error(msg);
}); });
function CloudCmdProto({DOM, Listeners}) { function CloudCmdProto(DOM) {
let Key;
let Listeners;
const log = (...a) => {
if (!isDev )
return;
console.log(...a);
};
Emitify.call(this); Emitify.call(this);
const CloudCmd = this; const CloudCmd = this;
const Info = DOM.CurrentInfo; const Info = DOM.CurrentInfo;
const Storage = DOM.Storage;
const Files = DOM.Files;
const {Storage, Files} = DOM; this.log = log;
this.PREFIX = '';
this.log = () => {
if (!isDev)
return;
};
this.prefix = '';
this.prefixSocket = ''; this.prefixSocket = '';
this.prefixURL = ''; this.PREFIX_URL = '';
this.DIRCLIENT = '/dist/';
this.DIRCLIENT_MODULES = this.DIRCLIENT + 'modules/';
this.MIN_ONE_PANEL_WIDTH = 1155;
this.HOST = location.origin ||
location.protocol + '//' + location.host;
const TITLE = 'Cloud Commander';
this.TITLE = TITLE;
this.MIN_ONE_PANEL_WIDTH = DOM.getCSSVar('min-one-panel-width');
this.HOST = location.origin || location.protocol + '//' + location.host;
this.sort = { this.sort = {
left: 'name', left: 'name',
right: 'name', right: 'name',
@ -64,38 +81,46 @@ function CloudCmdProto({DOM, Listeners}) {
right: 'asc', right: 'asc',
}; };
this.changeDir = async (path, overrides = {}) => { /**
const { * Функция привязываеться ко всем ссылкам и
isRefresh, * загружает содержимое каталогов
panel, *
history = true, * @param params - {
noCurrent, * paramLink - ссылка
currentName, * needRefresh - необходимость обязательной загрузки данных с сервера
} = overrides; * panel
* }
* @param callback
*/
this.loadDir = (params, callback) => {
const p = params;
const refresh = p.isRefresh;
const panel = p.panel;
const history = p.history;
const noCurrent = p.noCurrent;
const currentName = p.currentName;
const refresh = isRefresh;
let panelChanged; let panelChanged;
if (!noCurrent)
if (!noCurrent && panel && panel !== Info.panel) { if (panel && panel !== Info.panel) {
DOM.changePanel(); DOM.changePanel();
panelChanged = true; panelChanged = true;
} }
let imgPosition; let imgPosition;
if (panelChanged || refresh || !history) if (panelChanged || refresh || !history)
imgPosition = 'top'; imgPosition = 'top';
Images.show.load(imgPosition, panel); Images.show.load(imgPosition, panel);
/* загружаем содержимое каталога */ /* загружаем содержимое каталога */
await ajaxLoad(addSlashToEnd(path), { ajaxLoad(p.path, {
refresh, refresh,
history, history,
noCurrent, noCurrent,
currentName, currentName,
showDotFiles: CloudCmd.config('showDotFiles'), }, panel, callback);
}, panel);
}; };
/** /**
@ -103,12 +128,23 @@ function CloudCmdProto({DOM, Listeners}) {
* выполняет весь функционал по * выполняет весь функционал по
* инициализации * инициализации
*/ */
this.init = async (prefix, config) => { this.init = (prefix, config) => {
CloudCmd.prefix = prefix; const func = bind(exec.series, [
CloudCmd.prefixURL = `${prefix}${apiURL}`; initModules,
baseInit,
loadPlugins,
loadStyle,
exec.with(CloudCmd.route, location.hash),
], noop);
const funcBefore = (callback) => {
const src = prefix + CloudCmd.DIRCLIENT_MODULES + 'polyfill.js';
load.js(src, callback);
};
CloudCmd.PREFIX = prefix;
CloudCmd.PREFIX_URL = prefix + apiURL;
CloudCmd.prefixSocket = config.prefixSocket; CloudCmd.prefixSocket = config.prefixSocket;
CloudCmd.DIR_DIST = `${prefix}/dist`;
CloudCmd.DIR_MODULES = `${this.DIR_DIST}/modules`;
CloudCmd.config = (key) => config[key]; CloudCmd.config = (key) => config[key];
CloudCmd.config.if = currify((key, fn, a) => config[key] && fn(a)); CloudCmd.config.if = currify((key, fn, a) => config[key] && fn(a));
@ -117,6 +153,7 @@ function CloudCmdProto({DOM, Listeners}) {
* should be called from config.js only * should be called from config.js only
* after successful update on server * after successful update on server
*/ */
if (key === 'password') if (key === 'password')
return; return;
@ -126,15 +163,23 @@ function CloudCmdProto({DOM, Listeners}) {
if (config.oneFilePanel) if (config.oneFilePanel)
CloudCmd.MIN_ONE_PANEL_WIDTH = Infinity; CloudCmd.MIN_ONE_PANEL_WIDTH = Infinity;
if (!document.body.scrollIntoViewIfNeeded) exec.if(document.body.scrollIntoViewIfNeeded, func, funcBefore);
await load.js(`${CloudCmd.DIR_MODULES}/polyfill.js`);
await initModules();
await baseInit();
CloudCmd.route(location.hash);
}; };
function loadStyle(callback) {
const prefix = CloudCmd.PREFIX;
const name = prefix + '/dist/cloudcmd.common.css';
load.css(name, callback);
}
function loadPlugins(callback) {
const prefix = CloudCmd.PREFIX;
const plugins = prefix + '/plugins.js';
load.js(plugins, callback);
}
this.route = (path) => { this.route = (path) => {
const query = path.split('/'); const query = path.split('/');
@ -142,15 +187,14 @@ function CloudCmdProto({DOM, Listeners}) {
return; return;
const [kebabModule] = query; const [kebabModule] = query;
const module = noJS(pascalCase(kebabModule.slice(1))); const module = kebabToCamelCase(kebabModule.slice(1));
const [, file] = query; const file = query[1];
const current = DOM.getCurrentByName(file); const current = DOM.getCurrentByName(file);
if (file && !current) { if (file && !current) {
const msg = formatMsg('set current file', file, 'error'); const msg = formatMsg('set current file', file, 'error');
CloudCmd.log(msg); CloudCmd.log(msg);
return; return;
} }
@ -158,29 +202,39 @@ function CloudCmdProto({DOM, Listeners}) {
CloudCmd.execFromModule(module, 'show'); CloudCmd.execFromModule(module, 'show');
}; };
this.logOut = async () => { this.logOut = () => {
const url = CloudCmd.prefix + '/logout'; const url = CloudCmd.PREFIX + '/logout';
const error = () => document.location.reload(); const error = () => document.location.reload();
const {prefix} = CloudCmd;
await DOM.Storage.clear(); DOM.Storage.clear();
unregisterSW(prefix); unregisterSW();
DOM.load.ajax({ DOM.load.ajax({
url, url,
error, error,
}); });
}; };
const initModules = async () => { function initModules(callback) {
exec.if(CloudCmd.Key, () => {
Key = new CloudCmd.Key();
CloudCmd.Key = Key; CloudCmd.Key = Key;
CloudCmd.Key.bind(); Key.bind();
}, (func) => {
/* привязываем клавиши к функциям */
const path = 'key.js';
const [, modules] = await tryToCatch(Files.get, 'modules'); loadModule({
path,
func
});
});
Files.get('modules', (error, modules) => {
const showLoad = Images.show.load; const showLoad = Images.show.load;
const doBefore = { const doBefore = {
edit: showLoad, 'edit': showLoad,
menu: showLoad, 'menu': showLoad,
}; };
const load = (name, path, dobefore) => { const load = (name, path, dobefore) => {
@ -192,37 +246,32 @@ function CloudCmdProto({DOM, Listeners}) {
}; };
if (!modules) if (!modules)
return; modules = [];
for (const module of modules.local) { modules.local.forEach((module) => {
load(null, module, doBefore[module]); load(null, module, doBefore[module]);
} });
};
async function saveCurrentName(currentName) { callback();
await Storage.set('current-name', currentName); });
} }
async function baseInit() { function baseInit(callback) {
const files = DOM.getFiles(); const files = DOM.getFiles();
CloudCmd.on('current-file', DOM.updateCurrentInfo); CloudCmd.on('current-file', DOM.updateCurrentInfo);
CloudCmd.on('current-name', saveCurrentName);
const name = await Storage.get('current-name');
const currentFile = name && DOM.getCurrentByName(name) || files[0];
/* выделяем строку с первым файлом */ /* выделяем строку с первым файлом */
if (files) if (files)
DOM.setCurrentFile(currentFile, { DOM.setCurrentFile(files[0], {
// when hash is present // when hash is present
// it should be handled with this.route // it should be handled with this.route
// overwre otherwise // overwre otherwise
history: !location.hash, history: !location.hash
}); });
const dirPath = DOM.getCurrentDirPath(); const dirPath = DOM.getCurrentDirPath();
Listeners = CloudCmd.Listeners;
Listeners.init(); Listeners.init();
const panels = getPanels(); const panels = getPanels();
@ -231,12 +280,15 @@ function CloudCmdProto({DOM, Listeners}) {
Listeners.initKeysPanel(); Listeners.initKeysPanel();
if (!CloudCmd.config('dirStorage')) if (!CloudCmd.config('dirStorage'))
return; return callback();
const data = await Storage.get(dirPath); Storage.get(dirPath, (error, data) => {
if (!data) {
if (!data) data = getJSONfromFileTable();
await Storage.setJson(dirPath, getJsonFromFileTable()); Storage.set(dirPath, data);
}
callback();
});
} }
function getPanels() { function getPanels() {
@ -258,22 +310,28 @@ function CloudCmdProto({DOM, Listeners}) {
func(...args); func(...args);
}; };
this.refresh = async (options = {}) => { this.refresh = (options = {}, callback) => {
const {panel = Info.panel, currentName} = options; if (!callback && typeof options === 'function') {
callback = options;
options = {};
}
const panel = options.panel || Info.panel;
const path = DOM.getCurrentDirPath(panel); const path = DOM.getCurrentDirPath(panel);
const isRefresh = true; const isRefresh = true;
const history = false; const history = false;
const noCurrent = options?.noCurrent; const noCurrent = options ? options.noCurrent : false;
const currentName = options.currentName;
await CloudCmd.changeDir(path, { CloudCmd.loadDir({
path,
isRefresh, isRefresh,
history, history,
panel, panel,
noCurrent, noCurrent,
currentName, currentName,
}); }, callback);
}; };
/** /**
@ -283,21 +341,19 @@ function CloudCmdProto({DOM, Listeners}) {
* @param options * @param options
* { refresh, history } - необходимость обновить данные о каталоге * { refresh, history } - необходимость обновить данные о каталоге
* @param panel * @param panel
* @param callback
* *
*/ */
async function ajaxLoad(path, options = {}, panel) { function ajaxLoad(path, options, panel, callback) {
const {RESTful} = DOM; const create = (error, json) => {
const RESTful = DOM.RESTful;
CloudCmd.log(`reading dir: "${path}";`);
const dirStorage = CloudCmd.config('dirStorage');
const json = dirStorage && await Storage.getJson(path);
const name = options.currentName || Info.name; const name = options.currentName || Info.name;
const {noCurrent, refresh} = options; const obj = jonny.parse(json);
const isRefresh = options.refresh;
const noCurrent = options.noCurrent;
if (!refresh && json) if (!isRefresh && json)
return await createFileTable(json, panel, options); return createFileTable(obj, panel, options, callback);
const position = DOM.getPanelPosition(panel); const position = DOM.getPanelPosition(panel);
const sort = CloudCmd.sort[position]; const sort = CloudCmd.sort[position];
@ -308,62 +364,66 @@ function CloudCmdProto({DOM, Listeners}) {
order, order,
}); });
const [, newObj] = await RESTful.read(path + query, 'json'); RESTful.read(path + query, 'json', (error, obj) => {
if (error)
if (!newObj)
// that's OK, error handled by RESTful
return; return;
options.sort = sort; options.sort = sort;
options.order = order; options.order = order;
await createFileTable(newObj, panel, options); createFileTable(obj, panel, options, () => {
if (isRefresh && !noCurrent)
if (refresh && !noCurrent)
DOM.setCurrentByName(name); DOM.setCurrentByName(name);
exec(callback);
});
if (!CloudCmd.config('dirStorage')) if (!CloudCmd.config('dirStorage'))
return; return;
Storage.setJson(path, newObj); Storage.set(path, obj);
});
};
if (!options)
options = {};
CloudCmd.log('reading dir: "' + path + '";');
if (!CloudCmd.config('dirStorage'))
return create();
Storage.get(path, create);
} }
/** /**
* Функция строит файловую таблицу * Функция строит файловую таблицу
* @param data - данные о файлах * @param json - данные о файлах
* @param panelParam * @param panelParam
* @param options - history, noCurrent, showDotFiles * @param history
* @param callback
*/ */
async function createFileTable(data, panelParam, options) { function createFileTable(json, panelParam, options, callback) {
const { const {
history, history,
noCurrent, noCurrent,
showDotFiles,
} = options; } = options;
const names = [ const names = ['file', 'path', 'link', 'pathLink'];
'file',
'path',
'link',
'pathLink',
];
const [error, [ Files.get(names, (error, templFile, templPath, templLink, templPathLink) => {
file, const Dialog = DOM.Dialog;
path, const panel = panelParam || DOM.getPanel();
link,
pathLink, const {
]] = await tryToCatch(Files.get, names); dir,
name,
} = Info;
if (error) if (error)
return DOM.Dialog.alert(error.responseText); return Dialog.alert(TITLE, error.responseText);
const panel = panelParam || DOM.getPanel(); const childNodes = panel.childNodes;
const {prefix} = CloudCmd;
const {dir, name} = Info;
const {childNodes} = panel;
let i = childNodes.length; let i = childNodes.length;
while (i--) while (i--)
@ -372,16 +432,15 @@ function CloudCmdProto({DOM, Listeners}) {
panel.innerHTML = buildFromJSON({ panel.innerHTML = buildFromJSON({
sort : options.sort, sort : options.sort,
order : options.order, order : options.order,
data, data : json,
id : panel.id, id : panel.id,
prefix, prefix : CloudCmd.PREFIX,
showDotFiles,
template : { template : {
file, file : templFile,
path, path : templPath,
pathLink, pathLink : templPathLink,
link, link : templLink
}, }
}); });
Listeners.setOnPanel(panel); Listeners.setOnPanel(panel);
@ -393,7 +452,7 @@ function CloudCmdProto({DOM, Listeners}) {
current = DOM.getCurrentByName(dir); current = DOM.getCurrentByName(dir);
if (!current) if (!current)
[current] = DOM.getFiles(panel); current = DOM.getFiles(panel)[0];
DOM.setCurrentFile(current, { DOM.setCurrentFile(current, {
history, history,
@ -401,14 +460,57 @@ function CloudCmdProto({DOM, Listeners}) {
CloudCmd.emit('active-dir', Info.dirPath); CloudCmd.emit('active-dir', Info.dirPath);
} }
exec(callback);
});
} }
this.goToParentDir = async () => { /**
* Функция генерирует JSON из html-таблицы файлов и
* используеться при первом заходе в корень
*/
function getJSONfromFileTable() {
const path = DOM.getCurrentDirPath();
const infoFiles = Info.files || [];
const notParent = (current) => {
const name = DOM.getCurrentName(current);
return name !== '..';
};
const parse = (current) => {
const name = DOM.getCurrentName(current);
const size = DOM.getCurrentSize(current);
const owner = DOM.getCurrentOwner(current);
const mode = DOM.getCurrentMode(current);
const date = DOM.getCurrentDate(current);
return {
name,
size,
mode,
owner,
date,
};
};
const files = infoFiles
.filter(notParent)
.map(parse);
const fileTable = {
path,
files,
};
return fileTable;
}
this.goToParentDir = () => {
const dir = Info.dir;
const { const {
dir,
dirPath, dirPath,
parentDirPath, parentDirPath,
panel,
} = Info; } = Info;
if (dirPath === parentDirPath) if (dirPath === parentDirPath)
@ -416,13 +518,15 @@ function CloudCmdProto({DOM, Listeners}) {
const path = parentDirPath; const path = parentDirPath;
await CloudCmd.changeDir(path); CloudCmd.loadDir({path}, () => {
const {panel} = Info;
const current = DOM.getCurrentByName(dir); const current = DOM.getCurrentByName(dir);
const [first] = DOM.getFiles(panel); const first = DOM.getFiles(panel)[0];
DOM.setCurrentFile(current || first, { DOM.setCurrentFile(current || first, {
history, history
});
}); });
}; };
} }

View file

@ -1,44 +1,41 @@
import '../css/main.css'; 'use strict';
import process from 'node:process';
import wraptile from 'wraptile';
import load from 'load.js';
import * as Util from '#common/util';
import * as CloudFunc from '#common/cloudfunc';
import DOM from '#dom';
import {registerSW, listenSW} from './sw/register.js';
import {initSortPanel, sortPanel} from './sort.js';
import {createCloudCmd} from './client.js';
import * as Listeners from './listeners/index.js';
const isDev = process.env.NODE_ENV === 'development'; require('../css/main.css');
require('../css/nojs.css');
require('../css/columns/name-size-date.css');
require('../css/columns/name-size.css');
export default init; const {promisify} = require('es6-promisify');
const wraptile = require('wraptile/legacy');
const load = require('load.js');
globalThis.CloudCmd = init; const {
registerSW,
} = require('./sw/register');
async function init(config) { // prevent additional loading of emitify
globalThis.CloudCmd = createCloudCmd({ window.Emitify = require('emitify/legacy');
DOM,
Listeners,
});
globalThis.DOM = DOM;
globalThis.Util = Util;
globalThis.CloudFunc = CloudFunc;
await register(config); module.exports = window.CloudCmd = (config) => {
window.Promise = window.Promise || require('es6-promise');
window.Util = require('../common/util');
window.CloudFunc = require('../common/cloudfunc');
const DOM = require('./dom');
window.DOM = DOM;
window.CloudCmd = require('./client');
register(config);
require('./listeners');
require('./key');
require('./sort');
initSortPanel();
globalThis.CloudCmd.sortPanel = sortPanel;
const prefix = getPrefix(config.prefix); const prefix = getPrefix(config.prefix);
globalThis.CloudCmd.init(prefix, config); window.CloudCmd.init(prefix, config);
};
if (globalThis.CloudCmd.config('menu') === 'aleman')
setTimeout(() => {
import('https://esm.sh/@putout/processor-html');
import('https://esm.sh/@putout/bundle@5.5');
}, 100);
}
function getPrefix(prefix) { function getPrefix(prefix) {
if (!prefix) if (!prefix)
@ -51,24 +48,22 @@ function getPrefix(prefix) {
} }
const onUpdateFound = wraptile(async (config) => { const onUpdateFound = wraptile(async (config) => {
if (isDev) const {DOM} = window;
return;
const {DOM} = globalThis;
const prefix = getPrefix(config.prefix); const prefix = getPrefix(config.prefix);
const js = promisify(load.js);
await load.js(`${prefix}/dist/cloudcmd.common.js`); await js(`${prefix}/dist/cloudcmd.common.js`);
await load.js(`${prefix}/dist/cloudcmd.js`); await js(`${prefix}/dist/cloudcmd.js`);
console.log('cloudcmd: sw: updated'); console.log('cloudcmd: sw: updated');
DOM.Events.removeAll(); DOM.Events.removeAll();
globalThis.CloudCmd(config); window.CloudCmd(config);
}); });
async function register(config) { async function register(config) {
const {prefix} = config; const {prefix} = config;
const sw = await registerSW(prefix); const sw = await registerSW(prefix);
listenSW(sw, 'updatefound', onUpdateFound(config)); sw.addEventListener('updatefound', onUpdateFound(config));
} }

View file

@ -1,124 +1,133 @@
/* global CloudCmd*/ 'use strict';
import * as Storage from '#dom/storage';
import tryToPromiseAll from '../../common/try-to-promise-all.js';
/* global CloudCmd */
const jonny = require('jonny/legacy');
const exec = require('execon');
const Storage = require('./storage');
const DOM = require('./');
module.exports = new BufferProto();
function BufferProto() {
const Info = DOM.CurrentInfo;
const CLASS = 'cut-file'; const CLASS = 'cut-file';
const COPY = 'copy'; const COPY = 'copy';
const CUT = 'cut'; const CUT = 'cut';
const TITLE = 'Buffer';
const Buffer = {
cut : callIfEnabled.bind(null, cut),
copy : callIfEnabled.bind(null, copy),
clear : callIfEnabled.bind(null, clear),
paste : callIfEnabled.bind(null, paste)
};
function showMessage(msg) { function showMessage(msg) {
globalThis.DOM.Dialog.alert(msg); DOM.Dialog.alert(TITLE, msg);
} }
function getNames() { function getNames() {
const {DOM} = globalThis;
const files = DOM.getActiveFiles(); const files = DOM.getActiveFiles();
const names = DOM.getFilenames(files);
return DOM.getFilenames(files); return names;
} }
function addCutClass() { function addCutClass() {
const {DOM} = globalThis;
const files = DOM.getActiveFiles(); const files = DOM.getActiveFiles();
for (const element of files) { files.forEach((element) => {
element.classList.add(CLASS); element.classList.add(CLASS);
} });
} }
function rmCutClass() { function rmCutClass() {
const {DOM} = globalThis;
const files = DOM.getByClassAll(CLASS); const files = DOM.getByClassAll(CLASS);
for (const element of files) { [...files].forEach((element) => {
element.classList.remove(CLASS); element.classList.remove(CLASS);
} });
} }
const checkEnabled = (fn) => () => { function callIfEnabled(callback) {
const is = CloudCmd.config('buffer'); const is = CloudCmd.config('buffer');
if (is) if (is)
return fn(); return callback();
showMessage('Buffer disabled in config!'); showMessage('Buffer disabled in config!');
};
async function readBuffer() {
const [e, cp, ct] = await tryToPromiseAll([
Storage.getJson(COPY),
Storage.getJson(CUT),
]);
return [
e,
cp,
ct,
];
} }
export const copy = checkEnabled(async () => { function copy() {
const Info = globalThis.DOM.CurrentInfo;
const names = getNames(); const names = getNames();
const from = Info.dirPath; const from = Info.dirPath;
await clear(); clear();
if (!names.length) if (!names.length)
return; return;
await Storage.remove(CUT); Storage.remove(CUT)
await Storage.setJson(COPY, { .set(COPY, {
from, from,
names, names,
}); });
}); }
export const cut = checkEnabled(async () => { function cut() {
const Info = globalThis.DOM.CurrentInfo;
const names = getNames(); const names = getNames();
const from = Info.dirPath; const from = Info.dirPath;
await clear(); clear();
if (!names.length) if (!names.length)
return; return;
addCutClass(); addCutClass();
await Storage.setJson(CUT, { Storage
.set(CUT, {
from, from,
names, names,
}); });
}); }
export const clear = checkEnabled(async () => { function clear() {
await Storage.remove(COPY); Storage.remove(COPY)
await Storage.remove(CUT); .remove(CUT);
rmCutClass(); rmCutClass();
}); }
export const paste = checkEnabled(async () => { function paste() {
const Info = globalThis.DOM.CurrentInfo; const copy = Storage.get.bind(Storage, COPY);
const [error, cp, ct] = await readBuffer(); const cut = Storage.get.bind(Storage, CUT);
if (error || !cp && !ct)
return showMessage(error || 'Buffer is empty!');
exec.parallel([copy, cut], (error, cp, ct) => {
const opStr = cp ? 'copy' : 'move'; const opStr = cp ? 'copy' : 'move';
const data = cp || ct; const opData = cp || ct;
const {Operation} = CloudCmd; const Operation = CloudCmd.Operation;
const msg = 'Path is same!'; const msg = 'Path is same!';
const to = Info.dirPath; const path = Info.dirPath;
if (data.from === to) if (!error && !cp && !ct)
error = 'Buffer is empty!';
if (error)
return showMessage(error);
const data = jonny.parse(opData);
data.to = path;
if (data.from === path)
return showMessage(msg); return showMessage(msg);
Operation.show(opStr, { Operation.show(opStr, data);
...data, clear();
to,
}); });
}
await clear(); return Buffer;
}); }

View file

@ -1,83 +0,0 @@
/* global DOM */
const SELECTED_FILE = 'selected-file';
const Cmd = {
getSelectedFiles,
isSelected,
unselectFile,
selectFile,
selectAllFiles,
toggleSelectedFile,
toggleAllSelectedFiles,
};
/**
* selected file check
*
* @param currentFile
*/
export function isSelected(currentFile) {
if (!currentFile)
return false;
return DOM.isContainClass(currentFile, SELECTED_FILE);
}
/**
* select current file
* @param currentFile
*/
export function selectFile(currentFile) {
const current = currentFile || DOM.getCurrentFile();
current.classList.add(SELECTED_FILE);
return Cmd;
}
export function unselectFile(currentFile) {
const current = currentFile || DOM.getCurrentFile();
current.classList.remove(SELECTED_FILE);
return Cmd;
}
export function toggleSelectedFile(currentFile) {
const current = currentFile || DOM.getCurrentFile();
const name = DOM.getCurrentName(current);
if (name === '..')
return Cmd;
current.classList.toggle(SELECTED_FILE);
return Cmd;
}
export function toggleAllSelectedFiles() {
DOM
.getAllFiles()
.map(DOM.toggleSelectedFile);
return Cmd;
}
export function selectAllFiles() {
DOM
.getAllFiles()
.map(DOM.selectFile);
return Cmd;
}
/**
* unified way to get selected files
*
* @currentFile
*/
export function getSelectedFiles() {
const panel = DOM.getPanel();
const selected = DOM.getByClassAll(SELECTED_FILE, panel);
return Array.from(selected);
}

View file

@ -1,16 +1,28 @@
'use strict';
/* global DOM */ /* global DOM */
/* global CloudCmd */ /* global CloudCmd */
import createElement from '@cloudcmd/create-element';
import {getTitle, FS} from '#common/cloudfunc'; const btoa = require('../../common/btoa');
import {encode, decode} from '#common/entity'; const createElement = require('@cloudcmd/create-element');
const {
encode,
decode,
} = require('../../common/entity');
const {
getTitle,
FS,
} = require('../../common/cloudfunc');
let Title; let Title;
const CURRENT_FILE = 'current-file'; const CURRENT_FILE = 'current-file';
const encodeNBSP = (a) => a?.replace('\xa0', '&nbsp;'); const NBSP_REG = RegExp(String.fromCharCode(160), 'g');
const decodeNBSP = (a) => a?.replace('&nbsp;', '\xa0'); const SPACE = ' ';
export const _CURRENT_FILE = CURRENT_FILE; module.exports._CURRENT_FILE = CURRENT_FILE;
/** /**
* set name from current (or param) file * set name from current (or param) file
@ -18,18 +30,18 @@ export const _CURRENT_FILE = CURRENT_FILE;
* @param name * @param name
* @param current * @param current
*/ */
export const setCurrentName = (name, current) => { module.exports.setCurrentName = (name, current) => {
const Info = DOM.CurrentInfo; const Info = DOM.CurrentInfo;
const {link} = Info; const {link} = Info;
const {prefix} = CloudCmd; const {PREFIX} = CloudCmd;
const dir = prefix + FS + Info.dirPath; const dir = PREFIX + FS + Info.dirPath;
const encoded = encode(name); const encoded = encode(name);
link.title = encoded; link.title = encoded;
link.href = dir + encoded; link.href = dir + encoded;
link.innerHTML = encoded; link.innerHTML = encoded;
current.setAttribute('data-name', createNameAttribute(name)); current.setAttribute('data-name', 'js-file-' + btoa(encodeURI(name)));
CloudCmd.emit('current-file', current); CloudCmd.emit('current-file', current);
return link; return link;
@ -40,48 +52,28 @@ export const setCurrentName = (name, current) => {
* *
* @param currentFile * @param currentFile
*/ */
export const getCurrentName = (currentFile) => { module.exports.getCurrentName = (currentFile) => {
const current = currentFile || DOM.getCurrentFile(); const current = currentFile || DOM.getCurrentFile();
if (!current) if (!current)
return ''; return '';
return parseNameAttribute(current.getAttribute('data-name')); const link = DOM.getCurrentLink(current);
if (!link)
return '';
return decode(link.title)
.replace(NBSP_REG, SPACE);
}; };
/** /**
* Generate a `data-name` attribute for the given filename * get current direcotory path
* @param name The string name to encode
*/ */
const createNameAttribute = (name) => { module.exports.getCurrentDirPath = (panel = DOM.getPanel()) => {
const encoded = btoa(encodeURI(name));
return `js-file-${encoded}`;
};
/**
* Parse a `data-name` attribute string back into the original filename
* @param attribute The string we wish to decode
*/
const parseNameAttribute = (attribute) => {
attribute = attribute.replace('js-file-', '');
return decodeNBSP(decodeURI(atob(attribute)));
};
export const _parseNameAttribute = parseNameAttribute;
const parseHrefAttribute = (prefix, attribute) => {
attribute = attribute.replace(RegExp('^' + prefix + FS), '');
return decode(decodeNBSP(attribute));
};
export const _parseHrefAttribute = parseHrefAttribute;
/**
* get current directory path
*/
export const getCurrentDirPath = (panel = DOM.getPanel()) => {
const path = DOM.getByDataName('js-path', panel); const path = DOM.getByDataName('js-path', panel);
return path.textContent; return path.textContent
.replace(NBSP_REG, SPACE);
}; };
/** /**
@ -89,31 +81,36 @@ export const getCurrentDirPath = (panel = DOM.getPanel()) => {
* *
* @param currentFile - current file by default * @param currentFile - current file by default
*/ */
export const getCurrentPath = (currentFile) => { module.exports.getCurrentPath = (currentFile) => {
const current = currentFile || DOM.getCurrentFile(); const current = currentFile || DOM.getCurrentFile();
const [element] = DOM.getByTag('a', current); const element = DOM.getByTag('a', current)[0];
const {prefix} = CloudCmd; const prefix = CloudCmd.PREFIX;
return parseHrefAttribute(prefix, element.getAttribute('href')); const path = element
.getAttribute('href')
.replace(RegExp('^' + prefix + FS), '')
.replace(NBSP_REG, SPACE);
return decode(path);
}; };
/** /**
* get current directory name * get current direcotory name
*/ */
export const getCurrentDirName = () => { module.exports.getCurrentDirName = () => {
const href = DOM const href = DOM.getCurrentDirPath()
.getCurrentDirPath()
.replace(/\/$/, ''); .replace(/\/$/, '');
const substr = href.substr(href, href.lastIndexOf('/')); const substr = href.substr(href, href.lastIndexOf('/'));
const ret = href.replace(substr + '/', '') || '/';
return href.replace(`${substr}/`, '') || '/'; return ret;
}; };
/** /**
* get current directory path * get current direcotory path
*/ */
export const getParentDirPath = (panel) => { module.exports.getParentDirPath = (panel) => {
const path = DOM.getCurrentDirPath(panel); const path = DOM.getCurrentDirPath(panel);
const dirName = DOM.getCurrentDirName() + '/'; const dirName = DOM.getCurrentDirName() + '/';
const index = path.lastIndexOf(dirName); const index = path.lastIndexOf(dirName);
@ -125,11 +122,11 @@ export const getParentDirPath = (panel) => {
}; };
/** /**
* get not current directory path * get not current direcotory path
*/ */
export const getNotCurrentDirPath = () => { module.exports.getNotCurrentDirPath = () => {
const panel = DOM.getPanel({ const panel = DOM.getPanel({
active: false, active: false
}); });
return DOM.getCurrentDirPath(panel); return DOM.getCurrentDirPath(panel);
@ -140,20 +137,20 @@ export const getNotCurrentDirPath = () => {
* *
* @currentFile * @currentFile
*/ */
export const getCurrentFile = () => { module.exports.getCurrentFile = () => {
return DOM.getByClass(CURRENT_FILE); return DOM.getByClass(CURRENT_FILE);
}; };
/** /**
* get current file by name * get current file by name
*/ */
export const getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => { module.exports.getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => {
const dataName = 'js-file-' + btoa(encodeURI(encodeNBSP(name))); const dataName = 'js-file-' + btoa(encodeURI(name));
return DOM.getByDataName(dataName, panel); return DOM.getByDataName(dataName, panel);
}; };
/** /**
* private function that unset currentFile * private function thet unset currentfile
* *
* @currentFile * @currentFile
*/ */
@ -169,8 +166,9 @@ function unsetCurrentFile(currentFile) {
/** /**
* unified way to set current file * unified way to set current file
*/ */
export const setCurrentFile = (currentFile, options) => { module.exports.setCurrentFile = (currentFile, options) => {
const o = options; const o = options;
const CENTER = true;
const currentFileWas = DOM.getCurrentFile(); const currentFileWas = DOM.getCurrentFile();
if (!currentFile) if (!currentFile)
@ -198,15 +196,13 @@ export const setCurrentFile = (currentFile, options) => {
* but it should be false * but it should be false
* to prevent default behavior * to prevent default behavior
*/ */
if (!o || o.history) { if (!o || o.history !== false) {
const historyPath = path === '/' ? path : FS + path; const historyPath = path === '/' ? path : FS + path;
DOM.setHistory(historyPath, null, historyPath); DOM.setHistory(historyPath, null, historyPath);
} }
} }
/* scrolling to current file */ /* scrolling to current file */
const CENTER = true;
DOM.scrollIntoViewIfNeeded(currentFile, CENTER); DOM.scrollIntoViewIfNeeded(currentFile, CENTER);
CloudCmd.emit('current-file', currentFile); CloudCmd.emit('current-file', currentFile);
@ -216,18 +212,13 @@ export const setCurrentFile = (currentFile, options) => {
return DOM; return DOM;
}; };
export const setCurrentByName = (name) => {
const current = DOM.getCurrentByName(name);
return DOM.setCurrentFile(current);
};
/* /*
* set current file by position * set current file by position
* *
* @param layer - element * @param layer - element
* @param - position {x, y} * @param - position {x, y}
*/ */
export const getCurrentByPosition = ({x, y}) => { module.exports.getCurrentByPosition = ({x, y}) => {
const element = document.elementFromPoint(x, y); const element = document.elementFromPoint(x, y);
const getEl = (el) => { const getEl = (el) => {
@ -259,7 +250,7 @@ export const getCurrentByPosition = ({x, y}) => {
* *
* @param currentFile * @param currentFile
*/ */
export const isCurrentFile = (currentFile) => { module.exports.isCurrentFile = (currentFile) => {
if (!currentFile) if (!currentFile)
return false; return false;
@ -271,11 +262,12 @@ export const isCurrentFile = (currentFile) => {
* *
* @param name * @param name
*/ */
export const setTitle = (name) => {
module.exports.setTitle = (name) => {
if (!Title) if (!Title)
Title = DOM.getByTag('title')[0] || createElement('title', { Title = DOM.getByTag('title')[0] || createElement('title', {
innerHTML: name, innerHTML: name,
parent: document.head, parent: document.head
}); });
Title.textContent = name; Title.textContent = name;
@ -283,29 +275,3 @@ export const setTitle = (name) => {
return DOM; return DOM;
}; };
/**
* check is current file is a directory
*
* @param currentFile
*/
export const isCurrentIsDir = (currentFile) => {
const current = currentFile || DOM.getCurrentFile();
const path = DOM.getCurrentPath(current);
const fileType = DOM.getCurrentType(current);
const isZip = path.endsWith('.zip');
const isDir = /^directory(-link)?/.test(fileType);
return isDir || isZip;
};
export const getCurrentType = (currentFile) => {
const current = currentFile || DOM.getCurrentFile();
const el = DOM.getByDataName('js-type', current);
const type = el.className
.split(' ')
.pop();
return type;
};

View file

@ -1,114 +1,129 @@
import {test, stub} from 'supertape'; 'use strict';
import {create} from 'auto-globals';
import wraptile from 'wraptile';
import * as currentFile from './current-file.js';
const test = require('tape');
const diff = require('sinon-called-with-diff');
const sinon = diff(require('sinon'));
const id = (a) => a; const id = (a) => a;
const wraptile = require('wraptile');
const returns = wraptile(id); const returns = wraptile(id);
const currentFile = require('./current-file');
const {_CURRENT_FILE} = currentFile; const {_CURRENT_FILE} = currentFile;
test('current-file: setCurrentName: setAttribute', (t) => { test('current-file: setCurrentName: setAttribute', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM(); global.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const current = create(); const setAttribute = sinon.stub();
const {setAttribute} = current; const current = {
setAttribute
};
currentFile.setCurrentName('hello', current); currentFile.setCurrentName('hello', current);
globalThis.DOM = DOM; t.ok(setAttribute.calledWith('data-name', 'js-file-aGVsbG8='), 'should call setAttribute');
globalThis.CloudCmd = CloudCmd;
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.calledWith(setAttribute, [
'data-name',
'js-file-aGVsbG8=',
], 'should call setAttribute');
t.end(); t.end();
}); });
test('current-file: setCurrentName: setAttribute: cyrillic', (t) => { test('current-file: setCurrentName: setAttribute: cyrillic', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM(); global.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const current = create(); const setAttribute = sinon.stub();
const {setAttribute} = current; const current = {
setAttribute
};
currentFile.setCurrentName('ай', current); currentFile.setCurrentName('ай', current);
globalThis.DOM = DOM; t.ok(setAttribute.calledWith('data-name', 'js-file-JUQwJUIwJUQwJUI5'), 'should call setAttribute');
globalThis.CloudCmd = CloudCmd;
t.calledWith(setAttribute, [ global.DOM = DOM;
'data-name', global.CloudCmd = CloudCmd;
'js-file-JUQwJUIwJUQwJUI5',
], 'should call setAttribute');
t.end();
});
test('current-file: getCurrentName', (t) => {
const current = create();
current.getAttribute.returns('js-file-Ymlu');
const result = currentFile.getCurrentName(current);
t.equal(result, 'bin');
t.end(); t.end();
}); });
test('current-file: emit', (t) => { test('current-file: emit', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
const emit = stub(); const emit = sinon.stub();
const setAttribute = sinon.stub();
globalThis.DOM = getDOM(); global.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd({ global.CloudCmd = getCloudCmd({
emit, emit,
}); });
const current = create(); const current = {
setAttribute,
};
currentFile.setCurrentName('hello', current); currentFile.setCurrentName('hello', current);
globalThis.DOM = DOM; t.ok(emit.calledWith('current-file', current), 'should call emit');
globalThis.CloudCmd = CloudCmd;
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.calledWith(emit, ['current-file', current], 'should call emit');
t.end(); t.end();
}); });
test('current-file: setCurrentName: return', (t) => { test('current-file: setCurrentName: return', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
const setAttribute = sinon.stub();
const link = {}; const link = {};
globalThis.DOM = getDOM({ global.DOM = getDOM({
link, link
}); });
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const current = {
setAttribute,
};
const current = create();
const result = currentFile.setCurrentName('hello', current); const result = currentFile.setCurrentName('hello', current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.equal(result, link, 'should return link'); t.equal(result, link, 'should return link');
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.end(); t.end();
}); });
test('current-file: getParentDirPath: result', (t) => { test('current-file: getParentDirPath: result', (t) => {
const {DOM} = globalThis; const {
DOM,
} = global;
const getCurrentDirPath = returns('/D/Films/+++favorite films/'); const getCurrentDirPath = returns('/D/Films/+++favorite films/');
const getCurrentDirName = returns('+++favorite films'); const getCurrentDirName = returns('+++favorite films');
globalThis.DOM = getDOM({ global.DOM = getDOM({
getCurrentDirPath, getCurrentDirPath,
getCurrentDirName, getCurrentDirName,
}); });
@ -116,193 +131,78 @@ test('current-file: getParentDirPath: result', (t) => {
const result = currentFile.getParentDirPath(); const result = currentFile.getParentDirPath();
const expected = '/D/Films/'; const expected = '/D/Films/';
globalThis.DOM = DOM; global.DOM = DOM;
t.equal(result, expected, 'should return parent dir path'); t.equal(result, expected, 'should return parent dir path');
t.end(); t.end();
}); });
test('current-file: isCurrentFile: no', (t) => { test('current-file: isCurrentFile: no', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM(); global.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const result = currentFile.isCurrentFile(); const result = currentFile.isCurrentFile();
const expect = false;
globalThis.DOM = DOM; global.DOM = DOM;
globalThis.CloudCmd = CloudCmd; global.CloudCmd = CloudCmd;
t.notOk(result); t.equal(result, expect, 'should equal');
t.end(); t.end();
}); });
test('current-file: isCurrentFile', (t) => { test('current-file: isCurrentFile', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
const isContainClass = stub(); const isContainClass = sinon.stub();
globalThis.DOM = getDOM({ global.DOM = getDOM({
isContainClass, isContainClass
}); });
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const current = {}; const current = {};
currentFile.isCurrentFile(current); currentFile.isCurrentFile(current);
globalThis.DOM = DOM; global.DOM = DOM;
globalThis.CloudCmd = CloudCmd; global.CloudCmd = CloudCmd;
t.calledWith(isContainClass, [current, _CURRENT_FILE], 'should call isContainClass'); t.ok(isContainClass.calledWith(current, _CURRENT_FILE), 'should call isContainClass');
t.end(); t.end();
}); });
test('current-file: getCurrentType', (t) => { function getCloudCmd({emit} = {}) {
const {DOM, CloudCmd} = globalThis; return {
PREFIX: '',
emit: emit || sinon.stub(),
};
}
globalThis.DOM = getDOM(); function getDOM({
globalThis.CloudCmd = getCloudCmd();
const {getByDataName} = globalThis.DOM;
getByDataName.returns({
className: 'mini-icon directory',
});
const current = create();
currentFile.getCurrentType(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.calledWith(getByDataName, ['js-type', current]);
t.end();
});
test('current-file: isCurrentIsDir: getCurrentType', (t) => {
const {DOM, CloudCmd} = globalThis;
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
const {getCurrentType} = globalThis.DOM;
const current = create();
currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.calledWith(getCurrentType, [current]);
t.end();
});
test('current-file: isCurrentIsDir: directory', (t) => {
const {DOM, CloudCmd} = globalThis;
globalThis.DOM = getDOM({
getCurrentType: stub().returns('directory'),
});
globalThis.CloudCmd = getCloudCmd();
const current = create();
const result = currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.ok(result);
t.end();
});
test('current-file: isCurrentIsDir: directory-link', (t) => {
const {DOM, CloudCmd} = globalThis;
globalThis.DOM = getDOM({
getCurrentType: stub().returns('directory-link'),
});
globalThis.CloudCmd = getCloudCmd();
const current = create();
const result = currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.ok(result);
t.end();
});
test('current-file: isCurrentIsDir: file', (t) => {
const {DOM, CloudCmd} = globalThis;
globalThis.DOM = getDOM({
getCurrentType: stub().returns('file'),
});
globalThis.CloudCmd = getCloudCmd();
const current = create();
const result = currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.notOk(result);
t.end();
});
const getCloudCmd = ({emit} = {}) => ({
prefix: '',
emit: emit || stub(),
});
test('current-file: parseNameAttribute', (t) => {
const result = currentFile._parseNameAttribute('js-file-aGVsbG8mbmJzcDt3b3JsZA==');
const expected = 'hello\xa0world';
t.equal(result, expected);
t.end();
});
test('current-file: parseHrefAttribute', (t) => {
const prefix = '/api/v1';
const result = currentFile._parseHrefAttribute(prefix, '/api/v1/fs/hello&nbsp;world');
const expected = '/hello\xa0world';
t.equal(result, expected);
t.end();
});
function getDOM(overrides = {}) {
const {
link = {}, link = {},
getCurrentDirPath = stub(), getCurrentDirPath = sinon.stub(),
getCurrentDirName = stub(), getCurrentDirName = sinon.stub(),
getByDataName = stub(), getByDataName = sinon.stub(),
isContainClass = stub(), isContainClass = sinon.stub(),
getCurrentType = stub(), } = {}) {
getCurrentPath = stub().returns(''),
} = overrides;
return { return {
getCurrentDirPath, getCurrentDirPath,
getCurrentDirName, getCurrentDirName,
getCurrentPath,
getByDataName, getByDataName,
isContainClass, isContainClass,
getCurrentType,
CurrentInfo: { CurrentInfo: {
link, link,
dirPath: '/', dirPath: '/',
}, }
}; };
} }

View file

@ -1,18 +1,19 @@
import {tryToCatch} from 'try-to-catch';
import * as smalltalk from 'smalltalk';
const title = 'Cloud Commander'; 'use strict';
export const alert = (...a) => smalltalk.alert(title, ...a, { const {
cancel: false, alert,
}); prompt,
confirm,
}= require('smalltalk');
export const prompt = (...a) => tryToCatch(smalltalk.prompt, title, ...a); module.exports = {
export const confirm = (...a) => tryToCatch(smalltalk.confirm, title, ...a); alert,
export const progress = (...a) => smalltalk.progress(title, ...a); prompt,
confirm,
alert.noFiles = () => {
return smalltalk.alert(title, 'No files selected!', {
cancel: false,
});
}; };
module.exports.alert.noFiles = (title) => {
return alert(title, 'No files selected!');
};

View file

@ -1,29 +1,33 @@
/* global DOM, CloudCmd */ /* global CloudCmd */
import philip from 'philip';
import * as Dialog from '#dom/dialog'; 'use strict';
import {FS} from '#common/cloudfunc';
import * as Images from '#dom/images'; const philip = require('philip');
const Images = require('./images');
const {FS} = require('../../common/cloudfunc');
const DOM = require('.');
const {
getCurrentDirPath: getPathWhenRootEmpty,
} = DOM;
module.exports = (items) => {
const Dialog = DOM.Dialog;
export const uploadDirectory = (items) => {
if (items.length) if (items.length)
Images.show('top'); Images.show('top');
const entries = Array const entries = [...items].map((item) => {
.from(items) return item.webkitGetAsEntry();
.map((item) => item.webkitGetAsEntry());
const dirPath = DOM.getCurrentDirPath();
const path = dirPath.replace(/\/$/, '');
const progress = Dialog.progress('Uploading...');
progress.catch(() => {
Dialog.alert('Upload aborted');
uploader.abort();
}); });
const dirPath = getPathWhenRootEmpty();
const path = dirPath
.replace(/\/$/, '');
const uploader = philip(entries, (type, name, data, i, n, callback) => { const uploader = philip(entries, (type, name, data, i, n, callback) => {
const {prefixURL} = CloudCmd; const prefixURL = CloudCmd.PREFIX_URL;
const full = prefixURL + FS + path + name; const full = prefixURL + FS + path + name;
let upload; let upload;
@ -45,7 +49,7 @@ export const uploadDirectory = (items) => {
const max = next - current; const max = next - current;
const value = current + percent(count, 100, max); const value = current + percent(count, 100, max);
progress.setProgress(value); setProgress(value);
}); });
}); });
@ -54,11 +58,25 @@ export const uploadDirectory = (items) => {
uploader.abort(); uploader.abort();
}); });
uploader.on('progress', setProgress);
uploader.on('end', CloudCmd.refresh); uploader.on('end', CloudCmd.refresh);
}; };
const percent = (i, n, per = 100) => Math.round(i * per / n); function percent(i, n, per = 100) {
return Math.round(i * per / n);
}
const uploadFile = (url, data) => DOM.load.put(url, data); function setProgress(count) {
DOM.Images
.setProgress(count)
.show('top');
}
function uploadFile(url, data) {
return DOM.load.put(url, data);
}
function uploadDir(url) {
return DOM.load.put(url + '?dir');
}
const uploadDir = (url) => DOM.load.put(`${url}?dir`);

View file

@ -1,15 +1,8 @@
import currify from 'currify'; 'use strict';
const DOM = { const currify = require('currify/legacy');
show,
hide, const DOM = module.exports;
getByClass,
getByClassAll,
getByDataName,
getById,
getByTag,
isContainClass,
};
/** /**
* check class of element * check class of element
@ -17,7 +10,7 @@ const DOM = {
* @param element * @param element
* @param className * @param className
*/ */
export function isContainClass(element, className) { const isContainClass = (element, className) => {
if (!element) if (!element)
throw Error('element could not be empty!'); throw Error('element could not be empty!');
@ -25,68 +18,67 @@ export function isContainClass(element, className) {
throw Error('className could not be empty!'); throw Error('className could not be empty!');
if (Array.isArray(className)) if (Array.isArray(className))
return className.some(currify( return className.some(currify(isContainClass, element));
isContainClass,
element,
));
const {classList} = element; const classList = element.classList;
return classList.contains(className); return classList.contains(className);
} };
module.exports.isContainClass = isContainClass;
/** /**
* Function search element by tag * Function search element by tag
* @param tag - className * @param tag - className
* @param element - element * @param element - element
*/ */
export function getByTag(tag, element = document) { module.exports.getByTag = (tag, element = document) => {
return element.getElementsByTagName(tag); return element.getElementsByTagName(tag);
} };
/** /**
* Function search element by id * Function search element by id
* @param id * @param Id - id
* @param element
*/ */
export function getById(id, element = document) { module.exports.getById = (id, element = document) => {
return element.querySelector(`#${id}`); return element.querySelector('#' + id);
} };
/** /**
* Function search first element by class name * Function search first element by class name
* @param className - className * @param className - className
* @param element - element * @param element - element
*/ */
export function getByClass(className, element = document) { module.exports.getByClass = (className, element = document) => {
return DOM.getByClassAll(className, element)[0]; return DOM.getByClassAll(className, element)[0];
} };
export function getByDataName(attribute, element = document) { module.exports.getByDataName = (attribute, element = document) => {
const selector = '[' + 'data-name="' + attribute + '"]'; const selector = '[' + 'data-name="' + attribute + '"]';
return element.querySelector(selector); return element.querySelector(selector);
} };
/** /**
* Function search element by class name * Function search element by class name
* @param className * @param pClass - className
* @param element * @param element - element
*/ */
export function getByClassAll(className, element) { module.exports.getByClassAll = (className, element) => {
return (element || document).getElementsByClassName(className); return (element || document).getElementsByClassName(className);
} };
/** /**
* add class=hidden to element * add class=hidden to element
* *
* @param element * @param element
*/ */
export function hide(element) { module.exports.hide = (element) => {
element.classList.add('hidden'); element.classList.add('hidden');
return DOM; return DOM;
} };
export function show(element) { module.exports.show = (element) => {
element.classList.remove('hidden'); element.classList.remove('hidden');
return DOM; return DOM;
} };

View file

@ -1,152 +1,57 @@
import {test, stub} from 'supertape'; 'use strict';
import {create} from 'auto-globals';
import {tryCatch} from 'try-catch'; const test = require('tape');
import { const diff = require('sinon-called-with-diff');
const sinon = diff(require('sinon'));
const tryCatch = require('try-catch');
const {
isContainClass, isContainClass,
getByTag, } = require('./dom-tree');
getById,
getByClass,
getByDataName,
getByClassAll,
hide,
show,
} from './dom-tree.js';
test('dom: isContainClass: no element', (t) => { test('dom: isContainClass: no element', (t) => {
const [e] = tryCatch(isContainClass); const [e] = tryCatch(isContainClass);
t.equal(e.message, 'element could not be empty!', 'should throw when no element'); t.equal(e.message, 'element could not be empty!', 'should throw when no element');
t.end(); t.end();
}); });
test('dom: isContainClass: no className', (t) => { test('dom: isContainClass: no className', (t) => {
const [e] = tryCatch(isContainClass, {}); const [e] = tryCatch(isContainClass, {});
t.equal(e.message, 'className could not be empty!', 'should throw when no element'); t.equal(e.message, 'className could not be empty!', 'should throw when no element');
t.end(); t.end();
}); });
test('dom: isContainClass: contains', (t) => { test('dom: isContainClass: contains', (t) => {
const el = create(); const contains = sinon.stub();
const {contains} = el.classList; const el = {
classList: {
contains,
}
};
const className = 'hello'; const className = 'hello';
isContainClass(el, className); isContainClass(el, className);
t.calledWith(contains, [className], 'should call contains'); t.ok(contains.calledWith(className), 'should call contains');
t.end(); t.end();
}); });
test('dom: isContainClass: contains: array', (t) => { test('dom: isContainClass: contains: array', (t) => {
const el = create(); const contains = sinon.stub();
const {contains} = el.classList; const el = {
classList: {
contains,
}
};
const className = 'hello'; const className = 'hello';
isContainClass(el, ['world', className, 'hello']); isContainClass(el, [
'world',
className,
'hello',
]);
t.calledWith(contains, [className], 'should call contains'); t.ok(contains.calledWith(className), 'should call contains');
t.end(); t.end();
}); });
test('dom: getByTag', (t) => {
const getElementsByTagName = stub();
const element = {
getElementsByTagName,
};
getByTag('div', element);
t.calledWith(getElementsByTagName, ['div'], 'should call getElementsByTagName');
t.end();
});
test('dom: getById', (t) => {
const querySelector = stub();
const element = {
querySelector,
};
getById('my-id', element);
t.calledWith(querySelector, ['#my-id'], 'should call querySelector with id selector');
t.end();
});
test('dom: getByClassAll', (t) => {
const getElementsByClassName = stub();
const element = {
getElementsByClassName,
};
getByClassAll('my-class', element);
t.calledWith(getElementsByClassName, ['my-class'], 'should call getElementsByClassName');
t.end();
});
test('dom: getByClass: calls getByClassAll', (t) => {
const element = {
getElementsByClassName: stub().returns(['first']),
};
const result = getByClass('my-class', element);
t.equal(result, 'first', 'should return first element from class list');
t.end();
});
test('dom: getByDataName', (t) => {
const querySelector = stub();
const element = {
querySelector,
};
getByDataName('hello', element);
t.calledWith(querySelector, ['[data-name="hello"]'], 'should call querySelector with data-name selector');
t.end();
});
test('dom: hide', (t) => {
const add = stub();
const element = {
classList: {
add,
},
};
hide(element);
t.calledWith(add, ['hidden'], 'should add hidden class');
t.end();
});
test('dom: show', (t) => {
const remove = stub();
const element = {
classList: {
remove,
},
};
show(element);
t.calledWith(remove, ['hidden'], 'should remove hidden class');
t.end();
});
test('dom: getByClassAll: without element uses document', (t) => {
const getElementsByClassName = stub();
const prevDocument = globalThis.document;
globalThis.document = {
getElementsByClassName,
};
getByClassAll('my-class');
globalThis.document = prevDocument;
t.calledWith(getElementsByClassName, ['my-class'], 'should fallback to document when no element');
t.end();
});

View file

@ -1,6 +1,8 @@
'use strict';
let list = []; let list = [];
export const add = (el, name, fn) => { module.exports.add = (el, name, fn) => {
list.push([ list.push([
el, el,
name, name,
@ -8,8 +10,11 @@ export const add = (el, name, fn) => {
]); ]);
}; };
export const clear = () => { module.exports.clear = () => {
list = []; list = [];
}; };
export const get = () => list; module.exports.get = () => {
return list;
};

View file

@ -1,5 +1,7 @@
import {test} from 'supertape'; 'use strict';
import * as eventStore from './event-store.js';
const test = require('tape');
const eventStore = require('./event-store');
test('event-store: get', (t) => { test('event-store: get', (t) => {
const el = {}; const el = {};
@ -8,16 +10,11 @@ test('event-store: get', (t) => {
eventStore.add(el, name, fn); eventStore.add(el, name, fn);
const result = eventStore.get(); const result = eventStore.get();
const expected = [ const expected = [
[ [el, name, fn],
el,
name,
fn,
],
]; ];
t.deepEqual(result, expected); t.deepEqual(expected, result, 'should equal');
t.end(); t.end();
}); });
@ -32,6 +29,6 @@ test('event-store: clear', (t) => {
const result = eventStore.get(); const result = eventStore.get();
const expected = []; const expected = [];
t.deepEqual(result, expected); t.deepEqual(expected, result, 'should equal');
t.end(); t.end();
}); });

View file

@ -1,132 +1,19 @@
import itype from 'itype'; 'use strict';
import * as EventStore from './event-store.js';
/** const itype = require('itype/legacy');
* safe add event listener const EventStore = require('./event-store');
*
* @param type
* @param element - document by default
* @param listener
*/
export const add = (type, element, listener) => {
checkType(type);
parseArgs(type, element, listener, (element, args) => { module.exports = new EventsProto();
const [name, fn, options] = args;
element.addEventListener(name, fn, options); function EventsProto() {
EventStore.add(element, name, fn); const Events = this;
});
return Events;
};
/**
* safe add event listener
*
* @param type
* @param listener
* @param element - document by default
*/
export const addOnce = (type, element, listener) => {
const once = (event) => {
Events.remove(type, element, once);
listener(event);
};
if (!listener) {
listener = element;
element = null;
}
add(type, element, once);
return Events;
};
/**
* safe remove event listener
*
* @param type
* @param listener
* @param element - document by default
*/
export const remove = (type, element, listener) => {
checkType(type);
parseArgs(type, element, listener, (element, args) => {
element.removeEventListener(...args);
});
return Events;
};
/**
* remove all added event listeners
*/
export const removeAll = () => {
const events = EventStore.get();
for (const [el, name, fn] of events)
el.removeEventListener(name, fn);
EventStore.clear();
};
/**
* safe add event keydown listener
*
* @param args
*/
export const addKey = function(...args) {
return add('keydown', ...args);
};
/**
* safe remove event click listener
*
* @param args
*/
export const rmKey = function(...args) {
return Events.remove('keydown', ...args);
};
/**
* safe add event click listener
*/
export const addClick = function(...args) {
return Events.add('click', ...args);
};
/**
* safe remove event click listener
*/
export const rmClick = function(...args) {
return remove('click', ...args);
};
export const addContextMenu = function(...args) {
return add('contextmenu', ...args);
};
/**
* safe add load listener
*/
export const addLoad = function(...args) {
return add('load', ...args);
};
function checkType(type) {
if (!type)
throw Error('type could not be empty!');
}
const getEventOptions = (eventName) => { const getEventOptions = (eventName) => {
if (eventName !== 'touchstart') if (eventName !== 'touchstart')
return false; return false;
return { return {
passive: true, passive: true
}; };
}; };
@ -145,10 +32,15 @@ function parseArgs(eventName, element, listener, callback) {
switch(type) { switch(type) {
default: default:
if (!type.endsWith('element')) if (!/element$/.test(type))
throw Error(`unknown eventName: ${type}`); throw Error('unknown eventName: ' + type);
parseArgs(args[EVENT_NAME], args[ELEMENT], listener, callback); parseArgs(
args[EVENT_NAME],
args[ELEMENT],
listener,
callback
);
break; break;
case 'string': case 'string':
@ -170,34 +62,188 @@ function parseArgs(eventName, element, listener, callback) {
break; break;
case 'array': case 'array':
eventName.forEach((eventName) => {
for (const name of eventName) { parseArgs(
parseArgs(name, element, listener, callback); eventName,
} element,
listener,
callback
);
});
break; break;
case 'object': case 'object':
Object.keys(eventName).forEach((name) => {
for (const name of Object.keys(eventName)) {
const eventListener = eventName[name]; const eventListener = eventName[name];
parseArgs(name, element, eventListener, callback); parseArgs(
} name,
element,
eventListener,
callback
);
});
break; break;
} }
} }
const Events = { /**
add, * safe add event listener
addClick, *
addContextMenu, * @param type
addKey, * @param element {document by default}
addLoad, * @param listener
addOnce, */
remove, this.add = (type, element, listener) => {
removeAll, checkType(type);
rmClick,
rmKey, parseArgs(type, element, listener, (element, args) => {
const [name, fn, options] = args;
element.addEventListener(name, fn, options);
EventStore.add(element, name, fn);
});
return Events;
}; };
/**
* safe add event listener
*
* @param type
* @param listener
* @param element {document by default}
*/
this.addOnce = (type, element, listener) => {
const once = (event) => {
Events.remove(type, element, once);
listener(event);
};
if (!listener) {
listener = element;
element = null;
}
this.add(type, element, once);
return Events;
};
/**
* safe remove event listener
*
* @param type
* @param listener
* @param element {document by default}
*/
this.remove = (type, element, listener) => {
checkType(type);
parseArgs(type, element, listener, (element, args) => {
element.removeEventListener.apply(element, args);
});
return Events;
};
/**
* remove all added event listeners
*
* @param listener
*/
this.removeAll = () => {
const events = EventStore.get();
for (const [el, name, fn] of events)
el.removeEventListener(name, fn);
EventStore.clear();
};
/**
* safe add event keydown listener
*
* @param listener
*/
this.addKey = function(...argsArr) {
const name = 'keydown';
const args = [name, ...argsArr];
return Events.add(...args);
};
/**
* safe remove event click listener
*
* @param listener
*/
this.rmKey = function(...argsArr) {
const name = 'keydown';
const args = [name, ...argsArr];
return Events.remove(...args);
};
/**
* safe add event click listener
*
* @param listener
*/
this.addClick = function(...argsArr) {
const name = 'click';
const args = [name, ...argsArr];
return Events.add(...args);
};
/**
* safe remove event click listener
*
* @param listener
*/
this.rmClick = function(...argsArr) {
const name = 'click';
const args = [name, ...argsArr];
return Events.remove(...args);
};
this.addContextMenu = function(...argsArr) {
const name = 'contextmenu';
const args = [name, ...argsArr];
return Events.add(...args);
};
/**
* safe add event click listener
*
* @param listener
*/
this.addError = function(...argsArr) {
const name = 'error';
const args = [name, ...argsArr];
return Events.add(...args);
};
/**
* safe add load click listener
*
* @param listener
*/
this.addLoad = function(...argsArr) {
const name = 'load';
const args = [name, ...argsArr];
return Events.add(...args);
};
function checkType(type) {
if (!type)
throw Error('type could not be empty!');
}
}

View file

@ -1,57 +1,79 @@
/* global CloudCmd */ /* global CloudCmd */
import itype from 'itype';
import {promisify} from 'es6-promisify'; 'use strict';
import * as load from '#dom/load';
import * as RESTful from '#dom/rest'; const itype = require('itype/legacy');
const currify = require('currify/legacy');
const exec = require('execon');
const load = require('./load');
const RESTful = require('./rest');
const Promises = {}; const Promises = {};
const FILES_JSON = 'config|modules'; const FILES_JSON = 'config|modules';
const FILES_HTML = 'file|path|link|pathLink|media'; const FILES_HTML = 'file|path|link|pathLink|media';
const FILES_HTML_ROOT = 'view/media-tmpl|config-tmpl|upload'; const FILES_HTML_ROOT = 'view/media-tmpl|config-tmpl|upload';
const DIR_HTML = '/tmpl/'; const DIR_HTML = '/tmpl/';
const DIR_HTML_FS = `${DIR_HTML}fs/`; const DIR_HTML_FS = DIR_HTML + 'fs/';
const DIR_JSON = '/json/'; const DIR_JSON = '/json/';
const timeout = getTimeoutOnce(2000); const timeout = getTimeoutOnce(2000);
export const get = getFile; const get = currify(getFile);
const unaryMap = (array, fn) => array.map((a) => fn(a));
function getFile(name) { module.exports.get = get;
function getFile(name, callback) {
const type = itype(name); const type = itype(name);
check(name); let array;
if (type === 'string') check(name, callback);
return getModule(name);
if (type === 'array') switch(type) {
return Promise.all(name.map(getFile)); case 'string':
getModule(name, callback);
break;
case 'array':
array = unaryMap(name, get);
exec.parallel(array, callback);
break;
}
} }
function check(name) { function check(name, callback) {
if (!name) if (!name)
throw Error('name could not be empty!'); throw Error('name could not be empty!');
if (typeof callback !== 'function')
throw Error('callback should be a function');
} }
function getModule(name) { function getModule(name, callback) {
const regExpHTML = RegExp(FILES_HTML + '|' + FILES_HTML_ROOT); let path;
const regExpJSON = RegExp(FILES_JSON);
const regExpHTML = new RegExp(FILES_HTML + '|' + FILES_HTML_ROOT);
const regExpJSON = new RegExp(FILES_JSON);
const isHTML = regExpHTML.test(name); const isHTML = regExpHTML.test(name);
const isJSON = regExpJSON.test(name); const isJSON = regExpJSON.test(name);
if (!isHTML && !isJSON) if (!isHTML && !isJSON) {
return showError(name); showError(name);
} else if (name === 'config') {
getConfig(callback);
} else {
path = getPath(name, isHTML, isJSON);
if (name === 'config') getSystemFile(path, callback);
return getConfig(); }
const path = getPath(name, isHTML, isJSON);
return getSystemFile(path);
} }
function getPath(name, isHTML, isJSON) { function getPath(name, isHTML, isJSON) {
let path; let path;
const regExp = RegExp(FILES_HTML_ROOT); const regExp = new RegExp(FILES_HTML_ROOT);
const isRoot = regExp.test(name); const isRoot = regExp.test(name);
if (isHTML) { if (isHTML) {
@ -69,14 +91,14 @@ function getPath(name, isHTML, isJSON) {
} }
function showError(name) { function showError(name) {
const str = `Wrong file name: ${name}`; const str = 'Wrong file name: ' + name;
const error = Error(str); const error = new Error(str);
throw error; throw(error);
} }
const getSystemFile = promisify((file, callback) => { function getSystemFile(file, callback) {
const {prefix} = CloudCmd; const prefix = CloudCmd.PREFIX;
if (!Promises[file]) if (!Promises[file])
Promises[file] = new Promise((success, error) => { Promises[file] = new Promise((success, error) => {
@ -85,7 +107,7 @@ const getSystemFile = promisify((file, callback) => {
load.ajax({ load.ajax({
url, url,
success, success,
error, error
}); });
}); });
@ -95,29 +117,36 @@ const getSystemFile = promisify((file, callback) => {
Promises[file] = null; Promises[file] = null;
callback(error); callback(error);
}); });
}); }
const getConfig = async () => { function getConfig(callback) {
let is; let is;
if (!Promises.config) if (!Promises.config)
Promises.config = () => { Promises.config = new Promise((resolve, reject) => {
is = true; is = true;
return RESTful.Config.read(); RESTful.Config.read((error, data) => {
}; if (error)
return reject(error);
const [, data] = await Promises.config(); resolve(data);
});
});
if (data) Promises.config.then((data) => {
is = false; is = false;
callback(null, data);
timeout(() => { timeout(() => {
if (!is) if (!is)
Promises.config = null; Promises.config = null;
}); });
}, () => {
return data; if (!is)
}; Promises.config = null;
});
}
function getTimeoutOnce(time) { function getTimeoutOnce(time) {
let is; let is;
@ -134,3 +163,4 @@ function getTimeoutOnce(time) {
}, time); }, time);
}; };
} }

View file

@ -1,13 +1,22 @@
/* global DOM */ /* global DOM */
import createElement from '@cloudcmd/create-element';
'use strict';
const createElement = require('@cloudcmd/create-element');
const Images = module.exports;
const LOADING = 'loading'; const LOADING = 'loading';
const HIDDEN = 'hidden'; const HIDDEN = 'hidden';
const ERROR = 'error'; const ERROR = 'error';
const getLoadingType = () => isSVG() ? '-svg' : '-gif'; const LoadingImage = LOADING + getLoadingType();
export const get = getElement; function getLoadingType() {
return isSVG() ? '-svg' : '-gif';
}
module.exports.get = getElement;
/** /**
* check SVG SMIL animation support * check SVG SMIL animation support
@ -31,49 +40,41 @@ function getElement() {
id: 'js-status-image', id: 'js-status-image',
className: 'icon', className: 'icon',
dataName: 'progress', dataName: 'progress',
notAppend: true, notAppend: true
}); });
} }
/* Функция создаёт картинку загрузки */ /* Функция создаёт картинку загрузки */
export const loading = () => { module.exports.loading = () => {
const element = getElement(); const element = getElement();
const {classList} = element; const classList = element.classList;
const loadingImage = LOADING + getLoadingType();
classList.add(LOADING, loadingImage); classList.add(LOADING, LoadingImage);
classList.remove(ERROR, HIDDEN); classList.remove(ERROR, HIDDEN);
return element; return element;
}; };
/* Функция создаёт картинку ошибки загрузки */ /* Функция создаёт картинку ошибки загрузки */
export const error = () => { module.exports.error = () => {
const element = getElement(); const element = getElement();
const {classList} = element; const classList = element.classList;
const loadingImage = LOADING + getLoadingType();
classList.add(ERROR); classList.add(ERROR);
classList.remove(HIDDEN, LOADING, loadingImage); classList.remove(HIDDEN, LOADING, LoadingImage);
return element; return element;
}; };
show.load = show; module.exports.show = show;
show.error = (text) => { module.exports.show.load = show;
const image = Images.error(); module.exports.show.error = error;
DOM.show(image);
image.title = text;
return image;
};
/** /**
* Function shows loading spinner * Function shows loading spinner
* position = {top: true}; * position = {top: true};
*/ */
export function show(position, panel) { function show(position, panel) {
const image = Images.loading(); const image = Images.loading();
const parent = image.parentElement; const parent = image.parentElement;
const refreshButton = DOM.getRefreshButton(panel); const refreshButton = DOM.getRefreshButton(panel);
@ -91,7 +92,7 @@ export function show(position, panel) {
current = refreshButton.parentElement; current = refreshButton.parentElement;
} }
if (!parent || parent && parent !== current) if (!parent || (parent && parent !== current))
current.appendChild(image); current.appendChild(image);
DOM.show(image); DOM.show(image);
@ -99,25 +100,34 @@ export function show(position, panel) {
return image; return image;
} }
function error(text) {
const image = Images.error();
DOM.show(image);
image.title = text;
return image;
}
/** /**
* hide load image * hide load image
*/ */
export function hide() { module.exports.hide = () => {
const element = Images.get(); const element = Images.get();
DOM.hide(element); DOM.hide(element);
return Images; return Images;
} };
export const setProgress = (value, title) => { module.exports.setProgress = (value, title) => {
const DATA = 'data-progress'; const DATA = 'data-progress';
const element = Images.get(); const element = Images.get();
if (!element) if (!element)
return Images; return Images;
element.setAttribute(DATA, `${value}%`); element.setAttribute(DATA, value + '%');
if (title) if (title)
element.title = title; element.title = title;
@ -125,7 +135,7 @@ export const setProgress = (value, title) => {
return Images; return Images;
}; };
export const clearProgress = () => { module.exports.clearProgress = () => {
const DATA = 'data-progress'; const DATA = 'data-progress';
const element = Images.get(); const element = Images.get();
@ -138,12 +148,3 @@ export const clearProgress = () => {
return Images; return Images;
}; };
const Images = {
clearProgress,
setProgress,
show,
hide,
get,
error,
loading,
};

File diff suppressed because it is too large Load diff

View file

@ -1,52 +0,0 @@
import {test, stub} from 'supertape';
import {getCSSVar, goToDirectory} from '#dom';
globalThis.CloudCmd = {};
test('cloudcmd: client: dom: goToDirectory', async (t) => {
const path = '';
const changeDir = stub();
const prompt = stub().returns([null, path]);
await goToDirectory({
prompt,
changeDir,
});
t.calledWith(changeDir, [path]);
t.end();
});
test('cloudcmd: client: dom: getCSSVar', (t) => {
const body = {};
const getPropertyValue = stub().returns(0);
globalThis.getComputedStyle = stub().returns({
getPropertyValue,
});
const result = getCSSVar('hello', {
body,
});
delete globalThis.getComputedStyle;
t.notOk(result);
t.end();
});
test('cloudcmd: client: dom: getCSSVar: 1', (t) => {
const body = {};
const getPropertyValue = stub().returns(1);
globalThis.getComputedStyle = stub().returns({
getPropertyValue,
});
const result = getCSSVar('hello', {
body,
});
delete globalThis.getComputedStyle;
t.ok(result);
t.end();
});

View file

@ -1,166 +0,0 @@
import {FS} from '#common/cloudfunc';
import {sendRequest as _sendRequest} from './send-request.js';
const {assign} = Object;
const imgPosition = {
top: true,
};
export const remove = async (url, data, overrides = {}) => {
const {
sendRequest = _sendRequest,
} = overrides;
const request = {
method: 'DELETE',
url: FS + url,
imgPosition: {
top: Boolean(data),
},
};
if (data)
assign(request, {
data,
url: `${request.url}?files`,
});
return await sendRequest(request);
};
export const patch = async (url, data) => {
return await _sendRequest({
method: 'PATCH',
url: FS + url,
data,
imgPosition,
});
};
export const write = async (url, data) => {
return await _sendRequest({
method: 'PUT',
url: FS + url,
data,
imgPosition,
});
};
export const createDirectory = async (url, overrides = {}) => {
const {
sendRequest = _sendRequest,
} = overrides;
return await sendRequest({
method: 'PUT',
url: `${FS}${url}?dir`,
imgPosition,
});
};
export const read = async (url, dataType = 'text') => {
const notLog = !url.includes('?');
return await _sendRequest({
method: 'GET',
url: FS + url,
notLog,
dataType,
});
};
export const copy = async (from, to, names) => {
return await _sendRequest({
method: 'PUT',
url: '/copy',
data: {
from,
to,
names,
},
imgPosition,
});
};
export const pack = async (data) => {
return await _sendRequest({
method: 'PUT',
url: '/pack',
data,
});
};
export const extract = async (data) => {
return await _sendRequest({
method: 'PUT',
url: '/extract',
data,
});
};
export const move = async (from, to, names) => {
return await _sendRequest({
method: 'PUT',
url: '/move',
data: {
from,
to,
names,
},
imgPosition,
});
};
export const rename = async (from, to) => {
return await _sendRequest({
method: 'PUT',
url: '/rename',
data: {
from,
to,
},
imgPosition,
});
};
export const Config = {
read: async () => {
return await _sendRequest({
method: 'GET',
url: '/config',
imgPosition,
notLog: true,
});
},
write: async (data) => {
return await _sendRequest({
method: 'PATCH',
url: '/config',
data,
imgPosition,
});
},
};
export const Markdown = {
read: async (url) => {
return await _sendRequest({
method: 'GET',
url: `/markdown${url}`,
imgPosition,
notLog: true,
});
},
render: async (data) => {
return await _sendRequest({
method: 'PUT',
url: '/markdown',
data,
imgPosition,
notLog: true,
});
},
};

View file

@ -1,61 +0,0 @@
import {test, stub} from 'supertape';
import * as io from './index.js';
test('client: dom: io', (t) => {
const sendRequest = stub();
io.createDirectory('/hello', {
sendRequest,
});
const expected = {
imgPosition: {
top: true,
},
method: 'PUT',
url: '/fs/hello?dir',
};
t.calledWith(sendRequest, [expected]);
t.end();
});
test('client: dom: io: remove: no files', async (t) => {
const sendRequest = stub();
await io.remove('/hello', null, {
sendRequest,
});
const expected = {
imgPosition: {
top: false,
},
method: 'DELETE',
url: '/fs/hello',
};
t.calledWith(sendRequest, [expected]);
t.end();
});
test('client: dom: io: remove: files', async (t) => {
const sendRequest = stub();
const files = ['world'];
await io.remove('/hello', files, {
sendRequest,
});
const expected = {
imgPosition: {
top: true,
},
data: ['world'],
method: 'DELETE',
url: '/fs/hello?files',
};
t.calledWith(sendRequest, [expected]);
t.end();
});

View file

@ -1,48 +0,0 @@
/* global CloudCmd */
import {promisify} from 'es6-promisify';
import * as Images from '#dom/images';
import * as load from '#dom/load';
export const sendRequest = promisify((params, callback) => {
const p = params;
const {prefixURL} = CloudCmd;
p.url = prefixURL + p.url;
p.url = encodeURI(p.url);
p.url = replaceHash(p.url);
load.ajax({
method: p.method,
url: p.url,
data: p.data,
dataType: p.dataType,
error: (jqXHR) => {
const response = jqXHR.responseText;
const {statusText, status} = jqXHR;
const text = status === 404 ? response : statusText;
callback(Error(text));
},
success: (data) => {
Images.hide();
if (!p.notLog)
CloudCmd.log(data);
callback(null, data);
},
});
});
export const _replaceHash = replaceHash;
function replaceHash(url) {
/*
* if we send ajax request -
* no need in hash so we escape #
*/
return url.replace(/#/g, '%23');
}

View file

@ -1,11 +0,0 @@
import {test} from 'supertape';
import {_replaceHash} from './send-request.js';
test('cloudcmd: client: io: replaceHash', (t) => {
const url = '/hello/####world';
const result = _replaceHash(url);
const expected = '/hello/%23%23%23%23world';
t.equal(result, expected);
t.end();
});

View file

@ -1,58 +1,68 @@
/* global CloudCmd */ 'use strict';
import {callbackify} from 'node:util';
import {rendy} from 'rendy';
import itype from 'itype';
import * as load from 'load.js';
import {tryToCatch} from 'try-to-catch';
import {findObjByNameInArr} from '#common/util';
import * as Files from '#dom/files';
export const loadRemote = callbackify(async (name, options) => { /* global CloudCmd */
const {prefix, config} = CloudCmd;
const exec = require('execon');
const rendy = require('rendy/legacy');
const itype = require('itype/legacy');
const wraptile = require('wraptile/legacy');
const load = require('load.js');
const {findObjByNameInArr} = require('../../common/util');
const Files = require('./files');
const parallel = wraptile(load.parallel);
module.exports = (name, options, callback = options) => {
const {PREFIX, config} = CloudCmd;
const o = options; const o = options;
if (o.name && window[o.name]) if (o.name && window[o.name])
return; return callback();
const modules = await Files.get('modules');
Files.get('modules', (error, modules) => {
const online = config('online') && navigator.onLine; const online = config('online') && navigator.onLine;
const module = findObjByNameInArr(modules.remote, name); const module = findObjByNameInArr(modules.remote, name);
const isArray = itype.array(module.local); const isArray = itype.array(module.local);
const {version} = module; const version = module.version;
let remoteTmpls;
let local;
let remoteTmpls, local;
if (isArray) { if (isArray) {
remoteTmpls = module.remote; remoteTmpls = module.remote;
({local} = module); local = module.local;
} else { } else {
remoteTmpls = [module.remote]; remoteTmpls = [module.remote];
local = [module.local]; local = [module.local];
} }
const localURL = []; const localURL = local.map((url) => {
return PREFIX + url;
for (const url of local) {
localURL.push(prefix + url);
}
const remoteURL = [];
for (const tmpl of remoteTmpls) {
remoteURL.push(rendy(tmpl, {
version,
}));
}
if (online) {
const [e] = await tryToCatch(load.parallel, remoteURL);
if (!e)
return;
}
await load.parallel(localURL);
}); });
const remoteURL = remoteTmpls.map((tmpl) => {
return rendy(tmpl, {
version: version
});
});
const on = funcON(localURL, remoteURL, callback);
const off = funcOFF(localURL, callback);
exec.if(online, on, off);
});
};
function funcOFF(local, callback) {
return parallel(local, callback);
}
function funcON (local, remote,callback) {
return parallel(remote, (error) => {
if (error)
return funcOFF();
callback();
});
}

View file

@ -1,8 +1,12 @@
import itype from 'itype'; 'use strict';
import jonny from 'jonny';
import Emitify from 'emitify'; const itype = require('itype/legacy');
import exec from 'execon'; const jonny = require('jonny/legacy');
import * as Images from '#dom/images'; const Emitify = require('emitify/legacy');
const exec = require('execon');
const Images = require('./images');
module.exports.getIdBySrc = getIdBySrc;
/** /**
* Function gets id by src * Function gets id by src
@ -10,18 +14,17 @@ import * as Images from '#dom/images';
* *
* Example: http://domain.com/1.js -> 1_js * Example: http://domain.com/1.js -> 1_js
*/ */
export function getIdBySrc(src) { function getIdBySrc(src) {
const isStr = itype.string(src); const isStr = itype.string(src);
if (!isStr) if (!isStr)
return; return;
if (src.includes(':')) if (~src.indexOf(':'))
src += '-join'; src += '-join';
const num = src.lastIndexOf('/') + 1; const num = src.lastIndexOf('/') + 1;
const sub = src.substr(src, num); const sub = src.substr(src, num);
const id = src const id = src
.replace(sub, '') .replace(sub, '')
.replace(/\./g, '-'); .replace(/\./g, '-');
@ -34,33 +37,30 @@ export function getIdBySrc(src) {
* *
* @param params * @param params
*/ */
export const ajax = (params) => { module.exports.ajax = (params) => {
const p = params; const p = params;
const isObject = itype.object(p.data); const isObject = itype.object(p.data);
const isArray = itype.array(p.data); const isArray = itype.array(p.data);
const isArrayBuf = itype(p.data) === 'arraybuffer'; const isArrayBuf = itype(p.data) === 'arraybuffer';
const type = p.type || p.method || 'GET'; const type = p.type || p.method || 'GET';
const headers = p.headers || {};
const {headers = {}} = p;
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open(type, p.url, true); xhr.open(type, p.url, true);
for (const name of Object.keys(headers)) { Object.keys(headers).forEach((name) => {
const value = headers[name]; const value = headers[name];
xhr.setRequestHeader(name, value); xhr.setRequestHeader(name, value);
} });
if (p.responseType) if (p.responseType)
xhr.responseType = p.responseType; xhr.responseType = p.responseType;
let data; let data;
if (!isArrayBuf && isObject || isArray) if (!isArrayBuf && isObject || isArray)
data = jonny.stringify(p.data); data = jonny.stringify(p.data);
else else
({data} = p); data = p.data;
xhr.onreadystatechange = (event) => { xhr.onreadystatechange = (event) => {
const xhr = event.target; const xhr = event.target;
@ -78,10 +78,9 @@ export const ajax = (params) => {
return exec(p.error, xhr); return exec(p.error, xhr);
const notText = p.dataType !== 'text'; const notText = p.dataType !== 'text';
const isContain = type.includes(TYPE_JSON); const isContain = ~type.indexOf(TYPE_JSON);
let data = xhr.response; let data = xhr.response;
if (type && isContain && notText) if (type && isContain && notText)
data = jonny.parse(xhr.response) || xhr.response; data = jonny.parse(xhr.response) || xhr.response;
@ -91,11 +90,12 @@ export const ajax = (params) => {
xhr.send(data); xhr.send(data);
}; };
export const put = (url, body) => { module.exports.put = (url, body) => {
const emitter = Emitify(); const emitter = Emitify();
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
url = encodeURI(url).replace(/#/g, '#'); url = encodeURI(url)
.replace('#', '%23');
xhr.open('put', url, true); xhr.open('put', url, true);
@ -103,7 +103,7 @@ export const put = (url, body) => {
if (!event.lengthComputable) if (!event.lengthComputable)
return; return;
const percent = event.loaded / event.total * 100; const percent = (event.loaded / event.total) * 100;
const count = Math.round(percent); const count = Math.round(percent);
emitter.emit('progress', count); emitter.emit('progress', count);
@ -116,10 +116,8 @@ export const put = (url, body) => {
if (!over) if (!over)
return; return;
if (xhr.status === OK) { if (xhr.status === OK)
emitter.emit('progress', 100);
return emitter.emit('end'); return emitter.emit('end');
}
const error = Error(xhr.responseText); const error = Error(xhr.responseText);
emitter.emit('error', error); emitter.emit('error', error);
@ -129,3 +127,4 @@ export const put = (url, body) => {
return emitter; return emitter;
}; };

View file

@ -1,66 +0,0 @@
/* global CloudCmd */
import capitalize from 'just-capitalize';
import * as _Dialog from '#dom/dialog';
import * as Storage from '#dom/storage';
import * as RESTful from '#dom/rest';
import * as _currentFile from '../current-file.js';
export default async (current, overrides = {}) => {
const {
refresh = CloudCmd.refresh,
Dialog = _Dialog,
currentFile = _currentFile,
} = overrides;
const {
isCurrentFile,
getCurrentName,
getCurrentFile,
getCurrentByName,
getCurrentType,
getCurrentDirPath,
setCurrentName,
} = currentFile;
if (!isCurrentFile(current))
current = getCurrentFile();
const from = getCurrentName(current);
if (from === '..')
return Dialog.alert.noFiles();
const [cancel, to] = await Dialog.prompt('Rename', from);
if (cancel)
return;
const nextFile = getCurrentByName(to);
if (nextFile) {
const type = getCurrentType(nextFile);
const msg = `${capitalize(type)} "${to}" already exists. Proceed?`;
const [cancel] = await Dialog.confirm(msg);
if (cancel)
return;
}
if (from === to)
return;
const dirPath = getCurrentDirPath();
const fromFull = `${dirPath}${from}`;
const toFull = `${dirPath}${to}`;
const [e] = await RESTful.rename(fromFull, toFull);
if (e)
return;
setCurrentName(to, current);
Storage.remove(dirPath);
refresh();
};

View file

@ -1,86 +0,0 @@
import {test, stub} from 'supertape';
import renameCurrent from './rename-current.js';
test('cloudcmd: client: dom: renameCurrent: isCurrentFile', async (t) => {
const current = {};
const isCurrentFile = stub();
const currentFile = stubCurrentFile({
isCurrentFile,
});
await renameCurrent(current, {
Dialog: stubDialog(),
currentFile,
});
t.calledWith(isCurrentFile, [current], 'should call isCurrentFile');
t.end();
});
test('cloudcmd: client: dom: renameCurrent: file exist', async (t) => {
const current = {};
const name = 'hello';
const prompt = stub().returns([null, name]);
const confirm = stub().returns([true]);
const getCurrentByName = stub().returns(current);
const getCurrentType = stub().returns('directory');
const Dialog = stubDialog({
confirm,
prompt,
});
const currentFile = stubCurrentFile({
getCurrentByName,
getCurrentType,
});
await renameCurrent(null, {
Dialog,
currentFile,
});
const expected = 'Directory "hello" already exists. Proceed?';
t.calledWith(confirm, [expected], 'should call confirm');
t.end();
});
const stubDialog = (fns = {}) => {
const {
alert = stub().returns([]),
confirm = stub().returns([]),
prompt = stub().returns([]),
} = fns;
return {
alert,
confirm,
prompt,
};
};
const stubCurrentFile = (fns = {}) => {
const {
isCurrentFile = stub(),
getCurrentName = stub(),
getCurrentFile = stub(),
getCurrentByName = stub(),
getCurrentType = stub(),
getCurrentDirPath = stub(),
setCurrentName = stub(),
} = fns;
return {
isCurrentFile,
getCurrentName,
getCurrentFile,
getCurrentByName,
getCurrentType,
getCurrentDirPath,
setCurrentName,
};
};

View file

@ -1,40 +1,220 @@
import {tryToCatch} from 'try-to-catch'; 'use strict';
import * as Dialog from '#dom/dialog';
import * as Images from '#dom/images';
import {encode} from '#common/entity';
import * as IO from './io/index.js';
const handleError = (promise) => async (...args) => { /* global CloudCmd, DOM */
const [e, data] = await tryToCatch(promise, ...args);
if (!e) const itype = require('itype/legacy');
return [e, data];
const encoded = encode(e.message); const {FS} = require('../../common/cloudfunc');
const {
encode,
} = require('../../common/entity');
module.exports = new RESTful();
const Images = require('./images');
const load = require('./load');
function RESTful() {
this.delete = (url, data, callback) => {
const isFunc = itype.function(data);
if (!callback && isFunc) {
callback = data;
data = null;
}
sendRequest({
method : 'DELETE',
url : FS + url,
data,
callback,
imgPosition : { top: !!data }
});
};
this.patch = (url, data, callback) => {
const isFunc = itype.function(data);
if (!callback && isFunc) {
callback = data;
data = null;
}
const imgPosition = {
top: true
};
sendRequest({
method: 'PATCH',
url: FS + url,
data,
callback,
imgPosition,
});
};
this.write = (url, data, callback) => {
const isFunc = itype.function(data);
if (!callback && isFunc) {
callback = data;
data = null;
}
sendRequest({
method: 'PUT',
url: FS + url,
data,
callback,
imgPosition : { top: true }
});
};
this.read = (url, dataType, callback) => {
const isQuery = /\?/.test(url);
const isBeautify = /\?beautify$/.test(url);
const isMinify = /\?minify$/.test(url);
const notLog = !isQuery || isBeautify || isMinify;
const isFunc = itype.function(dataType);
if (!callback && isFunc) {
callback = dataType;
dataType = 'text';
}
sendRequest({
method: 'GET',
url: FS + url,
callback,
notLog,
dataType,
});
};
this.cp = (data, callback) => {
sendRequest({
method: 'PUT',
url: '/cp',
data,
callback,
imgPosition : { top: true }
});
};
this.pack = (data, callback) => {
sendRequest({
method : 'PUT',
url : '/pack',
data,
callback,
});
};
this.extract = function(data, callback) {
sendRequest({
method : 'PUT',
url : '/extract',
data : data,
callback : callback
});
};
this.mv = function(data, callback) {
sendRequest({
method : 'PUT',
url : '/mv',
data : data,
callback : callback,
imgPosition : { top: true }
});
};
this.Config = {
read: function(callback) {
sendRequest({
method : 'GET',
url : '/config',
callback : callback,
imgPosition : { top: true },
notLog : true
});
},
write: function(data, callback) {
sendRequest({
method : 'PATCH',
url : '/config',
data : data,
callback : callback,
imgPosition : { top: true }
});
}
};
this.Markdown = {
read : function(url, callback) {
sendRequest({
method : 'GET',
url : '/markdown' + url,
callback : callback,
imgPosition : { top: true },
notLog : true
});
},
render : function(data, callback) {
sendRequest({
method : 'PUT',
url : '/markdown',
data : data,
callback : callback,
imgPosition : { top: true },
notLog : true
});
}
};
function sendRequest(params) {
const p = params;
const prefixUrl = CloudCmd.PREFIX_URL;
p.url = prefixUrl + p.url;
p.url = encodeURI(p.url);
/*
* if we send ajax request -
* no need in hash so we escape #
*/
p.url = p.url.replace('#', '%23');
load.ajax({
method : p.method,
url : p.url,
data : p.data,
dataType : p.dataType,
error : (jqXHR) => {
const response = jqXHR.responseText;
const statusText = jqXHR.statusText;
const status = jqXHR.status;
const text = status === 404 ? response : statusText;
const encoded = encode(text);
Images.show.error(encoded); Images.show.error(encoded);
Dialog.alert(encoded);
return [e, data]; setTimeout(() => {
}; DOM.Dialog.alert(CloudCmd.TITLE, encoded);
}, 100);
export const remove = handleError(IO.remove); p.callback(Error(text));
export const patch = handleError(IO.patch); },
export const write = handleError(IO.write); success: (data) => {
export const createDirectory = handleError(IO.createDirectory); Images.hide();
export const read = handleError(IO.read);
export const copy = handleError(IO.copy);
export const pack = handleError(IO.pack);
export const extract = handleError(IO.extract);
export const move = handleError(IO.move);
export const rename = handleError(IO.rename);
export const Config = { if (!p.notLog)
read: handleError(IO.Config.read), CloudCmd.log(data);
write: handleError(IO.Config.write),
};
export const Markdown = { p.callback(null, data);
read: handleError(IO.Markdown.read), }
render: handleError(IO.Markdown.render), });
}; }
}

View file

@ -1,46 +1,52 @@
import {alert, prompt} from '#dom/dialog'; 'use strict';
import {getRegExp} from '#common/util';
import {getCurrentName} from './current-file.js'; /* global DOM */
import {
isSelected,
toggleSelectedFile,
} from './cmd.js';
let SelectType = '*.*'; let SelectType = '*.*';
const TITLE = 'Cloud Commander';
export const selectByPattern = async (msg, files) => { const {getRegExp} = require('../../common/util');
if (!files)
return;
module.exports = (msg, files) => {
const allMsg = `Specify file type for ${msg} selection`; const allMsg = `Specify file type for ${msg} selection`;
const [cancel, type] = await prompt(allMsg, SelectType); const cancel = false;
const {Dialog} = DOM;
if (cancel)
return;
Dialog.prompt(TITLE, allMsg, SelectType, {cancel}).then((type) => {
SelectType = type; SelectType = type;
const regExp = getRegExp(type); const regExp = getRegExp(type);
if (!files)
return;
let matches = 0; let matches = 0;
for (const current of files) { files.forEach((current) => {
const name = getCurrentName(current); const name = DOM.getCurrentName(current);
if (name === '..' || !regExp.test(name)) if (name === '..')
continue; return;
const isMatch = regExp.test(name);
if (!isMatch)
return;
++matches; ++matches;
let selected = isSelected(current); let isSelected = DOM.isSelected(current);
const shouldSel = msg === 'expand'; const shouldSel = msg === 'expand';
if (shouldSel) if (shouldSel)
selected = !selected; isSelected = !isSelected;
if (selected) if (isSelected)
toggleSelectedFile(current); DOM.toggleSelectedFile(current);
} });
if (!matches) if (!matches)
alert('No matches found!'); Dialog.alert('Select Files', 'No matches found!');
});
}; };

View file

@ -1,26 +1,66 @@
const {parse, stringify} = JSON; 'use strict';
export const set = (name, data) => { const itype = require('itype/legacy');
localStorage.setItem(name, data); const jonny = require('jonny/legacy');
}; const exec = require('execon');
const tryCatch = require('try-catch');
const setItem = localStorage.setItem.bind(localStorage);
export const setJson = (name, data) => { /** remove element */
localStorage.setItem(name, stringify(data)); module.exports.remove = (item, callback) => {
};
export const get = (name) => {
return localStorage.getItem(name);
};
export const getJson = (name) => {
const data = localStorage.getItem(name);
return parse(data);
};
export const clear = () => {
localStorage.clear();
};
export const remove = (item) => {
localStorage.removeItem(item); localStorage.removeItem(item);
exec(callback, null);
return module.exports;
}; };
module.exports.removeMatch = (string, callback) => {
const reg = RegExp('^' + string + '.*$');
const test = (a) => reg.test(a);
const remove = (a) => localStorage.removeItem(a);
Object.keys(localStorage)
.filter(test)
.forEach(remove);
exec(callback);
return module.exports;
};
/** если доступен localStorage и
* в нём есть нужная нам директория -
* записываем данные в него
*/
module.exports.set = (name, data, callback) => {
let str, error;
if (itype.object(data))
str = jonny.stringify(data);
if (name)
[error] = tryCatch(setItem, name, str || data);
exec(callback, error);
return module.exports;
},
/** Если доступен Storage принимаем из него данные*/
module.exports.get = (name, callback) => {
const ret = localStorage.getItem(name);
exec(callback, null, ret);
return module.exports;
},
/** функция чистит весь кэш для всех каталогов*/
module.exports.clear = (callback) => {
localStorage.clear();
exec(callback);
return module.exports;
};

View file

@ -1,105 +0,0 @@
import {test, stub} from 'supertape';
import * as storage from '#dom/storage';
const {stringify} = JSON;
test('cloudcmd: client: storage: set', async (t) => {
const {localStorage} = globalThis;
const setItem = stub();
globalThis.localStorage = {
setItem,
};
await storage.set('hello', 'world');
globalThis.localStorage = localStorage;
t.calledWith(setItem, ['hello', 'world'], 'should call setItem');
t.end();
});
test('cloudcmd: client: storage: get', async (t) => {
const {localStorage} = globalThis;
const getItem = stub().returns('world');
globalThis.localStorage = {
getItem,
};
const result = await storage.get('hello');
globalThis.localStorage = localStorage;
t.equal(result, 'world');
t.end();
});
test('cloudcmd: client: storage: getJson', async (t) => {
const {localStorage} = globalThis;
const expected = {
hello: 'world',
};
const getItem = stub().returns(stringify(expected));
globalThis.localStorage = {
getItem,
};
const result = await storage.getJson('hello');
globalThis.localStorage = localStorage;
t.deepEqual(result, expected);
t.end();
});
test('cloudcmd: client: storage: setJson', async (t) => {
const {localStorage} = globalThis;
const data = {
hello: 'world',
};
const expected = stringify(data);
const setItem = stub();
globalThis.localStorage = {
setItem,
};
await storage.setJson('hello', data);
globalThis.localStorage = localStorage;
t.calledWith(setItem, ['hello', expected]);
t.end();
});
test('cloudcmd: client: storage: remove', async (t) => {
const {localStorage} = globalThis;
const removeItem = stub();
globalThis.localStorage = {
removeItem,
};
await storage.remove('hello');
globalThis.localStorage = localStorage;
t.calledWith(removeItem, ['hello'], 'should call removeItem');
t.end();
});
test('cloudcmd: client: storage: clear', async (t) => {
const {localStorage} = globalThis;
const clear = stub();
globalThis.localStorage = {
clear,
};
await storage.clear();
globalThis.localStorage = localStorage;
t.calledWithNoArgs(clear, 'should call clear');
t.end();
});

View file

@ -1,19 +1,30 @@
'use strict';
/* global CloudCmd */ /* global CloudCmd */
import {eachSeries} from 'execon';
import wraptile from 'wraptile';
import * as load from '#dom/load';
import {alert} from '#dom/dialog';
import {FS} from '#common/cloudfunc';
import * as Images from '#dom/images';
import {getCurrentDirPath} from './current-file.js';
const loadFile = wraptile(_loadFile); const {eachSeries} = require('execon');
const wraptile = require('wraptile/legacy');
const DOM = require('.');
const load = require('./load');
const Images = require('./images');
const {alert} = require('./dialog');
const {FS} = require('../../common/cloudfunc');
const {TITLE} = CloudCmd;
const onEnd = wraptile(_onEnd); const onEnd = wraptile(_onEnd);
const loadFile = wraptile(_loadFile);
export const uploadFiles = (dir, files) => { const {
getCurrentDirPath: getPathWhenRootEmpty
} = DOM;
module.exports = (dir, files) => {
if (!files) { if (!files) {
files = dir; files = dir;
dir = getCurrentDirPath(); dir = getPathWhenRootEmpty();
} }
const n = files.length; const n = files.length;
@ -21,7 +32,7 @@ export const uploadFiles = (dir, files) => {
if (!n) if (!n)
return; return;
const array = Array.from(files); const array = [...files];
const {name} = files[0]; const {name} = files[0];
eachSeries(array, loadFile(dir, n), onEnd(name)); eachSeries(array, loadFile(dir, n), onEnd(name));
@ -29,17 +40,17 @@ export const uploadFiles = (dir, files) => {
function _onEnd(currentName) { function _onEnd(currentName) {
CloudCmd.refresh({ CloudCmd.refresh({
currentName, currentName
}); });
} }
function _loadFile(dir, n, file, callback) { function _loadFile(dir, n, file, callback) {
let i = 0; let i = 0;
const {name} = file; const name = file.name;
const path = dir + name; const path = dir + name;
const {prefixURL} = CloudCmd; const {PREFIX_URL} = CloudCmd;
const api = prefixURL + FS; const api = PREFIX_URL + FS;
const percent = (i, n, per = 100) => { const percent = (i, n, per = 100) => {
return Math.round(i * per / n); return Math.round(i * per / n);
@ -49,8 +60,7 @@ function _loadFile(dir, n, file, callback) {
++i; ++i;
load load.put(api + path, file)
.put(api + path, file)
.on('error', showError) .on('error', showError)
.on('end', callback) .on('end', callback)
.on('progress', (count) => { .on('progress', (count) => {
@ -63,5 +73,6 @@ function _loadFile(dir, n, file, callback) {
} }
function showError({message}) { function showError({message}) {
alert(message); alert(TITLE, message);
} }

View file

@ -1,44 +0,0 @@
/* global DOM */
/**
* Функция генерирует JSON из html-таблицы файлов и
* используеться при первом заходе в корень
*/
export const getJsonFromFileTable = () => {
const Info = DOM.CurrentInfo;
const path = DOM.getCurrentDirPath();
const infoFiles = Info.files || [];
const files = infoFiles
.filter(notParent)
.map(parse);
const fileTable = {
path,
files,
};
return fileTable;
};
const notParent = (current) => {
const name = DOM.getCurrentName(current);
return name !== '..';
};
const parse = (current) => {
const name = DOM.getCurrentName(current);
const size = DOM.getCurrentSize(current);
const owner = DOM.getCurrentOwner(current);
const mode = DOM.getCurrentMode(current);
const date = DOM.getCurrentDate(current);
const type = DOM.getCurrentType(current);
return {
name,
size,
mode,
owner,
date,
type,
};
};

83
client/input.js Normal file
View file

@ -0,0 +1,83 @@
'use strict';
const currify = require('currify/legacy');
const isType = currify((type, object, name) => {
return typeof object[name] === type;
});
const isBool = isType('boolean');
module.exports.getElementByName = getElementByName;
function getElementByName(selector, element) {
const str = `[data-name="js-${selector}"]`;
return element
.querySelector(str);
}
module.exports.getName = (element) => {
const name = element
.getAttribute('data-name')
.replace(/^js-/, '');
return name;
};
module.exports.convert = (config) => {
const result = {
...config
};
const array = Object.keys(result);
array
.filter(isBool(result))
.forEach((name) => {
const item = result[name];
result[name] = setState(item);
});
return result;
};
function setState(state) {
if (state)
return ' checked';
return '';
}
module.exports.getValue = (name, element) => {
const el = getElementByName(name, element);
const type = el.type;
switch(type) {
case 'checkbox':
return el.checked;
case 'number':
return Number(el.value);
default:
return el.value;
}
};
module.exports.setValue = (name, value, element) => {
const el = getElementByName(name, element);
const type = el.type;
switch(type) {
case 'checkbox':
el.checked = value;
break;
default:
el.value = value;
break;
}
};

View file

@ -1,15 +0,0 @@
export const createBinder = () => {
let binded = false;
return {
isBind() {
return binded;
},
setBind() {
binded = true;
},
unsetBind() {
binded = false;
},
};
};

View file

@ -1,28 +0,0 @@
import {test} from 'supertape';
import {createBinder} from './binder.js';
test('client: key: binder: isBind: default', (t) => {
const binder = createBinder();
t.notOk(binder.isBind(), 'should not be bind by default');
t.end();
});
test('client: key: binder: setBind', (t) => {
const binder = createBinder();
binder.setBind();
t.ok(binder.isBind(), 'should be bind');
t.end();
});
test('client: key: binder: unsetBind', (t) => {
const binder = createBinder();
binder.setBind();
binder.unsetBind();
t.notOk(binder.isBind(), 'should not be bind');
t.end();
});

View file

@ -1,96 +1,89 @@
/* global CloudCmd, DOM */ /* global CloudCmd, DOM */
import clipboard from '@cloudcmd/clipboard';
import {fullstore} from 'fullstore';
import * as Events from '#dom/events';
import * as Buffer from '../dom/buffer.js';
import * as KEY from './key.js';
import _vim from './vim/index.js';
import setCurrentByChar from './set-current-by-char.js';
import {createBinder} from './binder.js';
'use strict';
const Info = DOM.CurrentInfo;
const exec = require('execon');
const clipboard = require('@cloudcmd/clipboard');
const wraptile = require('wraptile/legacy');
const Events = require('../dom/events');
const Buffer = require('../dom/buffer');
const KEY = require('./key');
const vim = require('./vim');
const setCurrentByChar = require('./set-current-by-char');
const fullstore = require('fullstore/legacy');
const Chars = fullstore(); const Chars = fullstore();
const toggleVim = (keyCode, overrides = {}) => {
const {_config, config} = overrides;
if (!config('vim') && keyCode === KEY.ESC)
_config('vim', true);
};
const isUndefined = (a) => typeof a === 'undefined';
Chars([]); Chars([]);
const {assign} = Object; KeyProto.prototype = KEY;
const binder = createBinder(); CloudCmd.Key = KeyProto;
const bind = () => { function KeyProto() {
Events.addKey(listener, true); let Binded;
binder.setBind();
const Key = this;
this.isBind = () => {
return Binded;
}; };
export const Key = assign(binder, KEY, { this.setBind = () => {
bind, Binded = true;
}); };
export const _listener = listener; this.unsetBind = () => {
Binded = false;
};
this.bind = () => {
Events.addKey(listener);
Binded = true;
};
function getChar(event) { function getChar(event) {
/* /*
* event.keyIdentifier deprecated in chrome v51 * event.keyIdentifier deprecated in chrome v51
* but event.key is absent in chrome <= v51 * but event.key is absent in chrome <= v51
*/ */
const {
key,
shift,
keyCode,
keyIdentifier,
} = event;
const char = key || fromCharCode(keyIdentifier); if (event.key)
const symbol = getSymbol(shift, keyCode); return event.key;
return [symbol, char]; return fromCharCode(event.keyIdentifier);
} }
async function listener(event, overrides = {}) { function listener(event) {
const { const keyCode = event.keyCode;
config = CloudCmd.config,
_config = CloudCmd._config,
switchKey = _switchKey,
vim = _vim,
} = overrides;
const {keyCode} = event;
// strange chrome bug calls listener twice
// in second time event misses a lot fields
if (isUndefined(event.altKey))
return;
const alt = event.altKey; const alt = event.altKey;
const ctrl = event.ctrlKey; const ctrl = event.ctrlKey;
const shift = event.shiftKey;
const meta = event.metaKey; const meta = event.metaKey;
const isBetween = keyCode >= KEY.ZERO && keyCode <= KEY.Z; const isBetween = keyCode >= KEY.ZERO && keyCode <= KEY.Z;
const isNumpad = /Numpad/.test(event.code); const isNumpad = /Numpad/.test(event.code);
const [symbol, char] = getChar(event); let char = getChar(event);
let isSymbol = ~['.', '_', '-', '+', '='].indexOf(char);
if (!binder.isBind()) if (!isSymbol) {
isSymbol = getSymbol(shift, keyCode);
if (isSymbol)
char = isSymbol;
}
if (!Key.isBind())
return; return;
toggleVim(keyCode, { const isVim = CloudCmd.config('vim');
config,
_config,
});
const isVim = config('vim'); if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || isSymbol))
if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || symbol))
return setCurrentByChar(char, Chars); return setCurrentByChar(char, Chars);
Chars([]); Chars([]);
await switchKey(event); switchKey(event);
if (keyCode >= KEY.F1 && keyCode <= KEY.F10) if (keyCode >= KEY.F1 && keyCode <= KEY.F10)
return; return;
@ -110,41 +103,29 @@ function getSymbol(shift, keyCode) {
case KEY.EQUAL: case KEY.EQUAL:
return shift ? '+' : '='; return shift ? '+' : '=';
} }
return '';
} }
function fromCharCode(keyIdentifier) { function fromCharCode(keyIdentifier) {
const code = keyIdentifier.substring(2); const code = keyIdentifier.substring(2);
const hex = parseInt(code, 16); const hex = parseInt(code, 16);
const char = String.fromCharCode(hex);
return String.fromCharCode(hex); return char;
} }
async function _switchKey(event) { function switchKey(event) {
const Info = DOM.CurrentInfo; let i, isSelected, prev, next;
let i;
let isSelected;
let prev;
let next;
let current = Info.element; let current = Info.element;
let dataName; let dataName;
const { const name = Info.name;
name,
panel,
path,
isDir,
} = Info;
const { const {Operation} = CloudCmd;
Operation, const panel = Info.panel;
changeDir, const path = Info.path;
config, const isDir = Info.isDir;
} = CloudCmd;
const {keyCode} = event;
const keyCode = event.keyCode;
const alt = event.altKey; const alt = event.altKey;
const shift = event.shiftKey; const shift = event.shiftKey;
const ctrl = event.ctrlKey; const ctrl = event.ctrlKey;
@ -157,72 +138,64 @@ async function _switchKey(event) {
} }
switch (keyCode) { switch (keyCode) {
case KEY.TAB: case Key.TAB:
DOM.changePanel(); DOM.changePanel();
event.preventDefault(); event.preventDefault();
break; break;
case KEY.INSERT: case Key.INSERT:
DOM DOM .toggleSelectedFile(current)
.toggleSelectedFile(current)
.setCurrentFile(next); .setCurrentFile(next);
break; break;
case KEY.INSERT_MAC: case Key.INSERT_MAC:
DOM DOM .toggleSelectedFile(current)
.toggleSelectedFile(current)
.setCurrentFile(next); .setCurrentFile(next);
break; break;
case KEY.DELETE: case Key.DELETE:
if (shift) if (shift)
Operation.show('delete:silent'); Operation.show('delete:silent');
else else
Operation.show('delete'); Operation.show('delete');
break; break;
case KEY.ASTERISK: case Key.ASTERISK:
DOM.toggleAllSelectedFiles(current); DOM.toggleAllSelectedFiles(current);
break; break;
case KEY.PLUS: case Key.PLUS:
DOM.expandSelection(); DOM.expandSelection();
event.preventDefault(); event.preventDefault();
break; break;
case KEY.MINUS: case Key.MINUS:
DOM.shrinkSelection(); DOM.shrinkSelection();
event.preventDefault(); event.preventDefault();
break; break;
case KEY.F1: case Key.F1:
CloudCmd.Help.show(); CloudCmd.Help.show();
event.preventDefault(); event.preventDefault();
break; break;
case KEY.F2: case Key.F2:
CloudCmd.UserMenu.show(); DOM.renameCurrent(current);
break; break;
case KEY.F3: case Key.F3:
event.preventDefault(); if (shift)
CloudCmd.Markdown.show(path);
if (Info.isDir)
await changeDir(path);
else if (shift)
CloudCmd.View.show(null, {
raw: true,
});
else if (ctrlMeta) else if (ctrlMeta)
CloudCmd.sortPanel('name'); CloudCmd.sortPanel('name');
else else
CloudCmd.View.show(); CloudCmd.View.show();
event.preventDefault();
break; break;
case KEY.F4: case Key.F4:
if (config('vim')) if (shift)
CloudCmd.EditFileVim.show(); CloudCmd.EditFileVim.show();
else else
CloudCmd.EditFile.show(); CloudCmd.EditFile.show();
@ -230,7 +203,7 @@ async function _switchKey(event) {
event.preventDefault(); event.preventDefault();
break; break;
case KEY.F5: case Key.F5:
if (ctrlMeta) if (ctrlMeta)
CloudCmd.sortPanel('date'); CloudCmd.sortPanel('date');
else if (alt) else if (alt)
@ -241,18 +214,16 @@ async function _switchKey(event) {
event.preventDefault(); event.preventDefault();
break; break;
case KEY.F6: case Key.F6:
if (ctrlMeta) if (ctrlMeta)
CloudCmd.sortPanel('size'); CloudCmd.sortPanel('size');
else if (shift)
DOM.renameCurrent(current);
else else
Operation.show('move'); Operation.show('move');
event.preventDefault(); event.preventDefault();
break; break;
case KEY.F7: case Key.F7:
if (shift) if (shift)
DOM.promptNewFile(); DOM.promptNewFile();
else else
@ -261,26 +232,25 @@ async function _switchKey(event) {
event.preventDefault(); event.preventDefault();
break; break;
case KEY.F8: case Key.F8:
Operation.show('delete'); Operation.show('delete');
event.preventDefault(); event.preventDefault();
break; break;
case KEY.F9: case Key.F9:
if (alt) if (alt)
Operation.show('extract'); Operation.show('extract');
else else
CloudCmd.Menu.show(); CloudCmd.Menu.show();
event.preventDefault(); event.preventDefault();
break; break;
case KEY.F10: case Key.F10:
CloudCmd.Config.show(); CloudCmd.Config.show();
event.preventDefault(); event.preventDefault();
break; break;
case KEY.TRA: case Key.TRA:
event.preventDefault(); event.preventDefault();
if (shift) if (shift)
@ -294,33 +264,32 @@ async function _switchKey(event) {
event.preventDefault(); event.preventDefault();
break; break;
case KEY.SPACE: case Key.SPACE:
event.preventDefault();
if (!isDir || name === '..') if (!isDir || name === '..')
isSelected = true; isSelected = true;
else else
isSelected = DOM.isSelected(current); isSelected = DOM.isSelected(current);
if (!isSelected) exec.if(isSelected, () => {
await DOM.loadCurrentSize(current);
DOM.toggleSelectedFile(current); DOM.toggleSelectedFile(current);
}, (callback) => {
DOM.loadCurrentSize(callback, current);
});
event.preventDefault();
break; break;
case KEY.U: case Key.U:
if (ctrlMeta) { if (ctrlMeta) {
DOM.swapPanels(); DOM.swapPanels();
event.preventDefault(); event.preventDefault();
} }
break; break;
/* navigation on file table: * /* navigation on file table: *
* in case of pressing button 'up', * * in case of pressing button 'up', *
* select previous row */ * select previous row */
case KEY.UP: case Key.UP:
if (shift) if (shift)
DOM.toggleSelectedFile(current); DOM.toggleSelectedFile(current);
@ -330,7 +299,7 @@ async function _switchKey(event) {
/* in case of pressing button 'down', * /* in case of pressing button 'down', *
* select next row */ * select next row */
case KEY.DOWN: case Key.DOWN:
if (shift) if (shift)
DOM.toggleSelectedFile(current); DOM.toggleSelectedFile(current);
@ -338,7 +307,7 @@ async function _switchKey(event) {
event.preventDefault(); event.preventDefault();
break; break;
case KEY.LEFT: case Key.LEFT:
if (!alt) if (!alt)
return; return;
@ -351,7 +320,7 @@ async function _switchKey(event) {
break; break;
case KEY.RIGHT: case Key.RIGHT:
if (!alt) if (!alt)
return; return;
@ -366,19 +335,19 @@ async function _switchKey(event) {
/* in case of pressing button 'Home', * /* in case of pressing button 'Home', *
* go to top element */ * go to top element */
case KEY.HOME: case Key.HOME:
DOM.setCurrentFile(Info.first); DOM.setCurrentFile(Info.first);
event.preventDefault(); event.preventDefault();
break; break;
/* in case of pressing button 'End', select last element */ /* in case of pressing button 'End', select last element */
case KEY.END: case Key.END:
DOM.setCurrentFile(Info.last); DOM.setCurrentFile(Info.last);
event.preventDefault(); event.preventDefault();
break; break;
/* если нажали клавишу page down проматываем экран */ /* если нажали клавишу page down проматываем экран */
case KEY.PAGE_DOWN: case Key.PAGE_DOWN:
DOM.scrollByPages(panel, 1); DOM.scrollByPages(panel, 1);
for (i = 0; i < 30; i++) { for (i = 0; i < 30; i++) {
@ -393,7 +362,7 @@ async function _switchKey(event) {
break; break;
/* если нажали клавишу page up проматываем экран */ /* если нажали клавишу page up проматываем экран */
case KEY.PAGE_UP: case Key.PAGE_UP:
DOM.scrollByPages(panel, -1); DOM.scrollByPages(panel, -1);
for (i = 0; i < 30; i++) { for (i = 0; i < 30; i++) {
@ -407,26 +376,27 @@ async function _switchKey(event) {
event.preventDefault(); event.preventDefault();
break; break;
case KEY.ENTER: /* open directory */
case Key.ENTER:
if (Info.isDir) if (Info.isDir)
await changeDir(path); CloudCmd.loadDir({
else path: path === '/' ? '/' : path + '/'
CloudCmd.View.show(); });
break; break;
case KEY.BACKSPACE: case Key.BACKSPACE:
CloudCmd.goToParentDir(); CloudCmd.goToParentDir();
event.preventDefault(); event.preventDefault();
break; break;
case KEY.BACKSLASH: case Key.BACKSLASH:
if (ctrlMeta) if (ctrlMeta)
await changeDir('/'); CloudCmd.loadDir({
path: '/'
});
break; break;
case KEY.A: case Key.A:
if (ctrlMeta) { if (ctrlMeta) {
DOM.selectAllFiles(); DOM.selectAllFiles();
event.preventDefault(); event.preventDefault();
@ -434,7 +404,7 @@ async function _switchKey(event) {
break; break;
case KEY.G: case Key.G:
if (alt) { if (alt) {
DOM.goToDirectory(); DOM.goToDirectory();
event.preventDefault(); event.preventDefault();
@ -442,17 +412,9 @@ async function _switchKey(event) {
break; break;
case KEY.L: case Key.M:
if (ctrlMeta && shift) {
CloudCmd.logOut();
event.preventDefault();
}
break;
case KEY.M:
if (ctrlMeta) { if (ctrlMeta) {
if (config('vim')) if (shift)
CloudCmd.EditNamesVim.show(); CloudCmd.EditNamesVim.show();
else else
CloudCmd.EditNames.show(); CloudCmd.EditNames.show();
@ -462,7 +424,7 @@ async function _switchKey(event) {
break; break;
case KEY.P: case Key.P:
if (!ctrlMeta) if (!ctrlMeta)
return; return;
@ -472,7 +434,6 @@ async function _switchKey(event) {
.catch(CloudCmd.log); .catch(CloudCmd.log);
break; break;
/** /**
* обновляем страницу, * обновляем страницу,
* загружаем содержимое каталога * загружаем содержимое каталога
@ -480,65 +441,43 @@ async function _switchKey(event) {
* сервера, а не из кэша * сервера, а не из кэша
* (обновляем кэш) * (обновляем кэш)
*/ */
case KEY.R: case Key.R:
if (ctrlMeta) { if (ctrlMeta) {
CloudCmd.log('reloading page...\n'); CloudCmd.log('reloading page...\n');
CloudCmd.refresh(); CloudCmd.refresh();
event.preventDefault(); event.preventDefault();
} }
break; break;
case KEY.C: case Key.C:
if (ctrlMeta) if (ctrlMeta)
Buffer.copy(); Buffer.copy();
break; break;
case KEY.X: case Key.X:
if (ctrlMeta) if (ctrlMeta)
Buffer.cut(); Buffer.cut();
break; break;
case KEY.V: case Key.V:
if (ctrlMeta) if (ctrlMeta)
Buffer.paste(); Buffer.paste();
break; break;
case KEY.Z: case Key.Z:
if (ctrlMeta) if (ctrlMeta)
Buffer.clear(); Buffer.clear();
break;
case KEY.COLON:
CloudCmd.CommandLine.show();
event.preventDefault();
break; break;
/* чистим хранилище */ /* чистим хранилище */
case KEY.D: case Key.D:
if (ctrlMeta) { if (ctrlMeta) {
CloudCmd.log('clearing storage...'); CloudCmd.log('clearing storage...');
await DOM.Storage.clear(); DOM.Storage.clear(wraptile(CloudCmd.log, 'storage cleared'));
CloudCmd.log('storage cleared');
event.preventDefault(); event.preventDefault();
} }
break;
case KEY.DOT:
if (meta && shift) {
const showDotFiles = !CloudCmd.config('showDotFiles');
CloudCmd._config('showDotFiles', showDotFiles);
CloudCmd.refresh();
await DOM.RESTful.Config.write({
showDotFiles,
});
}
break; break;
} }
} }
}

View file

@ -1,56 +0,0 @@
import autoGlobals from 'auto-globals';
import supertape from 'supertape';
import {ESC} from './key.js';
import {Key, _listener} from './index.js';
import {getDOM, getCloudCmd} from './vim/globals.fixture.js';
const test = autoGlobals(supertape);
const {stub} = supertape;
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
test('cloudcmd: client: key: enable vim', async (t) => {
const vim = stub();
const config = stub().returns(true);
const _config = stub();
const event = {
keyCode: ESC,
key: 'Escape',
altKey: false,
};
Key.setBind();
await _listener(event, {
vim,
config,
_config,
switchKey: stub(),
});
t.calledWith(vim, ['Escape', event]);
t.end();
});
test('cloudcmd: client: key: disable vim', async (t) => {
const _config = stub();
const config = stub();
const event = {
keyCode: ESC,
key: 'Escape',
altKey: false,
};
Key.setBind();
await _listener(event, {
config,
_config,
switchKey: stub(),
});
t.calledWith(_config, ['vim', true]);
t.end();
});

View file

@ -1,58 +1,81 @@
export const BACKSPACE = 8; 'use strict';
export const TAB = 9;
export const ENTER = 13; module.exports = {
export const CAPSLOCK = 20; BACKSPACE : 8,
export const ESC = 27; TAB : 9,
export const SPACE = 32; ENTER : 13,
export const PAGE_UP = 33; ESC : 27,
export const PAGE_DOWN = 34;
export const END = 35; SPACE : 32,
export const HOME = 36; PAGE_UP : 33,
export const LEFT = 37; PAGE_DOWN : 34,
export const UP = 38; END : 35,
export const RIGHT = 39; HOME : 36,
export const DOWN = 40;
export const INSERT = 45; LEFT : 37,
export const DELETE = 46; UP : 38,
export const ZERO = 48; RIGHT : 39,
export const SEMICOLON = 52; DOWN : 40,
export const A = 65;
export const C = 67; INSERT : 45,
export const D = 68; DELETE : 46,
export const G = 71;
export const J = 74; ZERO : 48,
export const K = 75;
export const L = 76; SEMICOLON : 52,
export const M = 77;
export const O = 79; COLON : 54,
export const P = 80;
export const Q = 81; A : 65,
export const R = 82;
export const S = 83; C : 67,
export const T = 84; D : 68,
export const U = 85;
export const V = 86; G : 71,
export const X = 88;
export const Z = 90; J : 74,
export const INSERT_MAC = 96; K : 75,
export const ASTERISK = 106;
export const PLUS = 107; M : 77,
export const MINUS = 109;
export const F1 = 112; O : 79,
export const F2 = 113; P : 80,
export const F3 = 114; Q : 81,
export const F4 = 115; R : 82,
export const F5 = 116; S : 83,
export const F6 = 117; T : 84,
export const F7 = 118; U : 85,
export const F8 = 119;
export const F9 = 120; V : 86,
export const F10 = 121;
export const COLON = 186; X : 88,
export const EQUAL = 187;
export const HYPHEN = 189; Z : 90,
export const DOT = 190;
export const SLASH = 191; INSERT_MAC : 96,
export const TRA = 192;
export const BACKSLASH = 220; ASTERISK : 106,
export const BRACKET_CLOSE = 221; PLUS : 107,
MINUS : 109,
F1 : 112,
F2 : 113,
F3 : 114,
F4 : 115,
F5 : 116,
F6 : 117,
F7 : 118,
F8 : 119,
F9 : 120,
F10 : 121,
EQUAL : 187,
HYPHEN : 189,
DOT : 190,
SLASH : 191,
TRA : 192, /* Typewritten Reverse Apostrophe (`) */
BACKSLASH : 220,
BRACKET_CLOSE: 221
};

View file

@ -1,15 +1,18 @@
/* global DOM */ /* global DOM */
import {escapeRegExp} from '#common/util';
export default function setCurrentByChar(char, charStore) { 'use strict';
const Info = DOM.CurrentInfo; const Info = DOM.CurrentInfo;
const {escapeRegExp} = require('../../common/util');
module.exports = function setCurrentByChar(char, charStore) {
let firstByName; let firstByName;
let skipCount = 0; let skipCount = 0;
let set = false; let setted = false;
let i = 0; let i = 0;
const escapeChar = escapeRegExp(char); const escapeChar = escapeRegExp(char);
const regExp = new RegExp(`^${escapeChar}.*$`, 'i'); const regExp = new RegExp('^' + escapeChar + '.*$', 'i');
const {files} = Info; const {files} = Info;
const chars = charStore(); const chars = charStore();
const n = chars.length; const n = chars.length;
@ -28,21 +31,19 @@ export default function setCurrentByChar(char, charStore) {
const isTest = (a) => regExp.test(a); const isTest = (a) => regExp.test(a);
const isRoot = (a) => a === '..'; const isRoot = (a) => a === '..';
const not = (f) => (a) => !f(a); const not = (f) => (a) => !f(a);
const setCurrent = (name) => { const setCurrent = (name) => {
const byName = DOM.getCurrentByName(name); const byName = DOM.getCurrentByName(name);
if (!skipCount) { if (!skipCount) {
set = true; setted = true;
DOM.setCurrentFile(byName); DOM.setCurrentFile(byName);
return true; return true;
} } else {
if (skipN === skipCount) if (skipN === skipCount)
firstByName = byName; firstByName = byName;
--skipCount; --skipCount;
}
}; };
names names
@ -50,8 +51,9 @@ export default function setCurrentByChar(char, charStore) {
.filter(not(isRoot)) .filter(not(isRoot))
.some(setCurrent); .some(setCurrent);
if (!set) { if (!setted) {
DOM.setCurrentFile(firstByName); DOM.setCurrentFile(firstByName);
charStore([char]); charStore([char]);
} }
} };

View file

@ -1,36 +1,41 @@
import {fullstore} from 'fullstore'; 'use strict';
import limier from 'limier'; /* global DOM */
const fullstore = require('fullstore/legacy');
const limier = require('limier/legacy');
const Info = DOM.CurrentInfo;
const searchStore = fullstore([]); const searchStore = fullstore([]);
const searchIndex = fullstore(0); const searchIndex = fullstore(0);
export const find = (value, names) => { module.exports.find = (value) => {
const names = Info.files.map(DOM.getCurrentName);
const result = limier(value, names); const result = limier(value, names);
searchStore(result); searchStore(result);
searchIndex(0); searchIndex(0);
return result; DOM.setCurrentByName(result[0]);
}; };
export const findNext = () => { module.exports.findNext = () => {
const names = searchStore(); const names = searchStore();
const index = next(searchIndex(), names.length); const index = next(searchIndex(), names.length);
searchIndex(index); searchIndex(index);
return names[searchIndex()]; DOM.setCurrentByName(names[searchIndex()]);
}; };
export const findPrevious = () => { module.exports.findPrevious = () => {
const names = searchStore(); const names = searchStore();
const index = previous(searchIndex(), names.length); const index = previous(searchIndex(), names.length);
searchIndex(index); searchIndex(index);
return names[index]; DOM.setCurrentByName(names[index]);
}; };
export const _next = next; module.exports._next = next;
export const _previous = previous; module.exports._previous = previous;
function next(index, length) { function next(index, length) {
if (index === length - 1) if (index === length - 1)
@ -45,3 +50,4 @@ function previous(index, length) {
return --index; return --index;
} }

View file

@ -1,14 +1,58 @@
import test from 'supertape'; 'use strict';
import {getDOM} from './globals.fixture.js';
import { const test = require('tape');
_next, const diff = require('sinon-called-with-diff');
_previous, const sinon = diff(require('sinon'));
const dir = './';
const {
getDOM,
} = require('./globals.fixture');
global.DOM = getDOM();
const {
find, find,
findNext, findNext,
findPrevious, findPrevious,
} from './find.js'; _next,
_previous,
} = require(dir + 'find');
globalThis.DOM = getDOM(); test('cloudcmd: client: vim: find', (t) => {
const {DOM} = global;
const setCurrentByName = sinon.stub();
DOM.setCurrentByName = setCurrentByName;
DOM.Dialog.prompt = Promise.resolve.bind(Promise);
find('');
t.ok(setCurrentByName.calledWith(null), 'should call setCurrentByName');
t.end();
});
test('cloudcmd: client: vim: findNext', (t) => {
const {DOM} = global;
const setCurrentByName = sinon.stub();
DOM.setCurrentByName = setCurrentByName;
findNext();
t.ok(setCurrentByName.calledWith(null), 'should call setCurrentByName');
t.end();
});
test('cloudcmd: client: vim: findPrevious', (t) => {
const {DOM} = global;
const setCurrentByName = sinon.stub();
DOM.setCurrentByName = setCurrentByName;
findPrevious();
t.ok(setCurrentByName.calledWith(null), 'should call setCurrentByName');
t.end();
});
test('cloudcmd: client: vim: _next', (t) => { test('cloudcmd: client: vim: _next', (t) => {
const result = _next(1, 2); const result = _next(1, 2);
@ -17,13 +61,6 @@ test('cloudcmd: client: vim: _next', (t) => {
t.end(); t.end();
}); });
test('cloudcmd: client: vim: _next: increment', (t) => {
const result = _next(0, 2);
t.equal(result, 1, 'should return 1');
t.end();
});
test('cloudcmd: client: vim: _previous', (t) => { test('cloudcmd: client: vim: _previous', (t) => {
const result = _previous(0, 2); const result = _previous(0, 2);
@ -31,25 +68,3 @@ test('cloudcmd: client: vim: _previous', (t) => {
t.end(); t.end();
}); });
test('cloudcmd: client: vim: _previous: decrement', (t) => {
const result = _previous(1, 2);
t.equal(result, 0, 'should return 0');
t.end();
});
test('cloudcmd: client: vim: findNext: after find', (t) => {
find('a', ['alpha', 'beta', 'apple']);
const result = findNext();
t.equal(result, 'beta', 'should return next found name');
t.end();
});
test('cloudcmd: client: vim: findPrevious: after find', (t) => {
find('a', ['alpha', 'beta', 'apple']);
const result = findPrevious();
t.equal(result, 'apple', 'should return previous found name');
t.end();
});

View file

@ -1,19 +1,20 @@
const noop = () => {}; 'use strict';
export const getDOM = () => { module.exports.getDOM = () => {
const prompt = Promise.resolve.bind(Promise); const resolve = Promise.resolve.bind(Promise);
const CurrentInfo = { const CurrentInfo = {
element: {}, element: {},
files: [], files: [],
}; };
const noop = () => {};
const Buffer = { const Buffer = {
copy: noop, copy: noop,
paste: noop, paste: noop,
}; };
const Dialog = { const Dialog = {
prompt, prompt: resolve
}; };
return { return {
@ -27,20 +28,16 @@ export const getDOM = () => {
getCurrentName: noop, getCurrentName: noop,
setCurrentByName: noop, setCurrentByName: noop,
toggleSelectedFile: noop, toggleSelectedFile: noop,
prompNewDirectory: noop,
promptNewFile: noop,
}; };
}; };
export const getCloudCmd = () => { module.exports.getCloudCmd = () => {
const show = () => {}; const show = () => {};
return { return {
Operation: { Operation: {
show, show
}, }
};
};
config: noop,
_config: noop,
};
};

View file

@ -1,167 +1,199 @@
import vim from './vim.js'; 'use strict';
import * as finder from './find.js'; /* global CloudCmd, DOM */
import {
setCurrent,
selectFileNotParent,
} from './set-current.js';
export default (key, event, overrides = {}) => { const KEY = require('../key');
const defaults = { const Info = DOM.CurrentInfo;
...globalThis.DOM, const Dialog = DOM.Dialog;
...globalThis.CloudCmd,
};
const deps = { const fullstore = require('fullstore/legacy');
...defaults, const store = fullstore('');
...overrides, const visual = fullstore(false);
};
const operations = getOperations(event, deps);
vim(key, operations, deps);
};
const getOperations = (event, deps) => {
const { const {
Info = globalThis.DOM.CurrentInfo, find,
CloudCmd = globalThis.CloudCmd, findNext,
Operation, findPrevious,
unselectFiles, } = require('./find');
setCurrentFile,
setCurrentByName,
getCurrentName,
prompt = globalThis.DOM.Dialog.prompt,
preventDefault = event?.preventDefault?.bind(event),
stopImmediatePropagation = event?.preventDefault?.bind(event),
promptNewFile = globalThis.DOM.promptNewFile,
toggleSelectedFile,
Buffer = {},
createFindNext = _createFindNext,
createFindPrevious = _createFindPrevious,
createMakeFile = _createMakeFile,
renameCurrent,
} = deps;
return { const TITLE = 'Cloud Commander';
makeFile: createMakeFile({
promptNewFile,
preventDefault,
stopImmediatePropagation,
}),
findNext: createFindNext({
setCurrentByName,
}),
findPrevious: createFindPrevious({
setCurrentByName,
}),
escape: unselectFiles,
rename: () => {
event.preventDefault();
renameCurrent();
},
remove: () => {
Operation.show('delete');
},
operationCopy: () => {
event.preventDefault();
Operation.show('copy');
},
operationMove: () => {
event.preventDefault();
Operation.show('move');
},
makeDirectory: () => { const stopVisual = () => {
event.stopImmediatePropagation(); visual(false);
event.preventDefault(); };
globalThis.DOM.promptNewDir();
},
terminal: () => { const end = () => {
CloudCmd.Terminal.show(); store('');
}, };
edit: () => { const rmFirst = (a) => {
CloudCmd.EditFileVim.show(); return a
}, .split('')
.slice(1)
.join('');
};
copy: () => { module.exports = (key, event) => {
Buffer.copy();
unselectFiles();
},
select: () => {
const current = Info.element; const current = Info.element;
toggleSelectedFile(current); const keyCode = event.keyCode;
}, const prevStore = store();
paste: Buffer.paste, const value = store(prevStore.concat(key));
moveNext: ({count, isVisual, isDelete}) => { if (keyCode === KEY.ENTER)
setCurrent('next', { return end();
count,
isVisual, if (keyCode === KEY.ESC) {
isDelete, DOM.unselectFiles();
}, { visual(false);
Info, return end();
setCurrentFile, }
unselectFiles,
Operation, if (key === 'j') {
move('next', {
prevStore,
current,
}); });
},
movePrevious: ({count, isVisual, isDelete}) => { return end();
setCurrent('previous', { }
count,
isVisual, if (key === 'k') {
isDelete, move('previous', {
}, { prevStore,
Info, current,
setCurrentFile,
unselectFiles,
Operation,
}); });
},
find: async () => { return end();
preventDefault(); }
const [, value] = await prompt('Find', '');
if (!value) if (/gg/.test(value)) {
move('previous', {
current,
prevStore,
max: Infinity,
});
return end();
}
if (key === 'd' && (visual() || prevStore === 'd')) {
CloudCmd.Operation.show('delete');
stopVisual();
return end();
}
if (key === 'G') {
move('next', {
current,
prevStore,
max: Infinity,
});
return end();
}
if (key === 'y') {
if (!visual())
return end();
DOM.Buffer.copy();
stopVisual();
DOM.unselectFiles();
return end();
}
if (/^p$/i.test(key)) {
DOM.Buffer.paste();
return end();
}
if (/^v$/i.test(key)) {
DOM.toggleSelectedFile(current);
visual(!visual());
return end();
}
if (key === '/') {
event.preventDefault();
Dialog.prompt(TITLE, 'Find', '', {cancel: false})
.then(find);
return end();
}
if (key === 'n') {
findNext();
return end();
}
if (key === 'N') {
findPrevious();
return end();
}
};
module.exports.selectFile = selectFile;
function move(sibling, {max, current, prevStore}) {
const isDelete = prevStore[0] === 'd';
if (isDelete) {
visual(true);
prevStore = rmFirst(prevStore);
}
const n = max || getNumber(prevStore);
if (isNaN(n))
return; return;
const names = Info.files.map(getCurrentName); setCurrent({
const [result] = finder.find(value, names); n,
current,
sibling,
visual: visual(),
});
setCurrentByName(result); if (isDelete)
}, CloudCmd.Operation.show('delete');
}; }
};
export const selectFile = selectFileNotParent; function getNumber(value) {
if (!value)
return 1;
const _createFindPrevious = (overrides = {}) => () => { if (value === 'g')
const {setCurrentByName} = overrides; return 1;
const name = finder.findPrevious();
setCurrentByName(name); return parseInt(value);
}; }
const _createFindNext = (overrides = {}) => () => { function selectFile(current) {
const {setCurrentByName} = overrides; const name = DOM.getCurrentName(current);
const name = finder.findNext();
setCurrentByName(name); if (name === '..')
}; return;
const _createMakeFile = (overrides = {}) => () => { DOM.selectFile(current);
const { }
promptNewFile,
stopImmediatePropagation, function setCurrent({n, current, visual, sibling}) {
preventDefault, const select = visual ? selectFile : DOM.unselectFile;
} = overrides;
select(current);
const position = `${sibling}Sibling`;
for (let i = 0; i < n; i++) {
const next = current[position];
if (!next)
break;
current = next;
select(current);
}
DOM.setCurrentFile(current);
}
stopImmediatePropagation();
preventDefault();
promptNewFile();
};

View file

@ -1,756 +1,426 @@
import {test, stub} from 'supertape'; 'use strict';
import {getDOM, getCloudCmd} from './globals.fixture.js';
import vim, {selectFile as vimSelectFile} from './index.js';
import * as finder from './find.js';
globalThis.DOM = getDOM(); const test = require('tape');
globalThis.CloudCmd = getCloudCmd(); const diff = require('sinon-called-with-diff');
const sinon = diff(require('sinon'));
const dir = '../';
const KEY = require(dir + 'key');
const {assign} = Object; const {
const {DOM} = globalThis; getDOM,
const {Buffer} = DOM; getCloudCmd,
} = require('./globals.fixture');
global.DOM = getDOM();
global.CloudCmd = getCloudCmd();
const DOM = global.DOM;
const Buffer = DOM.Buffer;
const vim = require(dir + 'vim');
test('cloudcmd: client: key: set next file: no', (t) => { test('cloudcmd: client: key: set next file: no', (t) => {
const element = {}; const element = {
const setCurrentFile = stub();
const unselectFiles = stub();
const Info = {
element,
}; };
vim('j', {}, { const setCurrentFile = sinon.stub();
Info,
setCurrentFile, global.DOM.CurrentInfo.element = element;
unselectFiles, global.DOM.setCurrentFile = setCurrentFile;
});
vim('j', {});
t.ok(setCurrentFile.calledWith(element), 'should set next file');
t.calledWith(setCurrentFile, [element], 'should set next file');
t.end(); t.end();
}); });
test('cloudcmd: client: key: set next file current: j', async (t) => { test('cloudcmd: client: key: set next file current', (t) => {
const nextSibling = 'hello'; const nextSibling = 'hello';
const element = { const element = {
nextSibling, nextSibling
}; };
const setCurrentFile = stub(); const setCurrentFile = sinon.stub();
const Info = { global.DOM.CurrentInfo.element = element;
element, global.DOM.setCurrentFile = setCurrentFile;
};
await vim('j', {}, { vim('j', {});
Info,
setCurrentFile, t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file');
unselectFiles: stub(),
});
t.calledWith(setCurrentFile, [nextSibling], 'should set next file');
t.end(); t.end();
}); });
test('cloudcmd: client: key: set next file current: mjj', (t) => { test('cloudcmd: client: key: set next file current', (t) => {
const nextSibling = 'hello'; const nextSibling = 'hello';
const element = { const element = {
nextSibling, nextSibling
}; };
const setCurrentFile = stub(); const setCurrentFile = sinon.stub();
const Info = { global.DOM.CurrentInfo.element = element;
element, global.DOM.setCurrentFile = setCurrentFile;
};
const deps = { vim('m', {});
Info, vim('j', {});
setCurrentFile, vim('j', {});
unselectFiles: stub(),
};
vim('m', {}, deps); t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file');
vim('j', {}, deps);
vim('j', {}, deps);
t.calledWith(setCurrentFile, [nextSibling], 'should set next file');
t.end(); t.end();
}); });
test('cloudcmd: client: key: set next file current: g', (t) => { test('cloudcmd: client: key: set next file current: g', (t) => {
const nextSibling = 'hello'; const nextSibling = 'hello';
const element = { const element = {
nextSibling, nextSibling
}; };
const setCurrentFile = stub(); const setCurrentFile = sinon.stub();
const Info = { global.DOM.CurrentInfo.element = element;
element, global.DOM.setCurrentFile = setCurrentFile;
};
const deps = { vim('g', {});
Info, vim('j', {});
setCurrentFile,
unselectFiles: stub(),
};
vim('g', {}, deps); t.ok(setCurrentFile.calledWith(nextSibling), 'should ignore g');
vim('j', {}, deps);
t.calledWith(setCurrentFile, [nextSibling], 'should ignore g');
t.end(); t.end();
}); });
test('cloudcmd: client: key: set +2 file current', (t) => { test('cloudcmd: client: key: set +2 file current', (t) => {
const last = {}; const last = {};
const setCurrentFile = stub(); const nextSibling = {
const element = {}; nextSibling: last
};
const Info = { const element = {
element, nextSibling
}; };
const deps = { const setCurrentFile = sinon.stub();
setCurrentFile,
Info, global.DOM.CurrentInfo.element = element;
unselectFiles: stub(), global.DOM.setCurrentFile = setCurrentFile;
};
const event = {}; const event = {};
vim('2', event, deps); vim('2', event);
vim('j', event, deps); vim('j', event);
t.ok(setCurrentFile.calledWith(last), 'should set next file');
t.calledWith(setCurrentFile, [last], 'should set next file');
t.end(); t.end();
}); });
test('cloudcmd: client: key: select +2 files from current before delete', (t) => { test('cloudcmd: client: key: select +2 files from current before delete', (t) => {
const last = {}; const last = {};
const nextSibling = { const nextSibling = {
nextSibling: last, nextSibling: last
}; };
const element = { const element = {
nextSibling, nextSibling
}; };
const setCurrentFile = stub(); const setCurrentFile = sinon.stub();
const Info = { global.DOM.CurrentInfo.element = element;
element, global.DOM.setCurrentFile = setCurrentFile;
}; global.DOM.selectFile = sinon.stub();
global.DOM.getCurrentName = () => false;
const Operation = { global.CloudCmd.Operation.show = sinon.stub();
show: stub(),
};
const selectFile = stub();
const getCurrentName = stub().returns('x');
const event = {}; const event = {};
const deps = { vim('d', event);
Info, vim('2', event);
setCurrentFile, vim('j', event);
selectFile,
getCurrentName,
Operation,
};
vim('d', event, deps); t.ok(setCurrentFile.calledWith(last), 'should set next file');
vim('2', event, deps);
vim('j', event, deps);
t.calledWith(setCurrentFile, [last], 'should set next file');
t.end(); t.end();
}); });
test('cloudcmd: client: key: delete +2 files from current', (t) => { test('cloudcmd: client: key: delete +2 files from current', (t) => {
const last = {}; const last = {};
const nextSibling = { const nextSibling = {
nextSibling: last, nextSibling: last
}; };
const element = { const element = {
nextSibling, nextSibling
}; };
const setCurrentFile = stub(); const setCurrentFile = sinon.stub();
const show = stub(); const show = sinon.stub();
const deps = { global.DOM.CurrentInfo.element = element;
Info: { global.DOM.setCurrentFile = setCurrentFile;
element, global.DOM.selectFile = sinon.stub();
}, global.DOM.getCurrentName = () => false;
Operation: { global.CloudCmd.Operation.show = show;
show,
},
setCurrentFile,
selectFile: stub(),
getCurrentName: stub().returns('x'),
unselectFiles: stub(),
};
const event = {}; const event = {};
vim('d', event, deps); vim('d', event);
vim('2', event, deps); vim('2', event);
vim('j', event, deps); vim('j', event);
t.ok(show.calledWith('delete'), 'should call delete');
t.calledWith(show, ['delete'], 'should call delete');
t.end(); t.end();
}); });
test('cloudcmd: client: key: set previous file current', (t) => { test('cloudcmd: client: key: set previous file current', (t) => {
const previousSibling = 'hello'; const previousSibling = 'hello';
const element = { const element = {
previousSibling, previousSibling
}; };
const setCurrentFile = stub(); const setCurrentFile = sinon.stub();
const unselectFiles = stub();
const Info = { global.DOM.CurrentInfo.element = element;
element, global.DOM.setCurrentFile = setCurrentFile;
};
const deps = { vim('k', {});
Info,
setCurrentFile,
unselectFiles,
};
vim('k', {}, deps); t.ok(setCurrentFile.calledWith(previousSibling), 'should set previous file');
t.calledWith(setCurrentFile, [previousSibling], 'should set previous file');
t.end(); t.end();
}); });
test('cloudcmd: client: key: copy: no', (t) => { test('cloudcmd: client: key: copy: no', (t) => {
const copy = stub(); const copy = sinon.stub();
vim('y', {}, { Buffer.copy = copy;
unselectFiles: stub(),
Buffer: { vim('y', {});
copy,
}, t.notOk(copy.called, 'should not copy files');
});
t.notCalled(copy, 'should not copy files');
t.end(); t.end();
}); });
test('cloudcmd: client: key: copy', (t) => { test('cloudcmd: client: key: copy', (t) => {
const copy = stub(); const copy = sinon.stub();
const Info = {
element: {},
};
const toggleSelectedFile = stub(); Buffer.copy = copy;
const unselectFiles = stub();
const deps = { vim('v', {});
Info, vim('y', {});
unselectFiles,
toggleSelectedFile,
Buffer: {
copy,
},
};
vim('v', {}, deps); t.ok(copy.calledWith(), 'should copy files');
vim('y', {}, deps);
t.calledWithNoArgs(copy, 'should copy files');
t.end(); t.end();
}); });
test('cloudcmd: client: key: copy: unselectFiles', (t) => { test('cloudcmd: client: key: copy: unselectFiles', (t) => {
const unselectFiles = stub(); const unselectFiles = sinon.stub();
const Info = {
element: {},
};
const toggleSelectedFile = stub(); DOM.unselectFiles = unselectFiles;
const deps = { vim('v', {});
Info, vim('y', {});
unselectFiles,
toggleSelectedFile,
Buffer: {
copy: stub(),
},
};
vim('v', {}, deps); t.ok(unselectFiles.calledWith(), 'should unselect files');
vim('y', {}, deps);
t.calledWithNoArgs(unselectFiles, 'should unselect files');
t.end(); t.end();
}); });
test('cloudcmd: client: key: paste', (t) => { test('cloudcmd: client: key: paste', (t) => {
const paste = stub(); const paste = sinon.stub();
Buffer.paste = paste; Buffer.paste = paste;
vim('p', {}, { vim('p', {});
Buffer,
}); t.ok(paste.calledWith(), 'should paste files');
t.calledWithNoArgs(paste, 'should paste files');
t.end(); t.end();
}); });
test('cloudcmd: client: key: selectFile: ..', (t) => { test('cloudcmd: client: key: selectFile: ..', (t) => {
const getCurrentName = stub().returns('..'); const selectFile = sinon.stub();
const selectFile = stub(); const getCurrentName = sinon.stub();
DOM.selectFile = selectFile;
DOM.getCurrentName = () => '..';
const current = {}; const current = {};
vim.selectFile(current);
vimSelectFile(current, { t.notOk(getCurrentName.called, 'should not call selectFile');
selectFile,
getCurrentName,
});
t.notCalled(selectFile, 'should not call selectFile');
t.end(); t.end();
}); });
test('cloudcmd: client: key: selectFile', (t) => { test('cloudcmd: client: key: selectFile', (t) => {
const selectFile = stub(); const selectFile = sinon.stub();
const getCurrentName = stub().returns('x');
DOM.selectFile = selectFile;
DOM.getCurrentName = (a) => a.name;
const current = {}; const current = {};
vimSelectFile(current, { vim.selectFile(current);
selectFile,
getCurrentName,
});
t.calledWith(selectFile, [current], 'should call selectFile'); t.ok(selectFile.calledWith(current), 'should call selectFile');
t.end(); t.end();
}); });
test('cloudcmd: client: key: set last file current: shift + g', async (t) => { test('cloudcmd: client: key: set last file current', (t) => {
const last = 'last'; const last = 'last';
const nextSibling = { const nextSibling = {
nextSibling: last, nextSibling: last
}; };
const element = { const element = {
nextSibling, nextSibling
}; };
const setCurrentFile = stub(); const setCurrentFile = sinon.stub();
await vim('G', {}, { global.DOM.CurrentInfo.element = element;
Info: { global.DOM.setCurrentFile = setCurrentFile;
element,
}, vim('G', {});
setCurrentFile,
unselectFiles: stub(), t.ok(setCurrentFile.calledWith(last), 'should set last file');
});
t.calledWith(setCurrentFile, [last], 'should set last file');
t.end(); t.end();
}); });
test('cloudcmd: client: key: set last file current: $', (t) => { test('cloudcmd: client: key: set first file current', (t) => {
const last = 'last';
const nextSibling = {
nextSibling: last,
};
const element = {
nextSibling,
};
const setCurrentFile = stub();
vim('$', {}, {
Info: {
element,
},
setCurrentFile,
unselectFiles: stub(),
});
t.calledWith(setCurrentFile, [last], 'should set last file');
t.end();
});
test('cloudcmd: client: key: set first file current: gg', (t) => {
const first = 'first'; const first = 'first';
const previousSibling= { const previousSibling= {
previousSibling: first, previousSibling: first
}; };
const element = { const element = {
previousSibling, previousSibling
}; };
const Operation = { const setCurrentFile = sinon.stub();
show: stub(),
};
const unselectFiles = stub(); global.DOM.CurrentInfo.element = element;
const setCurrentFile = stub(); global.DOM.setCurrentFile = setCurrentFile;
const deps = { vim('g', {});
Operation, vim('g', {});
unselectFiles,
setCurrentFile,
Info: {
element,
},
};
vim('g', {}, deps); t.ok(setCurrentFile.calledWith(first), 'should set first file');
vim('g', {}, deps);
t.calledWith(setCurrentFile, [first], 'should set first file');
t.end();
});
test('cloudcmd: client: key: set first file current: ^', async (t) => {
const first = 'first';
const previousSibling = {
previousSibling: first,
};
const element = {
previousSibling,
};
const Operation = {
show: stub(),
};
const unselectFiles = stub();
const setCurrentFile = stub();
const deps = {
setCurrentFile,
Info: {
element,
},
unselectFiles,
Operation,
};
await vim('^', {}, deps);
t.calledWith(setCurrentFile, [first], 'should set first file');
t.end(); t.end();
}); });
test('cloudcmd: client: key: visual', (t) => { test('cloudcmd: client: key: visual', (t) => {
const element = {}; const element = {
const toggleSelectedFile = stub();
const Info = {
element,
}; };
vim('v', {}, { const toggleSelectedFile = sinon.stub();
Info,
toggleSelectedFile, global.DOM.CurrentInfo.element = element;
}); global.DOM.toggleSelectedFile = toggleSelectedFile;
vim('v', {});
t.ok(toggleSelectedFile.calledWith(element), 'should toggle selection');
t.calledWith(toggleSelectedFile, [element], 'should toggle selection');
t.end(); t.end();
}); });
test('cloudcmd: client: key: ESC', (t) => { test('cloudcmd: client: key: ESC', (t) => {
const element = {}; const element = {
const unselectFiles = stub();
const Info = {
element,
}; };
vim('Escape', null, { const unselectFiles = sinon.stub();
Info,
unselectFiles, global.DOM.CurrentInfo.element = element;
global.DOM.unselectFiles = unselectFiles ;
vim('', {
keyCode: KEY.ESC
}); });
t.calledWithNoArgs(unselectFiles, 'should toggle selection'); t.ok(unselectFiles.calledWith(), 'should toggle selection');
t.end(); t.end();
}); });
test('cloudcmd: client: key: Enter', async (t) => { test('cloudcmd: client: key: Enter', (t) => {
const nextSibling = 'hello'; const nextSibling = 'hello';
const element = { const element = {
nextSibling, nextSibling
}; };
const unselectFiles = stub(); const setCurrentFile = sinon.stub();
const setCurrentFile = stub();
const Info = { DOM.CurrentInfo.element = element;
element, DOM.setCurrentFile = setCurrentFile;
};
await vim('Enter', null, { vim('', {
Info, keyCode: KEY.ENTER
setCurrentFile,
unselectFiles,
}); });
await vim('j', null, { vim('j', {});
Info,
setCurrentFile, t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file');
unselectFiles,
});
t.calledWith(setCurrentFile, [nextSibling], 'should set next file');
t.end(); t.end();
}); });
test('cloudcmd: client: key: /', (t) => { test('cloudcmd: client: key: /', (t) => {
const preventDefault = stub(); const preventDefault = sinon.stub();
const element = {}; const element = {};
const Info = { DOM.CurrentInfo.element = element;
element, DOM.getCurrentName = () => '';
files: [],
};
const getCurrentName = stub().returns(''); vim('/', {
preventDefault
const event = {
preventDefault,
};
const prompt = stub().returns([]);
vim('/', event, {
getCurrentName,
Info,
prompt,
}); });
t.calledWithNoArgs(preventDefault); t.ok(preventDefault.calledWith(), 'should call preventDefault');
t.end();
});
test('cloudcmd: client: find', (t) => {
assign(DOM.Dialog, {
prompt: stub().returns([]),
});
const setCurrentByName = stub();
assign(DOM, {
setCurrentByName,
});
const event = {
preventDefault: stub(),
};
vim('/', event);
t.notCalled(setCurrentByName);
t.end(); t.end();
}); });
test('cloudcmd: client: key: n', (t) => { test('cloudcmd: client: key: n', (t) => {
const findNext = stub(); const findNext = sinon.stub();
const createFindNext = stub().returns(findNext);
const event = {}; clean(dir + 'vim');
stub(dir + 'vim/find', {
vim('n', event, { findNext
createFindNext,
}); });
t.calledWithNoArgs(findNext, 'should call findNext'); const vim = require(dir + 'vim');
const event = {};
vim('n', event);
t.ok(findNext.calledWith(), 'should call findNext');
t.end(); t.end();
}); });
test('cloudcmd: client: key: N', (t) => { test('cloudcmd: client: key: N', (t) => {
const findPrevious = stub(); const findPrevious = sinon.stub();
const createFindPrevious = stub().returns(findPrevious);
clean(dir + 'vim');
stub(dir + 'vim/find', {
findPrevious,
});
const vim = require(dir + 'vim');
const event = {}; const event = {};
vim('N', event, { vim('N', event);
createFindPrevious,
});
t.calledWithNoArgs(findPrevious); t.ok(findPrevious.calledWith(), 'should call findPrevious');
t.end(); t.end();
}); });
test('cloudcmd: client: key: make directory', async (t) => { function clean(path) {
const {DOM} = globalThis; delete require.cache[require.resolve(path)];
}
assign(DOM, { function stub(name, fn) {
promptNewDir: stub(), require.cache[require.resolve(name)].exports = fn;
}); }
const event = {
stopImmediatePropagation: stub(),
preventDefault: stub(),
};
await vim('m', event);
await vim('d', event);
t.calledWithNoArgs(DOM.promptNewDir);
t.end();
});
test('cloudcmd: client: key: make file', (t) => {
const promptNewFile = stub();
const event = {
stopImmediatePropagation: stub(),
preventDefault: stub(),
};
vim('m', event);
vim('f', event, {
promptNewFile,
});
t.calledWithNoArgs(promptNewFile);
t.end();
});
test('cloudcmd: client: vim: terminal', (t) => {
const CloudCmd = {
Terminal: {
show: stub(),
},
};
const event = {};
vim('t', event, {
CloudCmd,
});
vim('t', event, {
CloudCmd,
});
t.calledWithNoArgs(CloudCmd.Terminal.show);
t.end();
});
test('cloudcmd: client: vim: edit', async (t) => {
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
const {CloudCmd} = globalThis;
assign(CloudCmd, {
EditFileVim: {
show: stub(),
},
});
const event = {};
await vim('e', event);
t.calledWithNoArgs(CloudCmd.EditFileVim.show);
t.end();
});
test('cloudcmd: client: vim: rename', async (t) => {
const DOM = getDOM();
const renameCurrent = stub();
assign(DOM, {
renameCurrent,
});
const event = {
preventDefault: stub(),
};
await vim('rr', event, DOM);
t.calledWithNoArgs(renameCurrent);
t.end();
});
test('cloudcmd: client: key: cc: operationCopy', (t) => {
const show = stub();
const preventDefault = stub();
const Operation = {
show,
};
const event = {
preventDefault,
};
vim('c', event, {
Operation,
});
vim('c', event, {
Operation,
});
t.calledWith(show, ['copy'], 'should show copy operation');
t.end();
});
test('cloudcmd: client: key: mm: operationMove', (t) => {
const show = stub();
const preventDefault = stub();
const Operation = {
show,
};
const event = {
preventDefault,
};
vim('m', event, {
Operation,
});
vim('m', event, {
Operation,
});
t.calledWith(show, ['move'], 'should show move operation');
t.end();
});
test('cloudcmd: client: key: n: findNext: real', (t) => {
const setCurrentByName = stub();
finder.find('a', ['alpha', 'beta', 'apple']);
const event = {};
vim('n', event, {
setCurrentByName,
});
t.calledWith(setCurrentByName, ['beta'], 'should set current by next found name');
t.end();
});
test('cloudcmd: client: key: N: findPrevious: real', (t) => {
const setCurrentByName = stub();
finder.find('a', ['alpha', 'beta', 'apple']);
const event = {};
vim('N', event, {
setCurrentByName,
});
t.calledWith(setCurrentByName, ['apple'], 'should set current by previous found name');
t.end();
});

View file

@ -1,33 +0,0 @@
/* global DOM */
export function selectFileNotParent(current, {getCurrentName, selectFile} = DOM) {
const name = getCurrentName(current);
if (name === '..')
return;
selectFile(current);
}
export const setCurrent = (sibling, {count, isVisual, isDelete}, {Info, setCurrentFile, unselectFiles, Operation}) => {
let current = Info.element;
const select = isVisual ? selectFileNotParent : unselectFiles;
select(current);
const position = `${sibling}Sibling`;
for (let i = 0; i < count; i++) {
const next = current[position];
if (!next)
break;
current = next;
select(current);
}
setCurrentFile(current);
if (isDelete)
Operation.show('delete');
};

View file

@ -1,220 +0,0 @@
import {fullstore} from 'fullstore';
const store = fullstore('');
const visual = fullstore(false);
const stopVisual = () => {
visual(false);
};
const end = () => {
store('');
};
const rmFirst = (a) => {
return a
.split('')
.slice(1)
.join('');
};
const noop = () => {};
export default (key, operations = {}) => {
const prevStore = store();
const isVisual = visual();
const value = store(prevStore.concat(key));
const {
escape = noop,
moveNext = noop,
movePrevious = noop,
remove = noop,
copy = noop,
paste = noop,
select = noop,
find = noop,
findNext = noop,
findPrevious = noop,
makeFile = noop,
makeDirectory = noop,
terminal = noop,
edit = noop,
operationCopy = noop,
operationMove = noop,
rename = noop,
} = operations;
if (key === 'Enter')
return end();
if (key === 'Escape') {
visual(false);
escape();
return end();
}
if (key === 'j' || key === 'w') {
const {
count,
isDelete,
isVisual,
} = handleDelete(prevStore);
!isNaN(count) && moveNext({
count,
isVisual,
isDelete,
});
return end();
}
if (key === 'k' || key === 'b') {
const {
count,
isDelete,
isVisual,
} = handleDelete(prevStore);
!isNaN(count) && movePrevious({
count,
isVisual,
isDelete,
});
return end();
}
if (value === 'gg' || key === '^') {
const {isDelete, isVisual} = handleDelete(prevStore);
movePrevious({
count: Infinity,
isVisual,
isDelete,
});
return end();
}
if (value === 'md') {
makeDirectory();
return end();
}
if (value === 'tt') {
terminal();
return end();
}
if (value === 'e') {
edit();
return end();
}
if (value === 'cc') {
operationCopy();
return end();
}
if (value === 'mm') {
operationMove();
return end();
}
if (value === 'mf') {
makeFile();
return end();
}
if (value === 'rr') {
rename();
return end();
}
if (key === 'd' && (visual() || prevStore === 'd')) {
stopVisual();
remove();
return end();
}
if (key === 'G' || key === '$') {
moveNext({
count: Infinity,
isVisual,
});
return end();
}
if (key === 'y') {
if (!visual())
return end();
stopVisual();
copy();
return end();
}
if (/^p$/i.test(key)) {
paste();
return end();
}
if (/^v$/i.test(key)) {
visual(!visual());
select();
return end();
}
if (key === '/') {
find();
return end();
}
if (key === 'n') {
findNext();
return end();
}
if (key === 'N') {
findPrevious();
return end();
}
if (key === ' ')
return end();
};
function handleDelete(prevStore) {
const isDelete = prevStore[0] === 'd';
if (isDelete) {
visual(true);
prevStore = rmFirst(prevStore);
}
const count = getNumber(prevStore);
const isVisual = visual();
return {
count,
isDelete,
isVisual,
};
}
function getNumber(value) {
if (!value)
return 1;
if (value === 'g')
return 1;
return parseInt(value);
}

View file

@ -1,100 +0,0 @@
import {test, stub} from 'supertape';
import vim from './vim.js';
test('vim: no operations', (t) => {
const result = vim('hello', {});
t.notOk(result);
t.end();
});
test('vim: space', (t) => {
const moveNext = stub();
vim(' ');
vim('j', {
moveNext,
});
const args = [{
count: 1,
isDelete: false,
isVisual: false,
}];
t.calledWith(moveNext, args);
t.end();
});
test('vim: ^', (t) => {
const movePrevious = stub();
vim('^', {
movePrevious,
});
const expected = {
count: Infinity,
isVisual: false,
isDelete: false,
};
t.calledWith(movePrevious, [expected], 'should call movePrevious');
t.end();
});
test('vim: cc', (t) => {
const operationCopy = stub();
vim('cc', {
operationCopy,
});
t.calledWithNoArgs(operationCopy);
t.end();
});
test('vim: mm', (t) => {
const operationMove = stub();
vim('mm', {
operationMove,
});
t.calledWithNoArgs(operationMove);
t.end();
});
test('vim: w', (t) => {
const moveNext = stub();
vim('w', {
moveNext,
});
const expected = {
count: 1,
isVisual: false,
isDelete: false,
};
t.calledWith(moveNext, [expected], 'should call moveNext');
t.end();
});
test('vim: b', (t) => {
const movePrevious = stub();
vim('b', {
movePrevious,
});
const expected = {
count: 1,
isVisual: false,
isDelete: false,
};
t.calledWith(movePrevious, [expected], 'should call movePrevious');
t.end();
});

View file

@ -1,10 +1,11 @@
import currify from 'currify'; 'use strict';
export const getIndex = currify((array, item) => { module.exports = (array, item) => {
const index = array.indexOf(item); const index = array.indexOf(item);
if (!~index) if (!~index)
return 0; return 0;
return index; return index;
}); };

View file

@ -1,4 +1,6 @@
export const getRange = (indexFrom, indexTo, files) => { 'use strict';
module.exports = (indexFrom, indexTo, files) => {
if (indexFrom < indexTo) if (indexFrom < indexTo)
return files.slice(indexFrom, indexTo + 1); return files.slice(indexFrom, indexTo + 1);
@ -7,3 +9,4 @@ export const getRange = (indexFrom, indexTo, files) => {
return [files[indexFrom]]; return [files[indexFrom]];
}; };

View file

@ -1,52 +0,0 @@
import test from 'supertape';
import {getRange} from './get-range.js';
test('cloudcmd: client: listeners: getRange: direct', (t) => {
const expected = [
'hello',
'world',
];
const files = [
...expected,
'how',
'come',
];
const result = getRange(0, 1, files);
t.deepEqual(result, expected, 'should return range');
t.end();
});
test('cloudcmd: client: listeners: getRange: reverse', (t) => {
const expected = [
'hello',
'world',
];
const files = [
...expected,
'how',
'come',
];
const result = getRange(1, 0, files);
t.deepEqual(result, expected, 'should return range');
t.end();
});
test('cloudcmd: client: listeners: getRange: one', (t) => {
const expected = ['hello'];
const files = [
...expected,
'how',
'come',
];
const result = getRange(0, 0, files);
t.deepEqual(result, expected, 'should return range');
t.end();
});

View file

@ -1,30 +1,38 @@
/* global DOM, CloudCmd */ /* global DOM, CloudCmd */
import exec from 'execon';
import itype from 'itype'; 'use strict';
import currify from 'currify';
import {tryToCatch} from 'try-to-catch'; const exec = require('execon');
import clipboard from '@cloudcmd/clipboard'; const itype = require('itype/legacy');
import * as Events from '#dom/events'; const currify = require('currify/legacy');
import {uploadFiles} from '#dom/upload-files'; const clipboard = require('@cloudcmd/clipboard');
import {FS} from '#common/cloudfunc';
import {getRange} from './get-range.js'; const getRange = require('./get-range');
import {getIndex} from './get-index.js'; const getIndex = currify(require('./get-index'));
const uploadFiles = require('../dom/upload-files');
const {
FS,
apiURL,
} = require('../../common/cloudfunc');
const NBSP_REG = RegExp(String.fromCharCode(160), 'g'); const NBSP_REG = RegExp(String.fromCharCode(160), 'g');
const SPACE = ' '; const SPACE = ' ';
export async function init() { module.exports.init = () => {
contextMenu(); contextMenu();
dragndrop(); dragndrop();
unload(); unload();
pop(); pop();
resize(); resize();
config();
header(); header();
await config(); };
}
CloudCmd.Listeners = module.exports;
const unselect = (event) => { const unselect = (event) => {
const isMac = /Mac/.test(globalThis.navigator.platform); const isMac = /Mac/.test(window.navigator.platform);
const { const {
shiftKey, shiftKey,
metaKey, metaKey,
@ -38,13 +46,19 @@ const unselect = (event) => {
}; };
const execAll = currify((funcs, event) => { const execAll = currify((funcs, event) => {
for (const fn of funcs) funcs.forEach((fn) => {
fn(event); fn(event);
}); });
});
const Info = DOM.CurrentInfo;
const Events = DOM.Events;
const EventsFiles = { const EventsFiles = {
mousedown: exec.with(execIfNotUL, setCurrentFileByEvent), mousedown: exec.with(execIfNotUL, setCurrentFileByEvent),
click: execAll([onClick, exec.with(execIfNotMobile, unselect)]), click: execAll([
onClick,
unselect,
]),
dragstart: exec.with(execIfNotUL, onDragStart), dragstart: exec.with(execIfNotUL, onDragStart),
dblclick: exec.with(execIfNotUL, onDblClick), dblclick: exec.with(execIfNotUL, onDblClick),
touchstart: exec.with(execIfNotUL, onTouch), touchstart: exec.with(execIfNotUL, onTouch),
@ -55,7 +69,6 @@ let EXT;
function header() { function header() {
const fm = DOM.getFM(); const fm = DOM.getFM();
const isDataset = (el) => el.dataset; const isDataset = (el) => el.dataset;
const isPanel = (el) => { const isPanel = (el) => {
return /^js-(left|right)$/.test(el.dataset.name); return /^js-(left|right)$/.test(el.dataset.name);
}; };
@ -67,7 +80,8 @@ function header() {
if (parent.dataset.name !== 'js-fm-header') if (parent.dataset.name !== 'js-fm-header')
return; return;
const name = (el.dataset.name || '').replace('js-', ''); const name = (el.dataset.name || '')
.replace('js-', '');
if (!/^(name|size|date)$/.test(name)) if (!/^(name|size|date)$/.test(name))
return; return;
@ -88,42 +102,38 @@ function getPath(el, path = []) {
return getPath(el.parentElement, path.concat(el)); return getPath(el.parentElement, path.concat(el));
} }
async function config() { function config() {
const [, config] = await tryToCatch(DOM.Files.get, 'config'); DOM.Files.get('config', (e, config) => {
const type = config?.packer; const type = config && config.packer;
EXT = DOM.getPackerExt(type); EXT = DOM.getPackerExt(type);
});
} }
export const initKeysPanel = () => { module.exports.initKeysPanel = () => {
const keysElement = DOM.getById('js-keyspanel'); const keysElement = DOM.getById('js-keyspanel');
if (!keysElement) if (!keysElement)
return; return;
Events.addClick(keysElement, (event) => { Events.addClick(keysElement, ({target}) => {
const {target} = event; const id = target.id;
const {id} = target;
const operation = (name) => { const operation = (name) => {
const {Operation} = CloudCmd; const Operation = CloudCmd.Operation;
const fn = Operation.show.bind(null, name);
return Operation.show.bind(null, name); return fn;
}; };
const clickFuncs = { const clickFuncs = {
'f1' : CloudCmd.Help.show, 'f1' : CloudCmd.Help.show,
'f2': CloudCmd.UserMenu.show, 'f2' : DOM.renameCurrent,
'f3' : CloudCmd.View.show, 'f3' : CloudCmd.View.show,
'f4' : CloudCmd.EditFile.show, 'f4' : CloudCmd.EditFile.show,
'f5' : operation('copy'), 'f5' : operation('copy'),
'f6' : operation('move'), 'f6' : operation('move'),
'f7' : DOM.promptNewDir, 'f7' : DOM.promptNewDir,
'f8' : operation('delete'), 'f8' : operation('delete'),
'f9': () => { 'f9' : CloudCmd.Menu.show,
event.stopPropagation();
CloudCmd.Menu.show();
},
'f10' : CloudCmd.Config.show, 'f10' : CloudCmd.Config.show,
'~' : CloudCmd.Konsole.show, '~' : CloudCmd.Konsole.show,
'shift~' : CloudCmd.Terminal.show, 'shift~' : CloudCmd.Terminal.show,
@ -138,10 +148,10 @@ const getPanel = (side) => {
if (!itype.string(side)) if (!itype.string(side))
return side; return side;
return DOM.getByDataName(`js-${side}`); return DOM.getByDataName('js-' + side);
}; };
export const setOnPanel = (side) => { module.exports.setOnPanel = (side) => {
const panel = getPanel(side); const panel = getPanel(side);
const filesElement = DOM.getByDataName('js-files', panel); const filesElement = DOM.getByDataName('js-files', panel);
@ -157,7 +167,6 @@ function getPathListener(panel) {
} }
function isNoCurrent(panel) { function isNoCurrent(panel) {
const Info = DOM.CurrentInfo;
const infoPanel = Info.panel; const infoPanel = Info.panel;
if (!infoPanel) if (!infoPanel)
@ -171,18 +180,18 @@ function isNoCurrent(panel) {
function decodePath(path){ function decodePath(path){
const url = CloudCmd.HOST; const url = CloudCmd.HOST;
const {prefix} = CloudCmd; const prefix = CloudCmd.PREFIX;
const prefixReg = RegExp('^' + prefix + FS); const prefixReg = RegExp('^' + prefix + FS);
return decodeURI(path) return decodeURI(path)
.replace(url, '') .replace(url, '')
.replace(prefixReg, '') // browser doesn't replace % -> %25% do it for him .replace(prefixReg, '')
// browser doesn't replace % -> %25% do it for him
.replace('%%', '%25%') .replace('%%', '%25%')
.replace(NBSP_REG, SPACE) || '/'; .replace(NBSP_REG, SPACE) || '/';
} }
async function onPathElementClick(panel, event) { function onPathElementClick(panel, event) {
const Info = DOM.CurrentInfo;
event.preventDefault(); event.preventDefault();
const element = event.target; const element = event.target;
@ -195,7 +204,7 @@ async function onPathElementClick(panel, event) {
if (attr === 'js-refresh') if (attr === 'js-refresh')
return CloudCmd.refresh({ return CloudCmd.refresh({
panel, panel,
noCurrent, noCurrent
}); });
if (attr !== 'js-path-link') if (attr !== 'js-path-link')
@ -204,26 +213,19 @@ async function onPathElementClick(panel, event) {
const {href} = element; const {href} = element;
const path = decodePath(href); const path = decodePath(href);
await CloudCmd.changeDir(path, { CloudCmd.loadDir({
path,
isRefresh: false, isRefresh: false,
panel: noCurrent ? panel : Info.panel, panel: noCurrent ? panel : Info.panel
}); });
} }
function copyPath(el) { function copyPath(el) {
clipboard clipboard.writeText(el.parentElement.title)
.writeText(el.parentElement.title)
.then(CloudCmd.log) .then(CloudCmd.log)
.catch(CloudCmd.log); .catch(CloudCmd.log);
} }
function execIfNotMobile(callback, event) {
const isMobile = DOM.getCSSVar('is-mobile');
if (!isMobile)
callback(event);
}
function execIfNotUL(callback, event) { function execIfNotUL(callback, event) {
const {target} = event; const {target} = event;
const {tagName} = target; const {tagName} = target;
@ -233,27 +235,30 @@ function execIfNotUL(callback, event) {
} }
function onClick(event) { function onClick(event) {
const ctrl = event.ctrlKey;
if (!ctrl)
event.preventDefault(); event.preventDefault();
changePanel(event.target); changePanel(event.target);
} }
function toggleSelect(key, files) { function toggleSelect(key, files) {
const isMac = /Mac/.test(globalThis.navigator.platform); const isMac = /Mac/.test(window.navigator.platform);
if (!key) if (!key)
throw Error('key should not be undefined!'); throw Error('key should not be undefined!');
const [file] = files; const [file] = files;
if (isMac && key.meta) if (isMac && key.meta || key.ctrl)
return DOM.toggleSelectedFile(file); return DOM.toggleSelectedFile(file);
if (key.shift) if (key.shift)
return files.map(DOM.selectFile); return files.forEach(DOM.selectFile);
} }
function changePanel(element) { function changePanel(element) {
const Info = DOM.CurrentInfo;
const {panel} = Info; const {panel} = Info;
const files = DOM.getByDataName('js-files', panel); const files = DOM.getByDataName('js-files', panel);
const ul = getULElement(element); const ul = getULElement(element);
@ -262,20 +267,21 @@ function changePanel(element) {
DOM.changePanel(); DOM.changePanel();
} }
async function onDblClick(event) { function onDblClick(event) {
event.preventDefault();
const current = getLIElement(event.target); const current = getLIElement(event.target);
const isDir = DOM.isCurrentIsDir(current); const isDir = DOM.isCurrentIsDir(current);
const path = DOM.getCurrentPath(current); const path = DOM.getCurrentPath(current);
if (!isDir) if (isDir) {
return CloudCmd.View.show(); CloudCmd.loadDir({
path: path === '/' ? '/' : path + '/'
});
await CloudCmd.changeDir(path); event.preventDefault();
}
} }
async function onTouch(event) { function onTouch(event) {
const current = getLIElement(event.target); const current = getLIElement(event.target);
const isDir = DOM.isCurrentIsDir(current); const isDir = DOM.isCurrentIsDir(current);
@ -287,7 +293,9 @@ async function onTouch(event) {
if (!isCurrent) if (!isCurrent)
return; return;
await CloudCmd.changeDir(DOM.getCurrentPath(current)); CloudCmd.loadDir({
path: DOM.getCurrentPath(current)
});
} }
/* /*
@ -295,10 +303,8 @@ async function onTouch(event) {
* in Chrome (HTML5) * in Chrome (HTML5)
*/ */
function onDragStart(event) { function onDragStart(event) {
const Info = DOM.CurrentInfo;
const {prefixURL} = CloudCmd;
const element = getLIElement(event.target); const element = getLIElement(event.target);
const {isDir} = Info; const isDir = Info.isDir;
let link = DOM.getCurrentLink(element); let link = DOM.getCurrentLink(element);
let name = DOM.getCurrentName(element); let name = DOM.getCurrentName(element);
@ -307,15 +313,13 @@ function onDragStart(event) {
name += EXT; name += EXT;
link = document.createElement('a'); link = document.createElement('a');
link.textContent = name; link.textContent = name;
link.href = prefixURL + '/pack' + Info.path + EXT; link.href = apiURL + '/pack' + Info.path + EXT;
} }
event.dataTransfer.setData( event.dataTransfer.setData('DownloadURL',
'DownloadURL', 'application/octet-stream' + ':' +
'application/octet-stream' + ':' + name + name + ':' +
':' + link);
link,
);
} }
function getLIElement(element) { function getLIElement(element) {
@ -336,14 +340,13 @@ function getULElement(element) {
} }
function setCurrentFileByEvent(event) { function setCurrentFileByEvent(event) {
const Info = DOM.CurrentInfo;
const BUTTON_LEFT = 0; const BUTTON_LEFT = 0;
const key = { const key = {
alt: event.altKey, alt: event.altKey,
ctrl: event.ctrlKey, ctrl: event.ctrlKey,
meta: event.metaKey, meta: event.metaKey,
shift: event.shiftKey, shift: event.shiftKey
}; };
const element = getLIElement(event.target); const element = getLIElement(event.target);
@ -380,7 +383,7 @@ function contextMenu() {
Events.addOnce('contextmenu', fm, (event) => { Events.addOnce('contextmenu', fm, (event) => {
CloudCmd.Menu.show({ CloudCmd.Menu.show({
x: event.clientX, x: event.clientX,
y: event.clientY, y: event.clientY
}); });
}); });
@ -391,17 +394,21 @@ function contextMenu() {
function dragndrop() { function dragndrop() {
const panels = DOM.getByClassAll('panel'); const panels = DOM.getByClassAll('panel');
const select = ({target}) => { const select = () => {
target.classList.add('selected-panel'); [...panels].forEach((panel) => {
panel.classList.add('selected-panel');
});
}; };
const unselect = ({target}) => { const unselect = () => {
target.classList.remove('selected-panel'); [...panels].forEach((panel) => {
panel.classList.remove('selected-panel');
});
}; };
const onDrop = (event) => { const onDrop = (event) => {
const {files, items} = event.dataTransfer; const files = event.dataTransfer.files;
const items = event.dataTransfer.items;
const {length: filesCount} = files; const {length: filesCount} = files;
event.preventDefault(); event.preventDefault();
@ -410,10 +417,7 @@ function dragndrop() {
return uploadFiles(files); return uploadFiles(files);
const isFile = (item) => item.kind === 'file'; const isFile = (item) => item.kind === 'file';
const dirFiles = [...items].filter(isFile);
const dirFiles = Array
.from(items)
.filter(isFile);
if (dirFiles.length) if (dirFiles.length)
return DOM.uploadDirectory(dirFiles); return DOM.uploadDirectory(dirFiles);
@ -425,13 +429,13 @@ function dragndrop() {
}; };
/** /**
* In macOS Chrome dropEffect = 'none' * In Mac OS Chrome dropEffect = 'none'
* so drop do not firing up when try * so drop do not firing up when try
* to upload file from download bar * to upload file from download bar
*/ */
const onDragOver = (event) => { const onDragOver = (event) => {
const {dataTransfer} = event; const dataTransfer = event.dataTransfer;
const {effectAllowed} = dataTransfer; const effectAllowed = dataTransfer.effectAllowed;
if (/move|linkMove/.test(effectAllowed)) if (/move|linkMove/.test(effectAllowed))
dataTransfer.dropEffect = 'move'; dataTransfer.dropEffect = 'move';
@ -441,18 +445,19 @@ function dragndrop() {
event.preventDefault(); event.preventDefault();
}; };
for (const panel of panels) Events.add('dragenter', select);
Events Events.add(['dragleave', 'drop'], unselect);
.add('dragover', panel, onDragOver)
.add('drop', panel, onDrop) [...panels].forEach((panel) => {
.add('dragenter', select) Events.add('dragover', panel, onDragOver)
.add(['dragleave', 'drop'], unselect); .add('drop', panel, onDrop);
});
} }
function unload() { function unload() {
Events.add(['unload', 'beforeunload'], (event) => { DOM.Events.add(['unload', 'beforeunload'], (event) => {
const {Key} = CloudCmd; const Key = CloudCmd.Key;
const isBind = Key?.isBind(); const isBind = Key && Key.isBind();
if (isBind) if (isBind)
return; return;
@ -463,15 +468,15 @@ function unload() {
} }
function pop() { function pop() {
Events.add('popstate', async ({state}) => { Events.add('popstate', ({state}) => {
const path = (state || '').replace(FS, ''); const path = (state || '').replace(FS, '');
if (!path) if (!path)
return CloudCmd.route(location.hash); return CloudCmd.route(location.hash);
const history = false; const history = false;
CloudCmd.loadDir({
await CloudCmd.changeDir(path, { path,
history, history,
}); });
}); });
@ -479,8 +484,7 @@ function pop() {
function resize() { function resize() {
Events.add('resize', () => { Events.add('resize', () => {
const Info = DOM.CurrentInfo; const is = window.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH;
const is = globalThis.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH;
if (!is) if (!is)
return; return;
@ -500,3 +504,4 @@ function resize() {
DOM.changePanel(); DOM.changePanel();
}); });
} }

View file

@ -1,45 +1,49 @@
/* global CloudCmd */ 'use strict';
import exec from 'execon';
import {tryToCatch} from 'try-to-catch';
import {js as loadJS} from 'load.js';
import pascalCase from 'just-pascal-case';
const noJS = (a) => a.replace(/.js$/, ''); /* global CloudCmd */
const exec = require('execon');
const tryToCatch = require('try-to-catch/legacy');
const {promisify} = require('es6-promisify');
const loadJS = promisify(require('load.js').js);
const {
kebabToCamelCase,
} = require('../common/util');
/** /**
* function load modules * function load modules
* @params = {name, path, func, dobefore, arg} * @params = {name, path, func, dobefore, arg}
*/ */
export const loadModule = (params) => { module.exports = function loadModule(params) {
if (!params) if (!params)
return; return;
const {path} = params; const {path} = params;
const name = params.name || path && kebabToCamelCase(path);
const name = path && noJS(pascalCase(path));
const doBefore = params.dobefore; const doBefore = params.dobefore;
if (CloudCmd[name]) if (CloudCmd[name])
return; return;
CloudCmd[name] = async () => { CloudCmd[name] = () => {
exec(doBefore); exec(doBefore);
const prefix = CloudCmd.PREFIX;
const pathFull = prefix + CloudCmd.DIRCLIENT_MODULES + path + '.js';
const {DIR_MODULES} = CloudCmd; return loadJS(pathFull).then(async () => {
const pathFull = `${DIR_MODULES}/${path}.js`;
await loadJS(pathFull);
const newModule = async (f) => f && f(); const newModule = async (f) => f && f();
const module = CloudCmd[name]; const module = CloudCmd[name];
Object.assign(newModule, module); Object.assign(newModule, module);
CloudCmd[name] = newModule; CloudCmd[name] = newModule;
CloudCmd.log('init', name);
CloudCmd.log('init', name);
await module.init(); await module.init();
return newModule; return newModule;
});
}; };
CloudCmd[name].show = async (...args) => { CloudCmd[name].show = async (...args) => {
@ -49,8 +53,9 @@ export const loadModule = (params) => {
const [e, a] = await tryToCatch(m); const [e, a] = await tryToCatch(m);
if (e) if (e)
return; return console.error(e);
return await a.show(...args); a.show(...args);
}; };
}; };

View file

@ -1,32 +1,28 @@
/* global CloudCmd, filepicker */ /* global CloudCmd, filepicker */
import exec from 'execon';
import currify from 'currify'; 'use strict';
import load from 'load.js';
import {ajax} from '#dom/load'; const exec = require('execon');
import * as Files from '#dom/files'; const currify = require('currify/legacy');
import * as Images from '#dom/images'; const {promisify} = require('es6-promisify');
const loadJS = require('load.js').js;
const {log} = CloudCmd; const {log} = CloudCmd;
const {ajax} = require('../dom/load');
const Files = require('../dom/files');
const Images = require('../dom/images');
const upload = currify(_upload); const upload = currify(_upload);
const Name = 'Cloud'; const Name = 'Cloud';
CloudCmd[Name] = module.exports;
CloudCmd[Name] = { module.exports.init = async () => {
init, await loadFiles();
uploadFile,
saveFile,
}; };
export async function init() { module.exports.uploadFile = (filename, data) => {
const [modules] = await loadFiles();
const {key} = modules.data.FilePicker;
filepicker.setKey(key);
Images.hide();
}
export function uploadFile(filename, data) {
const mimetype = ''; const mimetype = '';
filepicker.store(data, { filepicker.store(data, {
@ -35,14 +31,17 @@ export function uploadFile(filename, data) {
}, (fpFile) => { }, (fpFile) => {
filepicker.exportFile(fpFile, log, log); filepicker.exportFile(fpFile, log, log);
}); });
} };
export function saveFile(callback) { module.exports.saveFile = (callback) => {
filepicker.pick(upload(callback)); filepicker.pick(upload(callback));
} };
function _upload(callback, file) { function _upload(callback, file) {
const {url, filename} = file; const {
url,
filename,
} = file;
const responseType = 'arraybuffer'; const responseType = 'arraybuffer';
const success = exec.with(callback, filename); const success = exec.with(callback, filename);
@ -54,11 +53,18 @@ function _upload(callback, file) {
}); });
} }
function loadFiles() { const loadFiles = promisify((callback) => {
const js = '//api.filepicker.io/v2/filepicker.js'; const js = '//api.filepicker.io/v2/filepicker.js';
return Promise.all([ loadJS(js, () => {
Files.get('modules'), Files.get('modules', (error, modules) => {
load.js(js), const {key} = modules.data.FilePicker;
]);
} filepicker.setKey(key);
Images.hide();
exec(callback);
});
});
});

View file

@ -1,31 +0,0 @@
/* global CloudCmd */
import * as Dialog from '#dom/dialog';
export function init() {}
CloudCmd.CommandLine = {
init,
show,
hide,
};
export async function show() {
const [, cmd] = await Dialog.prompt('Command Line', '');
const TERMINAL = '^(t|terminal)';
if (RegExp(`${TERMINAL}$`).test(cmd))
return await CloudCmd.Terminal.show();
if (RegExp(TERMINAL).test(cmd)) {
const command = cmd.replace(RegExp(`${TERMINAL} `), '');
const exitCode = await CloudCmd.TerminalRun.show({
command: `bash -c '${command}'`,
});
if (exitCode === -1)
await Dialog.alert(`☝️ Looks like Terminal is disabled, start Cloud Coammnder with '--terminal' flag.`);
return;
}
}
export function hide() {}

252
client/modules/config.js Normal file
View file

@ -0,0 +1,252 @@
'use strict';
/* global CloudCmd, DOM, io */
require('../../css/config.css');
const rendy = require('rendy/legacy');
const exec = require('execon');
const currify = require('currify/legacy');
const wraptile = require('wraptile/legacy');
const squad = require('squad/legacy');
const {promisify} = require('es6-promisify');
const load = require('load.js');
const createElement = require('@cloudcmd/create-element');
const input = require('../input');
const Images = require('../dom/images');
const Events = require('../dom/events');
const Files = require('../dom/files');
const {getTitle} = require('../../common/cloudfunc');
const {Dialog, setTitle} = DOM;
const Name = 'Config';
CloudCmd[Name] = module.exports;
const alert = currify(Dialog.alert, Name);
const loadSocket = promisify(DOM.loadSocket);
const showLoad = () => {
Images.show.load('top');
};
const addKey = currify((fn, input) => {
Events.addKey(input, fn);
return input;
});
const addChange = currify((fn, input) => {
Events.add('change', input, fn);
return input;
});
const Config = {};
let Loading = true;
module.exports.init = async () => {
if (!CloudCmd.config('configDialog'))
return;
showLoad();
await CloudCmd.View();
await loadSocket();
initSocket();
Loading = false;
};
const config = CloudCmd.config;
const {Key} = CloudCmd;
let Element;
let Template;
function getHost() {
const {host, origin, protocol} = location;
const href = origin || `${protocol}//${host}`;
return href;
}
function initSocket() {
const href = getHost();
const {
prefixSocket,
PREFIX,
} = CloudCmd;
const ONE_MINUTE = 60 * 1000;
const socket = io.connect(href + prefixSocket + '/config', {
reconnectionAttempts: Infinity,
reconnectionDelay: ONE_MINUTE,
path: PREFIX + '/socket.io'
});
const save = (data) => {
onSave(data);
socket.send(data);
};
authCheck(socket);
socket.on('connect', () => {
Config.save = save;
});
socket.on('message', onSave);
socket.on('log', CloudCmd.log);
socket.on('disconnect', () => {
Config.save = saveHttp;
});
socket.on('err', alert);
}
function authCheck(socket) {
socket.emit('auth', config('username'), config('password'));
socket.on('reject', wraptile(alert, 'Wrong credentials!'));
}
Config.save = saveHttp;
module.exports.show = show;
function show() {
if (!CloudCmd.config('configDialog'))
return;
const prefix = CloudCmd.PREFIX;
const funcs = [
exec.with(Files.get, 'config-tmpl'),
exec.with(load.css, prefix + '/dist/config.css'),
];
if (Loading)
return;
showLoad();
exec.parallel(funcs, fillTemplate);
}
function fillTemplate(error, template) {
if (!Template)
Template = template;
Files.get('config', (error, config) => {
if (error)
return alert('Could not load config!');
const {
editor,
packer,
columns,
configAuth,
...obj
} = input.convert(config);
obj[editor + '-selected'] = 'selected';
obj[packer + '-selected'] = 'selected';
obj[columns + '-selected'] = 'selected';
obj.configAuth = configAuth ? '' : 'hidden';
const innerHTML = rendy(Template, obj);
Element = createElement('form', {
className : 'config',
innerHTML,
});
const inputs = document.querySelectorAll('input, select', Element);
const [inputFirst] = inputs;
let afterShow;
if (inputFirst) {
onAuthChange(inputFirst.checked);
afterShow = inputFirst.focus.bind(inputFirst);
}
const getTarget = ({target}) => target;
const handleChange = squad(onChange, getTarget);
[...inputs]
.map(addKey(onKey))
.map(addChange(handleChange));
const autoSize = true;
CloudCmd.View.show(Element, {
autoSize,
afterShow,
});
});
}
module.exports.hide = hide;
function hide() {
CloudCmd.View.hide();
}
function onChange(el) {
const obj = {};
const name = input.getName(el);
const data = input.getValue(name, Element);
if (name === 'name')
onNameChange(data);
else if (name === 'auth')
onAuthChange(data);
obj[name] = data;
Config.save(obj);
}
function onSave(obj) {
Object.keys(obj).forEach((name) => {
const data = obj[name];
CloudCmd._config(name, data);
input.setValue(name, data, Element);
});
}
function saveHttp(obj) {
const {RESTful} = DOM;
RESTful.Config.write(obj, (error) => {
if (error)
return;
onSave(obj);
});
}
function onAuthChange(checked) {
const elUsername = input.getElementByName('username', Element);
const elPassword = input.getElementByName('password', Element);
elUsername.disabled =
elPassword.disabled = !checked;
}
function onNameChange(name) {
setTitle(getTitle({
name
}));
}
function onKey({keyCode, target}) {
switch (keyCode) {
case Key.ESC:
return hide();
case Key.ENTER:
return onChange(target);
}
}

Some files were not shown because too many files have changed in this diff Show more