Compare commits

..

No commits in common. "master" and "v14.6.0" have entirely different histories.

281 changed files with 7545 additions and 13217 deletions

8
.babelrc Normal file
View file

@ -0,0 +1,8 @@
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"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 +1,5 @@
last 2 Chrome versions
last 2 Safari versions
Firefox ESR
maintained node versions
not dead

View file

@ -1,31 +1,25 @@
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'),
'use strict';
module.exports = {
'F2 - Rename file': async ({DOM}) => {
await DOM.renameCurrent();
},
'D - Build Dev': async ({CloudCmd}) => {
await CloudCmd.TerminalRun.show({
command: 'npm run build:client:dev',
autoClose: false,
closeMessage: 'Press any button to close Terminal',
});
CloudCmd.refresh();
},
'P - Build Prod': async ({CloudCmd}) => {
await CloudCmd.TerminalRun.show({
command: 'npm run build:client',
autoClose: true,
});
CloudCmd.refresh();
},
};
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

@ -19,5 +19,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

34
.eslintrc.js Normal file
View file

@ -0,0 +1,34 @@
'use strict';
module.exports = {
extends: [
'plugin:putout/recommended',
],
plugins: [
'putout',
'node',
],
overrides: [{
files: ['bin/release.js'],
rules: {
'no-console': 'off',
'node/shebang': 'off',
},
extends: [
'plugin:node/recommended',
],
}, {
files: ['bin/cloudcmd.js'],
rules: {
'no-console': 'off',
},
extends: [
'plugin:node/recommended',
],
}, {
files: ['{client,common}/**/*.js'],
env: {
browser: true,
},
}],
};

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 +1,20 @@
name: Node CI
on:
- push
permissions:
contents: write
on: [push]
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 }}
- uses: actions/checkout@master
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
version: 12.x
- name: npm install, build, and test
run: |
npm install
npm run build
npm test

21
.gitignore vendored
View file

@ -1,17 +1,18 @@
*.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
.putoutcache
dist
dist-dev

View file

@ -1,62 +1,178 @@
import {run, cutEnv} from 'madrun';
import {defineEnv} from 'supertape/env';
'use strict';
const testEnv = defineEnv({
timeout: 7000,
css: true,
});
const {
run,
parallel,
predefined,
} = require('madrun');
const buildEnv = {
NODE_ENV: 'production',
};
const {version} = require('./package');
export default {
const names = [
'bin/cloudcmd.js',
'client',
'common',
'server',
'test',
'bin/release.js',
'webpack.config.js',
'cssnano.config.js',
'.webpack',
'.eslintrc.js',
'.madrun.js',
'{client,server,common}/**/*.spec.js',
];
const {putout} = predefined;
const env = 'THREAD_IT_COUNT=0';
const dockerName = 'coderaiser/cloudcmd';
module.exports = {
'start': () => 'node bin/cloudcmd.js',
'start:dev': async () => await run('start', null, {
NODE_ENV: 'development',
}),
'start:dev': () => `NODE_ENV=development ${run('start')}`,
'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'`,
'build:start:dev': () => run(['build:client:dev', 'start:dev']),
'lint:all': () => run(['lint', 'lint:css', 'spell']),
'lint:base': () => putout({
names,
}),
'lint:css': () => 'stylelint css/*.css',
'spell': () => 'yaspeller .',
'fix:lint': () => run(['lint', 'lint:css'], '--fix'),
'lint': () => run('lint:progress', '--cache'),
'lint:progress': () => run('lint:base', '-f progress'),
'lint:stream': () => run('lint:base', '-f stream'),
'test:base': () => {
const cmd = 'tape';
const names = `'test/**/*.js' '{client,static,common,server}/**/*.spec.js'`;
return `${cmd} ${names}`;
},
'test': () => `${env} ${run('test:base')}`,
'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': () => run(['lint:all', 'build', 'test']),
'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'],
'docker:pull': () => 'docker pull node',
'docker:pull:alpine': () => 'docker pull mhart/alpine-node',
'docker:pull:arm32': () => 'docker pull arm32v7/node:slim',
'docker:pull:arm64': () => 'docker pull arm64v8/node:slim',
'docker:push': () => dockerPush('x64', version),
'docker:push:latest': () => dockerPush('x64'),
'docker:push:alpine': () => dockerPush('alpine', version),
'docker:push:alpine:latest': () => dockerPush('alpine'),
'docker:push:arm32': () => dockerPush('arm32', version),
'docker:push:arm32:latest': () => dockerPush('arm32'),
'docker:push:arm64': () => dockerPush('arm64', version),
'docker:push:arm64:latest': () => dockerPush('arm64'),
'docker:build': () => dockerBuild('docker/Dockerfile', 'x64', version),
'docker:build:alpine': () => dockerBuild('docker/Dockerfile.alpine', 'alpine', version),
'docker:build:arm32': () => dockerBuild('docker/arm/Dockerfile.arm32v7', 'arm32', version),
'docker:build:arm64': () => dockerBuild('docker/arm/Dockerfile.arm64v8', 'arm64', version),
'docker:manifest:create': () => {
const images = [
`${dockerName}:latest`,
`${dockerName}:latest-x64`,
// `${dockerName}:latest-arm32`,
// `${dockerName}:latest-arm64`,
].join(' ');
return `docker manifest create ${images}`;
},
'docker:manifest:push': () => `docker manifest push ${dockerName}:latest`,
'docker': () => run(['docker:x64', 'docker:alpine', 'docker:manifest:*']),
'docker-ci': () => run(['build', 'docker-login', 'docker']),
'docker-login': () => 'docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD',
'docker:x64': () => run([
'docker:pull',
'docker:build',
'docker:tag',
'docker:push',
'docker:push:latest',
]),
'docker:alpine': () => run([
'docker:pull:alpine',
'docker:build:alpine',
'docker:tag:alpine',
'docker:push:alpine',
'docker:push:alpine:latest',
]),
/*
'docker:arm32': () => run([
'docker:pull:arm32',
'docker:build:arm32',
'docker:tag:arm32',
'docker:push:arm32',
'docker:push:arm32:latest',
]),
'docker:arm64': () => run([
'docker:pull:arm64',
'docker:build:arm64',
'docker:tag:arm64',
'docker:push:arm64',
'docker:push:arm64:latest',
]),
*/
'docker:manifest': () => run([
'docker:manifest:create',
'docker:manifest:push',
]),
'docker:tag': () => dockerTag('x64', version),
'docker:tag:alpine': () => dockerTag('alpine', version),
'docker:tag:arm32': () => dockerTag('arm32', version),
'docker:tag:arm64': () => dockerTag('arm64', version),
'docker:rm:version': () => dockerRmi('x64', version),
'docker:rm:latest': () => dockerRmi('x64'),
'docker:rm:alpine': () => dockerRmi('alpine', version),
'docker:rm:latest-alpine': () => dockerRmi('alpine'),
'docker:rm:arm32': () => dockerRmi('arm32', version),
'docker:rm:latest-arm32': () => dockerRmi('arm32'),
'docker:rm:arm64': () => dockerRmi('arm64', version),
'docker:rm:latest-arm64': () => dockerRmi('arm64'),
'docker:rm-old': () => `${parallel('docker:rm:*')} || true`,
'coverage': () => `${env} nyc ${run('test:base')}`,
'report': () => 'nyc report --reporter=text-lcov | coveralls',
'6to5': () => 'webpack --progress',
'6to5:client': () => run('6to5', '--mode production'),
'6to5:client:dev': async () => await run('6to5', '--mode development', {
NODE_ENV: 'development',
}),
'6to5:client:dev': () => `NODE_ENV=development ${run('6to5', '--mode 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'),
'watch:lint': () => `nodemon -w client -w server -w webpack.config.js -x ${run('lint')}`,
'watch:test': () => `nodemon -w client -w server -w test -w common -x ${run('test')}`,
'watch:test:client': () => `nodemon -w client -w test/client -x ${run('test:client')}`,
'watch:test:server': () => `nodemon -w client -w test/client -x ${run('test:server')}`,
'watch:coverage': () => `nodemon -w server -w test -w common -x ${run('coverage')}`,
'build': () => run('6to5:*'),
'build:client': () => run('6to5:client'),
'build:client:dev': () => run('6to5:client:dev'),
'heroku-postbuild': () => run('6to5:client'),
};
function dockerPush(type, version = 'latest') {
return `docker push coderaiser/cloudcmd:${version}-${type}`;
}
function dockerBuild(file, type, version) {
return `docker build -f ${file} -t coderaiser/cloudcmd:${version}-${type} .`;
}
function dockerTag(type, version) {
return `docker tag coderaiser/cloudcmd:${version}-${type} coderaiser/cloudcmd:latest-${type}`;
}
function dockerRmi(type, version = 'latest') {
return `docker rmi -f coderaiser/cloudcmd:${version}-${type}`;
}

View file

@ -1,29 +1,30 @@
*.spec.*
*.config.*
*.fixture.js*
*.ai
*.cdr
*.eps
*.log
*.lock
.*
*.spec.js
*.fixture.js*
manifest.yml
docker-compose.yml
now.json
app.json
bower.json
deno.json
bin/release.*
img/logo/cloudcmd-hq.png
webpack.config.js
docker
test*
docker-compose.yml
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
*.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,27 @@
{
"plugins": ["cloudcmd"],
"ignore": [
"*.md",
"app.json",
"fontello.json",
"html",
"fixture*"
],
"rules": {
"package-json/add-type": "off"
},
"match": {
".filesystem.json": {
"nodejs/rename-file-cjs-to-js": "off"
},
"base64": {
"types/convert-typeof-to-is-type": "off"
},
"*.md": {
"nodejs/convert-commonjs-to-esm": "on"
},
".webpack": {
"webpack": "on"
},
"server": {
"nodejs/remove-process-exit": "on"
"remove-process-exit": true
},
"server/{server,exit}.js": {
"nodejs/remove-process-exit": "off"
"server/(server|exit).js": {
"remove-process-exit": false,
"remove-console": false
},
"server/{server,exit,terminal,distribute/log}.{js,mjs}": {
"remove-console": "off"
"server/(terminal|distribute/log).js": {
"remove-console": false
},
"client/{client,cloudcmd,load-module}.{js,mjs}": {
"remove-console": "off"
"client/(client|cloudcmd|load-module).js": {
"remove-console": false
},
"client": {
"nodejs": "off"
},
"client/sw": {
"remove-console": "off"
"client/modules/config/index.js": {
"apply-shorthand-properties": [{
"ignore": [
"ONE_MINUTE"
]
}]
},
"test/common/cloudfunc.js": {
"remove-console": "off"
},
"storage.js": {
"promises/remove-useless-async": "off"
},
"docker.yml": {
"github/set-node-versions": "off"
},
"vim.js": {
"merge-duplicate-functions": "off"
},
"common": {
"nodejs/declare": "off"
"remove-console": false
}
}
}

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

52
.travis.yml Normal file
View file

@ -0,0 +1,52 @@
language: node_js
node_js:
- 14
- 12
- 10
cache:
npm: false
matrix:
allow_failures:
- os: windows
os:
- linux
- osx
- windows
# https://docs.travis-ci.com/user/customizing-the-build/#git-end-of-line-conversion-control
# need for windows
git:
autocrlf: input
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
before_deploy:
- echo '{"experimental":"enabled"}' | sudo tee /etc/docker/daemon.json
- mkdir -p $HOME/.docker
- echo '{"experimental":"enabled"}' | tee $HOME/.docker/config.json
- sudo service docker start
deploy:
provider: script
script: npm run docker-ci
skip_cleanup: true
on:
node: 12
condition: $TRAVIS_OS_NAME = linux
tags: true
all_branches: false

View file

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

81
.webpack/css.js Normal file
View file

@ -0,0 +1,81 @@
'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',
'user-menu',
...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|user-menu|columns.*)\.css/,
use: extractMain.extract([
'css-loader',
]),
},
...cssPlugins.map(extract), {
test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/,
use: {
loader: 'url-loader',
options: {
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 {NODE_ENV} = env;
const isDev = 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 options = {
babelrc: false,
plugins: [
'module:babel-plugin-macros',
],
};
const rules = clean([
!isDev && {
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
isDev && {
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options,
}]);
const plugins = [
new EnvironmentPlugin({
NODE_ENV,
}),
new ServiceWorkerWebpackPlugin({
entry: join(__dirname, '..', 'client', 'sw', 'sw.js'),
excludes: ['*'],
}),
];
const splitChunks = {
name: 'cloudcmd.common',
chunks: 'all',
};
module.exports = {
resolve: {
symlinks: false,
},
devtool,
optimization: {
splitChunks,
},
entry: {
cloudcmd: `${dir}/cloudcmd.js`,
[modules + '/edit']: `${dirModules}/edit.js`,
[modules + '/edit-file']: `${dirModules}/edit-file.js`,
[modules + '/edit-file-vim']: `${dirModules}/edit-file-vim.js`,
[modules + '/edit-names']: `${dirModules}/edit-names.js`,
[modules + '/edit-names-vim']: `${dirModules}/edit-names-vim.js`,
[modules + '/menu']: `${dirModules}/menu.js`,
[modules + '/view']: `${dirModules}/view.js`,
[modules + '/help']: `${dirModules}/help.js`,
[modules + '/markdown']: `${dirModules}/markdown.js`,
[modules + '/config']: `${dirModules}/config/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`,
},
output: {
filename: '[name].js',
path: isDev ? distDev : dist,
pathinfo: isDev,
devtoolModuleFilenameTemplate,
publicPath: '/dist/',
},
externals: [
externals,
],
module: {
rules,
noParse,
},
plugins,
performance: {
maxEntrypointSize: 600000,
maxAssetSize: 600000,
},
};
function externals(context, request, fn) {
if (!isDev)
return fn();
const list = [];
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

@ -22,14 +22,12 @@
"Iptables",
"JitSu",
"Node",
"IO",
"Olena",
"TarZak",
"Termux",
"Zalitok",
"WebSocket",
"auth",
"autostart",
"binded",
"cd",
"cloudcmd",
@ -46,7 +44,6 @@
"gz",
"io",
"js",
"linux",
"maintainers",
"markdown",
"microservice",
@ -56,8 +53,6 @@
"nginx",
"npm",
"or io",
"patreon",
"rc",
"refactor",
"sexualized",
"sslPort",
@ -66,7 +61,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)

1953
ChangeLog

File diff suppressed because it is too large Load diff

1010
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

View file

@ -1,22 +1,25 @@
# Cloud Commander v19.19.1 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![Deploy on InstaPods][DeployInstaPodsIMG]][DeployInstaPodsURL]
# Cloud Commander v14.6.0 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fcoderaiser%2Fcloudcmd.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fcoderaiser%2Fcloudcmd?ref=badge_shield)
### [Main][MainURL] [Blog][BlogURL] [Support][SupportURL] [Demo][DemoURL]
### [Main][MainURL] [Blog][BlogURL] Live([Heroku][HerokuURL])
[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"
[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 +30,6 @@
```
npm i cloudcmd -g
```
## Start
For starting just type in console:
@ -45,8 +47,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,19 +72,17 @@ 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 = {
@ -93,8 +93,8 @@ const filePicker = {
data: {
FilePicker: {
key: 'key',
},
},
}
}
};
// override option from json/modules.json
@ -109,11 +109,11 @@ const {
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)
modules, // optional
configManager, // optional
}));
@ -121,21 +121,12 @@ app.use(prefix, cloudcmd({
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,15 +152,11 @@ 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");

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",

View file

@ -1,48 +1,37 @@
#!/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 tryToCatch = require('try-to-catch');
const choose = (a, b) => {
if (isUndefined(a))
return b;
return a;
};
const exit = require(DIR_SERVER + 'exit');
const {
createConfig,
configPath,
} = require(DIR_SERVER + 'config');
const config = createConfig({
configPath,
});
const maybeRoot = (a) => {
if (a === '.')
return process.cwd();
const env = require(DIR_SERVER + 'env');
const prefixer = require(DIR_SERVER + '/prefixer');
const choose = (a, b) => {
if (a === undefined)
return b;
return a;
};
const yargsOptions = {
configuration: {
'strip-aliased': true,
'strip-dashed': true,
},
coerce: {
root: maybeRoot,
},
process.on('unhandledRejection', exit);
const {argv} = process;
const args = require('minimist')(argv.slice(2), {
string: [
'name',
'port',
@ -57,8 +46,6 @@ const yargsOptions = {
'terminal-path',
'terminal-command',
'columns',
'menu',
'theme',
'import-url',
'import-token',
'export-token',
@ -73,7 +60,6 @@ const yargsOptions = {
'open',
'config-dialog',
'config-auth',
'config-port',
'console',
'sync-console-path',
'contact',
@ -83,7 +69,6 @@ const yargsOptions = {
'confirm-copy',
'confirm-move',
'show-config',
'show-dot-files',
'show-file-name',
'vim',
'keys-panel',
@ -92,70 +77,65 @@ 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' : choose(env('cloudcmd_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('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();
@ -165,21 +145,8 @@ 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);
if (args.repl)
await repl();
validate.columns(args.columns);
validate.theme(args.theme);
repl();
port(args.port);
@ -189,41 +156,37 @@ async function main() {
config('open', args.open);
config('username', args.username);
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 +195,42 @@ 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);
if (args.showConfig)
await showConfig();
if (args['show-config'])
showConfig();
const {distributeImport} = await import('../server/distribute/import.js');
const importConfig = promisify(distributeImport);
const distribute = require('../server/distribute');
const importConfig = promisify(distribute.import);
await start(options, config);
await importConfig(config);
if (args.save)
config.write();
start(options, config);
await tryToCatch(checkUpdate);
await importConfig(config);
}
function validateRoot(root, config) {
const validate = require(DIR_SERVER + 'validate');
validate.root(root, config);
if (root === '/')
return;
if (config('log'))
console.log(`root: ${root}`);
console.log(`root: ${root}`);
}
async function getPassword(password) {
const {default: criton} = await import('criton');
function getPassword(password) {
const criton = require('criton');
return criton(password, config('algo'));
}
@ -277,11 +238,13 @@ function version() {
console.log('v' + Info.version);
}
async function start(options, config) {
function start(options, config) {
const SERVER = DIR_SERVER + 'server';
if (!args.server)
return;
const {default: server} = await import('../server/server.js');
const server = require(SERVER);
server(options, config);
}
@ -294,38 +257,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 +299,28 @@ 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');
const load = require('package-json');
await showUpdateInfo(version);
const {version} = await load(Info.name, 'latest');
showUpdateInfo(version);
}
async function showUpdateInfo(version) {
function showUpdateInfo(version) {
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,74 @@
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 {
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';
'use strict';
/* global DOM */
const Emitify = require('emitify');
const inherits = require('inherits');
const rendy = require('rendy');
const load = require('load.js');
const tryToCatch = require('try-to-catch');
const pascalCase = require('just-pascal-case');
const isDev = process.env.NODE_ENV === 'development';
const Images = require('./dom/images');
const {unregisterSW} = require('./sw/register');
const getJsonFromFileTable = require('./get-json-from-file-table');
const currify = require('currify');
const noJS = (a) => a.replace(/.js$/, '');
const isDev = process.env.NODE_ENV === 'development';
const {
apiURL,
formatMsg,
buildFromJSON,
} = require('../common/cloudfunc');
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 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 = DOM.getCSSVar('min-one-panel-width');
this.MIN_ONE_PANEL_WIDTH = 1155;
this.HOST = location.origin || location.protocol + '//' + location.host;
const TITLE = 'Cloud Commander';
this.TITLE = TITLE;
this.sort = {
left: 'name',
right: 'name',
@ -64,16 +79,28 @@ function CloudCmdProto({DOM, Listeners}) {
right: 'asc',
};
this.changeDir = async (path, overrides = {}) => {
/**
* Функция привязываеться ко всем ссылкам и
* загружает содержимое каталогов
*
* @param params - {
* paramLink - ссылка
* needRefresh - необходимость обязательной загрузки данных с сервера
* panel
* }
* @param callback
*/
this.loadDir = async (params) => {
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) {
@ -89,12 +116,11 @@ function CloudCmdProto({DOM, Listeners}) {
Images.show.load(imgPosition, panel);
/* загружаем содержимое каталога */
await ajaxLoad(addSlashToEnd(path), {
await ajaxLoad(p.path, {
refresh,
history,
noCurrent,
currentName,
showDotFiles: CloudCmd.config('showDotFiles'),
}, panel);
};
@ -107,8 +133,6 @@ function CloudCmdProto({DOM, Listeners}) {
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 +141,7 @@ function CloudCmdProto({DOM, Listeners}) {
* should be called from config.js only
* after successful update on server
*/
if (key === 'password')
return;
@ -127,14 +152,22 @@ function CloudCmdProto({DOM, Listeners}) {
CloudCmd.MIN_ONE_PANEL_WIDTH = Infinity;
if (!document.body.scrollIntoViewIfNeeded)
await load.js(`${CloudCmd.DIR_MODULES}/polyfill.js`);
await load.js(prefix + CloudCmd.DIRCLIENT_MODULES + 'polyfill.js');
await initModules();
await baseInit();
await loadStyle();
CloudCmd.route(location.hash);
};
async function loadStyle() {
const {prefix} = CloudCmd;
const name = prefix + '/dist/cloudcmd.common.css';
await load.css(name);
}
this.route = (path) => {
const query = path.split('/');
@ -144,13 +177,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;
}
@ -172,7 +204,7 @@ function CloudCmdProto({DOM, Listeners}) {
};
const initModules = async () => {
CloudCmd.Key = Key;
CloudCmd.Key = new CloudCmd.Key();
CloudCmd.Key.bind();
const [, modules] = await tryToCatch(Files.get, 'modules');
@ -222,7 +254,7 @@ function CloudCmdProto({DOM, Listeners}) {
});
const dirPath = DOM.getCurrentDirPath();
Listeners = CloudCmd.Listeners;
Listeners.init();
const panels = getPanels();
@ -236,7 +268,7 @@ function CloudCmdProto({DOM, Listeners}) {
const data = await Storage.get(dirPath);
if (!data)
await Storage.setJson(dirPath, getJsonFromFileTable());
await Storage.set(dirPath, getJsonFromFileTable());
}
function getPanels() {
@ -259,15 +291,19 @@ function CloudCmdProto({DOM, Listeners}) {
};
this.refresh = async (options = {}) => {
const {panel = Info.panel, currentName} = options;
const {
panel = Info.panel,
currentName,
} = options;
const path = DOM.getCurrentDirPath(panel);
const isRefresh = true;
const history = false;
const noCurrent = options?.noCurrent;
const noCurrent = options ? options.noCurrent : false;
await CloudCmd.changeDir(path, {
await CloudCmd.loadDir({
path,
isRefresh,
history,
panel,
@ -283,18 +319,22 @@ function CloudCmdProto({DOM, Listeners}) {
* @param options
* { refresh, history } - необходимость обновить данные о каталоге
* @param panel
* @param callback
*
*/
async function ajaxLoad(path, options = {}, panel) {
const {RESTful} = DOM;
CloudCmd.log(`reading dir: "${path}";`);
CloudCmd.log('reading dir: "' + path + '";');
const dirStorage = CloudCmd.config('dirStorage');
const json = dirStorage && await Storage.getJson(path);
const json = dirStorage && await Storage.get(path);
const name = options.currentName || Info.name;
const {noCurrent, refresh} = options;
const {
noCurrent,
refresh,
} = options;
if (!refresh && json)
return await createFileTable(json, panel, options);
@ -311,8 +351,7 @@ function CloudCmdProto({DOM, Listeners}) {
const [, newObj] = await RESTful.read(path + query, 'json');
if (!newObj)
// that's OK, error handled by RESTful
return;
return; // that's OK, error handled by RESTful
options.sort = sort;
options.order = order;
@ -325,35 +364,28 @@ function CloudCmdProto({DOM, Listeners}) {
if (!CloudCmd.config('dirStorage'))
return;
Storage.setJson(path, newObj);
Storage.set(path, newObj);
}
/**
* Функция строит файловую таблицу
* @param data - данные о файлах
* @param json - данные о файлах
* @param panelParam
* @param options - history, noCurrent, showDotFiles
* @param history
* @param callback
*/
async function createFileTable(data, panelParam, options) {
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);
const [
error,
[file, path, link, pathLink],
] = await tryToCatch(Files.get, names);
if (error)
return DOM.Dialog.alert(error.responseText);
@ -361,7 +393,10 @@ function CloudCmdProto({DOM, Listeners}) {
const panel = panelParam || DOM.getPanel();
const {prefix} = CloudCmd;
const {dir, name} = Info;
const {
dir,
name,
} = Info;
const {childNodes} = panel;
let i = childNodes.length;
@ -370,13 +405,12 @@ function CloudCmdProto({DOM, Listeners}) {
panel.removeChild(panel.lastChild);
panel.innerHTML = buildFromJSON({
sort: options.sort,
order: options.order,
sort : options.sort,
order : options.order,
data,
id: panel.id,
id : panel.id,
prefix,
showDotFiles,
template: {
template : {
file,
path,
pathLink,
@ -416,7 +450,9 @@ function CloudCmdProto({DOM, Listeners}) {
const path = parentDirPath;
await CloudCmd.changeDir(path);
await CloudCmd.loadDir({
path,
});
const current = DOM.getCurrentByName(dir);
const [first] = DOM.getFiles(panel);
@ -426,3 +462,4 @@ function CloudCmdProto({DOM, Listeners}) {
});
};
}

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';
require('../css/main.css');
require('../css/nojs.css');
require('../css/columns/name-size-date.css');
require('../css/columns/name-size.css');
const wraptile = require('wraptile');
const load = require('load.js');
const isDev = process.env.NODE_ENV === 'development';
export default init;
const {
registerSW,
listenSW,
} = require('./sw/register');
globalThis.CloudCmd = init;
// prevent additional loading of emitify
window.Emitify = require('emitify');
async function init(config) {
globalThis.CloudCmd = createCloudCmd({
DOM,
Listeners,
});
globalThis.DOM = DOM;
globalThis.Util = Util;
globalThis.CloudFunc = CloudFunc;
module.exports = window.CloudCmd = (config) => {
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)
@ -54,7 +52,7 @@ const onUpdateFound = wraptile(async (config) => {
if (isDev)
return;
const {DOM} = globalThis;
const {DOM} = window;
const prefix = getPrefix(config.prefix);
await load.js(`${prefix}/dist/cloudcmd.common.js`);
@ -63,7 +61,7 @@ const onUpdateFound = wraptile(async (config) => {
console.log('cloudcmd: sw: updated');
DOM.Events.removeAll();
globalThis.CloudCmd(config);
window.CloudCmd(config);
});
async function register(config) {
@ -72,3 +70,4 @@ async function register(config) {
listenSW(sw, 'updatefound', onUpdateFound(config));
}

View file

@ -1,124 +1,137 @@
/* 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 tryToPromiseAll = require('../../common/try-to-promise-all');
const Storage = require('./storage');
const DOM = require('./');
function getNames() {
const {DOM} = globalThis;
const files = DOM.getActiveFiles();
module.exports = new BufferProto();
function BufferProto() {
const Info = DOM.CurrentInfo;
const CLASS = 'cut-file';
const COPY = 'copy';
const CUT = 'cut';
const Buffer = {
cut : callIfEnabled.bind(null, cut),
copy : callIfEnabled.bind(null, copy),
clear : callIfEnabled.bind(null, clear),
paste : callIfEnabled.bind(null, paste),
};
return DOM.getFilenames(files);
}
function addCutClass() {
const {DOM} = globalThis;
const files = DOM.getActiveFiles();
for (const element of files) {
element.classList.add(CLASS);
function showMessage(msg) {
DOM.Dialog.alert(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();
for (const element of files) {
element.classList.add(CLASS);
}
}
function rmCutClass() {
const files = DOM.getByClassAll(CLASS);
for (const element of files) {
element.classList.remove(CLASS);
}
}
function callIfEnabled(callback) {
const is = CloudCmd.config('buffer');
if (is)
return callback();
showMessage('Buffer disabled in config!');
}
async function readBuffer() {
const [e, cp, ct] = await tryToPromiseAll([
Storage.get(COPY),
Storage.get(CUT),
]);
return [
e,
cp,
ct,
];
}
async function copy() {
const names = getNames();
const from = Info.dirPath;
await clear();
if (!names.length)
return;
await Storage.remove(CUT);
await Storage.set(COPY, {
from,
names,
});
}
async function cut() {
const names = getNames();
const from = Info.dirPath;
await clear();
if (!names.length)
return;
addCutClass();
await Storage.set(CUT, {
from,
names,
});
}
async function clear() {
await Storage.remove(COPY);
await Storage.remove(CUT);
rmCutClass();
}
async function paste() {
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();
}
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,7 +166,7 @@ function unsetCurrentFile(currentFile) {
/**
* unified way to set current file
*/
export const setCurrentFile = (currentFile, options) => {
module.exports.setCurrentFile = (currentFile, options) => {
const o = options;
const currentFileWas = DOM.getCurrentFile();
@ -198,7 +195,7 @@ 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);
}
@ -206,7 +203,6 @@ export const setCurrentFile = (currentFile, options) => {
/* scrolling to current file */
const CENTER = true;
DOM.scrollIntoViewIfNeeded(currentFile, CENTER);
CloudCmd.emit('current-file', currentFile);
@ -216,7 +212,7 @@ export const setCurrentFile = (currentFile, options) => {
return DOM;
};
export const setCurrentByName = (name) => {
this.setCurrentByName = (name) => {
const current = DOM.getCurrentByName(name);
return DOM.setCurrentFile(current);
};
@ -227,7 +223,7 @@ export const setCurrentByName = (name) => {
* @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 +255,7 @@ export const getCurrentByPosition = ({x, y}) => {
*
* @param currentFile
*/
export const isCurrentFile = (currentFile) => {
module.exports.isCurrentFile = (currentFile) => {
if (!currentFile)
return false;
@ -271,7 +267,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,
@ -288,24 +285,20 @@ export const setTitle = (name) => {
*
* @param currentFile
*/
export const isCurrentIsDir = (currentFile) => {
module.exports.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;
return /^directory(-link)?/.test(fileType);
};
export const getCurrentType = (currentFile) => {
module.exports.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,55 +119,65 @@ 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;
const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
global.DOM = getDOM();
global.CloudCmd = getCloudCmd();
const {getByDataName} = globalThis.DOM;
const {getByDataName} = global.DOM;
getByDataName.returns({
className: 'mini-icon directory',
@ -174,129 +187,122 @@ test('current-file: getCurrentType', (t) => {
currentFile.getCurrentType(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.calledWith(getByDataName, ['js-type', current]);
t.ok(getByDataName.calledWith('js-type', current));
t.end();
});
test('current-file: isCurrentIsDir: getCurrentType', (t) => {
const {DOM, CloudCmd} = globalThis;
const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd();
global.DOM = getDOM();
global.CloudCmd = getCloudCmd();
const {getCurrentType} = globalThis.DOM;
const {getCurrentType} = global.DOM;
const current = create();
currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.calledWith(getCurrentType, [current]);
t.ok(getCurrentType.calledWith(current));
t.end();
});
test('current-file: isCurrentIsDir: directory', (t) => {
const {DOM, CloudCmd} = globalThis;
const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM({
global.DOM = getDOM({
getCurrentType: stub().returns('directory'),
});
globalThis.CloudCmd = getCloudCmd();
global.CloudCmd = getCloudCmd();
const current = create();
const result = currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.ok(result);
t.end();
});
test('current-file: isCurrentIsDir: directory-link', (t) => {
const {DOM, CloudCmd} = globalThis;
const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM({
global.DOM = getDOM({
getCurrentType: stub().returns('directory-link'),
});
globalThis.CloudCmd = getCloudCmd();
global.CloudCmd = getCloudCmd();
const current = create();
const result = currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.ok(result);
t.end();
});
test('current-file: isCurrentIsDir: file', (t) => {
const {DOM, CloudCmd} = globalThis;
const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM({
global.DOM = getDOM({
getCurrentType: stub().returns('file'),
});
globalThis.CloudCmd = getCloudCmd();
global.CloudCmd = getCloudCmd();
const current = create();
const result = currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.notOk(result);
t.end();
});
const getCloudCmd = ({emit} = {}) => ({
prefix: '',
emit: emit || stub(),
});
function getCloudCmd({emit} = {}) {
return {
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(),
getCurrentType = stub(),
} = {}) {
return {
getCurrentDirPath,
getCurrentDirName,
getCurrentPath,
getByDataName,
isContainClass,
getCurrentType,
@ -306,3 +312,4 @@ function getDOM(overrides = {}) {
},
};
}

View file

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

View file

@ -1,19 +1,27 @@
/* global DOM, CloudCmd */
import philip from 'philip';
import * as Dialog from '#dom/dialog';
import {FS} from '#common/cloudfunc';
import * as Images from '#dom/images';
'use strict';
export const uploadDirectory = (items) => {
/* global CloudCmd */
const philip = require('philip');
const Images = require('./images');
const {FS} = require('../../common/cloudfunc');
const DOM = require('.');
const Dialog = require('./dialog');
const {getCurrentDirPath: getPathWhenRootEmpty} = DOM;
module.exports = (items) => {
if (items.length)
Images.show('top');
const entries = Array
.from(items)
.map((item) => item.webkitGetAsEntry());
const entries = Array.from(items).map((item) => {
return item.webkitGetAsEntry();
});
const dirPath = DOM.getCurrentDirPath();
const path = dirPath.replace(/\/$/, '');
const dirPath = getPathWhenRootEmpty();
const path = dirPath
.replace(/\/$/, '');
const progress = Dialog.progress('Uploading...');
@ -57,8 +65,15 @@ export const uploadDirectory = (items) => {
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 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');
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,250 @@
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');
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':
for (const name of eventName) {
parseArgs(
name,
element,
listener,
callback,
);
}
break;
case 'object':
for (const name of Object.keys(eventName)) {
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,29 +1,37 @@
/* 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');
const currify = require('currify');
const {promisify} = require('es6-promisify');
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;
async function getFile(name) {
const type = itype(name);
check(name);
if (type === 'string')
return getModule(name);
return await getModule(name);
if (type === 'array')
return Promise.all(name.map(getFile));
return Promise.all(unaryMap(name, get));
}
function check(name) {
@ -31,9 +39,9 @@ function check(name) {
throw Error('name could not be empty!');
}
function getModule(name) {
const regExpHTML = RegExp(FILES_HTML + '|' + FILES_HTML_ROOT);
const regExpJSON = RegExp(FILES_JSON);
async function getModule(name) {
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);
@ -45,13 +53,12 @@ function getModule(name) {
return getConfig();
const path = getPath(name, isHTML, isJSON);
return getSystemFile(path);
}
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,8 +76,8 @@ 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;
}
@ -134,3 +141,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);
@ -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();
});

216
client/dom/io.js Normal file
View file

@ -0,0 +1,216 @@
'use strict';
/* global CloudCmd*/
const itype = require('itype');
const {promisify} = require('es6-promisify');
const {FS} = require('../../common/cloudfunc');
const Images = require('./images');
const load = require('./load');
const imgPosition = {
top: true,
};
module.exports._replaceHash = replaceHash;
function replaceHash(url) {
/*
* if we send ajax request -
* no need in hash so we escape #
*/
return url.replace(/#/g, '%23');
}
module.exports.delete = promisify((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,
},
});
});
module.exports.patch = promisify((url, data, callback) => {
const isFunc = itype.function(data);
if (!callback && isFunc) {
callback = data;
data = null;
}
sendRequest({
method: 'PATCH',
url: FS + url,
data,
callback,
imgPosition,
});
});
module.exports.write = promisify((url, data, callback) => {
const isFunc = itype.function(data);
if (!callback && isFunc) {
callback = data;
data = null;
}
sendRequest({
method: 'PUT',
url: FS + url,
data,
callback,
imgPosition,
});
});
module.exports.read = promisify((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,
});
});
module.exports.cp = promisify((data, callback) => {
sendRequest({
method: 'PUT',
url: '/cp',
data,
callback,
imgPosition,
});
});
module.exports.pack = promisify((data, callback) => {
sendRequest({
method: 'PUT',
url: '/pack',
data,
callback,
});
});
module.exports.extract = promisify((data, callback) => {
sendRequest({
method : 'PUT',
url : '/extract',
data,
callback,
});
});
module.exports.mv = promisify((data, callback) => {
sendRequest({
method : 'PUT',
url : '/mv',
data,
callback,
imgPosition,
});
});
module.exports.Config = {
read: promisify((callback) => {
sendRequest({
method: 'GET',
url: '/config',
callback,
imgPosition,
notLog: true,
});
}),
write: promisify((data, callback) => {
sendRequest({
method: 'PATCH',
url: '/config',
data,
callback,
imgPosition,
});
}),
};
module.exports.Markdown = {
read: promisify((url, callback) => {
sendRequest({
method: 'GET',
url: '/markdown' + url,
callback,
imgPosition,
notLog: true,
});
}),
render: promisify((data, callback) => {
sendRequest({
method: 'PUT',
url: '/markdown',
data,
callback,
imgPosition,
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;
p.callback(Error(text));
},
success: (data) => {
Images.hide();
if (!p.notLog)
CloudCmd.log(data);
p.callback(null, data);
},
});
}

View file

@ -1,11 +1,15 @@
import {test} from 'supertape';
import {_replaceHash} from './send-request.js';
'use strict';
const test = require('supertape');
const io = require('./io');
test('cloudcmd: client: io: replaceHash', (t) => {
const {_replaceHash} = io;
const url = '/hello/####world';
const result = _replaceHash(url);
const expected = '/hello/%23%23%23%23world';
t.equal(result, expected);
t.equal(result, expected, 'should equal');
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,58 +1,60 @@
/* 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 rendy = require('rendy');
const itype = require('itype');
const load = require('load.js');
const tryToCatch = require('try-to-catch');
const {findObjByNameInArr} = require('../../common/util');
const Files = require('./files');
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').then(async (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,
});
});
if (online) {
const [e] = await tryToCatch(load.parallel, remoteURL);
if (!e)
return callback();
}
const [e] = await tryToCatch(load.parallel, localURL);
callback(e);
});
};

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');
const jonny = require('jonny');
const Emitify = require('emitify');
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,15 +37,15 @@ 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;
const xhr = new XMLHttpRequest();
xhr.open(type, p.url, true);
@ -60,7 +63,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 +81,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 +94,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);
@ -116,10 +120,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 +131,4 @@ export const put = (url, body) => {
return emitter;
};

View file

@ -1,27 +1,23 @@
/* 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';
'use strict';
export default async (current, overrides = {}) => {
const {
refresh = CloudCmd.refresh,
Dialog = _Dialog,
currentFile = _currentFile,
} = overrides;
const {
isCurrentFile,
getCurrentName,
getCurrentFile,
getCurrentByName,
getCurrentType,
getCurrentDirPath,
setCurrentName,
} = currentFile;
/* global CloudCmd */
const capitalize = require('just-capitalize');
const Dialog = require('../dialog');
const Storage = require('../storage');
const RESTful = require('../rest');
const {
isCurrentFile,
getCurrentName,
getCurrentFile,
getCurrentByName,
getCurrentType,
getCurrentDirPath,
setCurrentName,
} = require('../current-file');
module.exports = async (current) => {
if (!isCurrentFile(current))
current = getCurrentFile();
@ -50,11 +46,12 @@ export default async (current, overrides = {}) => {
return;
const dirPath = getCurrentDirPath();
const files = {
from : dirPath + from,
to : dirPath + to,
};
const fromFull = `${dirPath}${from}`;
const toFull = `${dirPath}${to}`;
const [e] = await RESTful.rename(fromFull, toFull);
const [e] = await RESTful.mv(files);
if (e)
return;
@ -62,5 +59,5 @@ export default async (current, overrides = {}) => {
setCurrentName(to, current);
Storage.remove(dirPath);
refresh();
CloudCmd.refresh();
};

View file

@ -1,26 +1,37 @@
import {test, stub} from 'supertape';
import renameCurrent from './rename-current.js';
'use strict';
const test = require('supertape');
const stub = require('@cloudcmd/stub');
const mockRequire = require('mock-require');
const {reRequire} = mockRequire;
test('cloudcmd: client: dom: renameCurrent: isCurrentFile', async (t) => {
const current = {};
const isCurrentFile = stub();
const currentFile = stubCurrentFile({
mockRequire('../dialog', stubDialog());
mockRequire('../current-file', stubCurrentFile({
isCurrentFile,
});
}));
await renameCurrent(current, {
Dialog: stubDialog(),
currentFile,
});
const renameCurrent = reRequire('./rename-current');
await renameCurrent(current);
t.calledWith(isCurrentFile, [current], 'should call isCurrentFile');
t.ok(isCurrentFile.calledWith(current), 'should call isCurrentFile');
t.end();
});
test('cloudcmd: client: dom: renameCurrent: file exist', async (t) => {
const current = {};
const name = 'hello';
const {CloudCmd} = global;
const CloudCmdStub = {
refresh: stub(),
};
global.CloudCmd = CloudCmdStub;
const prompt = stub().returns([null, name]);
const confirm = stub().returns([true]);
@ -28,24 +39,23 @@ test('cloudcmd: client: dom: renameCurrent: file exist', async (t) => {
const getCurrentByName = stub().returns(current);
const getCurrentType = stub().returns('directory');
const Dialog = stubDialog({
mockRequire('../dialog', stubDialog({
confirm,
prompt,
});
}));
const currentFile = stubCurrentFile({
mockRequire('../current-file', stubCurrentFile({
getCurrentByName,
getCurrentType,
});
}));
await renameCurrent(null, {
Dialog,
currentFile,
});
const renameCurrent = reRequire('./rename-current');
await renameCurrent();
const expected = 'Directory "hello" already exists. Proceed?';
global.CloudCmd = CloudCmd;
t.calledWith(confirm, [expected], 'should call confirm');
t.ok(confirm.calledWith(expected), 'should call confirm');
t.end();
});
@ -84,3 +94,4 @@ const stubCurrentFile = (fns = {}) => {
setCurrentName,
};
};

View file

@ -1,8 +1,12 @@
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 tryToCatch = require('try-to-catch');
const {encode} = require('../../common/entity');
const Images = require('./images');
const IO = require('./io');
const Dialog = require('./dialog');
const handleError = (promise) => async (...args) => {
const [e, data] = await tryToCatch(promise, ...args);
@ -18,23 +22,22 @@ const handleError = (promise) => async (...args) => {
return [e, data];
};
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);
module.exports.delete = handleError(IO.delete);
module.exports.patch = handleError(IO.patch);
module.exports.write = handleError(IO.write);
module.exports.read = handleError(IO.read);
module.exports.cp = handleError(IO.cp);
module.exports.pack = handleError(IO.pack);
module.exports.extract = handleError(IO.extract);
module.exports.mv = handleError(IO.mv);
export const Config = {
module.exports.Config = {
read: handleError(IO.Config.read),
write: handleError(IO.Config.write),
};
export const Markdown = {
module.exports.Markdown = {
read: handleError(IO.Markdown.read),
render: handleError(IO.Markdown.render),
};

View file

@ -1,18 +1,22 @@
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';
let SelectType = '*.*';
export const selectByPattern = async (msg, files) => {
const {getRegExp} = require('../../common/util');
const {
alert,
prompt,
} = require('./dialog');
const DOM = require('.');
module.exports = async (msg, files) => {
if (!files)
return;
const allMsg = `Specify file type for ${msg} selection`;
/* eslint require-atomic-updates: 0 */
const [cancel, type] = await prompt(allMsg, SelectType);
if (cancel)
@ -24,23 +28,24 @@ export const selectByPattern = async (msg, files) => {
let matches = 0;
for (const current of files) {
const name = getCurrentName(current);
const name = DOM.getCurrentName(current);
if (name === '..' || !regExp.test(name))
continue;
++matches;
let selected = isSelected(current);
let isSelected = DOM.isSelected(current);
const shouldSel = msg === 'expand';
if (shouldSel)
selected = !selected;
isSelected = !isSelected;
if (selected)
toggleSelectedFile(current);
if (isSelected)
DOM.toggleSelectedFile(current);
}
if (!matches)
alert('No matches found!');
};

View file

@ -1,26 +1,38 @@
'use strict';
const tryCatch = require('try-catch');
const {parse, stringify} = JSON;
const isObj = (a) => typeof a === 'object';
export const set = (name, data) => {
localStorage.setItem(name, data);
module.exports.set = (name, data) => {
const primitive = !isObj(data) ? data : stringify(data);
localStorage.setItem(name, primitive);
};
export const setJson = (name, data) => {
localStorage.setItem(name, stringify(data));
};
export const get = (name) => {
return localStorage.getItem(name);
};
export const getJson = (name) => {
module.exports.get = async (name) => {
const data = localStorage.getItem(name);
return parse(data);
const [, result = data] = tryCatch(parse, data);
return result;
};
export const clear = () => {
module.exports.clear = () => {
localStorage.clear();
};
export const remove = (item) => {
module.exports.remove = (item) => {
localStorage.removeItem(item);
};
module.exports.removeMatch = (string) => {
const reg = RegExp('^' + string + '.*$');
const test = (a) => reg.test(a);
const remove = (a) => localStorage.removeItem(a);
Object.keys(localStorage)
.filter(test)
.forEach(remove);
};

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,25 @@
'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');
const load = require('./load');
const Images = require('./images');
const {alert} = require('./dialog');
const {FS} = require('../../common/cloudfunc');
const onEnd = wraptile(_onEnd);
const loadFile = wraptile(_loadFile);
export const uploadFiles = (dir, files) => {
const {getCurrentDirPath: getPathWhenRootEmpty} = require('.');
module.exports = (dir, files) => {
if (!files) {
files = dir;
dir = getCurrentDirPath();
dir = getPathWhenRootEmpty();
}
const n = files.length;
@ -49,8 +55,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) => {
@ -65,3 +70,4 @@ function _loadFile(dir, n, file, callback) {
function showError({message}) {
alert(message);
}

View file

@ -1,13 +1,40 @@
'use strict';
/* global DOM */
const Info = DOM.CurrentInfo;
/**
* Функция генерирует JSON из html-таблицы файлов и
* используеться при первом заходе в корень
*/
export const getJsonFromFileTable = () => {
const Info = DOM.CurrentInfo;
module.exports = () => {
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);
const type = DOM.getCurrentType(current);
return {
name,
size,
mode,
owner,
date,
type,
};
};
const files = infoFiles
.filter(notParent)
.map(parse);
@ -20,25 +47,3 @@ export const getJsonFromFileTable = () => {
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,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();
});

File diff suppressed because it is too large Load diff

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,14 +31,12 @@ 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;
}
@ -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,10 +1,12 @@
import {fullstore} from 'fullstore';
import limier from 'limier';
'use strict';
const fullstore = require('fullstore');
const limier = require('limier');
const searchStore = fullstore([]);
const searchIndex = fullstore(0);
export const find = (value, names) => {
module.exports.find = (value, names) => {
const result = limier(value, names);
searchStore(result);
@ -13,7 +15,7 @@ export const find = (value, names) => {
return result;
};
export const findNext = () => {
module.exports.findNext = () => {
const names = searchStore();
const index = next(searchIndex(), names.length);
@ -21,7 +23,7 @@ export const findNext = () => {
return names[searchIndex()];
};
export const findPrevious = () => {
module.exports.findPrevious = () => {
const names = searchStore();
const index = previous(searchIndex(), names.length);
@ -29,8 +31,8 @@ export const findPrevious = () => {
return 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 +47,4 @@ function previous(index, length) {
return --index;
}

View file

@ -1,14 +1,16 @@
import test from 'supertape';
import {getDOM} from './globals.fixture.js';
import {
'use strict';
const test = require('supertape');
const dir = './';
const {getDOM} = require('./globals.fixture');
global.DOM = getDOM();
const {
_next,
_previous,
find,
findNext,
findPrevious,
} from './find.js';
globalThis.DOM = getDOM();
} = require(dir + 'find');
test('cloudcmd: client: vim: _next', (t) => {
const result = _next(1, 2);
@ -17,13 +19,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 +26,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,12 +1,13 @@
const noop = () => {};
'use strict';
export const getDOM = () => {
module.exports.getDOM = () => {
const prompt = Promise.resolve.bind(Promise);
const CurrentInfo = {
element: {},
files: [],
};
const noop = () => {};
const Buffer = {
copy: noop,
paste: noop,
@ -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,102 @@
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 */
/* global DOM */
const vim = require('./vim');
const finder = require('./find');
const Info = DOM.CurrentInfo;
const {Dialog} = DOM;
module.exports = async (key, event) => {
const operations = getOperations(event);
await vim(key, operations);
};
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;
const getOperations = (event) => {
return {
makeFile: createMakeFile({
promptNewFile,
preventDefault,
stopImmediatePropagation,
}),
findNext: createFindNext({
setCurrentByName,
}),
findPrevious: createFindPrevious({
setCurrentByName,
}),
escape: unselectFiles,
rename: () => {
event.preventDefault();
renameCurrent();
},
escape: DOM.unselectFiles,
remove: () => {
Operation.show('delete');
CloudCmd.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();
DOM.Buffer.copy();
DOM.unselectFiles();
},
select: () => {
const current = Info.element;
toggleSelectedFile(current);
DOM.toggleSelectedFile(current);
},
paste: Buffer.paste,
paste: DOM.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', '');
event.preventDefault();
const [, value] = await Dialog.prompt('Find', '');
if (!value)
return;
const names = Info.files.map(getCurrentName);
const names = Info.files.map(DOM.getCurrentName);
const [result] = finder.find(value, names);
setCurrentByName(result);
DOM.setCurrentByName(result);
},
findNext: () => {
const name = finder.findNext();
DOM.setCurrentByName(name);
},
findPrevious: () => {
const name = finder.findPrevious();
DOM.setCurrentByName(name);
},
};
};
export const selectFile = selectFileNotParent;
module.exports.selectFile = selectFile;
const _createFindPrevious = (overrides = {}) => () => {
const {setCurrentByName} = overrides;
const name = finder.findPrevious();
function selectFile(current) {
const name = DOM.getCurrentName(current);
setCurrentByName(name);
};
if (name === '..')
return;
DOM.selectFile(current);
}
const _createFindNext = (overrides = {}) => () => {
const {setCurrentByName} = overrides;
const name = finder.findNext();
function setCurrent(sibling, {count, isVisual, isDelete}) {
let current = Info.element;
const select = isVisual ? selectFile : DOM.unselectFile;
setCurrentByName(name);
};
select(current);
const position = `${sibling}Sibling`;
for (let i = 0; i < count; i++) {
const next = current[position];
if (!next)
break;
current = next;
select(current);
}
DOM.setCurrentFile(current);
if (isDelete)
CloudCmd.Operation.show('delete');
}
const _createMakeFile = (overrides = {}) => () => {
const {
promptNewFile,
stopImmediatePropagation,
preventDefault,
} = overrides;
stopImmediatePropagation();
preventDefault();
promptNewFile();
};

View file

@ -1,35 +1,46 @@
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, stopAll} = mockRequire;
const dir = '../';
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 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 +48,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 +66,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 +86,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 +126,25 @@ 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.calledWith(setCurrentFile, [last], 'should set next file');
t.ok(setCurrentFile.calledWith(last), 'should set next file');
t.end();
});
@ -172,7 +153,6 @@ test('cloudcmd: client: key: delete +2 files from current', (t) => {
const nextSibling = {
nextSibling: last,
};
const element = {
nextSibling,
};
@ -180,26 +160,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 +184,49 @@ 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.calledWith(setCurrentFile, [previousSibling], 'should set previous file');
t.ok(setCurrentFile.calledWith(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,91 +235,63 @@ 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 = {
previousSibling: first,
@ -390,121 +301,66 @@ 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();
t.calledWithNoArgs(unselectFiles, 'should toggle selection');
global.DOM.CurrentInfo.element = element;
global.DOM.unselectFiles = unselectFiles ;
vim('Escape');
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('Enter');
await vim('j', null, {
Info,
setCurrentFile,
unselectFiles,
});
vim('j');
t.calledWith(setCurrentFile, [nextSibling], 'should set next file');
t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file');
t.end();
});
@ -512,245 +368,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);
stopAll(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);
stopAll(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,5 +1,6 @@
import {fullstore} from 'fullstore';
'use strict';
const fullstore = require('fullstore');
const store = fullstore('');
const visual = fullstore(false);
@ -20,11 +21,10 @@ const rmFirst = (a) => {
const noop = () => {};
export default (key, operations = {}) => {
module.exports = (key, operations) => {
const prevStore = store();
const isVisual = visual();
const value = store(prevStore.concat(key));
const {
escape = noop,
moveNext = noop,
@ -36,13 +36,6 @@ export default (key, operations = {}) => {
find = noop,
findNext = noop,
findPrevious = noop,
makeFile = noop,
makeDirectory = noop,
terminal = noop,
edit = noop,
operationCopy = noop,
operationMove = noop,
rename = noop,
} = operations;
if (key === 'Enter')
@ -51,11 +44,10 @@ export default (key, operations = {}) => {
if (key === 'Escape') {
visual(false);
escape();
return end();
}
if (key === 'j' || key === 'w') {
if (key === 'j') {
const {
count,
isDelete,
@ -71,7 +63,7 @@ export default (key, operations = {}) => {
return end();
}
if (key === 'k' || key === 'b') {
if (key === 'k') {
const {
count,
isDelete,
@ -87,8 +79,11 @@ export default (key, operations = {}) => {
return end();
}
if (value === 'gg' || key === '^') {
const {isDelete, isVisual} = handleDelete(prevStore);
if (/^gg$/.test(value)) {
const {
isDelete,
isVisual,
} = handleDelete(prevStore);
movePrevious({
count: Infinity,
@ -99,49 +94,13 @@ export default (key, operations = {}) => {
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 === '$') {
if (key === 'G') {
moveNext({
count: Infinity,
isVisual,
@ -156,7 +115,6 @@ export default (key, operations = {}) => {
stopVisual();
copy();
return end();
}
@ -168,7 +126,6 @@ export default (key, operations = {}) => {
if (/^v$/i.test(key)) {
visual(!visual());
select();
return end();
}
@ -186,9 +143,6 @@ export default (key, operations = {}) => {
findPrevious();
return end();
}
if (key === ' ')
return end();
};
function handleDelete(prevStore) {
@ -218,3 +172,4 @@ function getNumber(value) {
return parseInt(value);
}

View file

@ -1,5 +1,7 @@
import {test, stub} from 'supertape';
import vim from './vim.js';
'use strict';
const test = require('supertape');
const vim = require('./vim');
test('vim: no operations', (t) => {
const result = vim('hello', {});
@ -8,93 +10,3 @@ test('vim: no operations', (t) => {
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,36 @@
/* 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');
const currify = require('currify');
const tryToCatch = require('try-to-catch');
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,
@ -42,9 +48,14 @@ const execAll = currify((funcs, event) => {
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;
@ -90,58 +101,58 @@ function getPath(el, path = []) {
async function config() {
const [, config] = await tryToCatch(DOM.Files.get, 'config');
const type = config?.packer;
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' : initF2,
'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]);
});
};
function initF2() {
CloudCmd.UserMenu.show();
}
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 +168,6 @@ function getPathListener(panel) {
}
function isNoCurrent(panel) {
const Info = DOM.CurrentInfo;
const infoPanel = Info.panel;
if (!infoPanel)
@ -176,13 +186,13 @@ function decodePath(path) {
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;
event.preventDefault();
const element = event.target;
@ -204,26 +214,19 @@ async function onPathElementClick(panel, event) {
const {href} = element;
const path = decodePath(href);
await CloudCmd.changeDir(path, {
await 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;
@ -238,14 +241,14 @@ function onClick(event) {
}
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)
@ -253,7 +256,6 @@ function toggleSelect(key, files) {
}
function changePanel(element) {
const Info = DOM.CurrentInfo;
const {panel} = Info;
const files = DOM.getByDataName('js-files', panel);
const ul = getULElement(element);
@ -263,16 +265,21 @@ function changePanel(element) {
}
async function onDblClick(event) {
event.preventDefault();
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) {
await CloudCmd.loadDir({
path: path === '/' ? '/' : path + '/',
});
event.preventDefault();
} else {
CloudCmd.View.show();
event.preventDefault();
}
}
async function onTouch(event) {
@ -287,7 +294,9 @@ async function onTouch(event) {
if (!isCurrent)
return;
await CloudCmd.changeDir(DOM.getCurrentPath(current));
await 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;
@ -312,8 +320,8 @@ function onDragStart(event) {
event.dataTransfer.setData(
'DownloadURL',
'application/octet-stream' + ':' + name +
':' +
'application/octet-stream' + ':' +
name + ':' +
link,
);
}
@ -336,7 +344,6 @@ function getULElement(element) {
}
function setCurrentFileByEvent(event) {
const Info = DOM.CurrentInfo;
const BUTTON_LEFT = 0;
const key = {
@ -400,7 +407,10 @@ function dragndrop() {
};
const onDrop = (event) => {
const {files, items} = event.dataTransfer;
const {
files,
items,
} = event.dataTransfer;
const {length: filesCount} = files;
@ -410,10 +420,7 @@ function dragndrop() {
return uploadFiles(files);
const isFile = (item) => item.kind === 'file';
const dirFiles = Array
.from(items)
.filter(isFile);
const dirFiles = Array.from(items).filter(isFile);
if (dirFiles.length)
return DOM.uploadDirectory(dirFiles);
@ -425,7 +432,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
*/
@ -450,9 +457,9 @@ function dragndrop() {
}
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;
@ -470,8 +477,8 @@ function pop() {
return CloudCmd.route(location.hash);
const history = false;
await CloudCmd.changeDir(path, {
await CloudCmd.loadDir({
path,
history,
});
});
@ -479,8 +486,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 +506,4 @@ function resize() {
DOM.changePanel();
});
}

View file

@ -1,16 +1,19 @@
/* 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');
const loadJS = 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;
@ -22,24 +25,24 @@ export const loadModule = (params) => {
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);
await a.show(...args);
};
};

View file

@ -1,32 +1,31 @@
/* 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');
const load = require('load.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,
};
export async function init() {
module.exports.init = async () => {
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 +34,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,7 +56,7 @@ function _upload(callback, file) {
});
}
function loadFiles() {
async function loadFiles() {
const js = '//api.filepicker.io/v2/filepicker.js';
return Promise.all([
@ -62,3 +64,4 @@ function loadFiles() {
load.js(js),
]);
}

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() {}

View file

@ -1,23 +1,28 @@
import '../../../css/config.css';
import {rendy} from 'rendy';
import currify from 'currify';
import wraptile from 'wraptile';
import squad from 'squad';
import {promisify} from 'es6-promisify';
import {tryToCatch} from 'try-to-catch';
import load from 'load.js';
import createElement from '@cloudcmd/create-element';
import * as Events from '#dom/events';
import * as Files from '#dom/files';
import {getTitle} from '#common/cloudfunc';
import * as Images from '#dom/images';
import * as input from './input.js';
'use strict';
const {CloudCmd, DOM} = globalThis;
/* global CloudCmd, DOM, io */
require('../../../css/config.css');
const rendy = require('rendy');
const currify = require('currify');
const wraptile = require('wraptile');
const squad = require('squad');
const {promisify} = require('es6-promisify');
const tryToCatch = require('try-to-catch');
const load = require('load.js');
const createElement = require('@cloudcmd/create-element');
const input = require('./input');
const Images = require('../../dom/images');
const Events = require('../../dom/events');
const Files = require('../../dom/files');
const {getTitle} = require('../../../common/cloudfunc');
const {Dialog, setTitle} = DOM;
const Name = 'Config';
CloudCmd[Name] = module.exports;
const loadSocket = promisify(DOM.loadSocket);
@ -41,25 +46,28 @@ let Template;
const loadCSS = load.css;
export async function init() {
module.exports.init = async () => {
if (!CloudCmd.config('configDialog'))
return;
showLoad();
const {DIR_DIST} = CloudCmd;
const {prefix} = CloudCmd;
[Template] = await Promise.all([
Files.get('config-tmpl'),
loadSocket(),
loadCSS(`${DIR_DIST}/config.css`),
loadCSS(prefix + '/dist/config.css'),
CloudCmd.View(),
]);
initSocket();
}
};
const {config, Key} = CloudCmd;
const {
config,
Key,
} = CloudCmd;
let Element;
@ -69,20 +77,24 @@ function getHost() {
origin,
protocol,
} = location;
const href = origin || `${protocol}//${host}`;
return origin || `${protocol}//${host}`;
return href;
}
function initSocket() {
const href = getHost();
const {prefixSocket, prefix} = CloudCmd;
const {
prefixSocket,
prefix,
} = CloudCmd;
const ONE_MINUTE = 60 * 1000;
const socket = globalThis.io.connect(href + prefixSocket + '/config', {
const socket = io.connect(href + prefixSocket + '/config', {
reconnectionAttempts: Infinity,
reconnectionDelay: ONE_MINUTE,
path: `${prefix}/socket.io`,
path: prefix + '/socket.io',
});
const save = (data) => {
@ -113,7 +125,9 @@ function authCheck(socket) {
Config.save = saveHttp;
export async function show() {
module.exports.show = show;
async function show() {
if (!CloudCmd.config('configDialog'))
return;
@ -128,27 +142,21 @@ async function fillTemplate() {
const {
editor,
menu,
packer,
columns,
theme,
configAuth,
configPort,
...obj
} = input.convert(config);
obj[`${menu}-selected`] = 'selected';
obj[`${editor}-selected`] = 'selected';
obj[`${packer}-selected`] = 'selected';
obj[`${columns}-selected`] = 'selected';
obj[`${theme}-selected`] = 'selected';
obj[editor + '-selected'] = 'selected';
obj[packer + '-selected'] = 'selected';
obj[columns + '-selected'] = 'selected';
obj.configAuth = configAuth ? '' : 'hidden';
obj.configPort = configPort ? '' : 'hidden';
const innerHTML = rendy(Template, obj);
Element = createElement('form', {
className: 'config',
className : 'config',
innerHTML,
});
@ -165,20 +173,20 @@ async function fillTemplate() {
const getTarget = ({target}) => target;
const handleChange = squad(onChange, getTarget);
Array
.from(inputs)
Array.from(inputs)
.map(addKey(onKey))
.map(addChange(handleChange));
const autoSize = true;
CloudCmd.View.show(Element, {
autoSize,
afterShow,
});
}
export function hide() {
module.exports.hide = hide;
function hide() {
CloudCmd.View.hide();
}
@ -220,7 +228,7 @@ function onAuthChange(checked) {
const elUsername = input.getElementByName('username', Element);
const elPassword = input.getElementByName('password', Element);
elUsername.disabled = !checked;
elUsername.disabled =
elPassword.disabled = !checked;
}
@ -230,18 +238,13 @@ function onNameChange(name) {
}));
}
async function onKey({keyCode, target}) {
function onKey({keyCode, target}) {
switch(keyCode) {
case Key.ESC:
return hide();
case Key.ENTER:
return await onChange(target);
return onChange(target);
}
}
CloudCmd[Name] = {
init,
show,
hide,
};

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');
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,21 +25,17 @@ export const getName = (element) => {
return name;
};
export const convert = (config) => {
const result = config;
module.exports.convert = (config) => {
const result = {
...config,
};
const array = Object.keys(config);
for (const name of keys(config)) {
const filtered = array.filter(isBool(config));
for (const name of filtered) {
const item = config[name];
if (isBool(item)) {
result[name] = setState(item);
continue;
}
if (isString(item)) {
result[name] = encode(item);
continue;
}
result[name] = setState(item);
}
return result;
@ -46,7 +48,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 +64,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 +78,4 @@ export const setValue = (name, value, element) => {
break;
}
};

View file

@ -1,140 +0,0 @@
import {test, stub} from 'supertape';
import {
convert,
getName,
getValue,
setValue,
} from './input.js';
test('cloudcmd: client: config: input: convert', (t) => {
const result = convert({
name: 'hello <world>',
});
const expected = {
name: 'hello &lt;world&gt;',
};
t.deepEqual(result, expected);
t.end();
});
test('cloudcmd: client: config: input: convert: bool', (t) => {
const result = convert({
auth: true,
});
const expected = {
auth: ' checked',
};
t.deepEqual(result, expected);
t.end();
});
test('cloudcmd: client: config: input: convert: bool false', (t) => {
const result = convert({
auth: false,
});
const expected = {
auth: '',
};
t.deepEqual(result, expected);
t.end();
});
test('cloudcmd: client: config: input: getName', (t) => {
const getAttribute = stub().returns('js-hello');
const element = {
getAttribute,
};
const result = getName(element);
t.equal(result, 'hello', 'should strip js- prefix');
t.end();
});
test('cloudcmd: client: config: input: getValue: checkbox', (t) => {
const querySelector = stub().returns({
type: 'checkbox',
checked: true,
});
const element = {
querySelector,
};
const result = getValue('auth', element);
t.ok(result, 'should return checked value');
t.end();
});
test('cloudcmd: client: config: input: getValue: number', (t) => {
const querySelector = stub().returns({
type: 'number',
value: '42',
});
const element = {
querySelector,
};
const result = getValue('port', element);
t.equal(result, 42, 'should return number');
t.end();
});
test('cloudcmd: client: config: input: getValue: default', (t) => {
const querySelector = stub().returns({
type: 'text',
value: 'hello',
});
const element = {
querySelector,
};
const result = getValue('name', element);
t.equal(result, 'hello', 'should return value as is');
t.end();
});
test('cloudcmd: client: config: input: setValue: checkbox', (t) => {
const el = {
type: 'checkbox',
checked: false,
};
const querySelector = stub().returns(el);
const element = {
querySelector,
};
setValue('auth', true, element);
t.ok(el.checked, 'should set checked');
t.end();
});
test('cloudcmd: client: config: input: setValue: default', (t) => {
const el = {
type: 'text',
value: 'old',
};
const querySelector = stub().returns(el);
const element = {
querySelector,
};
setValue('name', 'new', element);
t.equal(el.value, 'new', 'should set value');
t.end();
});

View file

@ -1,34 +1,36 @@
/* global CloudCmd */
/* global DOM */
import olark from '@cloudcmd/olark';
import * as Images from '#dom/images';
CloudCmd.Contact = {
init,
show,
hide,
};
'use strict';
CloudCmd.Contact = exports;
const olark = require('@cloudcmd/olark');
const Images = require('../dom/images');
const {Events} = DOM;
const {Key} = CloudCmd;
export function init() {
module.exports.show = show;
module.exports.hide = hide;
module.exports.init = () => {
Events.addKey(onKey);
olark.identify('6216-545-10-4223');
olark('api.box.onExpand', show);
olark('api.box.onShow', show);
olark('api.box.onShrink', hide);
}
};
export function show() {
function show() {
Key.unsetBind();
Images.hide();
olark('api.box.expand');
}
export function hide() {
function hide() {
Key.setBind();
olark('api.box.hide');
}
@ -37,3 +39,4 @@ function onKey({keyCode}) {
if (keyCode === Key.ESC)
hide();
}

View file

@ -1,12 +1,10 @@
import * as Events from '#dom/events';
'use strict';
const {CloudCmd} = globalThis;
/* global CloudCmd */
CloudCmd.EditFileVim = {
init,
show,
hide,
};
CloudCmd.EditFileVim = exports;
const Events = require('../dom/events');
const {Key} = CloudCmd;
@ -18,29 +16,34 @@ const ConfigView = {
},
};
export async function init() {
module.exports.init = async () => {
await CloudCmd.EditFile();
}
};
export async function show() {
module.exports.show = () => {
Events.addKey(listener);
const editFile = await CloudCmd.EditFile.show(ConfigView);
editFile
CloudCmd.EditFile
.show(ConfigView)
.getEditor()
.setKeyMap('vim');
}
};
export function hide() {
module.exports.hide = hide;
function hide() {
CloudCmd.Edit.hide();
}
function listener(event) {
const {keyCode, shiftKey} = event;
const {
keyCode,
shiftKey,
} = event;
if (shiftKey && keyCode === Key.ESC) {
event.preventDefault();
hide();
}
}

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