Compare commits

..

No commits in common. "master" and "v12.0.2" have entirely different histories.

295 changed files with 9074 additions and 16775 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%

20
.babelrc Normal file
View file

@ -0,0 +1,20 @@
{
"presets": [
["@babel/preset-env", {
"exclude": [
"transform-regenerator"
]
}],
],
"plugins": [
"@babel/plugin-transform-object-assign",
"@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
now.json
docker
Dockerfile
Dockerfile.alpine
webpack.config.js
cssnano.config.js
@ -19,5 +20,6 @@ cssnano.config.js
bin/release.js
client
legacy
server_

View file

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

11
.eslintrc.json Normal file
View file

@ -0,0 +1,11 @@
{
"rules": {
"no-console": 0
},
"extends": [
"plugin:putout/recommended"
],
"plugins": [
"putout"
]
}

11
.eslintrc.server Normal file
View file

@ -0,0 +1,11 @@
{
"rules": {
"no-process-exit": 0,
},
"extends": [
"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")
- [ ] `npm run fix:lint` is OK
- [ ] `npm run codestyle` 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
npm-debug.log*
yarn.lock
yarn-error.log
node_modules
npm-debug.log*
coverage
modules/execon
modules/emitify
.nyc_output
*.swp
dist
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,31 @@
*.spec.*
*.config.*
*.fixture.js*
*.ai
*.cdr
*.eps
*.log
*.lock
.*
*.spec.js
*.fixture.js
manifest.yml
Dockerfile*
docker-compose.yml
now.json
app.json
bower.json
deno.json
bin/release.*
img/logo/cloudcmd-hq.png
webpack.config.js
docker
test*
test
fixture
fixture-*
coverage
css
html
yarn-error.log
yarn.lock
now.json
cssnano.config.js
app.json
bower.json
manifest.yml
bin/release.js
client
webpack.config.js
madrun.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 +1,20 @@
{
"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"
"server/(server|exit).js": {
"remove-process-exit": false,
"remove-console": false
},
"base64": {
"types/convert-typeof-to-is-type": "off"
"server/(terminal|distribute/log).js": {
"remove-console": false
},
"*.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"
"client/(client|cloudcmd|load-module).js": {
"remove-console": false
},
"test/common/cloudfunc.js": {
"remove-console": "off"
"remove-console": false
},
"storage.js": {
"promises/remove-useless-async": "off"
},
"docker.yml": {
"github/set-node-versions": "off"
},
"vim.js": {
"merge-duplicate-functions": "off"
},
"common": {
"nodejs/declare": "off"
"server": {
"remove-process-exit": true
}
}
}

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

134
.webpack/js.js Normal file
View file

@ -0,0 +1,134 @@
'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: 'production',
}),
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,
performance: {
maxEntrypointSize: 500000,
maxAssetSize: 500000,
},
};
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"
],
"dictionary":[
"CloudCmd",
"Dev",
"Dropbox",
"Deepword",
@ -22,22 +21,18 @@
"Iptables",
"JitSu",
"Node",
"IO",
"Olena",
"TarZak",
"Termux",
"Zalitok",
"WebSocket",
"auth",
"autostart",
"binded",
"cd",
"cloudcmd",
"coderaiser",
"com",
"deepword",
"dev",
"destructuring",
"deepword",
"dropbox",
"dword",
"edward",
@ -46,7 +41,6 @@
"gz",
"io",
"js",
"linux",
"maintainers",
"markdown",
"microservice",
@ -56,8 +50,6 @@
"nginx",
"npm",
"or io",
"patreon",
"rc",
"refactor",
"sexualized",
"sslPort",
@ -66,7 +58,6 @@
"v0",
"v1",
"v2",
"yml",
"systemd"
"yml"
]
}

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:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- 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
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* 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
## Our Responsibilities

View file

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

2404
ChangeLog

File diff suppressed because it is too large Load diff

25
Dockerfile Normal file
View file

@ -0,0 +1,25 @@
FROM node
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 /
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 /
ENV cloudcmd_terminal true
ENV cloudcmd_terminal_path gritty
ENV cloudcmd_open false
EXPOSE 8000
ENTRYPOINT ["/usr/src/app/bin/cloudcmd.js"]

1183
HELP.md

File diff suppressed because it is too large Load diff

View file

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

170
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 v12.0.2 [![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
[BuildStatusURL]: https://github.com/coderaiser/cloudcmd/actions/workflows/nodejs.yml "Build Status"
[BuildStatusIMGURL]: https://github.com/coderaiser/cloudcmd/actions/workflows/nodejs.yml/badge.svg
[CodacyURL]: https://www.codacy.com/app/coderaiser/cloudcmd
[CodacyIMG]: https://api.codacy.com/project/badge/Grade/ddda78be780549ce8754f8d47a8c0e36
[GitterURL]: https://gitter.im/cloudcmd/hello
[GitterIMGURL]: https://img.shields.io/gitter/room/coderaiser/cloudcmd.js.svg
[DeployURL]: https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy"
[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
[NPM_INFO_IMG]: https://nodei.co/npm/cloudcmd.png
[MainURL]: http://cloudcmd.io "Main"
[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
[CodacyIMG]: https://api.codacy.com/project/badge/Grade/ddda78be780549ce8754f8d47a8c0e36
[GitterURL]: https://gitter.im/cloudcmd/hello
[GitterIMGURL]: https://img.shields.io/gitter/room/coderaiser/cloudcmd.js.svg
[DeployURL]: https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy"
[DeployIMG]: https://www.herokucdn.com/deploy/button.png
**Cloud Commander** a file manager for the web with console and editor.
@ -27,7 +31,6 @@
```
npm i cloudcmd -g
```
## Start
For starting just type in console:
@ -45,8 +48,8 @@ Open url `http://localhost:8000` in browser.
You will see something similar to this.
![View](https://cloudcmd.io/img/screen/view.png "View")
## Deploy
## Deploy
`Cloud Commander` could be easily deployed to [Heroku][DeployURL].
[![Deploy][DeployIMG]][DeployURL]
@ -70,31 +73,33 @@ npm i cloudcmd express socket.io -S
And create `index.js`:
```js
import http from 'node:http';
import {cloudcmd} from 'cloudcmd';
import {Server} from 'socket.io';
import express from 'express';
const app = express();
const http = require('http');
const cloudcmd = require('cloudcmd');
const io = require('socket.io');
const app = require('express')();
const port = 1337;
const prefix = '/';
const server = http.createServer(app);
const socket = new Server(server, {
path: `${prefix}socket.io`,
const socket = io.listen(server, {
path: `${prefix}/socket.io`
});
const config = {
name: 'cloudcmd :)',
};
const plugins = [
__dirname + '/plugin.js'
];
const filePicker = {
data: {
FilePicker: {
key: 'key',
},
},
}
}
};
// override option from json/modules.json
@ -102,40 +107,22 @@ const modules = {
filePicker,
};
const {
createConfigManager,
configPath,
} = cloudcmd;
const configManager = createConfigManager({
configPath,
});
app.use(prefix, cloudcmd({
socket, // used by Config, Edit (optional) and Console (required)
config, // config data (optional)
socket, // used by Config, Edit (optional) and Console (required)
config, // config data (optional)
plugins, // optional
modules, // optional
configManager, // optional
}));
server.listen(port);
```
## 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 |
Docker
---------------
`Cloud Commander` could be used as a [docker container](https://hub.docker.com/r/coderaiser/cloudcmd/ "Docker container") this way:
```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 -w=/root -p 8000:8000 coderaiser/cloudcmd
```
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
```
## Documentation
More documentation you can find on https://cloudcmd.io/.
## Get involved
Get involved
---------------
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 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");
## 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
MIT

View file

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

View file

@ -1,48 +1,31 @@
#!/usr/bin/env node
import process from 'node:process';
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',
};
'use strict';
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) => {
if (isUndefined(a))
if (a === undefined)
return b;
return a;
};
const config = createConfig({
configPath,
});
process.on('unhandledRejection', exit);
const maybeRoot = (a) => {
if (a === '.')
return process.cwd();
return a;
};
const yargsOptions = {
configuration: {
'strip-aliased': true,
'strip-dashed': true,
},
coerce: {
root: maybeRoot,
},
const {argv} = process;
const args = require('minimist')(argv.slice(2), {
string: [
'name',
'port',
@ -57,8 +40,6 @@ const yargsOptions = {
'terminal-path',
'terminal-command',
'columns',
'menu',
'theme',
'import-url',
'import-token',
'export-token',
@ -71,9 +52,9 @@ const yargsOptions = {
'server',
'online',
'open',
'progress',
'config-dialog',
'config-auth',
'config-port',
'console',
'sync-console-path',
'contact',
@ -83,7 +64,6 @@ const yargsOptions = {
'confirm-copy',
'confirm-move',
'show-config',
'show-dot-files',
'show-file-name',
'vim',
'keys-panel',
@ -92,70 +72,66 @@ const yargsOptions = {
'import',
'import-listen',
'log',
'zip',
'dropbox',
],
default: {
'server': true,
'name': choose(env.parse('name'), config('name')),
'auth': choose(env.bool('auth'), config('auth')),
'port': config('port'),
'online': config('online'),
'open': choose(env.bool('open'), config('open')),
'editor': env.parse('editor') || config('editor'),
'menu': env.parse('menu') || config('menu'),
'packer': config('packer') || 'tar',
'zip': config('zip'),
'username': env.parse('username') || config('username'),
'root': choose(env.parse('root'), config('root')),
'prefix': choose(env.parse('prefix'), config('prefix')),
'console': choose(env.bool('console'), config('console')),
'contact': choose(env.bool('contact'), config('contact')),
'terminal': choose(env.bool('terminal'), config('terminal')),
'columns': env.parse('columns') || config('columns') || '',
'theme': env.parse('theme') || config('theme') || '',
'vim': choose(env.bool('vim'), config('vim')),
'log': choose(env.bool('log'), config('log')),
server : true,
name : choose(env('name'), config('name')),
auth : choose(env.bool('auth'), config('auth')),
port : config('port'),
online : config('online'),
open : choose(env.bool('open'), config('open')),
editor : env('editor') || config('editor'),
packer : config('packer') || 'tar',
zip : config('zip'),
username : env('username') || config('username'),
root : choose(env('root'), config('root')),
prefix : config('prefix'),
progress : config('progress'),
console : choose(env.bool('console'), config('console')),
contact : choose(env.bool('contact'), config('contact')),
terminal : choose(env.bool('terminal'), config('terminal')),
columns : env('columns') || config('columns') || '',
vim : choose(env.bool('vim'), config('vim')),
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': choose(env.bool('import'), config('import')),
'export': choose(env.bool('export'), config('export')),
import : choose(env.bool('import'), config('import')),
export : choose(env.bool('export'), config('export')),
'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')),
'sync-console-path': choose(env.bool('sync_console_path'), config('syncConsolePath')),
'config-dialog': choose(env.bool('config_dialog'), config('configDialog')),
'config-auth': choose(env.bool('config_auth'), config('configAuth')),
'config-port': choose(env.bool('config_port'), config('configPort')),
'terminal-path': env.parse('terminal_path') || config('terminalPath'),
'terminal-command': env.parse('terminal_command') || config('terminalCommand'),
'terminal-path': env('terminal_path') || config('terminalPath'),
'terminal-command': env('terminal_command') || config('terminalCommand'),
'terminal-auto-restart': choose(env.bool('terminal_auto_restart'), config('terminalAutoRestart')),
'one-file-panel': choose(env.bool('one_file_panel'), config('oneFilePanel')),
'confirm-copy': choose(env.bool('confirm_copy'), config('confirmCopy')),
'confirm-move': choose(env.bool('confirm_move'), config('confirmMove')),
'keys-panel': env.bool('keys_panel') || config('keysPanel'),
'import-token': env.parse('import_token') || config('importToken'),
'export-token': env.parse('export_token') || config('exportToken'),
'import-token': env('import_token') || config('importToken'),
'export-token': env('export_token') || config('exportToken'),
'dropbox': config('dropbox'),
'dropbox-token': config('dropboxToken') || '',
'dropbox-token': config('dropboxToken'),
},
alias: {
version: 'v',
help: 'h',
password: 'p',
online: 'o',
username: 'u',
save: 's',
auth: 'a',
config: 'c',
v: 'version',
h: 'help',
p: 'password',
o: 'online',
u: 'username',
s: 'save',
a: 'auth',
c: 'config',
},
};
const {argv} = process;
const args = parse(argv.slice(2), yargsOptions);
unknown: (cmd) => {
exit('\'%s\' is not a cloudcmd option. See \'cloudcmd --help\'.', cmd);
},
});
if (args.version)
version();
@ -164,23 +140,11 @@ else if (args.help)
else
main();
async function main() {
const {validateArgs} = await import('@putout/cli-validate-args');
const error = await validateArgs(args, [
...yargsOptions.boolean,
...yargsOptions.string,
]);
if (error)
return exit(error);
function main() {
if (args.repl)
await repl();
validate.columns(args.columns);
validate.theme(args.theme);
repl();
checkUpdate();
port(args.port);
config('name', args.name);
@ -188,42 +152,39 @@ async function main() {
config('online', args.online);
config('open', args.open);
config('username', args.username);
config('progress', args.progress);
config('console', args.console);
config('syncConsolePath', args.syncConsolePath);
config('showDotFiles', args.showDotFiles);
config('showFileName', args.showFileName);
config('syncConsolePath', args['sync-console-path']);
config('showFileName', args['show-file-name']);
config('contact', args.contact);
config('terminal', args.terminal);
config('terminalPath', args.terminalPath);
config('terminalCommand', args.terminalCommand);
config('terminalAutoRestart', args.terminalAutoRestart);
config('terminalPath', args['terminal-path']);
config('terminalCommand', args['terminal-command']);
config('terminalAutoRestart', args['terminal-auto-restart']);
config('editor', args.editor);
config('menu', args.menu);
config('prefix', prefixer(args.prefix));
config('prefixSocket', prefixer(args.prefixSocket));
config('prefixSocket', prefixer(args['prefix-socket']));
config('root', args.root || '/');
config('vim', args.vim);
config('theme', args.theme);
config('columns', args.columns);
config('log', args.log);
config('confirmCopy', args.confirmCopy);
config('confirmMove', args.confirmMove);
config('oneFilePanel', args.oneFilePanel);
config('configDialog', args.configDialog);
config('configAuth', args.configAuth);
config('configPort', args.configPort);
config('keysPanel', args.keysPanel);
config('confirmCopy', args['confirm-copy']);
config('confirmMove', args['confirm-move']);
config('oneFilePanel', args['one-file-panel']);
config('configDialog', args['config-dialog']);
config('configAuth', args['config-auth']);
config('keysPanel', args['keys-panel']);
config('export', args.export);
config('exportToken', args.exportToken);
config('exportToken', args['export-token']);
config('import', args.import);
config('importToken', args.importToken);
config('importListen', args.importListen);
config('importUrl', args.importUrl);
config('importToken', args['import-token']);
config('importListen', args['import-listen']);
config('importUrl', args['import-url']);
config('dropbox', args.dropbox);
config('dropboxToken', args.dropboxToken);
config('dropbox', args['dropbox']);
config('dropboxToken', args['dropbox-token'] || '');
await readConfig(args.config);
readConfig(args.config);
const options = {
root: config('root'),
@ -232,44 +193,35 @@ async function main() {
prefix: config('prefix'),
prefixSocket: config('prefixSocket'),
columns: config('columns'),
theme: config('theme'),
menu: config('menu'),
};
const password = env.parse('password') || args.password;
const password = env('password') || args.password;
if (password)
config('password', await getPassword(password));
config('password', getPassword(password));
validateRoot(options.root, config);
validateRoot(options.root);
if (args.showConfig)
await showConfig();
if (args['show-config'])
showConfig();
const {distributeImport} = await import('../server/distribute/import.js');
const importConfig = promisify(distributeImport);
const startWraped = wraptile(start, options);
const distribute = require('../server/distribute');
const importConfig = promisify(distribute.import);
const caller = (fn) => fn();
await start(options, config);
if (args.save)
config.write();
await tryToCatch(checkUpdate);
await importConfig(config);
importConfig()
.then(args.save ? caller(config.save) : noop)
.then(startWraped(options));
}
function validateRoot(root, config) {
validate.root(root, config);
if (root === '/')
return;
if (config('log'))
console.log(`root: ${root}`);
function validateRoot(root) {
const validate = require(DIR_SERVER + 'validate');
validate.root(root, console.log);
}
async function getPassword(password) {
const {default: criton} = await import('criton');
function getPassword(password) {
const criton = require('criton');
return criton(password, config('algo'));
}
@ -277,12 +229,14 @@ function version() {
console.log('v' + Info.version);
}
async function start(options, config) {
function start(config) {
const SERVER = DIR_SERVER + 'server';
if (!args.server)
return;
const {default: server} = await import('../server/server.js');
server(options, config);
const server = require(SERVER);
server(config);
}
function port(arg) {
@ -294,38 +248,38 @@ function port(arg) {
exit('cloudcmd --port: should be a number');
}
async function showConfig() {
const {showConfig} = await import('../server/show-config.js');
const data = showConfig(config('*'));
function showConfig() {
const show = require('../server/show-config');
const data = show(config('*'));
console.log(data);
}
async function readConfig(name) {
function readConfig(name) {
if (!name)
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, {
with: {
type: 'json',
},
const readjsonSync = (name) => jju.parse(fs.readFileSync(name, 'utf8'), {
mode: 'json',
});
const [error, data] = tryCatch(readjsonSync, name);
if (error)
return exit(error.message);
forEachKey(config, data);
}
async function help() {
const {default: bin} = await import('../json/help.json', {
with: {
type: 'json',
},
});
const {default: forEachKey} = await import('for-each-key');
const {default: currify} = await import('currify');
function help() {
const bin = require('../json/help');
const forEachKey = require('for-each-key');
const currify = require('currify');
const usage = 'Usage: cloudcmd [options]';
const url = Info.homepage;
const log = currify((a, b, c) => console.log(a, b, c));
@ -336,27 +290,31 @@ async function help() {
console.log('\nGeneral help using Cloud Commander: <%s>', url);
}
async function repl() {
function repl() {
console.log('REPL mode enabled (telnet localhost 1337)');
await import('../server/repl.js');
require(DIR_SERVER + 'repl');
}
async function checkUpdate() {
const {default: load} = await import('package-json');
const {version} = await load(Info.name, 'latest');
function checkUpdate() {
const load = require('package-json');
await showUpdateInfo(version);
load(Info.name, 'latest')
.then(showUpdateInfo)
.catch(noop);
}
async function showUpdateInfo(version) {
function showUpdateInfo(data) {
const {version} = data;
if (version === Info.version)
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 current = chalk.dim(`(current: v${Info.version})`);
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
import {promisify} from 'node:util';
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',
};
'use strict';
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 history = '## Version history\n\n';
const ERROR = Error('ERROR: version is missing. release --patch|--minor|--major');
main((error) => {
if (error)
console.error(error.message);
});
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';
const template = '- *{{ date }}*, ' +
'**[v{{ version }}]' +
'(' + link + 'v{{ version }})**\n';
const {version} = Info;
const [error, versionNew] = await tryToCatch(cl);
if (error)
return console.error(error);
await replaceVersion('README.md', version, versionNew);
await replaceVersion('HELP.md', version, versionNew);
const historyNew = history +
rendy(template, {
date: shortdate(),
version: versionNew,
cl((error, versionNew) => {
if (error)
return callback(error);
replaceVersion('README.md', version, versionNew, callback);
replaceVersion('HELP.md', version, versionNew, () => {
const historyNew = history + rendy(template, {
date : shortdate(),
version : versionNew,
});
replaceVersion('HELP.md', history, historyNew, callback);
});
await replaceVersion('HELP.md', history, historyNew);
});
}
async function replaceVersion(name, version, versionNew) {
const [error] = await tryToCatch(place, name, version, versionNew);
if (error)
return console.error(error);
console.log(`done: ${name}`);
function replaceVersion(name, version, versionNew, callback) {
place(name, version, versionNew, (error) => {
if (error)
return callback(error);
callback(null, 'done: ' + name);
});
}
async function cl() {
function cl(callback) {
const {argv} = process;
const length = argv.length - 1;
const last = process.argv[length];
const regExp = /^--(major|minor|patch)$/;
const [, match] = last.match(regExp) || [];
console.log(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) {
@ -75,3 +70,4 @@ function getVersionNew(last, match) {
return last.substr(3);
}

View file

@ -1,59 +1,78 @@
import process from 'node:process';
import Emitify from 'emitify';
import inherits from 'inherits';
import {rendy} from 'rendy';
import load from 'load.js';
import {tryToCatch} from 'try-to-catch';
import {addSlashToEnd} from 'format-io';
import pascalCase from 'just-pascal-case';
import currify from 'currify';
import {
'use strict';
/* global DOM */
const Emitify = require('emitify/legacy');
const inherits = require('inherits');
const rendy = require('rendy/legacy');
const exec = require('execon');
const load = require('load.js');
const pascalCase = require('just-pascal-case');
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 noJS = (a) => a.replace(/.js$/, '');
const {
apiURL,
formatMsg,
buildFromJSON,
} from '#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';
} = require('../common/cloudfunc');
const noJS = (a) => a.replace(/.js$/, '');
const isDev = process.env.NODE_ENV === 'development';
const loadModule = require('./load-module');
inherits(CloudCmdProto, Emitify);
export const createCloudCmd = ({DOM, Listeners}) => {
return new CloudCmdProto({
DOM,
Listeners,
});
};
module.exports = new CloudCmdProto(DOM);
load.addErrorListener((e, src) => {
const msg = `file ${src} could not be loaded`;
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);
const CloudCmd = this;
const Info = DOM.CurrentInfo;
const {Storage, Files} = DOM;
const {
Storage,
Files,
} = DOM;
this.log = () => {
if (!isDev)
return;
};
this.log = log;
this.prefix = '';
this.prefixSocket = '';
this.prefixURL = '';
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 = {
left: 'name',
right: 'name',
@ -64,22 +83,36 @@ function CloudCmdProto({DOM, Listeners}) {
right: 'asc',
};
this.changeDir = async (path, overrides = {}) => {
/**
* Функция привязываеться ко всем ссылкам и
* загружает содержимое каталогов
*
* @param params - {
* paramLink - ссылка
* needRefresh - необходимость обязательной загрузки данных с сервера
* panel
* }
* @param callback
*/
this.loadDir = (params, callback) => {
const p = params;
const refresh = p.isRefresh;
const {
isRefresh,
panel,
history = true,
history,
noCurrent,
currentName,
} = overrides;
} = p;
const refresh = isRefresh;
let panelChanged;
if (!noCurrent && panel && panel !== Info.panel) {
DOM.changePanel();
panelChanged = true;
}
if (!noCurrent)
if (panel && panel !== Info.panel) {
DOM.changePanel();
panelChanged = true;
}
let imgPosition;
@ -89,13 +122,12 @@ function CloudCmdProto({DOM, Listeners}) {
Images.show.load(imgPosition, panel);
/* загружаем содержимое каталога */
await ajaxLoad(addSlashToEnd(path), {
ajaxLoad(p.path, {
refresh,
history,
noCurrent,
currentName,
showDotFiles: CloudCmd.config('showDotFiles'),
}, panel);
}, panel, callback);
};
/**
@ -103,12 +135,23 @@ function CloudCmdProto({DOM, Listeners}) {
* выполняет весь функционал по
* инициализации
*/
this.init = async (prefix, config) => {
this.init = (prefix, config) => {
const func = bind(exec.series, [
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.prefixURL = `${prefix}${apiURL}`;
CloudCmd.prefixSocket = config.prefixSocket;
CloudCmd.DIR_DIST = `${prefix}/dist`;
CloudCmd.DIR_MODULES = `${this.DIR_DIST}/modules`;
CloudCmd.config = (key) => config[key];
CloudCmd.config.if = currify((key, fn, a) => config[key] && fn(a));
@ -117,6 +160,7 @@ function CloudCmdProto({DOM, Listeners}) {
* should be called from config.js only
* after successful update on server
*/
if (key === 'password')
return;
@ -126,15 +170,23 @@ function CloudCmdProto({DOM, Listeners}) {
if (config.oneFilePanel)
CloudCmd.MIN_ONE_PANEL_WIDTH = Infinity;
if (!document.body.scrollIntoViewIfNeeded)
await load.js(`${CloudCmd.DIR_MODULES}/polyfill.js`);
await initModules();
await baseInit();
CloudCmd.route(location.hash);
exec.if(document.body.scrollIntoViewIfNeeded, func, funcBefore);
};
function loadStyle(callback) {
const {prefix} = CloudCmd;
const name = prefix + '/dist/cloudcmd.common.css';
load.css(name, callback);
}
function loadPlugins(callback) {
const {prefix} = CloudCmd;
const plugins = prefix + '/plugins.js';
load.js(plugins, callback);
}
this.route = (path) => {
const query = path.split('/');
@ -144,13 +196,12 @@ function CloudCmdProto({DOM, Listeners}) {
const [kebabModule] = query;
const module = noJS(pascalCase(kebabModule.slice(1)));
const [, file] = query;
const file = query[1];
const current = DOM.getCurrentByName(file);
if (file && !current) {
const msg = formatMsg('set current file', file, 'error');
CloudCmd.log(msg);
return;
}
@ -158,12 +209,12 @@ function CloudCmdProto({DOM, Listeners}) {
CloudCmd.execFromModule(module, 'show');
};
this.logOut = async () => {
this.logOut = () => {
const url = CloudCmd.prefix + '/logout';
const error = () => document.location.reload();
const {prefix} = CloudCmd;
await DOM.Storage.clear();
DOM.Storage.clear();
unregisterSW(prefix);
DOM.load.ajax({
url,
@ -171,50 +222,56 @@ function CloudCmdProto({DOM, Listeners}) {
});
};
const initModules = async () => {
CloudCmd.Key = Key;
CloudCmd.Key.bind();
const [, modules] = await tryToCatch(Files.get, 'modules');
const showLoad = Images.show.load;
const doBefore = {
edit: showLoad,
menu: showLoad,
};
const load = (name, path, dobefore) => {
function initModules(callback) {
exec.if(CloudCmd.Key, () => {
Key = new CloudCmd.Key();
CloudCmd.Key = Key;
Key.bind();
}, (func) => {
/* привязываем клавиши к функциям */
const path = 'key.js';
loadModule({
name,
path,
dobefore,
func,
});
};
});
if (!modules)
return;
for (const module of modules.local) {
load(null, module, doBefore[module]);
}
};
async function saveCurrentName(currentName) {
await Storage.set('current-name', currentName);
Files.get('modules', (error, modules) => {
const showLoad = Images.show.load;
const doBefore = {
'edit': showLoad,
'menu': showLoad,
};
const load = (name, path, dobefore) => {
loadModule({
name,
path,
dobefore,
});
};
if (!modules)
modules = [];
modules.local.forEach((module) => {
load(null, module, doBefore[module]);
});
callback();
});
}
async function baseInit() {
function baseInit(callback) {
const files = DOM.getFiles();
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)
DOM.setCurrentFile(currentFile, {
DOM.setCurrentFile(files[0], {
// when hash is present
// it should be handled with this.route
// overwre otherwise
@ -222,7 +279,7 @@ function CloudCmdProto({DOM, Listeners}) {
});
const dirPath = DOM.getCurrentDirPath();
Listeners = CloudCmd.Listeners;
Listeners.init();
const panels = getPanels();
@ -231,12 +288,15 @@ function CloudCmdProto({DOM, Listeners}) {
Listeners.initKeysPanel();
if (!CloudCmd.config('dirStorage'))
return;
return callback();
const data = await Storage.get(dirPath);
if (!data)
await Storage.setJson(dirPath, getJsonFromFileTable());
Storage.get(dirPath, (error, data) => {
if (!data) {
data = getJSONfromFileTable();
Storage.set(dirPath, data);
}
callback();
});
}
function getPanels() {
@ -258,22 +318,28 @@ function CloudCmdProto({DOM, Listeners}) {
func(...args);
};
this.refresh = async (options = {}) => {
const {panel = Info.panel, currentName} = options;
this.refresh = (options = {}, callback) => {
if (!callback && typeof options === 'function') {
callback = options;
options = {};
}
const panel = options.panel || Info.panel;
const path = DOM.getCurrentDirPath(panel);
const isRefresh = true;
const history = false;
const noCurrent = options?.noCurrent;
const noCurrent = options ? options.noCurrent : false;
const {currentName} = options;
await CloudCmd.changeDir(path, {
CloudCmd.loadDir({
path,
isRefresh,
history,
panel,
noCurrent,
currentName,
});
}, callback);
};
/**
@ -283,132 +349,177 @@ function CloudCmdProto({DOM, Listeners}) {
* @param options
* { refresh, history } - необходимость обновить данные о каталоге
* @param panel
* @param callback
*
*/
async function ajaxLoad(path, options = {}, panel) {
const {RESTful} = DOM;
function ajaxLoad(path, options, panel, callback) {
const create = (error, json) => {
const {RESTful} = DOM;
const name = options.currentName || Info.name;
const obj = jonny.parse(json);
const isRefresh = options.refresh;
const {noCurrent} = options;
if (!isRefresh && json)
return createFileTable(obj, panel, options, callback);
const position = DOM.getPanelPosition(panel);
const sort = CloudCmd.sort[position];
const order = CloudCmd.order[position];
const query = rendy('?sort={{ sort }}&order={{ order }}', {
sort,
order,
});
RESTful.read(path + query, 'json', (error, obj) => {
if (error)
return;
options.sort = sort;
options.order = order;
createFileTable(obj, panel, options, () => {
if (isRefresh && !noCurrent)
DOM.setCurrentByName(name);
exec(callback);
});
if (!CloudCmd.config('dirStorage'))
return;
Storage.set(path, obj);
});
};
CloudCmd.log(`reading dir: "${path}";`);
if (!options)
options = {};
const dirStorage = CloudCmd.config('dirStorage');
const json = dirStorage && await Storage.getJson(path);
const name = options.currentName || Info.name;
const {noCurrent, refresh} = options;
if (!refresh && json)
return await createFileTable(json, panel, options);
const position = DOM.getPanelPosition(panel);
const sort = CloudCmd.sort[position];
const order = CloudCmd.order[position];
const query = rendy('?sort={{ sort }}&order={{ order }}', {
sort,
order,
});
const [, newObj] = await RESTful.read(path + query, 'json');
if (!newObj)
// that's OK, error handled by RESTful
return;
options.sort = sort;
options.order = order;
await createFileTable(newObj, panel, options);
if (refresh && !noCurrent)
DOM.setCurrentByName(name);
CloudCmd.log('reading dir: "' + path + '";');
if (!CloudCmd.config('dirStorage'))
return;
return create();
Storage.setJson(path, newObj);
Storage.get(path, create);
}
/**
* Функция строит файловую таблицу
* @param data - данные о файлах
* @param json - данные о файлах
* @param panelParam
* @param options - history, noCurrent, showDotFiles
* @param history
* @param callback
*/
async function createFileTable(data, panelParam, options) {
function createFileTable(json, panelParam, options, callback) {
const {
history,
noCurrent,
showDotFiles,
} = options;
const names = [
'file',
'path',
'link',
'pathLink',
];
const names = ['file', 'path', 'link', 'pathLink'];
const [error, [
file,
path,
link,
pathLink,
]] = await tryToCatch(Files.get, names);
if (error)
return DOM.Dialog.alert(error.responseText);
const panel = panelParam || DOM.getPanel();
const {prefix} = CloudCmd;
const {dir, name} = Info;
const {childNodes} = panel;
let i = childNodes.length;
while (i--)
panel.removeChild(panel.lastChild);
panel.innerHTML = buildFromJSON({
sort: options.sort,
order: options.order,
data,
id: panel.id,
prefix,
showDotFiles,
template: {
file,
path,
pathLink,
link,
},
});
Listeners.setOnPanel(panel);
if (!noCurrent) {
let current;
Files.get(names, (error, templFile, templPath, templLink, templPathLink) => {
const {Dialog} = DOM;
const panel = panelParam || DOM.getPanel();
const {prefix} = CloudCmd;
if (name === '..' && dir !== '/')
current = DOM.getCurrentByName(dir);
const {
dir,
name,
} = Info;
if (!current)
[current] = DOM.getFiles(panel);
if (error)
return Dialog.alert(TITLE, error.responseText);
DOM.setCurrentFile(current, {
history,
const {childNodes} = panel;
let i = childNodes.length;
while (i--)
panel.removeChild(panel.lastChild);
panel.innerHTML = buildFromJSON({
sort : options.sort,
order : options.order,
data : json,
id : panel.id,
prefix,
template : {
file : templFile,
path : templPath,
pathLink : templPathLink,
link : templLink,
},
});
CloudCmd.emit('active-dir', Info.dirPath);
}
Listeners.setOnPanel(panel);
if (!noCurrent) {
let current;
if (name === '..' && dir !== '/')
current = DOM.getCurrentByName(dir);
if (!current)
[current] = DOM.getFiles(panel);
DOM.setCurrentFile(current, {
history,
});
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,
dirPath,
parentDirPath,
panel,
} = Info;
if (dirPath === parentDirPath)
@ -416,13 +527,15 @@ function CloudCmdProto({DOM, Listeners}) {
const path = parentDirPath;
await CloudCmd.changeDir(path);
const current = DOM.getCurrentByName(dir);
const [first] = DOM.getFiles(panel);
DOM.setCurrentFile(current || first, {
history,
CloudCmd.loadDir({path}, () => {
const {panel} = Info;
const current = DOM.getCurrentByName(dir);
const [first] = DOM.getFiles(panel);
DOM.setCurrentFile(current || first, {
history,
});
});
};
}

View file

@ -1,44 +1,42 @@
import '../css/main.css';
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';
'use strict';
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,
listenSW,
} = require('./sw/register');
async function init(config) {
globalThis.CloudCmd = createCloudCmd({
DOM,
Listeners,
});
globalThis.DOM = DOM;
globalThis.Util = Util;
globalThis.CloudFunc = CloudFunc;
// prevent additional loading of emitify
window.Emitify = require('emitify/legacy');
module.exports = window.CloudCmd = (config) => {
window.Promise = window.Promise || require('es6-promise');
window.Util = require('../common/util');
window.CloudFunc = require('../common/cloudfunc');
await register(config);
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);
globalThis.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);
}
window.CloudCmd.init(prefix, config);
};
function getPrefix(prefix) {
if (!prefix)
@ -51,19 +49,17 @@ function getPrefix(prefix) {
}
const onUpdateFound = wraptile(async (config) => {
if (isDev)
return;
const {DOM} = globalThis;
const {DOM} = window;
const prefix = getPrefix(config.prefix);
const js = promisify(load.js);
await load.js(`${prefix}/dist/cloudcmd.common.js`);
await load.js(`${prefix}/dist/cloudcmd.js`);
await js(`${prefix}/dist/cloudcmd.common.js`);
await js(`${prefix}/dist/cloudcmd.js`);
console.log('cloudcmd: sw: updated');
DOM.Events.removeAll();
globalThis.CloudCmd(config);
window.CloudCmd(config);
});
async function register(config) {
@ -72,3 +68,4 @@ async function register(config) {
listenSW(sw, 'updatefound', onUpdateFound(config));
}

View file

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

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 CloudCmd */
import createElement from '@cloudcmd/create-element';
import {getTitle, FS} from '#common/cloudfunc';
import {encode, decode} from '#common/entity';
const btoa = require('../../common/btoa');
const createElement = require('@cloudcmd/create-element');
const {
encode,
decode,
} = require('../../common/entity');
const {
getTitle,
FS,
} = require('../../common/cloudfunc');
let Title;
const CURRENT_FILE = 'current-file';
const encodeNBSP = (a) => a?.replace('\xa0', '&nbsp;');
const decodeNBSP = (a) => a?.replace('&nbsp;', '\xa0');
const NBSP_REG = RegExp(String.fromCharCode(160), 'g');
const SPACE = ' ';
export const _CURRENT_FILE = CURRENT_FILE;
module.exports._CURRENT_FILE = CURRENT_FILE;
/**
* set name from current (or param) file
@ -18,7 +30,7 @@ export const _CURRENT_FILE = CURRENT_FILE;
* @param name
* @param current
*/
export const setCurrentName = (name, current) => {
module.exports.setCurrentName = (name, current) => {
const Info = DOM.CurrentInfo;
const {link} = Info;
const {prefix} = CloudCmd;
@ -29,7 +41,7 @@ export const setCurrentName = (name, current) => {
link.href = dir + encoded;
link.innerHTML = encoded;
current.setAttribute('data-name', createNameAttribute(name));
current.setAttribute('data-name', 'js-file-' + btoa(encodeURI(name)));
CloudCmd.emit('current-file', current);
return link;
@ -40,48 +52,28 @@ export const setCurrentName = (name, current) => {
*
* @param currentFile
*/
export const getCurrentName = (currentFile) => {
module.exports.getCurrentName = (currentFile) => {
const current = currentFile || DOM.getCurrentFile();
if (!current)
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
* @param name The string name to encode
* get current direcotory path
*/
const createNameAttribute = (name) => {
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()) => {
module.exports.getCurrentDirPath = (panel = DOM.getPanel()) => {
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
*/
export const getCurrentPath = (currentFile) => {
module.exports.getCurrentPath = (currentFile) => {
const current = currentFile || DOM.getCurrentFile();
const [element] = DOM.getByTag('a', current);
const {prefix} = CloudCmd;
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 = () => {
const href = DOM
.getCurrentDirPath()
module.exports.getCurrentDirName = () => {
const href = DOM.getCurrentDirPath()
.replace(/\/$/, '');
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 dirName = DOM.getCurrentDirName() + '/';
const index = path.lastIndexOf(dirName);
@ -125,9 +122,9 @@ export const getParentDirPath = (panel) => {
};
/**
* get not current directory path
* get not current direcotory path
*/
export const getNotCurrentDirPath = () => {
module.exports.getNotCurrentDirPath = () => {
const panel = DOM.getPanel({
active: false,
});
@ -140,20 +137,20 @@ export const getNotCurrentDirPath = () => {
*
* @currentFile
*/
export const getCurrentFile = () => {
module.exports.getCurrentFile = () => {
return DOM.getByClass(CURRENT_FILE);
};
/**
* get current file by name
*/
export const getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => {
const dataName = 'js-file-' + btoa(encodeURI(encodeNBSP(name)));
module.exports.getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => {
const dataName = 'js-file-' + btoa(encodeURI(name));
return DOM.getByDataName(dataName, panel);
};
/**
* private function that unset currentFile
* private function thet unset currentfile
*
* @currentFile
*/
@ -169,8 +166,9 @@ function unsetCurrentFile(currentFile) {
/**
* unified way to set current file
*/
export const setCurrentFile = (currentFile, options) => {
module.exports.setCurrentFile = (currentFile, options) => {
const o = options;
const CENTER = true;
const currentFileWas = DOM.getCurrentFile();
if (!currentFile)
@ -198,15 +196,13 @@ export const setCurrentFile = (currentFile, options) => {
* but it should be false
* to prevent default behavior
*/
if (!o || o.history) {
if (!o || o.history !== false) {
const historyPath = path === '/' ? path : FS + path;
DOM.setHistory(historyPath, null, historyPath);
}
}
/* scrolling to current file */
const CENTER = true;
DOM.scrollIntoViewIfNeeded(currentFile, CENTER);
CloudCmd.emit('current-file', currentFile);
@ -216,18 +212,13 @@ export const setCurrentFile = (currentFile, options) => {
return DOM;
};
export const setCurrentByName = (name) => {
const current = DOM.getCurrentByName(name);
return DOM.setCurrentFile(current);
};
/*
* set current file by position
*
* @param layer - element
* @param - position {x, y}
*/
export const getCurrentByPosition = ({x, y}) => {
module.exports.getCurrentByPosition = ({x, y}) => {
const element = document.elementFromPoint(x, y);
const getEl = (el) => {
@ -259,7 +250,7 @@ export const getCurrentByPosition = ({x, y}) => {
*
* @param currentFile
*/
export const isCurrentFile = (currentFile) => {
module.exports.isCurrentFile = (currentFile) => {
if (!currentFile)
return false;
@ -271,7 +262,8 @@ export const isCurrentFile = (currentFile) => {
*
* @param name
*/
export const setTitle = (name) => {
module.exports.setTitle = (name) => {
if (!Title)
Title = DOM.getByTag('title')[0] || createElement('title', {
innerHTML: name,
@ -283,29 +275,3 @@ export const setTitle = (name) => {
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,72 +1,69 @@
import {test, stub} from 'supertape';
import {create} from 'auto-globals';
import wraptile from 'wraptile';
import * as currentFile from './current-file.js';
'use strict';
const test = require('supertape');
const {create} = require('auto-globals');
const stub = require('@cloudcmd/stub');
const id = (a) => a;
const wraptile = require('wraptile');
const returns = wraptile(id);
const currentFile = require('./current-file');
const {_CURRENT_FILE} = currentFile;
test('current-file: setCurrentName: setAttribute', (t) => {
const {DOM, CloudCmd} = globalThis;
const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
global.DOM = getDOM();
global.CloudCmd = getCloudCmd();
const current = create();
const {setAttribute} = current;
currentFile.setCurrentName('hello', current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.ok(setAttribute.calledWith('data-name', 'js-file-aGVsbG8='), 'should call setAttribute');
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.calledWith(setAttribute, [
'data-name',
'js-file-aGVsbG8=',
], 'should call setAttribute');
t.end();
});
test('current-file: setCurrentName: setAttribute: cyrillic', (t) => {
const {DOM, CloudCmd} = globalThis;
const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
global.DOM = getDOM();
global.CloudCmd = getCloudCmd();
const current = create();
const {setAttribute} = current;
currentFile.setCurrentName('ай', current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.ok(setAttribute.calledWith('data-name', 'js-file-JUQwJUIwJUQwJUI5'), 'should call setAttribute');
t.calledWith(setAttribute, [
'data-name',
'js-file-JUQwJUIwJUQwJUI5',
], 'should call setAttribute');
t.end();
});
test('current-file: getCurrentName', (t) => {
const current = create();
current.getAttribute.returns('js-file-Ymlu');
global.DOM = DOM;
global.CloudCmd = CloudCmd;
const result = currentFile.getCurrentName(current);
t.equal(result, 'bin');
t.end();
});
test('current-file: emit', (t) => {
const {DOM, CloudCmd} = globalThis;
const {
DOM,
CloudCmd,
} = global;
const emit = stub();
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd({
global.DOM = getDOM();
global.CloudCmd = getCloudCmd({
emit,
});
@ -74,41 +71,47 @@ test('current-file: emit', (t) => {
currentFile.setCurrentName('hello', current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.ok(emit.calledWith('current-file', current), 'should call emit');
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.calledWith(emit, ['current-file', current], 'should call emit');
t.end();
});
test('current-file: setCurrentName: return', (t) => {
const {DOM, CloudCmd} = globalThis;
const {
DOM,
CloudCmd,
} = global;
const link = {};
globalThis.DOM = getDOM({
global.DOM = getDOM({
link,
});
globalThis.CloudCmd = getCloudCmd();
global.CloudCmd = getCloudCmd();
const current = create();
const result = currentFile.setCurrentName('hello', current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.equal(result, link, 'should return link');
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.end();
});
test('current-file: getParentDirPath: result', (t) => {
const {DOM} = globalThis;
const {DOM} = global;
const getCurrentDirPath = returns('/D/Films/+++favorite films/');
const getCurrentDirName = returns('+++favorite films');
globalThis.DOM = getDOM({
global.DOM = getDOM({
getCurrentDirPath,
getCurrentDirName,
});
@ -116,193 +119,78 @@ test('current-file: getParentDirPath: result', (t) => {
const result = currentFile.getParentDirPath();
const expected = '/D/Films/';
globalThis.DOM = DOM;
global.DOM = DOM;
t.equal(result, expected, 'should return parent dir path');
t.end();
});
test('current-file: isCurrentFile: no', (t) => {
const {DOM, CloudCmd} = globalThis;
const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
global.DOM = getDOM();
global.CloudCmd = getCloudCmd();
const result = currentFile.isCurrentFile();
const expect = false;
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.notOk(result);
t.equal(result, expect, 'should equal');
t.end();
});
test('current-file: isCurrentFile', (t) => {
const {DOM, CloudCmd} = globalThis;
const {
DOM,
CloudCmd,
} = global;
const isContainClass = stub();
globalThis.DOM = getDOM({
global.DOM = getDOM({
isContainClass,
});
globalThis.CloudCmd = getCloudCmd();
global.CloudCmd = getCloudCmd();
const current = {};
currentFile.isCurrentFile(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.calledWith(isContainClass, [current, _CURRENT_FILE], 'should call isContainClass');
t.ok(isContainClass.calledWith(current, _CURRENT_FILE), 'should call isContainClass');
t.end();
});
test('current-file: getCurrentType', (t) => {
const {DOM, CloudCmd} = globalThis;
globalThis.DOM = 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();
});
function getCloudCmd({emit} = {}) {
return {
prefix: '',
emit: emit || stub(),
};
}
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 = {},
getCurrentDirPath = stub(),
getCurrentDirName = stub(),
getByDataName = stub(),
isContainClass = stub(),
getCurrentType = stub(),
getCurrentPath = stub().returns(''),
} = overrides;
function getDOM({
link = {},
getCurrentDirPath = stub(),
getCurrentDirName = stub(),
getByDataName = stub(),
isContainClass = stub(),
} = {}) {
return {
getCurrentDirPath,
getCurrentDirName,
getCurrentPath,
getByDataName,
isContainClass,
getCurrentType,
CurrentInfo: {
link,
dirPath: '/',
},
};
}

View file

@ -1,18 +1,21 @@
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, {
cancel: false,
});
const {
alert,
prompt,
confirm,
}= require('smalltalk');
export const prompt = (...a) => tryToCatch(smalltalk.prompt, title, ...a);
export const confirm = (...a) => tryToCatch(smalltalk.confirm, title, ...a);
export const progress = (...a) => smalltalk.progress(title, ...a);
module.exports = {
alert,
prompt,
confirm,
};
alert.noFiles = () => {
return smalltalk.alert(title, 'No files selected!', {
module.exports.alert.noFiles = (title) => {
return alert(title, 'No files selected!', {
cancel: false,
});
};

View file

@ -1,27 +1,29 @@
/* global DOM, CloudCmd */
import philip from 'philip';
import * as Dialog from '#dom/dialog';
import {FS} from '#common/cloudfunc';
import * as Images from '#dom/images';
/* global CloudCmd */
export const uploadDirectory = (items) => {
'use strict';
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;
if (items.length)
Images.show('top');
const entries = Array
.from(items)
.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 entries = [...items].map((item) => {
return item.webkitGetAsEntry();
});
const dirPath = getPathWhenRootEmpty();
const path = dirPath
.replace(/\/$/, '');
const uploader = philip(entries, (type, name, data, i, n, callback) => {
const {prefixURL} = CloudCmd;
const full = prefixURL + FS + path + name;
@ -45,7 +47,7 @@ export const uploadDirectory = (items) => {
const max = next - current;
const value = current + percent(count, 100, max);
progress.setProgress(value);
setProgress(value);
});
});
@ -54,11 +56,25 @@ export const uploadDirectory = (items) => {
uploader.abort();
});
uploader.on('progress', setProgress);
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 = {
show,
hide,
getByClass,
getByClassAll,
getByDataName,
getById,
getByTag,
isContainClass,
};
const currify = require('currify/legacy');
const DOM = module.exports;
/**
* check class of element
@ -17,7 +10,7 @@ const DOM = {
* @param element
* @param className
*/
export function isContainClass(element, className) {
const isContainClass = (element, className) => {
if (!element)
throw Error('element could not be empty!');
@ -25,68 +18,67 @@ export function isContainClass(element, className) {
throw Error('className could not be empty!');
if (Array.isArray(className))
return className.some(currify(
isContainClass,
element,
));
return className.some(currify(isContainClass, element));
const {classList} = element;
return classList.contains(className);
}
};
module.exports.isContainClass = isContainClass;
/**
* Function search element by tag
* @param tag - className
* @param element - element
*/
export function getByTag(tag, element = document) {
module.exports.getByTag = (tag, element = document) => {
return element.getElementsByTagName(tag);
}
};
/**
* Function search element by id
* @param id
* @param element
* @param Id - id
*/
export function getById(id, element = document) {
return element.querySelector(`#${id}`);
}
module.exports.getById = (id, element = document) => {
return element.querySelector('#' + id);
};
/**
* Function search first element by class name
* @param className - className
* @param element - element
*/
export function getByClass(className, element = document) {
module.exports.getByClass = (className, element = document) => {
return DOM.getByClassAll(className, element)[0];
}
};
export function getByDataName(attribute, element = document) {
module.exports.getByDataName = (attribute, element = document) => {
const selector = '[' + 'data-name="' + attribute + '"]';
return element.querySelector(selector);
}
};
/**
* Function search element by class name
* @param className
* @param element
* @param pClass - className
* @param element - element
*/
export function getByClassAll(className, element) {
module.exports.getByClassAll = (className, element) => {
return (element || document).getElementsByClassName(className);
}
};
/**
* add class=hidden to element
*
* @param element
*/
export function hide(element) {
module.exports.hide = (element) => {
element.classList.add('hidden');
return DOM;
}
};
export function show(element) {
module.exports.show = (element) => {
element.classList.remove('hidden');
return DOM;
}
};

View file

@ -1,27 +1,19 @@
import {test, stub} from 'supertape';
import {create} from 'auto-globals';
import {tryCatch} from 'try-catch';
import {
isContainClass,
getByTag,
getById,
getByClass,
getByDataName,
getByClassAll,
hide,
show,
} from './dom-tree.js';
'use strict';
const test = require('supertape');
const {create} = require('auto-globals');
const tryCatch = require('try-catch');
const {isContainClass} = require('./dom-tree');
test('dom: isContainClass: no element', (t) => {
const [e] = tryCatch(isContainClass);
t.equal(e.message, 'element could not be empty!', 'should throw when no element');
t.end();
});
test('dom: isContainClass: no className', (t) => {
const [e] = tryCatch(isContainClass, {});
t.equal(e.message, 'className could not be empty!', 'should throw when no element');
t.end();
});
@ -33,7 +25,7 @@ test('dom: isContainClass: contains', (t) => {
const className = 'hello';
isContainClass(el, className);
t.calledWith(contains, [className], 'should call contains');
t.ok(contains.calledWith(className), 'should call contains');
t.end();
});
@ -42,111 +34,13 @@ test('dom: isContainClass: contains: array', (t) => {
const {contains} = el.classList;
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();
});
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 = [];
export const add = (el, name, fn) => {
module.exports.add = (el, name, fn) => {
list.push([
el,
name,
@ -8,8 +10,11 @@ export const add = (el, name, fn) => {
]);
};
export const clear = () => {
module.exports.clear = () => {
list = [];
};
export const get = () => list;
module.exports.get = () => {
return list;
};

View file

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

View file

@ -1,203 +1,249 @@
import itype from 'itype';
import * as EventStore from './event-store.js';
'use strict';
/**
* safe add event listener
*
* @param type
* @param element - document by default
* @param listener
*/
export const add = (type, element, listener) => {
checkType(type);
const itype = require('itype/legacy');
const EventStore = require('./event-store');
module.exports = new EventsProto();
function EventsProto() {
const Events = this;
parseArgs(type, element, listener, (element, args) => {
const [name, fn, options] = args;
const getEventOptions = (eventName) => {
if (eventName !== 'touchstart')
return false;
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
*/
export const addOnce = (type, element, listener) => {
const once = (event) => {
Events.remove(type, element, once);
listener(event);
return {
passive: true,
};
};
if (!listener) {
listener = element;
element = null;
function parseArgs(eventName, element, listener, callback) {
let isFunc;
const args = [
eventName,
element,
listener,
callback,
];
const EVENT_NAME = 1;
const ELEMENT = 0;
const type = itype(eventName);
switch(type) {
default:
if (!/element$/.test(type))
throw Error('unknown eventName: ' + type);
parseArgs(
args[EVENT_NAME],
args[ELEMENT],
listener,
callback
);
break;
case 'string':
isFunc = itype.function(element);
if (isFunc) {
listener = element;
element = null;
}
if (!element)
element = window;
callback(element, [
eventName,
listener,
getEventOptions(eventName),
]);
break;
case 'array':
eventName.forEach((eventName) => {
parseArgs(
eventName,
element,
listener,
callback
);
});
break;
case 'object':
Object.keys(eventName).forEach((name) => {
const eventListener = eventName[name];
parseArgs(
name,
element,
eventListener,
callback
);
});
break;
}
}
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) => {
if (eventName !== 'touchstart')
return false;
return {
passive: true,
/**
* safe add event listener
*
* @param type
* @param element {document by default}
* @param listener
*/
this.add = (type, element, listener) => {
checkType(type);
parseArgs(type, element, listener, (element, args) => {
const [name, fn, options] = args;
element.addEventListener(name, fn, options);
EventStore.add(element, name, fn);
});
return Events;
};
};
function parseArgs(eventName, element, listener, callback) {
let isFunc;
const args = [
eventName,
element,
listener,
callback,
];
const EVENT_NAME = 1;
const ELEMENT = 0;
const type = itype(eventName);
switch(type) {
default:
if (!type.endsWith('element'))
throw Error(`unknown eventName: ${type}`);
/**
* 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);
};
parseArgs(args[EVENT_NAME], args[ELEMENT], listener, callback);
break;
case 'string':
isFunc = itype.function(element);
if (isFunc) {
if (!listener) {
listener = element;
element = null;
}
if (!element)
element = window;
this.add(type, element, once);
callback(element, [
eventName,
listener,
getEventOptions(eventName),
]);
break;
return Events;
};
case 'array':
/**
* safe remove event listener
*
* @param type
* @param listener
* @param element {document by default}
*/
this.remove = (type, element, listener) => {
checkType(type);
for (const name of eventName) {
parseArgs(name, element, listener, callback);
}
parseArgs(type, element, listener, (element, args) => {
element.removeEventListener(...args);
});
break;
return Events;
};
case 'object':
/**
* remove all added event listeners
*
* @param listener
*/
this.removeAll = () => {
const events = EventStore.get();
for (const name of Object.keys(eventName)) {
const eventListener = eventName[name];
parseArgs(name, element, eventListener, callback);
}
for (const [el, name, fn] of events)
el.removeEventListener(name, fn);
break;
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!');
}
}
const Events = {
add,
addClick,
addContextMenu,
addKey,
addLoad,
addOnce,
remove,
removeAll,
rmClick,
rmKey,
};

View file

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

View file

@ -1,13 +1,20 @@
/* global DOM */
import createElement from '@cloudcmd/create-element';
'use strict';
const createElement = require('@cloudcmd/create-element');
const Images = module.exports;
const LOADING = 'loading';
const HIDDEN = 'hidden';
const ERROR = 'error';
const getLoadingType = () => isSVG() ? '-svg' : '-gif';
function getLoadingType() {
return isSVG() ? '-svg' : '-gif';
}
export const get = getElement;
module.exports.get = getElement;
/**
* check SVG SMIL animation support
@ -36,7 +43,7 @@ function getElement() {
}
/* Функция создаёт картинку загрузки */
export const loading = () => {
module.exports.loading = () => {
const element = getElement();
const {classList} = element;
const loadingImage = LOADING + getLoadingType();
@ -48,7 +55,7 @@ export const loading = () => {
};
/* Функция создаёт картинку ошибки загрузки */
export const error = () => {
module.exports.error = () => {
const element = getElement();
const {classList} = element;
const loadingImage = LOADING + getLoadingType();
@ -59,21 +66,15 @@ export const error = () => {
return element;
};
show.load = show;
show.error = (text) => {
const image = Images.error();
DOM.show(image);
image.title = text;
return image;
};
module.exports.show = show;
module.exports.show.load = show;
module.exports.show.error = error;
/**
* Function shows loading spinner
* position = {top: true};
*/
export function show(position, panel) {
function show(position, panel) {
const image = Images.loading();
const parent = image.parentElement;
const refreshButton = DOM.getRefreshButton(panel);
@ -91,7 +92,7 @@ export function show(position, panel) {
current = refreshButton.parentElement;
}
if (!parent || parent && parent !== current)
if (!parent || (parent && parent !== current))
current.appendChild(image);
DOM.show(image);
@ -99,25 +100,34 @@ export function show(position, panel) {
return image;
}
function error(text) {
const image = Images.error();
DOM.show(image);
image.title = text;
return image;
}
/**
* hide load image
*/
export function hide() {
module.exports.hide = () => {
const element = Images.get();
DOM.hide(element);
return Images;
}
};
export const setProgress = (value, title) => {
module.exports.setProgress = (value, title) => {
const DATA = 'data-progress';
const element = Images.get();
if (!element)
return Images;
element.setAttribute(DATA, `${value}%`);
element.setAttribute(DATA, value + '%');
if (title)
element.title = title;
@ -125,7 +135,7 @@ export const setProgress = (value, title) => {
return Images;
};
export const clearProgress = () => {
module.exports.clearProgress = () => {
const DATA = 'data-progress';
const element = Images.get();
@ -138,12 +148,3 @@ export const clearProgress = () => {
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,70 @@
/* global CloudCmd */
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';
'use strict';
export const loadRemote = callbackify(async (name, options) => {
/* global 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;
if (o.name && window[o.name])
return;
return callback();
const modules = await Files.get('modules');
const online = config('online') && navigator.onLine;
const module = findObjByNameInArr(modules.remote, name);
const isArray = itype.array(module.local);
const {version} = module;
let remoteTmpls;
let local;
if (isArray) {
remoteTmpls = module.remote;
({local} = module);
} else {
remoteTmpls = [module.remote];
local = [module.local];
}
const localURL = [];
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);
Files.get('modules', (error, modules) => {
const online = config('online') && navigator.onLine;
const module = findObjByNameInArr(modules.remote, name);
if (!e)
return;
}
await load.parallel(localURL);
});
const isArray = itype.array(module.local);
const {version} = module;
let remoteTmpls;
let local;
if (isArray) {
remoteTmpls = module.remote;
local = module.local;
} else {
remoteTmpls = [module.remote];
local = [module.local];
}
const localURL = local.map((url) => {
return prefix + url;
});
const remoteURL = remoteTmpls.map((tmpl) => {
return rendy(tmpl, {
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';
import jonny from 'jonny';
import Emitify from 'emitify';
import exec from 'execon';
import * as Images from '#dom/images';
'use strict';
const itype = require('itype/legacy');
const jonny = require('jonny/legacy');
const Emitify = require('emitify/legacy');
const exec = require('execon');
const Images = require('./images');
module.exports.getIdBySrc = getIdBySrc;
/**
* Function gets id by src
@ -10,18 +14,17 @@ import * as Images from '#dom/images';
*
* Example: http://domain.com/1.js -> 1_js
*/
export function getIdBySrc(src) {
function getIdBySrc(src) {
const isStr = itype.string(src);
if (!isStr)
return;
if (src.includes(':'))
if (~src.indexOf(':'))
src += '-join';
const num = src.lastIndexOf('/') + 1;
const sub = src.substr(src, num);
const id = src
.replace(sub, '')
.replace(/\./g, '-');
@ -34,23 +37,21 @@ export function getIdBySrc(src) {
*
* @param params
*/
export const ajax = (params) => {
module.exports.ajax = (params) => {
const p = params;
const isObject = itype.object(p.data);
const isArray = itype.array(p.data);
const isArrayBuf = itype(p.data) === 'arraybuffer';
const type = p.type || p.method || 'GET';
const {headers = {}} = p;
const headers = p.headers || {};
const xhr = new XMLHttpRequest();
xhr.open(type, p.url, true);
for (const name of Object.keys(headers)) {
Object.keys(headers).forEach((name) => {
const value = headers[name];
xhr.setRequestHeader(name, value);
}
});
if (p.responseType)
xhr.responseType = p.responseType;
@ -60,7 +61,7 @@ export const ajax = (params) => {
if (!isArrayBuf && isObject || isArray)
data = jonny.stringify(p.data);
else
({data} = p);
data = p.data;
xhr.onreadystatechange = (event) => {
const xhr = event.target;
@ -78,7 +79,7 @@ export const ajax = (params) => {
return exec(p.error, xhr);
const notText = p.dataType !== 'text';
const isContain = type.includes(TYPE_JSON);
const isContain = ~type.indexOf(TYPE_JSON);
let data = xhr.response;
@ -91,11 +92,12 @@ export const ajax = (params) => {
xhr.send(data);
};
export const put = (url, body) => {
module.exports.put = (url, body) => {
const emitter = Emitify();
const xhr = new XMLHttpRequest();
url = encodeURI(url).replace(/#/g, '#');
url = encodeURI(url)
.replace('#', '%23');
xhr.open('put', url, true);
@ -103,7 +105,7 @@ export const put = (url, body) => {
if (!event.lengthComputable)
return;
const percent = event.loaded / event.total * 100;
const percent = (event.loaded / event.total) * 100;
const count = Math.round(percent);
emitter.emit('progress', count);
@ -116,10 +118,8 @@ export const put = (url, body) => {
if (!over)
return;
if (xhr.status === OK) {
emitter.emit('progress', 100);
if (xhr.status === OK)
return emitter.emit('end');
}
const error = Error(xhr.responseText);
emitter.emit('error', error);
@ -129,3 +129,4 @@ export const put = (url, body) => {
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,224 @@
import {tryToCatch} from 'try-to-catch';
import * as Dialog from '#dom/dialog';
import * as Images from '#dom/images';
import {encode} from '#common/entity';
import * as IO from './io/index.js';
'use strict';
const handleError = (promise) => async (...args) => {
const [e, data] = await tryToCatch(promise, ...args);
if (!e)
return [e, data];
const encoded = encode(e.message);
Images.show.error(encoded);
Dialog.alert(encoded);
return [e, data];
};
/* global CloudCmd, DOM */
export const remove = handleError(IO.remove);
export const patch = handleError(IO.patch);
export const write = handleError(IO.write);
export const createDirectory = handleError(IO.createDirectory);
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);
const itype = require('itype/legacy');
export const Config = {
read: handleError(IO.Config.read),
write: handleError(IO.Config.write),
};
const {FS} = require('../../common/cloudfunc');
const {encode} = require('../../common/entity');
export const Markdown = {
read: handleError(IO.Markdown.read),
render: handleError(IO.Markdown.render),
};
module.exports = new RESTful();
const Images = require('./images');
const load = require('./load');
module.exports._replaceHash = replaceHash;
function replaceHash(url) {
/*
* if we send ajax request -
* no need in hash so we escape #
*/
return url.replace(/#/g, '%23');
}
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 notLog = !url.includes('?');
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,
callback,
});
};
this.mv = function(data, callback) {
sendRequest({
method : 'PUT',
url : '/mv',
data,
callback,
imgPosition : { top: true },
});
};
this.Config = {
read(callback) {
sendRequest({
method : 'GET',
url : '/config',
callback,
imgPosition : { top: true },
notLog : true,
});
},
write(data, callback) {
sendRequest({
method : 'PATCH',
url : '/config',
data,
callback,
imgPosition : { top: true },
});
},
};
this.Markdown = {
read(url, callback) {
sendRequest({
method : 'GET',
url : '/markdown' + url,
callback,
imgPosition : { top: true },
notLog : true,
});
},
render(data, callback) {
sendRequest({
method : 'PUT',
url : '/markdown',
data,
callback,
imgPosition : { top: true },
notLog : true,
});
},
};
function sendRequest(params) {
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;
const encoded = encode(text);
Images.show.error(encoded);
setTimeout(() => {
DOM.Dialog.alert(CloudCmd.TITLE, encoded);
}, 100);
p.callback(Error(text));
},
success: (data) => {
Images.hide();
if (!p.notLog)
CloudCmd.log(data);
p.callback(null, data);
},
});
}
}

15
client/dom/rest.spec.js Normal file
View file

@ -0,0 +1,15 @@
'use strict';
const test = require('supertape');
const rest = require('./rest');
test('cloudcmd: client: rest: replaceHash', (t) => {
const {_replaceHash} = rest;
const url = '/hello/####world';
const result = _replaceHash(url);
const expected = '/hello/%23%23%23%23world';
t.equal(result, expected, 'should equal');
t.end();
});

View file

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

View file

@ -1,26 +1,67 @@
const {parse, stringify} = JSON;
'use strict';
export const set = (name, data) => {
localStorage.setItem(name, data);
};
const itype = require('itype/legacy');
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) => {
localStorage.setItem(name, stringify(data));
};
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) => {
/** remove element */
module.exports.remove = (item, callback) => {
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;
let 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,28 @@
'use strict';
/* 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 loadFile = wraptile(_loadFile);
export const uploadFiles = (dir, files) => {
const {getCurrentDirPath: getPathWhenRootEmpty} = DOM;
module.exports = (dir, files) => {
if (!files) {
files = dir;
dir = getCurrentDirPath();
dir = getPathWhenRootEmpty();
}
const n = files.length;
@ -21,7 +30,7 @@ export const uploadFiles = (dir, files) => {
if (!n)
return;
const array = Array.from(files);
const array = [...files];
const {name} = files[0];
eachSeries(array, loadFile(dir, n), onEnd(name));
@ -49,8 +58,7 @@ function _loadFile(dir, n, file, callback) {
++i;
load
.put(api + path, file)
load.put(api + path, file)
.on('error', showError)
.on('end', callback)
.on('progress', (count) => {
@ -63,5 +71,6 @@ function _loadFile(dir, n, file, callback) {
}
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,
};
};

View file

@ -1,17 +1,23 @@
import {encode} from '#common/entity';
'use strict';
const isBool = (a) => typeof a === 'boolean';
const isString = (a) => typeof a === 'string';
const currify = require('currify/legacy');
const {keys} = Object;
const isType = currify((type, object, name) => {
return typeof object[name] === type;
});
export function getElementByName(selector, element) {
const isBool = isType('boolean');
module.exports.getElementByName = getElementByName;
function getElementByName(selector, element) {
const str = `[data-name="js-${selector}"]`;
return element.querySelector(str);
return element
.querySelector(str);
}
export const getName = (element) => {
module.exports.getName = (element) => {
const name = element
.getAttribute('data-name')
.replace(/^js-/, '');
@ -19,22 +25,20 @@ export const getName = (element) => {
return name;
};
export const convert = (config) => {
const result = config;
module.exports.convert = (config) => {
const result = {
...config,
};
for (const name of keys(config)) {
const item = config[name];
if (isBool(item)) {
const array = Object.keys(result);
array
.filter(isBool(result))
.forEach((name) => {
const item = result[name];
result[name] = setState(item);
continue;
}
if (isString(item)) {
result[name] = encode(item);
continue;
}
}
});
return result;
};
@ -46,7 +50,7 @@ function setState(state) {
return '';
}
export const getValue = (name, element) => {
module.exports.getValue = (name, element) => {
const el = getElementByName(name, element);
const {type} = el;
@ -62,7 +66,7 @@ export const getValue = (name, element) => {
}
};
export const setValue = (name, value, element) => {
module.exports.setValue = (name, value, element) => {
const el = getElementByName(name, element);
const {type} = el;
@ -76,3 +80,4 @@ export const setValue = (name, value, element) => {
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,544 +1,490 @@
/* 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 toggleVim = (keyCode, overrides = {}) => {
const {_config, config} = overrides;
if (!config('vim') && keyCode === KEY.ESC)
_config('vim', true);
};
const isUndefined = (a) => typeof a === 'undefined';
Chars([]);
const {assign} = Object;
const binder = createBinder();
KeyProto.prototype = KEY;
CloudCmd.Key = KeyProto;
const {loadDir} = CloudCmd;
const bind = () => {
Events.addKey(listener, true);
binder.setBind();
};
export const Key = assign(binder, KEY, {
bind,
});
export const _listener = listener;
function getChar(event) {
/*
* event.keyIdentifier deprecated in chrome v51
* but event.key is absent in chrome <= v51
*/
const {
key,
shift,
keyCode,
keyIdentifier,
} = event;
function KeyProto() {
let Binded;
const char = key || fromCharCode(keyIdentifier);
const symbol = getSymbol(shift, keyCode);
const Key = this;
return [symbol, char];
}
async function listener(event, overrides = {}) {
const {
config = CloudCmd.config,
_config = CloudCmd._config,
switchKey = _switchKey,
vim = _vim,
} = overrides;
this.isBind = () => {
return Binded;
};
const {keyCode} = event;
this.setBind = () => {
Binded = true;
};
// strange chrome bug calls listener twice
// in second time event misses a lot fields
if (isUndefined(event.altKey))
return;
this.unsetBind = () => {
Binded = false;
};
const alt = event.altKey;
const ctrl = event.ctrlKey;
const meta = event.metaKey;
const isBetween = keyCode >= KEY.ZERO && keyCode <= KEY.Z;
const isNumpad = /Numpad/.test(event.code);
this.bind = () => {
Events.addKey(listener);
Binded = true;
};
const [symbol, char] = getChar(event);
if (!binder.isBind())
return;
toggleVim(keyCode, {
config,
_config,
});
const isVim = config('vim');
if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || symbol))
return setCurrentByChar(char, Chars);
Chars([]);
await switchKey(event);
if (keyCode >= KEY.F1 && keyCode <= KEY.F10)
return;
if (isVim)
vim(char, event);
}
function getSymbol(shift, keyCode) {
switch(keyCode) {
case KEY.DOT:
return '.';
case KEY.HYPHEN:
return shift ? '_' : '-';
case KEY.EQUAL:
return shift ? '+' : '=';
function getChar(event) {
/*
* event.keyIdentifier deprecated in chrome v51
* but event.key is absent in chrome <= v51
*/
if (event.key)
return event.key;
return fromCharCode(event.keyIdentifier);
}
return '';
}
function fromCharCode(keyIdentifier) {
const code = keyIdentifier.substring(2);
const hex = parseInt(code, 16);
return String.fromCharCode(hex);
}
async function _switchKey(event) {
const Info = DOM.CurrentInfo;
let i;
let isSelected;
let prev;
let next;
let current = Info.element;
let dataName;
const {
name,
panel,
path,
isDir,
} = Info;
const {
Operation,
changeDir,
config,
} = CloudCmd;
const {keyCode} = event;
const alt = event.altKey;
const shift = event.shiftKey;
const ctrl = event.ctrlKey;
const meta = event.metaKey;
const ctrlMeta = ctrl || meta;
if (current) {
prev = current.previousSibling;
next = current.nextSibling;
function listener(event) {
const {keyCode} = event;
const alt = event.altKey;
const ctrl = event.ctrlKey;
const shift = event.shiftKey;
const meta = event.metaKey;
const isBetween = keyCode >= KEY.ZERO && keyCode <= KEY.Z;
const isNumpad = /Numpad/.test(event.code);
let char = getChar(event);
let isSymbol = ~['.', '_', '-', '+', '='].indexOf(char);
if (!isSymbol) {
isSymbol = getSymbol(shift, keyCode);
if (isSymbol)
char = isSymbol;
}
if (!Key.isBind())
return;
const isVim = CloudCmd.config('vim');
if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || isSymbol))
return setCurrentByChar(char, Chars);
Chars([]);
switchKey(event);
if (keyCode >= KEY.F1 && keyCode <= KEY.F10)
return;
if (isVim)
vim(char, event);
}
switch(keyCode) {
case KEY.TAB:
DOM.changePanel();
event.preventDefault();
break;
case KEY.INSERT:
DOM
.toggleSelectedFile(current)
.setCurrentFile(next);
break;
case KEY.INSERT_MAC:
DOM
.toggleSelectedFile(current)
.setCurrentFile(next);
break;
case KEY.DELETE:
if (shift)
Operation.show('delete:silent');
else
Operation.show('delete');
function getSymbol(shift, keyCode) {
switch(keyCode) {
case KEY.DOT:
return '.';
break;
case KEY.ASTERISK:
DOM.toggleAllSelectedFiles(current);
break;
case KEY.PLUS:
DOM.expandSelection();
event.preventDefault();
break;
case KEY.MINUS:
DOM.shrinkSelection();
event.preventDefault();
break;
case KEY.F1:
CloudCmd.Help.show();
event.preventDefault();
break;
case KEY.F2:
CloudCmd.UserMenu.show();
break;
case KEY.F3:
event.preventDefault();
case KEY.HYPHEN:
return shift ? '_' : '-';
if (Info.isDir)
await changeDir(path);
else if (shift)
CloudCmd.View.show(null, {
raw: true,
});
else if (ctrlMeta)
CloudCmd.sortPanel('name');
else
CloudCmd.View.show();
break;
case KEY.EQUAL:
return shift ? '+' : '=';
}
}
case KEY.F4:
if (config('vim'))
CloudCmd.EditFileVim.show();
else
CloudCmd.EditFile.show();
function fromCharCode(keyIdentifier) {
const code = keyIdentifier.substring(2);
const hex = parseInt(code, 16);
const char = String.fromCharCode(hex);
event.preventDefault();
break;
return char;
}
case KEY.F5:
if (ctrlMeta)
CloudCmd.sortPanel('date');
else if (alt)
Operation.show('pack');
else
Operation.show('copy');
function switchKey(event) {
let i;
let isSelected;
let prev;
let next;
let current = Info.element;
let dataName;
event.preventDefault();
break;
case KEY.F6:
if (ctrlMeta)
CloudCmd.sortPanel('size');
else if (shift)
DOM.renameCurrent(current);
else
Operation.show('move');
const {
name,
panel,
path,
isDir,
} = Info;
event.preventDefault();
break;
case KEY.F7:
if (shift)
DOM.promptNewFile();
else
DOM.promptNewDir();
const {Operation} = CloudCmd;
const {keyCode} = event;
event.preventDefault();
break;
case KEY.F8:
Operation.show('delete');
event.preventDefault();
break;
case KEY.F9:
if (alt)
Operation.show('extract');
else
CloudCmd.Menu.show();
const alt = event.altKey;
const shift = event.shiftKey;
const ctrl = event.ctrlKey;
const meta = event.metaKey;
const ctrlMeta = ctrl || meta;
event.preventDefault();
break;
case KEY.F10:
CloudCmd.Config.show();
event.preventDefault();
break;
case KEY.TRA:
event.preventDefault();
if (current) {
prev = current.previousSibling;
next = current.nextSibling;
}
if (shift)
return CloudCmd.Terminal.show();
CloudCmd.Konsole.show();
break;
case KEY.BRACKET_CLOSE:
CloudCmd.Konsole.show();
event.preventDefault();
break;
case KEY.SPACE:
event.preventDefault();
if (!isDir || name === '..')
isSelected = true;
else
isSelected = DOM.isSelected(current);
if (!isSelected)
await DOM.loadCurrentSize(current);
DOM.toggleSelectedFile(current);
break;
case KEY.U:
if (ctrlMeta) {
DOM.swapPanels();
switch(keyCode) {
case Key.TAB:
DOM.changePanel();
event.preventDefault();
}
break;
break;
/* navigation on file table: *
* in case of pressing button 'up', *
* select previous row */
case KEY.UP:
if (shift)
DOM.toggleSelectedFile(current);
case Key.INSERT:
DOM .toggleSelectedFile(current)
.setCurrentFile(next);
break;
DOM.setCurrentFile(prev);
event.preventDefault();
break;
/* in case of pressing button 'down', *
* select next row */
case KEY.DOWN:
if (shift)
DOM.toggleSelectedFile(current);
case Key.INSERT_MAC:
DOM .toggleSelectedFile(current)
.setCurrentFile(next);
break;
DOM.setCurrentFile(next);
event.preventDefault();
break;
case KEY.LEFT:
if (!alt)
return;
event.preventDefault();
dataName = Info.panel.getAttribute('data-name');
if (dataName === 'js-right')
DOM.duplicatePanel();
break;
case KEY.RIGHT:
if (!alt)
return;
event.preventDefault();
dataName = Info.panel.getAttribute('data-name');
if (dataName === 'js-left')
DOM.duplicatePanel();
break;
/* in case of pressing button 'Home', *
* go to top element */
case KEY.HOME:
DOM.setCurrentFile(Info.first);
event.preventDefault();
break;
/* in case of pressing button 'End', select last element */
case KEY.END:
DOM.setCurrentFile(Info.last);
event.preventDefault();
break;
/* если нажали клавишу page down проматываем экран */
case KEY.PAGE_DOWN:
DOM.scrollByPages(panel, 1);
for (i = 0; i < 30; i++) {
if (!current.nextSibling)
break;
current = current.nextSibling;
}
DOM.setCurrentFile(current);
event.preventDefault();
break;
/* если нажали клавишу page up проматываем экран */
case KEY.PAGE_UP:
DOM.scrollByPages(panel, -1);
for (i = 0; i < 30; i++) {
if (!current.previousSibling)
break;
current = current.previousSibling;
}
DOM.setCurrentFile(current);
event.preventDefault();
break;
case KEY.ENTER:
if (Info.isDir)
await changeDir(path);
else
CloudCmd.View.show();
break;
case KEY.BACKSPACE:
CloudCmd.goToParentDir();
event.preventDefault();
break;
case KEY.BACKSLASH:
if (ctrlMeta)
await changeDir('/');
break;
case KEY.A:
if (ctrlMeta) {
DOM.selectAllFiles();
event.preventDefault();
}
break;
case KEY.G:
if (alt) {
DOM.goToDirectory();
event.preventDefault();
}
break;
case KEY.L:
if (ctrlMeta && shift) {
CloudCmd.logOut();
event.preventDefault();
}
break;
case KEY.M:
if (ctrlMeta) {
if (config('vim'))
CloudCmd.EditNamesVim.show();
case Key.DELETE:
if (shift)
Operation.show('delete:silent');
else
CloudCmd.EditNames.show();
Operation.show('delete');
break;
case Key.ASTERISK:
DOM.toggleAllSelectedFiles(current);
break;
case Key.PLUS:
DOM.expandSelection();
event.preventDefault();
break;
case Key.MINUS:
DOM.shrinkSelection();
event.preventDefault();
break;
case Key.F1:
CloudCmd.Help.show();
event.preventDefault();
break;
case Key.F2:
DOM.renameCurrent(current);
break;
case Key.F3:
if (Info.isDir)
loadDir({path});
else if (shift)
CloudCmd.Markdown.show(path);
else if (ctrlMeta)
CloudCmd.sortPanel('name');
else
CloudCmd.View.show();
event.preventDefault();
}
break;
break;
case KEY.P:
if (!ctrlMeta)
return;
event.preventDefault();
clipboard
.writeText(Info.dirPath)
.catch(CloudCmd.log);
break;
/**
* обновляем страницу,
* загружаем содержимое каталога
* при этом данные берём всегда с
* сервера, а не из кэша
* (обновляем кэш)
*/
case KEY.R:
if (ctrlMeta) {
CloudCmd.log('reloading page...\n');
CloudCmd.refresh();
case Key.F4:
if (shift)
CloudCmd.EditFileVim.show();
else
CloudCmd.EditFile.show();
event.preventDefault();
}
break;
break;
case KEY.C:
if (ctrlMeta)
Buffer.copy();
break;
case KEY.X:
if (ctrlMeta)
Buffer.cut();
break;
case KEY.V:
if (ctrlMeta)
Buffer.paste();
break;
case KEY.Z:
if (ctrlMeta)
Buffer.clear();
break;
case KEY.COLON:
CloudCmd.CommandLine.show();
event.preventDefault();
break;
/* чистим хранилище */
case KEY.D:
if (ctrlMeta) {
CloudCmd.log('clearing storage...');
await DOM.Storage.clear();
CloudCmd.log('storage cleared');
case Key.F5:
if (ctrlMeta)
CloudCmd.sortPanel('date');
else if (alt)
Operation.show('pack');
else
Operation.show('copy');
event.preventDefault();
}
break;
break;
case KEY.DOT:
if (meta && shift) {
const showDotFiles = !CloudCmd.config('showDotFiles');
CloudCmd._config('showDotFiles', showDotFiles);
CloudCmd.refresh();
await DOM.RESTful.Config.write({
showDotFiles,
case Key.F6:
if (ctrlMeta)
CloudCmd.sortPanel('size');
else
Operation.show('move');
event.preventDefault();
break;
case Key.F7:
if (shift)
DOM.promptNewFile();
else
DOM.promptNewDir();
event.preventDefault();
break;
case Key.F8:
Operation.show('delete');
event.preventDefault();
break;
case Key.F9:
if (alt)
Operation.show('extract');
else
CloudCmd.Menu.show();
event.preventDefault();
break;
case Key.F10:
CloudCmd.Config.show();
event.preventDefault();
break;
case Key.TRA:
event.preventDefault();
if (shift)
return CloudCmd.Terminal.show();
CloudCmd.Konsole.show();
break;
case KEY.BRACKET_CLOSE:
CloudCmd.Konsole.show();
event.preventDefault();
break;
case Key.SPACE:
if (!isDir || name === '..')
isSelected = true;
else
isSelected = DOM.isSelected(current);
exec.if(isSelected, () => {
DOM.toggleSelectedFile(current);
}, (callback) => {
DOM.loadCurrentSize(callback, current);
});
}
event.preventDefault();
break;
break;
case Key.U:
if (ctrlMeta) {
DOM.swapPanels();
event.preventDefault();
}
break;
/* navigation on file table: *
* in case of pressing button 'up', *
* select previous row */
case Key.UP:
if (shift)
DOM.toggleSelectedFile(current);
DOM.setCurrentFile(prev);
event.preventDefault();
break;
/* in case of pressing button 'down', *
* select next row */
case Key.DOWN:
if (shift)
DOM.toggleSelectedFile(current);
DOM.setCurrentFile(next);
event.preventDefault();
break;
case Key.LEFT:
if (!alt)
return;
event.preventDefault();
dataName = Info.panel.getAttribute('data-name');
if (dataName === 'js-right')
DOM.duplicatePanel();
break;
case Key.RIGHT:
if (!alt)
return;
event.preventDefault();
dataName = Info.panel.getAttribute('data-name');
if (dataName === 'js-left')
DOM.duplicatePanel();
break;
/* in case of pressing button 'Home', *
* go to top element */
case Key.HOME:
DOM.setCurrentFile(Info.first);
event.preventDefault();
break;
/* in case of pressing button 'End', select last element */
case Key.END:
DOM.setCurrentFile(Info.last);
event.preventDefault();
break;
/* если нажали клавишу page down проматываем экран */
case Key.PAGE_DOWN:
DOM.scrollByPages(panel, 1);
for (i = 0; i < 30; i++) {
if (!current.nextSibling)
break;
current = current.nextSibling;
}
DOM.setCurrentFile(current);
event.preventDefault();
break;
/* если нажали клавишу page up проматываем экран */
case Key.PAGE_UP:
DOM.scrollByPages(panel, -1);
for (i = 0; i < 30; i++) {
if (!current.previousSibling)
break;
current = current.previousSibling;
}
DOM.setCurrentFile(current);
event.preventDefault();
break;
case Key.ENTER:
if (Info.isDir)
loadDir({path});
else
CloudCmd.View.show();
break;
case Key.BACKSPACE:
CloudCmd.goToParentDir();
event.preventDefault();
break;
case Key.BACKSLASH:
if (ctrlMeta)
loadDir({
path: '/',
});
break;
case Key.A:
if (ctrlMeta) {
DOM.selectAllFiles();
event.preventDefault();
}
break;
case Key.G:
if (alt) {
DOM.goToDirectory();
event.preventDefault();
}
break;
case Key.M:
if (ctrlMeta) {
if (shift)
CloudCmd.EditNamesVim.show();
else
CloudCmd.EditNames.show();
event.preventDefault();
}
break;
case Key.P:
if (!ctrlMeta)
return;
event.preventDefault();
clipboard
.writeText(Info.dirPath)
.catch(CloudCmd.log);
break;
/**
* обновляем страницу,
* загружаем содержимое каталога
* при этом данные берём всегда с
* сервера, а не из кэша
* (обновляем кэш)
*/
case Key.R:
if (ctrlMeta) {
CloudCmd.log('reloading page...\n');
CloudCmd.refresh();
event.preventDefault();
}
break;
case Key.C:
if (ctrlMeta)
Buffer.copy();
break;
case Key.X:
if (ctrlMeta)
Buffer.cut();
break;
case Key.V:
if (ctrlMeta)
Buffer.paste();
break;
case Key.Z:
if (ctrlMeta)
Buffer.clear();
break;
/* чистим хранилище */
case Key.D:
if (ctrlMeta) {
CloudCmd.log('clearing storage...');
DOM.Storage.clear(wraptile(CloudCmd.log, 'storage cleared'));
event.preventDefault();
}
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;
export const TAB = 9;
export const ENTER = 13;
export const CAPSLOCK = 20;
export const ESC = 27;
export const SPACE = 32;
export const PAGE_UP = 33;
export const PAGE_DOWN = 34;
export const END = 35;
export const HOME = 36;
export const LEFT = 37;
export const UP = 38;
export const RIGHT = 39;
export const DOWN = 40;
export const INSERT = 45;
export const DELETE = 46;
export const ZERO = 48;
export const SEMICOLON = 52;
export const A = 65;
export const C = 67;
export const D = 68;
export const G = 71;
export const J = 74;
export const K = 75;
export const L = 76;
export const M = 77;
export const O = 79;
export const P = 80;
export const Q = 81;
export const R = 82;
export const S = 83;
export const T = 84;
export const U = 85;
export const V = 86;
export const X = 88;
export const Z = 90;
export const INSERT_MAC = 96;
export const ASTERISK = 106;
export const PLUS = 107;
export const MINUS = 109;
export const F1 = 112;
export const F2 = 113;
export const F3 = 114;
export const F4 = 115;
export const F5 = 116;
export const F6 = 117;
export const F7 = 118;
export const F8 = 119;
export const F9 = 120;
export const F10 = 121;
export const COLON = 186;
export const EQUAL = 187;
export const HYPHEN = 189;
export const DOT = 190;
export const SLASH = 191;
export const TRA = 192;
export const BACKSLASH = 220;
export const BRACKET_CLOSE = 221;
'use strict';
module.exports = {
BACKSPACE : 8,
TAB : 9,
ENTER : 13,
ESC : 27,
SPACE : 32,
PAGE_UP : 33,
PAGE_DOWN : 34,
END : 35,
HOME : 36,
LEFT : 37,
UP : 38,
RIGHT : 39,
DOWN : 40,
INSERT : 45,
DELETE : 46,
ZERO : 48,
SEMICOLON : 52,
COLON : 54,
A : 65,
C : 67,
D : 68,
G : 71,
J : 74,
K : 75,
M : 77,
O : 79,
P : 80,
Q : 81,
R : 82,
S : 83,
T : 84,
U : 85,
V : 86,
X : 88,
Z : 90,
INSERT_MAC : 96,
ASTERISK : 106,
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 */
import {escapeRegExp} from '#common/util';
export default function setCurrentByChar(char, charStore) {
const Info = DOM.CurrentInfo;
'use strict';
const Info = DOM.CurrentInfo;
const {escapeRegExp} = require('../../common/util');
module.exports = function setCurrentByChar(char, charStore) {
let firstByName;
let skipCount = 0;
let set = false;
let setted = false;
let i = 0;
const escapeChar = escapeRegExp(char);
const regExp = new RegExp(`^${escapeChar}.*$`, 'i');
const regExp = new RegExp('^' + escapeChar + '.*$', 'i');
const {files} = Info;
const chars = charStore();
const n = chars.length;
@ -28,21 +31,19 @@ export default function setCurrentByChar(char, charStore) {
const isTest = (a) => regExp.test(a);
const isRoot = (a) => a === '..';
const not = (f) => (a) => !f(a);
const setCurrent = (name) => {
const byName = DOM.getCurrentByName(name);
if (!skipCount) {
set = true;
setted = true;
DOM.setCurrentFile(byName);
return true;
} else {
if (skipN === skipCount)
firstByName = byName;
--skipCount;
}
if (skipN === skipCount)
firstByName = byName;
--skipCount;
};
names
@ -50,8 +51,9 @@ export default function setCurrentByChar(char, charStore) {
.filter(not(isRoot))
.some(setCurrent);
if (!set) {
if (!setted) {
DOM.setCurrentFile(firstByName);
charStore([char]);
}
}
};

View file

@ -1,36 +1,42 @@
import {fullstore} from 'fullstore';
import limier from 'limier';
'use strict';
/* global DOM */
const fullstore = require('fullstore/legacy');
const limier = require('limier/legacy');
const Info = DOM.CurrentInfo;
const searchStore = fullstore([]);
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);
searchStore(result);
searchIndex(0);
return result;
DOM.setCurrentByName(result[0]);
};
export const findNext = () => {
module.exports.findNext = () => {
const names = searchStore();
const index = next(searchIndex(), names.length);
searchIndex(index);
return names[searchIndex()];
DOM.setCurrentByName(names[searchIndex()]);
};
export const findPrevious = () => {
module.exports.findPrevious = () => {
const names = searchStore();
const index = previous(searchIndex(), names.length);
searchIndex(index);
return names[index];
DOM.setCurrentByName(names[index]);
};
export const _next = next;
export const _previous = previous;
module.exports._next = next;
module.exports._previous = previous;
function next(index, length) {
if (index === length - 1)
@ -45,3 +51,4 @@ function previous(index, length) {
return --index;
}

View file

@ -1,14 +1,55 @@
import test from 'supertape';
import {getDOM} from './globals.fixture.js';
import {
_next,
_previous,
'use strict';
const test = require('supertape');
const stub = require('@cloudcmd/stub');
const dir = './';
const {getDOM} = require('./globals.fixture');
global.DOM = getDOM();
const {
find,
findNext,
findPrevious,
} from './find.js';
_next,
_previous,
} = require(dir + 'find');
globalThis.DOM = getDOM();
test('cloudcmd: client: vim: find', (t) => {
const {DOM} = global;
const setCurrentByName = stub();
DOM.setCurrentByName = setCurrentByName;
DOM.Dialog.prompt = Promise.resolve.bind(Promise);
find('');
t.ok(setCurrentByName.calledWith(undefined), 'should call setCurrentByName');
t.end();
});
test('cloudcmd: client: vim: findNext', (t) => {
const {DOM} = global;
const setCurrentByName = stub();
DOM.setCurrentByName = setCurrentByName;
findNext();
t.ok(setCurrentByName.calledWith(undefined), 'should call setCurrentByName');
t.end();
});
test('cloudcmd: client: vim: findPrevious', (t) => {
const {DOM} = global;
const setCurrentByName = stub();
DOM.setCurrentByName = setCurrentByName;
findPrevious();
t.ok(setCurrentByName.calledWith(undefined), 'should call setCurrentByName');
t.end();
});
test('cloudcmd: client: vim: _next', (t) => {
const result = _next(1, 2);
@ -17,13 +58,6 @@ test('cloudcmd: client: vim: _next', (t) => {
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) => {
const result = _previous(0, 2);
@ -31,25 +65,3 @@ test('cloudcmd: client: vim: _previous', (t) => {
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 = () => {
const prompt = Promise.resolve.bind(Promise);
module.exports.getDOM = () => {
const resolve = Promise.resolve.bind(Promise);
const CurrentInfo = {
element: {},
files: [],
};
const noop = () => {};
const Buffer = {
copy: noop,
paste: noop,
};
const Dialog = {
prompt,
prompt: resolve,
};
return {
@ -27,20 +28,16 @@ export const getDOM = () => {
getCurrentName: noop,
setCurrentByName: noop,
toggleSelectedFile: noop,
prompNewDirectory: noop,
promptNewFile: noop,
};
};
export const getCloudCmd = () => {
module.exports.getCloudCmd = () => {
const show = () => {};
return {
Operation: {
Operation: {
show,
},
config: noop,
_config: noop,
};
};

View file

@ -1,167 +1,200 @@
import vim from './vim.js';
import * as finder from './find.js';
import {
setCurrent,
selectFileNotParent,
} from './set-current.js';
'use strict';
export default (key, event, overrides = {}) => {
const defaults = {
...globalThis.DOM,
...globalThis.CloudCmd,
};
const deps = {
...defaults,
...overrides,
};
const operations = getOperations(event, deps);
vim(key, operations, deps);
/* global CloudCmd, DOM */
const KEY = require('../key');
const Info = DOM.CurrentInfo;
const {Dialog} = DOM;
const fullstore = require('fullstore/legacy');
const store = fullstore('');
const visual = fullstore(false);
const {
find,
findNext,
findPrevious,
} = require('./find');
const TITLE = 'Cloud Commander';
const stopVisual = () => {
visual(false);
};
const getOperations = (event, deps) => {
const {
Info = globalThis.DOM.CurrentInfo,
CloudCmd = globalThis.CloudCmd,
Operation,
unselectFiles,
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 {
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: () => {
event.stopImmediatePropagation();
event.preventDefault();
globalThis.DOM.promptNewDir();
},
terminal: () => {
CloudCmd.Terminal.show();
},
edit: () => {
CloudCmd.EditFileVim.show();
},
copy: () => {
Buffer.copy();
unselectFiles();
},
select: () => {
const current = Info.element;
toggleSelectedFile(current);
},
paste: Buffer.paste,
moveNext: ({count, isVisual, isDelete}) => {
setCurrent('next', {
count,
isVisual,
isDelete,
}, {
Info,
setCurrentFile,
unselectFiles,
Operation,
});
},
movePrevious: ({count, isVisual, isDelete}) => {
setCurrent('previous', {
count,
isVisual,
isDelete,
}, {
Info,
setCurrentFile,
unselectFiles,
Operation,
});
},
find: async () => {
preventDefault();
const [, value] = await prompt('Find', '');
if (!value)
return;
const names = Info.files.map(getCurrentName);
const [result] = finder.find(value, names);
setCurrentByName(result);
},
};
const end = () => {
store('');
};
export const selectFile = selectFileNotParent;
const _createFindPrevious = (overrides = {}) => () => {
const {setCurrentByName} = overrides;
const name = finder.findPrevious();
setCurrentByName(name);
const rmFirst = (a) => {
return a
.split('')
.slice(1)
.join('');
};
const _createFindNext = (overrides = {}) => () => {
const {setCurrentByName} = overrides;
const name = finder.findNext();
module.exports = (key, event) => {
const current = Info.element;
const {keyCode} = event;
const prevStore = store();
setCurrentByName(name);
const value = store(prevStore.concat(key));
if (keyCode === KEY.ENTER)
return end();
if (keyCode === KEY.ESC) {
DOM.unselectFiles();
visual(false);
return end();
}
if (key === 'j') {
move('next', {
prevStore,
current,
});
return end();
}
if (key === 'k') {
move('previous', {
prevStore,
current,
});
return end();
}
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();
}
};
const _createMakeFile = (overrides = {}) => () => {
const {
promptNewFile,
stopImmediatePropagation,
preventDefault,
} = overrides;
module.exports.selectFile = selectFile;
function move(sibling, {max, current, prevStore}) {
const isDelete = prevStore[0] === 'd';
stopImmediatePropagation();
preventDefault();
promptNewFile();
};
if (isDelete) {
visual(true);
prevStore = rmFirst(prevStore);
}
const n = max || getNumber(prevStore);
if (isNaN(n))
return;
setCurrent({
n,
current,
sibling,
visual: visual(),
});
if (isDelete)
CloudCmd.Operation.show('delete');
}
function getNumber(value) {
if (!value)
return 1;
if (value === 'g')
return 1;
return parseInt(value);
}
function selectFile(current) {
const name = DOM.getCurrentName(current);
if (name === '..')
return;
DOM.selectFile(current);
}
function setCurrent({n, current, visual, sibling}) {
const select = visual ? selectFile : DOM.unselectFile;
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);
}

View file

@ -1,35 +1,48 @@
import {test, stub} from 'supertape';
import {getDOM, getCloudCmd} from './globals.fixture.js';
import vim, {selectFile as vimSelectFile} from './index.js';
import * as finder from './find.js';
'use strict';
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
const {join} = require('path');
const {assign} = Object;
const {DOM} = globalThis;
const test = require('supertape');
const stub = require('@cloudcmd/stub');
const mockRequire = require('mock-require');
const {reRequire} = mockRequire;
const dir = '../';
const pathKey = join(dir, 'key');
const pathVim = join(dir, 'vim');
const pathFind = join(dir, 'vim', 'find');
const {
getDOM,
getCloudCmd,
} = require('./globals.fixture');
global.DOM = getDOM();
global.CloudCmd = getCloudCmd();
const {DOM} = global;
const {Buffer} = DOM;
const KEY = require(pathKey);
const vim = require(pathVim);
test('cloudcmd: client: key: set next file: no', (t) => {
const element = {};
const setCurrentFile = stub();
const unselectFiles = stub();
const Info = {
element,
const element = {
};
vim('j', {}, {
Info,
setCurrentFile,
unselectFiles,
});
const setCurrentFile = stub();
t.calledWith(setCurrentFile, [element], 'should set next file');
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
vim('j', {});
t.ok(setCurrentFile.calledWith(element), 'should set next file');
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 element = {
nextSibling,
@ -37,21 +50,17 @@ test('cloudcmd: client: key: set next file current: j', async (t) => {
const setCurrentFile = stub();
const Info = {
element,
};
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
await vim('j', {}, {
Info,
setCurrentFile,
unselectFiles: stub(),
});
vim('j', {});
t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file');
t.calledWith(setCurrentFile, [nextSibling], 'should set next file');
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 element = {
nextSibling,
@ -59,21 +68,15 @@ test('cloudcmd: client: key: set next file current: mjj', (t) => {
const setCurrentFile = stub();
const Info = {
element,
};
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
const deps = {
Info,
setCurrentFile,
unselectFiles: stub(),
};
vim('m', {});
vim('j', {});
vim('j', {});
vim('m', {}, deps);
vim('j', {}, deps);
vim('j', {}, deps);
t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file');
t.calledWith(setCurrentFile, [nextSibling], 'should set next file');
t.end();
});
@ -85,44 +88,38 @@ test('cloudcmd: client: key: set next file current: g', (t) => {
const setCurrentFile = stub();
const Info = {
element,
};
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
const deps = {
Info,
setCurrentFile,
unselectFiles: stub(),
};
vim('g', {});
vim('j', {});
vim('g', {}, deps);
vim('j', {}, deps);
t.ok(setCurrentFile.calledWith(nextSibling), 'should ignore g');
t.calledWith(setCurrentFile, [nextSibling], 'should ignore g');
t.end();
});
test('cloudcmd: client: key: set +2 file current', (t) => {
const last = {};
const nextSibling = {
nextSibling: last,
};
const element = {
nextSibling,
};
const setCurrentFile = stub();
const element = {};
const Info = {
element,
};
const deps = {
setCurrentFile,
Info,
unselectFiles: stub(),
};
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
const event = {};
vim('2', event, deps);
vim('j', event, deps);
vim('2', event);
vim('j', event);
t.ok(setCurrentFile.calledWith(last), 'should set next file');
t.calledWith(setCurrentFile, [last], 'should set next file');
t.end();
});
@ -131,39 +128,26 @@ test('cloudcmd: client: key: select +2 files from current before delete', (t) =>
const nextSibling = {
nextSibling: last,
};
const element = {
nextSibling,
};
const setCurrentFile = stub();
const Info = {
element,
};
const Operation = {
show: stub(),
};
const selectFile = stub();
const getCurrentName = stub().returns('x');
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
global.DOM.selectFile = stub();
global.DOM.getCurrentName = () => false;
global.CloudCmd.Operation.show = stub();
const event = {};
const deps = {
Info,
setCurrentFile,
selectFile,
getCurrentName,
Operation,
};
vim('d', event);
vim('2', event);
vim('j', event);
vim('d', event, deps);
vim('2', event, deps);
vim('j', event, deps);
t.ok(setCurrentFile.calledWith(last), 'should set next file');
t.calledWith(setCurrentFile, [last], 'should set next file');
t.end();
});
@ -172,7 +156,6 @@ test('cloudcmd: client: key: delete +2 files from current', (t) => {
const nextSibling = {
nextSibling: last,
};
const element = {
nextSibling,
};
@ -180,26 +163,20 @@ test('cloudcmd: client: key: delete +2 files from current', (t) => {
const setCurrentFile = stub();
const show = stub();
const deps = {
Info: {
element,
},
Operation: {
show,
},
setCurrentFile,
selectFile: stub(),
getCurrentName: stub().returns('x'),
unselectFiles: stub(),
};
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
global.DOM.selectFile = stub();
global.DOM.getCurrentName = () => false;
global.CloudCmd.Operation.show = show;
const event = {};
vim('d', event, deps);
vim('2', event, deps);
vim('j', event, deps);
vim('d', event);
vim('2', event);
vim('j', event);
t.ok(show.calledWith('delete'), 'should call delete');
t.calledWith(show, ['delete'], 'should call delete');
t.end();
});
@ -210,84 +187,50 @@ test('cloudcmd: client: key: set previous file current', (t) => {
};
const setCurrentFile = stub();
const unselectFiles = stub();
const Info = {
element,
};
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
const deps = {
Info,
setCurrentFile,
unselectFiles,
};
vim('k', {});
vim('k', {}, deps);
t.ok(setCurrentFile.calledWith(previousSibling), 'should set previous file');
t.calledWith(setCurrentFile, [previousSibling], 'should set previous file');
t.end();
});
test('cloudcmd: client: key: copy: no', (t) => {
const copy = stub();
vim('y', {}, {
unselectFiles: stub(),
Buffer: {
copy,
},
});
Buffer.copy = copy;
vim('y', {});
t.notOk(copy.called, 'should not copy files');
t.notCalled(copy, 'should not copy files');
t.end();
});
test('cloudcmd: client: key: copy', (t) => {
const copy = stub();
const Info = {
element: {},
};
const toggleSelectedFile = stub();
const unselectFiles = stub();
Buffer.copy = copy;
const deps = {
Info,
unselectFiles,
toggleSelectedFile,
Buffer: {
copy,
},
};
vim('v', {});
vim('y', {});
vim('v', {}, deps);
vim('y', {}, deps);
t.calledWithNoArgs(copy, 'should copy files');
t.ok(copy.calledWith(), 'should copy files');
t.end();
});
test('cloudcmd: client: key: copy: unselectFiles', (t) => {
const unselectFiles = stub();
const Info = {
element: {},
};
const toggleSelectedFile = stub();
DOM.unselectFiles = unselectFiles;
const deps = {
Info,
unselectFiles,
toggleSelectedFile,
Buffer: {
copy: stub(),
},
};
vim('v', {});
vim('y', {});
vim('v', {}, deps);
vim('y', {}, deps);
t.calledWithNoArgs(unselectFiles, 'should unselect files');
t.ok(unselectFiles.calledWith(), 'should unselect files');
t.end();
});
@ -296,93 +239,65 @@ test('cloudcmd: client: key: paste', (t) => {
Buffer.paste = paste;
vim('p', {}, {
Buffer,
});
vim('p', {});
t.ok(paste.calledWith(), 'should paste files');
t.calledWithNoArgs(paste, 'should paste files');
t.end();
});
test('cloudcmd: client: key: selectFile: ..', (t) => {
const getCurrentName = stub().returns('..');
const selectFile = stub();
const getCurrentName = stub();
DOM.selectFile = selectFile;
DOM.getCurrentName = () => '..';
const current = {};
vim.selectFile(current);
vimSelectFile(current, {
selectFile,
getCurrentName,
});
t.notCalled(selectFile, 'should not call selectFile');
t.notOk(getCurrentName.called, 'should not call selectFile');
t.end();
});
test('cloudcmd: client: key: selectFile', (t) => {
const selectFile = stub();
const getCurrentName = stub().returns('x');
DOM.selectFile = selectFile;
DOM.getCurrentName = (a) => a.name;
const current = {};
vimSelectFile(current, {
selectFile,
getCurrentName,
});
vim.selectFile(current);
t.calledWith(selectFile, [current], 'should call selectFile');
t.ok(selectFile.calledWith(current), 'should call selectFile');
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 nextSibling = {
nextSibling: last,
};
const element = {
nextSibling,
};
const setCurrentFile = stub();
await vim('G', {}, {
Info: {
element,
},
setCurrentFile,
unselectFiles: stub(),
});
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
vim('G', {});
t.ok(setCurrentFile.calledWith(last), 'should set last file');
t.calledWith(setCurrentFile, [last], 'should set last file');
t.end();
});
test('cloudcmd: client: key: set last 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) => {
test('cloudcmd: client: key: set first file current', (t) => {
const first = 'first';
const previousSibling = {
const previousSibling= {
previousSibling: first,
};
@ -390,121 +305,72 @@ test('cloudcmd: client: key: set first file current: gg', (t) => {
previousSibling,
};
const Operation = {
show: stub(),
};
const unselectFiles = stub();
const setCurrentFile = stub();
const deps = {
Operation,
unselectFiles,
setCurrentFile,
Info: {
element,
},
};
global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
vim('g', {}, deps);
vim('g', {}, deps);
vim('g', {});
vim('g', {});
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,
};
t.ok(setCurrentFile.calledWith(first), 'should set first file');
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();
});
test('cloudcmd: client: key: visual', (t) => {
const element = {};
const toggleSelectedFile = stub();
const Info = {
element,
const element = {
};
vim('v', {}, {
Info,
toggleSelectedFile,
});
const toggleSelectedFile = stub();
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();
});
test('cloudcmd: client: key: ESC', (t) => {
const element = {};
const unselectFiles = stub();
const Info = {
element,
const element = {
};
vim('Escape', null, {
Info,
unselectFiles,
const unselectFiles = stub();
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();
});
test('cloudcmd: client: key: Enter', async (t) => {
test('cloudcmd: client: key: Enter', (t) => {
const nextSibling = 'hello';
const element = {
nextSibling,
};
const unselectFiles = stub();
const setCurrentFile = stub();
const Info = {
element,
};
DOM.CurrentInfo.element = element;
DOM.setCurrentFile = setCurrentFile;
await vim('Enter', null, {
Info,
setCurrentFile,
unselectFiles,
vim('', {
keyCode: KEY.ENTER,
});
await vim('j', null, {
Info,
setCurrentFile,
unselectFiles,
});
vim('j', {});
t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file');
t.calledWith(setCurrentFile, [nextSibling], 'should set next file');
t.end();
});
@ -512,245 +378,50 @@ test('cloudcmd: client: key: /', (t) => {
const preventDefault = stub();
const element = {};
const Info = {
element,
files: [],
};
DOM.CurrentInfo.element = element;
DOM.getCurrentName = () => '';
const getCurrentName = stub().returns('');
const event = {
vim('/', {
preventDefault,
};
const prompt = stub().returns([]);
vim('/', event, {
getCurrentName,
Info,
prompt,
});
t.calledWithNoArgs(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.ok(preventDefault.calledWith(), 'should call preventDefault');
t.end();
});
test('cloudcmd: client: key: n', (t) => {
const findNext = stub();
const createFindNext = stub().returns(findNext);
const event = {};
vim('n', event, {
createFindNext,
mockRequire(pathFind, {
findNext,
});
t.calledWithNoArgs(findNext, 'should call findNext');
const vim = reRequire(pathVim);
const event = {};
vim('n', event);
mockRequire.stop(pathFind);
t.ok(findNext.calledWith(), 'should call findNext');
t.end();
});
test('cloudcmd: client: key: N', (t) => {
const findPrevious = stub();
const createFindPrevious = stub().returns(findPrevious);
mockRequire(pathFind, {
findPrevious,
});
const vim = reRequire(dir + 'vim');
const event = {};
vim('N', event, {
createFindPrevious,
});
vim('N', event);
t.calledWithNoArgs(findPrevious);
mockRequire.stop(pathFind);
t.ok(findPrevious.calledWith(), 'should call findPrevious');
t.end();
});
test('cloudcmd: client: key: make directory', async (t) => {
const {DOM} = globalThis;
assign(DOM, {
promptNewDir: stub(),
});
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);
if (!~index)
return 0;
return index;
});
};

View file

@ -1,4 +1,6 @@
export const getRange = (indexFrom, indexTo, files) => {
'use strict';
module.exports = (indexFrom, indexTo, files) => {
if (indexFrom < indexTo)
return files.slice(indexFrom, indexTo + 1);
@ -7,3 +9,4 @@ export const getRange = (indexFrom, indexTo, files) => {
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,35 @@
/* global DOM, CloudCmd */
import exec from 'execon';
import itype from 'itype';
import currify from 'currify';
import {tryToCatch} from 'try-to-catch';
import clipboard from '@cloudcmd/clipboard';
import * as Events from '#dom/events';
import {uploadFiles} from '#dom/upload-files';
import {FS} from '#common/cloudfunc';
import {getRange} from './get-range.js';
import {getIndex} from './get-index.js';
'use strict';
const exec = require('execon');
const itype = require('itype/legacy');
const currify = require('currify/legacy');
const clipboard = require('@cloudcmd/clipboard');
const getRange = require('./get-range');
const getIndex = currify(require('./get-index'));
const uploadFiles = require('../dom/upload-files');
const {FS} = require('../../common/cloudfunc');
const NBSP_REG = RegExp(String.fromCharCode(160), 'g');
const SPACE = ' ';
export async function init() {
module.exports.init = () => {
contextMenu();
dragndrop();
unload();
pop();
resize();
config();
header();
await config();
}
};
CloudCmd.Listeners = module.exports;
const unselect = (event) => {
const isMac = /Mac/.test(globalThis.navigator.platform);
const isMac = /Mac/.test(window.navigator.platform);
const {
shiftKey,
metaKey,
@ -38,13 +43,19 @@ const unselect = (event) => {
};
const execAll = currify((funcs, event) => {
for (const fn of funcs)
funcs.forEach((fn) => {
fn(event);
});
});
const Info = DOM.CurrentInfo;
const {Events} = DOM;
const EventsFiles = {
mousedown: exec.with(execIfNotUL, setCurrentFileByEvent),
click: execAll([onClick, exec.with(execIfNotMobile, unselect)]),
click: execAll([
onClick,
unselect,
]),
dragstart: exec.with(execIfNotUL, onDragStart),
dblclick: exec.with(execIfNotUL, onDblClick),
touchstart: exec.with(execIfNotUL, onTouch),
@ -55,7 +66,6 @@ let EXT;
function header() {
const fm = DOM.getFM();
const isDataset = (el) => el.dataset;
const isPanel = (el) => {
return /^js-(left|right)$/.test(el.dataset.name);
};
@ -67,7 +77,8 @@ function header() {
if (parent.dataset.name !== 'js-fm-header')
return;
const name = (el.dataset.name || '').replace('js-', '');
const name = (el.dataset.name || '')
.replace('js-', '');
if (!/^(name|size|date)$/.test(name))
return;
@ -88,46 +99,42 @@ function getPath(el, path = []) {
return getPath(el.parentElement, path.concat(el));
}
async function config() {
const [, config] = await tryToCatch(DOM.Files.get, 'config');
const type = config?.packer;
EXT = DOM.getPackerExt(type);
function config() {
DOM.Files.get('config', (e, config) => {
const type = config && config.packer;
EXT = DOM.getPackerExt(type);
});
}
export const initKeysPanel = () => {
module.exports.initKeysPanel = () => {
const keysElement = DOM.getById('js-keyspanel');
if (!keysElement)
return;
Events.addClick(keysElement, (event) => {
const {target} = event;
Events.addClick(keysElement, ({target}) => {
const {id} = target;
const operation = (name) => {
const {Operation} = CloudCmd;
const fn = Operation.show.bind(null, name);
return Operation.show.bind(null, name);
return fn;
};
const clickFuncs = {
'f1': CloudCmd.Help.show,
'f2': CloudCmd.UserMenu.show,
'f3': CloudCmd.View.show,
'f4': CloudCmd.EditFile.show,
'f5': operation('copy'),
'f6': operation('move'),
'f7': DOM.promptNewDir,
'f8': operation('delete'),
'f9': () => {
event.stopPropagation();
CloudCmd.Menu.show();
},
'f10': CloudCmd.Config.show,
'~': CloudCmd.Konsole.show,
'shift~': CloudCmd.Terminal.show,
'contact': CloudCmd.Contact.show,
'f1' : CloudCmd.Help.show,
'f2' : DOM.renameCurrent,
'f3' : CloudCmd.View.show,
'f4' : CloudCmd.EditFile.show,
'f5' : operation('copy'),
'f6' : operation('move'),
'f7' : DOM.promptNewDir,
'f8' : operation('delete'),
'f9' : CloudCmd.Menu.show,
'f10' : CloudCmd.Config.show,
'~' : CloudCmd.Konsole.show,
'shift~' : CloudCmd.Terminal.show,
'contact' : CloudCmd.Contact.show,
};
exec(clickFuncs[id]);
@ -138,10 +145,10 @@ const getPanel = (side) => {
if (!itype.string(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 filesElement = DOM.getByDataName('js-files', panel);
@ -157,7 +164,6 @@ function getPathListener(panel) {
}
function isNoCurrent(panel) {
const Info = DOM.CurrentInfo;
const infoPanel = Info.panel;
if (!infoPanel)
@ -169,20 +175,20 @@ function isNoCurrent(panel) {
return namePanel !== nameInfoPanel;
}
function decodePath(path) {
function decodePath(path){
const url = CloudCmd.HOST;
const {prefix} = CloudCmd;
const prefixReg = RegExp('^' + prefix + FS);
return decodeURI(path)
.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(NBSP_REG, SPACE) || '/';
}
async function onPathElementClick(panel, event) {
const Info = DOM.CurrentInfo;
function onPathElementClick(panel, event) {
event.preventDefault();
const element = event.target;
@ -204,26 +210,19 @@ async function onPathElementClick(panel, event) {
const {href} = element;
const path = decodePath(href);
await CloudCmd.changeDir(path, {
CloudCmd.loadDir({
path,
isRefresh: false,
panel: noCurrent ? panel : Info.panel,
});
}
function copyPath(el) {
clipboard
.writeText(el.parentElement.title)
clipboard.writeText(el.parentElement.title)
.then(CloudCmd.log)
.catch(CloudCmd.log);
}
function execIfNotMobile(callback, event) {
const isMobile = DOM.getCSSVar('is-mobile');
if (!isMobile)
callback(event);
}
function execIfNotUL(callback, event) {
const {target} = event;
const {tagName} = target;
@ -233,27 +232,30 @@ function execIfNotUL(callback, event) {
}
function onClick(event) {
event.preventDefault();
const ctrl = event.ctrlKey;
if (!ctrl)
event.preventDefault();
changePanel(event.target);
}
function toggleSelect(key, files) {
const isMac = /Mac/.test(globalThis.navigator.platform);
const isMac = /Mac/.test(window.navigator.platform);
if (!key)
throw Error('key should not be undefined!');
const [file] = files;
if (isMac && key.meta)
if (isMac && key.meta || key.ctrl)
return DOM.toggleSelectedFile(file);
if (key.shift)
return files.map(DOM.selectFile);
return files.forEach(DOM.selectFile);
}
function changePanel(element) {
const Info = DOM.CurrentInfo;
const {panel} = Info;
const files = DOM.getByDataName('js-files', panel);
const ul = getULElement(element);
@ -262,20 +264,25 @@ function changePanel(element) {
DOM.changePanel();
}
async function onDblClick(event) {
event.preventDefault();
function onDblClick(event) {
const current = getLIElement(event.target);
const isDir = DOM.isCurrentIsDir(current);
const path = DOM.getCurrentPath(current);
if (!isDir)
return CloudCmd.View.show();
await CloudCmd.changeDir(path);
if (isDir) {
CloudCmd.loadDir({
path: path === '/' ? '/' : path + '/',
});
event.preventDefault();
} else {
CloudCmd.View.show();
event.preventDefault();
}
}
async function onTouch(event) {
function onTouch(event) {
const current = getLIElement(event.target);
const isDir = DOM.isCurrentIsDir(current);
@ -287,7 +294,9 @@ async function onTouch(event) {
if (!isCurrent)
return;
await CloudCmd.changeDir(DOM.getCurrentPath(current));
CloudCmd.loadDir({
path: DOM.getCurrentPath(current),
});
}
/*
@ -295,7 +304,6 @@ async function onTouch(event) {
* in Chrome (HTML5)
*/
function onDragStart(event) {
const Info = DOM.CurrentInfo;
const {prefixURL} = CloudCmd;
const element = getLIElement(event.target);
const {isDir} = Info;
@ -310,12 +318,10 @@ function onDragStart(event) {
link.href = prefixURL + '/pack' + Info.path + EXT;
}
event.dataTransfer.setData(
'DownloadURL',
'application/octet-stream' + ':' + name +
':' +
link,
);
event.dataTransfer.setData('DownloadURL',
'application/octet-stream' + ':' +
name + ':' +
link);
}
function getLIElement(element) {
@ -336,7 +342,6 @@ function getULElement(element) {
}
function setCurrentFileByEvent(event) {
const Info = DOM.CurrentInfo;
const BUTTON_LEFT = 0;
const key = {
@ -391,16 +396,23 @@ function contextMenu() {
function dragndrop() {
const panels = DOM.getByClassAll('panel');
const select = ({target}) => {
target.classList.add('selected-panel');
const select = () => {
[...panels].forEach((panel) => {
panel.classList.add('selected-panel');
});
};
const unselect = ({target}) => {
target.classList.remove('selected-panel');
const unselect = () => {
[...panels].forEach((panel) => {
panel.classList.remove('selected-panel');
});
};
const onDrop = (event) => {
const {files, items} = event.dataTransfer;
const {
files,
items,
} = event.dataTransfer;
const {length: filesCount} = files;
@ -410,10 +422,7 @@ function dragndrop() {
return uploadFiles(files);
const isFile = (item) => item.kind === 'file';
const dirFiles = Array
.from(items)
.filter(isFile);
const dirFiles = [...items].filter(isFile);
if (dirFiles.length)
return DOM.uploadDirectory(dirFiles);
@ -425,7 +434,7 @@ function dragndrop() {
};
/**
* In macOS Chrome dropEffect = 'none'
* In Mac OS Chrome dropEffect = 'none'
* so drop do not firing up when try
* to upload file from download bar
*/
@ -441,18 +450,19 @@ function dragndrop() {
event.preventDefault();
};
for (const panel of panels)
Events
.add('dragover', panel, onDragOver)
.add('drop', panel, onDrop)
.add('dragenter', select)
.add(['dragleave', 'drop'], unselect);
Events.add('dragenter', select);
Events.add(['dragleave', 'drop'], unselect);
[...panels].forEach((panel) => {
Events.add('dragover', panel, onDragOver)
.add('drop', panel, onDrop);
});
}
function unload() {
Events.add(['unload', 'beforeunload'], (event) => {
DOM.Events.add(['unload', 'beforeunload'], (event) => {
const {Key} = CloudCmd;
const isBind = Key?.isBind();
const isBind = Key && Key.isBind();
if (isBind)
return;
@ -463,15 +473,15 @@ function unload() {
}
function pop() {
Events.add('popstate', async ({state}) => {
Events.add('popstate', ({state}) => {
const path = (state || '').replace(FS, '');
if (!path)
return CloudCmd.route(location.hash);
const history = false;
await CloudCmd.changeDir(path, {
CloudCmd.loadDir({
path,
history,
});
});
@ -479,8 +489,7 @@ function pop() {
function resize() {
Events.add('resize', () => {
const Info = DOM.CurrentInfo;
const is = globalThis.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH;
const is = window.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH;
if (!is)
return;
@ -500,3 +509,4 @@ function resize() {
DOM.changePanel();
});
}

View file

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

View file

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

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