From 9a6cd31469a5816c261896d0eb8b96dd05662ffc Mon Sep 17 00:00:00 2001 From: coderaiser Date: Fri, 29 Mar 2024 13:25:23 +0200 Subject: [PATCH] feature: dark theme: add (#332) --- .webpack/css.js | 5 +-- .webpack/html.js | 2 +- HELP.md | 8 ++--- bin/cloudcmd.mjs | 12 +++++-- client/cloudcmd.js | 5 +-- client/css.js | 8 +++++ client/modules/config/index.js | 2 ++ css/main.css | 2 -- css/style.css | 1 + css/vars.css | 4 --- html/index.html | 3 ++ json/config.json | 1 + json/help.json | 1 + man/cloudcmd.1 | 1 + server/cloudcmd.mjs | 4 +-- server/{columns.js => columns.mjs} | 25 +++++++------- server/{columns.spec.js => columns.spec.mjs} | 11 ++++--- server/route.mjs | 4 ++- server/theme.mjs | 33 +++++++++++++++++++ server/themes.spec.mjs | 31 ++++++++++++++++++ server/{validate.js => validate.mjs} | 34 ++++++++++++++------ server/validate.spec.mjs | 30 ++++++++++++++++- tmpl/config.hbs | 8 ++++- 23 files changed, 185 insertions(+), 50 deletions(-) create mode 100644 client/css.js delete mode 100644 css/vars.css rename server/{columns.js => columns.mjs} (58%) rename server/{columns.spec.js => columns.spec.mjs} (72%) create mode 100644 server/theme.mjs create mode 100644 server/themes.spec.mjs rename server/{validate.js => validate.mjs} (51%) 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 +
  • + +
  • -