diff --git a/.webpack/css.js b/.webpack/css.js
index 773a3da2e0..e1d6552db5 100644
--- a/.webpack/css.js
+++ b/.webpack/css.js
@@ -1,5 +1,6 @@
'use strict';
+const {env} = require('node:process');
const fs = require('node:fs');
const {
basename,
@@ -10,7 +11,6 @@ const {
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
-const {env} = require('node:process');
const isDev = env.NODE_ENV === 'development';
const extractCSS = (a) => new ExtractTextPlugin(`${a}.css`);
@@ -23,6 +23,7 @@ const cssNames = [
'terminal',
'user-menu',
...getCSSList('columns'),
+ ...getCSSList('themes'),
];
const cssPlugins = cssNames.map(extractCSS);
@@ -36,7 +37,7 @@ const plugins = clean([
const rules = [{
test: /\.css$/,
- exclude: /css\/(nojs|view|config|terminal|user-menu|columns.*)\.css/,
+ exclude: /css\/(nojs|view|config|terminal|user-menu|columns.*|themes.*)\.css/,
use: extractMain.extract(['css-loader']),
}, ...cssPlugins.map(extract), {
test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/,
diff --git a/.webpack/html.js b/.webpack/html.js
index 2bb2b98cb5..3717bcc914 100644
--- a/.webpack/html.js
+++ b/.webpack/html.js
@@ -1,7 +1,7 @@
'use strict';
-const HtmlWebpackPlugin = require('html-webpack-plugin');
const {env} = require('node:process');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
const isDev = env.NODE_ENV === 'development';
diff --git a/HELP.md b/HELP.md
index b9120dc524..49ffb0eac8 100644
--- a/HELP.md
+++ b/HELP.md
@@ -93,7 +93,7 @@ Cloud Commander supports the following command-line parameters:
| `--terminal-command` | set command to run in terminal (shell by default)
| `--terminal-auto-restart` | restart command on exit
| `--vim` | enable vim hot keys
-| `--columns` | set visible columns
+| `--themes` | set visible themes
| `--export` | enable export of config through a server
| `--export-token` | authorization token used by export server
| `--import` | enable import of config
@@ -122,7 +122,7 @@ Cloud Commander supports the following command-line parameters:
| `--no-terminal-command` | set default shell to run in terminal
| `--no-terminal-auto-restart` | do not restart command on exit
| `--no-vim` | disable vim hot keys
-| `--no-columns` | set default visible columns
+| `--no-themes` | set default visible themes
| `--no-export` | disable export config through a server
| `--no-import` | disable import of config
| `--no-import-listen` | disable listen on config updates from import server
@@ -408,7 +408,7 @@ Here's a description of all options:
"terminalCommand": "", // set command to run in terminal
"terminalAutoRestart": true, // restart command on exit
"vim": false, // disable vim hot keys
- "columns": "name-size-date-owner-mode", // set visible columns
+ "themes": "name-size-date-owner-mode", // set visible themes
"export": false, // enable export of config through a server
"exportToken": "root", // token used by export server
"import": false, // enable import of config
@@ -428,7 +428,7 @@ Some config options can be overridden with environment variables, such as:
- `CLOUDCMD_NAME` - set tab name in web browser
- `CLOUDCMD_OPEN` - open web browser when server started
- `CLOUDCMD_EDITOR` - set editor
-- `CLOUDCMD_COLUMNS` - set visible columns
+- `CLOUDCMD_COLUMNS` - set visible themes
- `CLOUDCMD_CONTACT` - enable contact
- `CLOUDCMD_CONFIG_DIALOG` - enable config dialog
- `CLOUDCMD_CONFIG_AUTH` - enable auth change in config dialog
diff --git a/bin/cloudcmd.mjs b/bin/cloudcmd.mjs
index 4bd8861061..5dade2b638 100755
--- a/bin/cloudcmd.mjs
+++ b/bin/cloudcmd.mjs
@@ -1,15 +1,17 @@
#!/usr/bin/env node
+import process from 'node:process';
import {createRequire} from 'node:module';
import {promisify} from 'node:util';
import tryToCatch from 'try-to-catch';
import {createSimport} from 'simport';
import parse from 'yargs-parser';
-import process from 'node:process';
+
import exit from '../server/exit.js';
import {createConfig, configPath} from '../server/config.js';
import env from '../server/env.js';
import prefixer from '../server/prefixer.js';
+import * as validate from '../server/validate.mjs';
process.on('unhandledRejection', exit);
@@ -61,6 +63,7 @@ const yargsOptions = {
'terminal-path',
'terminal-command',
'columns',
+ 'theme',
'import-url',
'import-token',
'export-token',
@@ -112,6 +115,7 @@ const yargsOptions = {
'contact': choose(env.bool('contact'), config('contact')),
'terminal': choose(env.bool('terminal'), config('terminal')),
'columns': env('columns') || config('columns') || '',
+ 'theme': env('theme') || config('theme') || '',
'vim': choose(env.bool('vim'), config('vim')),
'log': config('log'),
@@ -173,6 +177,9 @@ async function main() {
if (args.repl)
repl();
+
+ validate.columns(args.columns);
+ validate.theme(args.theme);
port(args.port);
@@ -194,6 +201,7 @@ async function main() {
config('prefixSocket', prefixer(args.prefixSocket));
config('root', args.root || '/');
config('vim', args.vim);
+ config('theme', args.theme);
config('columns', args.columns);
config('log', args.log);
config('confirmCopy', args.confirmCopy);
@@ -221,6 +229,7 @@ async function main() {
prefix: config('prefix'),
prefixSocket: config('prefixSocket'),
columns: config('columns'),
+ theme: config('theme'),
};
const password = env('password') || args.password;
@@ -246,7 +255,6 @@ async function main() {
}
async function validateRoot(root, config) {
- const validate = await simport(`${DIR_SERVER}validate.js`);
validate.root(root, config);
if (root === '/')
diff --git a/client/cloudcmd.js b/client/cloudcmd.js
index 59ebfe4ecf..937f6d77b9 100644
--- a/client/cloudcmd.js
+++ b/client/cloudcmd.js
@@ -1,10 +1,7 @@
'use strict';
const process = require('node:process');
-require('../css/main.css');
-require('../css/nojs.css');
-require('../css/columns/name-size-date.css');
-require('../css/columns/name-size.css');
+require('./css');
const wraptile = require('wraptile');
const load = require('load.js');
diff --git a/client/css.js b/client/css.js
new file mode 100644
index 0000000000..e404bf940b
--- /dev/null
+++ b/client/css.js
@@ -0,0 +1,8 @@
+'use strict';
+
+require('../css/main.css');
+require('../css/nojs.css');
+require('../css/columns/name-size-date.css');
+require('../css/columns/name-size.css');
+require('../css/themes/light.css');
+require('../css/themes/dark.css');
diff --git a/client/modules/config/index.js b/client/modules/config/index.js
index 76fed60b7e..cdd18da122 100644
--- a/client/modules/config/index.js
+++ b/client/modules/config/index.js
@@ -137,6 +137,7 @@ async function fillTemplate() {
editor,
packer,
columns,
+ theme,
configAuth,
...obj
} = input.convert(config);
@@ -144,6 +145,7 @@ async function fillTemplate() {
obj[`${editor}-selected`] = 'selected';
obj[`${packer}-selected`] = 'selected';
obj[`${columns}-selected`] = 'selected';
+ obj[`${theme}-selected`] = 'selected';
obj.configAuth = configAuth ? '' : 'hidden';
const innerHTML = rendy(Template, obj);
diff --git a/css/main.css b/css/main.css
index e742316ea0..b30c0ed3c3 100644
--- a/css/main.css
+++ b/css/main.css
@@ -1,5 +1,3 @@
-/* @import url(./themes/dark.css); */
-@import url(./themes/light.css);
@import url(./urls.css);
@import url(./reset.css);
@import url(./style.css);
diff --git a/css/style.css b/css/style.css
index 1aa3a55a0d..5a1f04b71f 100644
--- a/css/style.css
+++ b/css/style.css
@@ -41,6 +41,7 @@ code {
-ms-user-select: initial;
-o-user-select: initial;
user-select: text;
+ color: var(--column-color);
}
.panel,
diff --git a/css/vars.css b/css/vars.css
deleted file mode 100644
index ee64b015f9..0000000000
--- a/css/vars.css
+++ /dev/null
@@ -1,4 +0,0 @@
-:root {
- --text-color: rgb(49 123 249);
- --border-color: rgb(49 123 249 / 40%);
-}
diff --git a/html/index.html b/html/index.html
index 00ae977e2b..51fa889315 100644
--- a/html/index.html
+++ b/html/index.html
@@ -15,6 +15,9 @@
+
{{ fm }}
diff --git a/json/config.json b/json/config.json
index 7387e15881..fd66580fcf 100644
--- a/json/config.json
+++ b/json/config.json
@@ -34,6 +34,7 @@
"showFileName": false,
"vim": false,
"columns": "name-size-date-owner-mode",
+ "theme": "light",
"export": false,
"exportToken": "root",
"import": false,
diff --git a/json/help.json b/json/help.json
index 8efa9e60ac..056713ff1c 100644
--- a/json/help.json
+++ b/json/help.json
@@ -32,6 +32,7 @@
"--terminal-auto-restart ": "restart command on exit",
"--vim ": "enable vim hot keys",
"--columns ": "set visible columns",
+ "--theme ": "set theme 'light' or 'dark'",
"--export ": "enable export of config through a server",
"--export-token ": "authorization token used by export server",
"--import ": "enable import of config",
diff --git a/man/cloudcmd.1 b/man/cloudcmd.1
index 781e01e1bd..9ce86c601f 100644
--- a/man/cloudcmd.1
+++ b/man/cloudcmd.1
@@ -55,6 +55,7 @@ programs in browser from any computer, mobile or tablet device.
--terminal-auto-restart restart command on exit
--vim enable vim hot keys
--columns set visible columns
+ --theme set theme 'light' or 'dark'
--export enable export of config through a server
--export-token authorization token used by export server
--import enable import of config
diff --git a/server/cloudcmd.mjs b/server/cloudcmd.mjs
index 76f9e93735..5c2c9eefe0 100644
--- a/server/cloudcmd.mjs
+++ b/server/cloudcmd.mjs
@@ -21,7 +21,7 @@ import modulas from './modulas.js';
import userMenu from './user-menu.mjs';
import rest from './rest/index.js';
import route from './route.mjs';
-import validate from './validate.js';
+import * as validate from './validate.mjs';
import prefixer from './prefixer.js';
import terminal from './terminal.js';
import distribute from './distribute/index.js';
@@ -64,7 +64,7 @@ function cloudcmd(params) {
if (/root/.test(name))
validate.root(value, config);
- if (/editor|packer|columns/.test(name))
+ if (/editor|packer|themes/.test(name))
validate[name](value);
if (/prefix/.test(name))
diff --git a/server/columns.js b/server/columns.mjs
similarity index 58%
rename from server/columns.js
rename to server/columns.mjs
index 8cd911145e..f1fe7868b6 100644
--- a/server/columns.js
+++ b/server/columns.mjs
@@ -1,13 +1,14 @@
-'use strict';
-
-const fullstore = require('fullstore');
-const process = require('node:process');
-const path = require('node:path');
-const fs = require('node:fs');
-
-const {nanomemoize} = require('nano-memoize');
-const readFilesSync = require('@cloudcmd/read-files-sync');
-
+import path, {dirname} from 'node:path';
+import {fileURLToPath} from 'node:url';
+import process from 'node:process';
+import fs from 'node:fs';
+import fullstore from 'fullstore';
+import nanomemoizeDefault from 'nano-memoize';
+import readFilesSync from '@cloudcmd/read-files-sync';
+
+const {nanomemoize} = nanomemoizeDefault;
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
const isMap = (a) => /\.map$/.test(a);
const not = (fn) => (a) => !fn(a);
@@ -19,9 +20,9 @@ const defaultColumns = {
const _isDev = fullstore(process.env.NODE_ENV === 'development');
const getDist = (isDev) => isDev ? 'dist-dev' : 'dist';
-module.exports.isDev = _isDev;
+export const isDev = _isDev;
-module.exports.getColumns = ({isDev = _isDev()} = {}) => {
+export const getColumns = ({isDev = _isDev()} = {}) => {
const columns = readFilesSyncMemo(isDev);
return {
diff --git a/server/columns.spec.js b/server/columns.spec.mjs
similarity index 72%
rename from server/columns.spec.js
rename to server/columns.spec.mjs
index cdf7a6ab2c..1e62f5f743 100644
--- a/server/columns.spec.js
+++ b/server/columns.spec.mjs
@@ -1,8 +1,11 @@
-'use strict';
+import {dirname} from 'node:path';
+import {fileURLToPath} from 'node:url';
+import test from 'supertape';
+import fs from 'node:fs';
+import {getColumns, isDev} from './columns.mjs';
-const test = require('supertape');
-const fs = require('node:fs');
-const {getColumns, isDev} = require('./columns');
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
test('columns: prod', (t) => {
const columns = getColumns({
diff --git a/server/route.mjs b/server/route.mjs
index 60fd70f10e..5397c27f2c 100644
--- a/server/route.mjs
+++ b/server/route.mjs
@@ -13,8 +13,9 @@ import {contentType} from 'mime-types';
import root from './root.js';
import prefixer from './prefixer.js';
import CloudFunc from '../common/cloudfunc.js';
-import {getColumns} from './columns.js';
import Template from './template.js';
+import {getColumns} from './columns.mjs';
+import {getThemes} from './theme.mjs';
const require = createRequire(import.meta.url);
const {stringify} = JSON;
@@ -164,6 +165,7 @@ function indexProcessing(config, options) {
prefix: getPrefix(config),
config: stringify(config('*')),
columns: getColumns()[config('columns')],
+ themes: getThemes()[config('theme')],
});
return data;
diff --git a/server/theme.mjs b/server/theme.mjs
new file mode 100644
index 0000000000..1102e4e75e
--- /dev/null
+++ b/server/theme.mjs
@@ -0,0 +1,33 @@
+import path, {dirname} from 'node:path';
+import {fileURLToPath} from 'node:url';
+import process from 'node:process';
+import fs from 'node:fs';
+import fullstore from 'fullstore';
+import nanomemoizeDefault from 'nano-memoize';
+import readFilesSync from '@cloudcmd/read-files-sync';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+const isMap = (a) => /\.map$/.test(a);
+const not = (fn) => (a) => !fn(a);
+
+const _isDev = fullstore(process.env.NODE_ENV === 'development');
+const getDist = (isDev) => isDev ? 'dist-dev' : 'dist';
+
+export const isDev = _isDev;
+
+export const getThemes = ({isDev = _isDev()} = {}) => {
+ return readFilesSyncMemo(isDev);
+};
+
+const {nanomemoize} = nanomemoizeDefault;
+
+const readFilesSyncMemo = nanomemoize((isDev) => {
+ const dist = getDist(isDev);
+ const themesDir = path.join(__dirname, '..', dist, 'themes');
+ const names = fs
+ .readdirSync(themesDir)
+ .filter(not(isMap));
+
+ return readFilesSync(themesDir, names, 'utf8');
+});
diff --git a/server/themes.spec.mjs b/server/themes.spec.mjs
new file mode 100644
index 0000000000..9ff2a483fb
--- /dev/null
+++ b/server/themes.spec.mjs
@@ -0,0 +1,31 @@
+import {dirname} from 'node:path';
+import {fileURLToPath} from 'node:url';
+import test from 'supertape';
+import fs from 'node:fs';
+import {getThemes, isDev} from './theme.mjs';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+test('themes: dev', (t) => {
+ const themes = getThemes({
+ isDev: true,
+ });
+
+ const css = fs.readFileSync(`${__dirname}/../css/themes/dark.css`, 'utf8');
+
+ t.equal(themes['dark'], css);
+ t.end();
+});
+
+test('themes: no args', (t) => {
+ const currentIsDev = isDev();
+ isDev(true);
+ const themes = getThemes();
+
+ const css = fs.readFileSync(`${__dirname}/../css/themes/light.css`, 'utf8');
+ isDev(currentIsDev);
+
+ t.equal(themes.light, css);
+ t.end();
+});
diff --git a/server/validate.js b/server/validate.mjs
similarity index 51%
rename from server/validate.js
rename to server/validate.mjs
index 082dbd9094..4c683e3f6d 100644
--- a/server/validate.js
+++ b/server/validate.mjs
@@ -1,13 +1,12 @@
-'use strict';
+import {statSync as _statSync} from 'node:fs';
+import tryCatch from 'try-catch';
+import _exit from './exit.js';
+import {getColumns as _getColumns} from './columns.mjs';
+import {getThemes as _getThemes} from "./theme.mjs";
-const {statSync: _statSync} = require('node:fs');
-const tryCatch = require('try-catch');
-
-const _exit = require('./exit');
-const {getColumns: _getColumns} = require('./columns');
const isString = (a) => typeof a === 'string';
-module.exports.root = (dir, config, {exit = _exit, statSync = _statSync} = {}) => {
+export const root = (dir, config, {exit = _exit, statSync = _statSync} = {}) => {
if (!isString(dir))
throw Error('dir should be a string');
@@ -23,21 +22,21 @@ module.exports.root = (dir, config, {exit = _exit, statSync = _statSync} = {}) =
return exit('cloudcmd --root: %s', error.message);
};
-module.exports.editor = (name, {exit = _exit} = {}) => {
+export const editor = (name, {exit = _exit} = {}) => {
const reg = /^(dword|edward|deepword)$/;
if (!reg.test(name))
exit('cloudcmd --editor: could be "dword", "edward" or "deepword" only');
};
-module.exports.packer = (name, {exit = _exit} = {}) => {
+export const packer = (name, {exit = _exit} = {}) => {
const reg = /^(tar|zip)$/;
if (!reg.test(name))
exit('cloudcmd --packer: could be "tar" or "zip" only');
};
-module.exports.columns = (type, {exit = _exit, getColumns = _getColumns} = {}) => {
+export const columns = (type, {exit = _exit, getColumns = _getColumns} = {}) => {
const addQuotes = (a) => `"${a}"`;
const all = Object
.keys(getColumns())
@@ -51,3 +50,18 @@ module.exports.columns = (type, {exit = _exit, getColumns = _getColumns} = {}) =
if (!all.includes(type))
exit(`cloudcmd --columns: can be only one of: ${names}`);
};
+
+export const theme = (type, {exit = _exit, getThemes = _getThemes} = {}) => {
+ const addQuotes = (a) => `"${a}"`;
+ const all = Object
+ .keys(getThemes())
+ .concat('');
+
+ const names = all
+ .filter(Boolean)
+ .map(addQuotes)
+ .join(', ');
+
+ if (!all.includes(type))
+ exit(`cloudcmd --theme: can be only one of: ${names}`);
+};
diff --git a/server/validate.spec.mjs b/server/validate.spec.mjs
index 50f87f571d..0a238e9325 100644
--- a/server/validate.spec.mjs
+++ b/server/validate.spec.mjs
@@ -1,6 +1,6 @@
import {test, stub} from 'supertape';
import tryCatch from 'try-catch';
-import validate from './validate.js';
+import * as validate from './validate.mjs';
import cloudcmd from './cloudcmd.mjs';
test('validate: root: bad', (t) => {
@@ -102,3 +102,31 @@ test('validate: columns: wrong', (t) => {
t.calledWith(exit, [msg], 'should call exit');
t.end();
});
+test('validate: theme', (t) => {
+ const exit = stub();
+
+ validate.theme('dark', {
+ exit,
+ });
+
+ t.notCalled(exit, 'should not call exit');
+ t.end();
+});
+
+test('validate: theme: wrong', (t) => {
+ const getThemes = stub().returns({
+ 'light': '',
+ 'dark': '',
+ });
+
+ const exit = stub();
+ const msg = 'cloudcmd --theme: can be only one of: "light", "dark"';
+
+ validate.theme('hello', {
+ exit,
+ getThemes,
+ });
+
+ t.calledWith(exit, [msg], 'should call exit');
+ t.end();
+});
diff --git a/tmpl/config.hbs b/tmpl/config.hbs
index 058815e7a5..dfcc4c095d 100644
--- a/tmpl/config.hbs
+++ b/tmpl/config.hbs
@@ -43,8 +43,14 @@
Zip
+
+
+
-