From bafa44845a9d0963025fe0c4223fbc2114e56966 Mon Sep 17 00:00:00 2001 From: Labhansh Agrawal Date: Mon, 26 Dec 2022 10:31:04 +0530 Subject: [PATCH] Migrate config to json --- app/config/config-default.json | 69 ++++++ app/config/import.ts | 94 +++----- app/config/init.ts | 17 +- app/config/paths.ts | 30 ++- app/config/schema.json | 427 +++++++++++++++++++++++++++++++++ app/ui/window.ts | 3 +- cli/api.ts | 68 ++---- lib/config.d.ts | 148 +++++++++++- lib/hyper.d.ts | 2 +- 9 files changed, 725 insertions(+), 133 deletions(-) create mode 100644 app/config/config-default.json create mode 100644 app/config/schema.json diff --git a/app/config/config-default.json b/app/config/config-default.json new file mode 100644 index 00000000..39c4e9fc --- /dev/null +++ b/app/config/config-default.json @@ -0,0 +1,69 @@ +{ + "$schema": "./schema.json", + "config": { + "updateChannel": "stable", + "fontSize": 12, + "fontFamily": "Menlo, \"DejaVu Sans Mono\", Consolas, \"Lucida Console\", monospace", + "fontWeight": "normal", + "fontWeightBold": "bold", + "lineHeight": 1, + "letterSpacing": 0, + "scrollback": 1000, + "cursorColor": "rgba(248,28,229,0.8)", + "cursorAccentColor": "#000", + "cursorShape": "BLOCK", + "cursorBlink": false, + "foregroundColor": "#fff", + "backgroundColor": "#000", + "selectionColor": "rgba(248,28,229,0.3)", + "borderColor": "#333", + "css": "", + "termCSS": "", + "workingDirectory": "", + "showHamburgerMenu": "", + "showWindowControls": "", + "padding": "12px 14px", + "colors": { + "black": "#000000", + "red": "#C51E14", + "green": "#1DC121", + "yellow": "#C7C329", + "blue": "#0A2FC4", + "magenta": "#C839C5", + "cyan": "#20C5C6", + "white": "#C7C7C7", + "lightBlack": "#686868", + "lightRed": "#FD6F6B", + "lightGreen": "#67F86F", + "lightYellow": "#FFFA72", + "lightBlue": "#6A76FB", + "lightMagenta": "#FD7CFC", + "lightCyan": "#68FDFE", + "lightWhite": "#FFFFFF", + "limeGreen": "#32CD32", + "lightCoral": "#F08080" + }, + "shell": "", + "shellArgs": [ + "--login" + ], + "env": {}, + "bell": "SOUND", + "bellSound": null, + "bellSoundURL": null, + "copyOnSelect": false, + "defaultSSHApp": true, + "quickEdit": false, + "macOptionSelectionMode": "vertical", + "webGLRenderer": false, + "webLinksActivationKey": "", + "disableLigatures": true, + "disableAutoUpdates": false, + "autoUpdatePlugins": true, + "preserveCWD": true, + "screenReaderMode": false + }, + "plugins": [], + "localPlugins": [], + "keymaps": {} +} diff --git a/app/config/import.ts b/app/config/import.ts index 613f57ff..11810033 100644 --- a/app/config/import.ts +++ b/app/config/import.ts @@ -1,9 +1,20 @@ -import {moveSync, copySync, existsSync, writeFileSync, readFileSync, lstatSync} from 'fs-extra'; +import {copySync, existsSync, writeFileSync, readFileSync, copy} from 'fs-extra'; import {sync as mkdirpSync} from 'mkdirp'; -import {defaultCfg, cfgPath, legacyCfgPath, plugs, defaultPlatformKeyPath} from './paths'; +import { + defaultCfg, + cfgPath, + legacyCfgPath, + plugs, + defaultPlatformKeyPath, + schemaPath, + cfgDir, + schemaFile +} from './paths'; import {_init, _extractDefault} from './init'; import notify from '../notify'; import {rawConfig} from '../../lib/config'; +import _ from 'lodash'; +import {resolve} from 'path'; let defaultConfig: rawConfig; @@ -18,62 +29,33 @@ const _write = (path: string, data: string) => { writeFileSync(path, format, 'utf8'); }; -// Saves a file as backup by appending '.backup' or '.backup2', '.backup3', etc. -// so as to not override any existing files -const saveAsBackup = (src: string) => { - let attempt = 1; - while (attempt < 100) { - const backupPath = `${src}.backup${attempt === 1 ? '' : attempt}`; - if (!existsSync(backupPath)) { - moveSync(src, backupPath); - return backupPath; - } - attempt++; - } - throw new Error('Failed to create backup for config file. Too many backups'); -}; - -// Migrate Hyper2 config to Hyper3 but only if the user hasn't manually +// Migrate Hyper3 config to Hyper4 but only if the user hasn't manually // touched the new config and if the old config is not a symlink -const migrateHyper2Config = () => { - if (cfgPath === legacyCfgPath) { - // No need to migrate - return; - } - if (!existsSync(legacyCfgPath)) { - // Already migrated or user never used Hyper 2 - return; - } - const existsNew = existsSync(cfgPath); - if (lstatSync(legacyCfgPath).isSymbolicLink() || (existsNew && lstatSync(cfgPath).isSymbolicLink())) { - // One of the files is a symlink, there could be a number of complications - // in this case so let's avoid those and not do automatic migration +const migrateHyper3Config = () => { + copy(schemaPath, resolve(cfgDir, schemaFile), (err) => { + if (err) { + console.error(err); + } + }); + + if (existsSync(cfgPath)) { return; } - if (existsNew) { - const cfg1 = readFileSync(defaultCfg, 'utf8').replace(/\r|\n/g, ''); - const cfg2 = readFileSync(cfgPath, 'utf8').replace(/\r|\n/g, ''); - const hasNewConfigBeenTouched = cfg1 !== cfg2; - if (hasNewConfigBeenTouched) { - // Assume the user has migrated manually but rename old config to .backup so - // we don't keep trying to migrate on every launch - const backupPath = saveAsBackup(legacyCfgPath); - notify( - 'Hyper 3', - `Settings location has changed to ${cfgPath}.\nWe've backed up your old Hyper config to ${backupPath}` - ); - return; - } + if (!existsSync(legacyCfgPath)) { + copySync(defaultCfg, cfgPath); + return; } // Migrate - copySync(legacyCfgPath, cfgPath); - saveAsBackup(legacyCfgPath); + const defaultCfgData = JSON.parse(readFileSync(defaultCfg, 'utf8')); + const legacyCfgData = _extractDefault(readFileSync(legacyCfgPath, 'utf8')); + const newCfgData = _.merge(defaultCfgData, legacyCfgData); + _write(cfgPath, JSON.stringify(newCfgData, null, 2)); notify( - 'Hyper 3', - `Settings location has changed to ${cfgPath}.\nWe've automatically migrated your existing config!\nPlease restart Hyper now` + 'Hyper 4', + `Settings location and format has changed.\nWe've automatically migrated your existing config to ${cfgPath}` ); }; @@ -83,18 +65,18 @@ const _importConf = () => { mkdirpSync(plugs.local); try { - migrateHyper2Config(); + migrateHyper3Config(); } catch (err) { console.error(err); } - let defaultCfgRaw = ''; + let defaultCfgRaw = '{}'; try { defaultCfgRaw = readFileSync(defaultCfg, 'utf8'); } catch (err) { console.log(err); } - const _defaultCfg = _extractDefault(defaultCfgRaw) as rawConfig; + const _defaultCfg = JSON.parse(defaultCfgRaw) as rawConfig; // Importing platform specific keymap let content = '{}'; @@ -107,12 +89,12 @@ const _importConf = () => { _defaultCfg.keymaps = mapping; // Import user config - let userCfg: string; + let userCfg: rawConfig; try { - userCfg = readFileSync(cfgPath, 'utf8'); + userCfg = JSON.parse(readFileSync(cfgPath, 'utf8')); } catch (err) { _write(cfgPath, defaultCfgRaw); - userCfg = defaultCfgRaw; + userCfg = JSON.parse(defaultCfgRaw); } return {userCfg, defaultCfg: _defaultCfg}; @@ -121,7 +103,7 @@ const _importConf = () => { export const _import = () => { const imported = _importConf(); defaultConfig = imported.defaultCfg; - const result = _init(imported); + const result = _init(imported.userCfg, imported.defaultCfg); return result; }; diff --git a/app/config/init.ts b/app/config/init.ts index b9ac181a..cf510be0 100644 --- a/app/config/init.ts +++ b/app/config/init.ts @@ -2,6 +2,7 @@ import vm from 'vm'; import notify from '../notify'; import mapKeys from '../utils/map-keys'; import {parsedConfig, rawConfig, configOptions} from '../../lib/config'; +import _ from 'lodash'; const _extract = (script?: vm.Script): Record => { const module: Record = {}; @@ -27,23 +28,21 @@ const _extractDefault = (cfg: string) => { }; // init config -const _init = (cfg: {userCfg: string; defaultCfg: rawConfig}): parsedConfig => { - const script = _syntaxValidation(cfg.userCfg); - const _cfg = script && (_extract(script) as rawConfig); +const _init = (userCfg: rawConfig, defaultCfg: rawConfig): parsedConfig => { return { config: (() => { - if (_cfg?.config) { - return _cfg.config; + if (userCfg?.config) { + return _.merge(defaultCfg.config, userCfg.config); } else { notify('Error reading configuration: `config` key is missing'); - return cfg.defaultCfg.config || ({} as configOptions); + return defaultCfg.config || ({} as configOptions); } })(), // Merging platform specific keymaps with user defined keymaps - keymaps: mapKeys({...cfg.defaultCfg.keymaps, ..._cfg?.keymaps}), + keymaps: mapKeys({...defaultCfg.keymaps, ...userCfg?.keymaps}), // Ignore undefined values in plugin and localPlugins array Issue #1862 - plugins: (_cfg?.plugins && _cfg.plugins.filter(Boolean)) || [], - localPlugins: (_cfg?.localPlugins && _cfg.localPlugins.filter(Boolean)) || [] + plugins: (userCfg?.plugins && userCfg.plugins.filter(Boolean)) || [], + localPlugins: (userCfg?.localPlugins && userCfg.localPlugins.filter(Boolean)) || [] }; }; diff --git a/app/config/paths.ts b/app/config/paths.ts index 0b794542..a0b6fa52 100644 --- a/app/config/paths.ts +++ b/app/config/paths.ts @@ -5,22 +5,30 @@ import {statSync} from 'fs'; import {resolve, join} from 'path'; import isDev from 'electron-is-dev'; -const cfgFile = '.hyper.js'; -const defaultCfgFile = 'config-default.js'; +const cfgFile = 'hyper.json'; +const defaultCfgFile = 'config-default.json'; +const schemaFile = 'schema.json'; const homeDirectory = homedir(); // If the user defines XDG_CONFIG_HOME they definitely want their config there, // otherwise use the home directory in linux/mac and userdata in windows -const applicationDirectory = +let cfgDir = process.env.XDG_CONFIG_HOME + ? join(process.env.XDG_CONFIG_HOME, 'Hyper') + : process.platform === 'win32' + ? app.getPath('userData') + : join(homeDirectory, '.config', 'Hyper'); + +const legacyCfgPath = join( process.env.XDG_CONFIG_HOME !== undefined ? join(process.env.XDG_CONFIG_HOME, 'hyper') : process.platform == 'win32' ? app.getPath('userData') - : homedir(); + : homedir(), + '.hyper.js' +); -let cfgDir = applicationDirectory; -let cfgPath = join(applicationDirectory, cfgFile); -const legacyCfgPath = join(homeDirectory, cfgFile); // Hyper 2 config location +let cfgPath = join(cfgDir, cfgFile); +const schemaPath = resolve(__dirname, schemaFile); const devDir = resolve(__dirname, '../..'); const devCfg = join(devDir, cfgFile); @@ -38,10 +46,8 @@ if (isDev) { } } -const plugins = resolve(cfgDir, '.hyper_plugins'); +const plugins = resolve(cfgDir, 'plugins'); const plugs = { - legacyBase: resolve(homeDirectory, '.hyper_plugins'), - legacyLocal: resolve(homeDirectory, '.hyper_plugins', 'local'), base: plugins, local: resolve(plugins, 'local'), cache: resolve(plugins, 'cache') @@ -82,5 +88,7 @@ export { yarn, cliScriptPath, cliLinkPath, - homeDirectory + homeDirectory, + schemaFile, + schemaPath }; diff --git a/app/config/schema.json b/app/config/schema.json new file mode 100644 index 00000000..34820aad --- /dev/null +++ b/app/config/schema.json @@ -0,0 +1,427 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "config": { + "properties": { + "autoUpdatePlugins": { + "description": "if `true` (default), Hyper will update plugins every 5 hours\nyou can also set it to a custom time e.g. `1d` or `2h`", + "type": [ + "string", + "boolean" + ] + }, + "backgroundColor": { + "description": "terminal background color\n\nopacity is only supported on macOS", + "type": "string" + }, + "bell": { + "description": "Supported Options:\n1. 'SOUND' -> Enables the bell as a sound\n2. false: turns off the bell", + "type": "string" + }, + "bellSound": { + "description": "base64 encoded string of the sound file to use for the bell\nif null, the default bell will be used", + "type": [ + "string", + "null" + ] + }, + "bellSoundURL": { + "description": "An absolute file path to a sound file on the machine.", + "type": [ + "string", + "null" + ] + }, + "borderColor": { + "description": "border color (window, tabs)", + "type": "string" + }, + "colors": { + "description": "the full list. if you're going to provide the full color palette,\nincluding the 6 x 6 color cubes and the grayscale map, just provide\nan array here instead of a color map object", + "properties": { + "black": { + "type": "string" + }, + "blue": { + "type": "string" + }, + "cyan": { + "type": "string" + }, + "green": { + "type": "string" + }, + "lightBlack": { + "type": "string" + }, + "lightBlue": { + "type": "string" + }, + "lightCyan": { + "type": "string" + }, + "lightGreen": { + "type": "string" + }, + "lightMagenta": { + "type": "string" + }, + "lightRed": { + "type": "string" + }, + "lightWhite": { + "type": "string" + }, + "lightYellow": { + "type": "string" + }, + "magenta": { + "type": "string" + }, + "red": { + "type": "string" + }, + "white": { + "type": "string" + }, + "yellow": { + "type": "string" + } + }, + "required": [ + "black", + "blue", + "cyan", + "green", + "lightBlack", + "lightBlue", + "lightCyan", + "lightGreen", + "lightMagenta", + "lightRed", + "lightWhite", + "lightYellow", + "magenta", + "red", + "white", + "yellow" + ], + "type": "object" + }, + "copyOnSelect": { + "description": "if `true` selected text will automatically be copied to the clipboard", + "type": "boolean" + }, + "css": { + "description": "custom CSS to embed in the main window", + "type": "string" + }, + "cursorAccentColor": { + "description": "terminal text color under BLOCK cursor", + "type": "string" + }, + "cursorBlink": { + "description": "set to `true` for blinking cursor", + "type": "boolean" + }, + "cursorColor": { + "description": "terminal cursor background color and opacity (hex, rgb, hsl, hsv, hwb or cmyk)", + "type": "string" + }, + "cursorShape": { + "description": "`'BEAM'` for |, `'UNDERLINE'` for _, `'BLOCK'` for █", + "enum": [ + "BEAM", + "BLOCK", + "UNDERLINE" + ], + "type": "string" + }, + "defaultSSHApp": { + "description": "if `true` hyper will be set as the default protocol client for SSH", + "type": "boolean" + }, + "disableAutoUpdates": { + "description": "if `true` hyper will not check for updates", + "type": "boolean" + }, + "disableLigatures": { + "description": "if `false` Hyper will use ligatures provided by some fonts", + "type": "boolean" + }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "for environment variables", + "type": "object" + }, + "fontFamily": { + "description": "font family with optional fallbacks", + "type": "string" + }, + "fontSize": { + "description": "default font size in pixels for all tabs", + "type": "number" + }, + "fontWeight": { + "anyOf": [ + { + "enum": [ + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "900", + "bold", + "normal" + ], + "type": "string" + }, + { + "type": "number" + } + ], + "description": "default font weight eg:'normal', '400', 'bold'" + }, + "fontWeightBold": { + "anyOf": [ + { + "enum": [ + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "900", + "bold", + "normal" + ], + "type": "string" + }, + { + "type": "number" + } + ], + "description": "font weight for bold characters eg:'normal', '600', 'bold'" + }, + "foregroundColor": { + "description": "color of the text", + "type": "string" + }, + "letterSpacing": { + "description": "letter spacing as a relative unit", + "type": "number" + }, + "lineHeight": { + "description": "line height as a relative unit", + "type": "number" + }, + "macOptionSelectionMode": { + "description": "choose either `'vertical'`, if you want the column mode when Option key is hold during selection (Default)\nor `'force'`, if you want to force selection regardless of whether the terminal is in mouse events mode\n(inside tmux or vim with mouse mode enabled for example).", + "type": "string" + }, + "modifierKeys": { + "properties": { + "altIsMeta": { + "type": "boolean" + }, + "cmdIsMeta": { + "type": "boolean" + } + }, + "required": [ + "altIsMeta", + "cmdIsMeta" + ], + "type": "object" + }, + "padding": { + "description": "custom padding (CSS format, i.e.: `top right bottom left` or `top horizontal bottom` or `vertical horizontal` or `all`)", + "type": "string" + }, + "preserveCWD": { + "description": "set to true to preserve working directory when creating splits or tabs", + "type": "boolean" + }, + "quickEdit": { + "description": "if `true` on right click selected text will be copied or pasted if no\nselection is present (`true` by default on Windows and disables the context menu feature)", + "type": "boolean" + }, + "screenReaderMode": { + "description": "set to true to enable screen reading apps (like NVDA) to read the contents of the terminal", + "type": "boolean" + }, + "scrollback": { + "type": "number" + }, + "selectionColor": { + "description": "terminal selection color", + "type": "string" + }, + "shell": { + "description": "the shell to run when spawning a new session (i.e. /usr/local/bin/fish)\nif left empty, your system's login shell will be used by default\n\nWindows\n- Make sure to use a full path if the binary name doesn't work\n- Remove `--login` in shellArgs\n\nWindows Subsystem for Linux (WSL) - previously Bash on Windows\n- Example: `C:\\\\Windows\\\\System32\\\\wsl.exe`\n\nGit-bash on Windows\n- Example: `C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe`\n\nPowerShell on Windows\n- Example: `C:\\\\WINDOWS\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe`\n\nCygwin\n- Example: `C:\\\\cygwin64\\\\bin\\\\bash.exe`\n\nGit Bash\n- Example: `C:\\\\Program Files\\\\Git\\\\git-cmd.exe`\nThen Add `--command=usr/bin/bash.exe` to shellArgs", + "type": "string" + }, + "shellArgs": { + "description": "for setting shell arguments (i.e. for using interactive shellArgs: `['-i']`)\nby default `['--login']` will be used", + "items": { + "type": "string" + }, + "type": "array" + }, + "showHamburgerMenu": { + "description": "if you're using a Linux setup which show native menus, set to false\n\ndefault: `true` on Linux, `true` on Windows, ignored on macOS", + "enum": [ + "", + false, + true + ] + }, + "showWindowControls": { + "description": "set to `false` if you want to hide the minimize, maximize and close buttons\n\nadditionally, set to `'left'` if you want them on the left, like in Ubuntu\n\ndefault: `true` on Windows and Linux, ignored on macOS", + "enum": [ + "", + false, + "left", + true + ] + }, + "termCSS": { + "description": "custom CSS to embed in the terminal window", + "type": "string" + }, + "uiFontFamily": { + "type": "string" + }, + "updateChannel": { + "description": "choose either `'stable'` for receiving highly polished, or `'canary'` for less polished but more frequent updates", + "enum": [ + "canary", + "stable" + ], + "type": "string" + }, + "useConpty": { + "type": "boolean" + }, + "webGLRenderer": { + "description": "Whether to use the WebGL renderer. Set it to false to use canvas-based\nrendering (slower, but supports transparent backgrounds)", + "type": "boolean" + }, + "webLinksActivationKey": { + "description": "keypress required for weblink activation: [ctrl | alt | meta | shift]", + "enum": [ + "", + "alt", + "ctrl", + "meta", + "shift" + ], + "type": "string" + }, + "windowSize": { + "description": "Initial window size in pixels", + "items": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "maxItems": 2, + "minItems": 2, + "type": "array" + }, + "workingDirectory": { + "description": "set custom startup directory (must be an absolute path)", + "type": "string" + } + }, + "required": [ + "autoUpdatePlugins", + "backgroundColor", + "bell", + "bellSound", + "bellSoundURL", + "borderColor", + "colors", + "copyOnSelect", + "css", + "cursorAccentColor", + "cursorBlink", + "cursorColor", + "cursorShape", + "defaultSSHApp", + "disableAutoUpdates", + "disableLigatures", + "env", + "fontFamily", + "fontSize", + "fontWeight", + "fontWeightBold", + "foregroundColor", + "letterSpacing", + "lineHeight", + "macOptionSelectionMode", + "padding", + "preserveCWD", + "quickEdit", + "screenReaderMode", + "scrollback", + "selectionColor", + "shell", + "shellArgs", + "showHamburgerMenu", + "showWindowControls", + "termCSS", + "updateChannel", + "webGLRenderer", + "webLinksActivationKey", + "workingDirectory" + ], + "type": "object" + }, + "keymaps": { + "additionalProperties": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "description": "Example\n'window:devtools': 'cmd+alt+o',", + "type": "object" + }, + "localPlugins": { + "description": "in development, you can create a directory under\n`plugins/local/` and include it here\nto load it and avoid it being `npm install`ed", + "items": { + "type": "string" + }, + "type": "array" + }, + "plugins": { + "description": "a list of plugins to fetch and install from npm\nformat: [@org/]project[#version]\nexamples:\n `hyperpower`\n `@company/project`\n `project#1.0.1`", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" +} + diff --git a/app/ui/window.ts b/app/ui/window.ts index c8b61949..ee1a719e 100644 --- a/app/ui/window.ts +++ b/app/ui/window.ts @@ -17,6 +17,7 @@ import {decorateSessionOptions, decorateSessionClass} from '../plugins'; import {enable as remoteEnable} from '@electron/remote/main'; import {configOptions} from '../../lib/config'; import {getWorkingDirectoryFromPID} from 'native-process-working-directory'; +import {existsSync} from 'fs'; export function newWindow( options_: BrowserWindowConstructorOptions, @@ -138,7 +139,7 @@ export function newWindow( } catch (error) { console.error(error); } - cwd = cwd && isAbsolute(cwd) ? cwd : ''; + cwd = cwd && isAbsolute(cwd) && existsSync(cwd) ? cwd : ''; } // remove the rows and cols, the wrong value of them will break layout when init create diff --git a/cli/api.ts b/cli/api.ts index bac5d172..cac531a3 100644 --- a/cli/api.ts +++ b/cli/api.ts @@ -5,25 +5,22 @@ import os from 'os'; import got from 'got'; import registryUrlModule from 'registry-url'; const registryUrl = registryUrlModule(); -import pify from 'pify'; -import * as recast from 'recast'; import path from 'path'; // If the user defines XDG_CONFIG_HOME they definitely want their config there, // otherwise use the home directory in linux/mac and userdata in windows -const applicationDirectory = - process.env.XDG_CONFIG_HOME !== undefined - ? path.join(process.env.XDG_CONFIG_HOME, 'hyper') - : process.platform == 'win32' - ? path.join(process.env.APPDATA!, 'Hyper') - : os.homedir(); +const applicationDirectory = process.env.XDG_CONFIG_HOME + ? path.join(process.env.XDG_CONFIG_HOME, 'Hyper') + : process.platform === 'win32' + ? path.join(process.env.APPDATA!, 'Hyper') + : path.join(os.homedir(), '.config', 'Hyper'); -const devConfigFileName = path.join(__dirname, `../.hyper.js`); +const devConfigFileName = path.join(__dirname, `../hyper.json`); const fileName = process.env.NODE_ENV !== 'production' && fs.existsSync(devConfigFileName) ? devConfigFileName - : path.join(applicationDirectory, '.hyper.js'); + : path.join(applicationDirectory, 'hyper.json'); /** * We need to make sure the file reading and parsing is lazy so that failure to @@ -43,34 +40,12 @@ function memoize any>(fn: T): T { } const getFileContents = memoize(() => { - try { - return fs.readFileSync(fileName, 'utf8'); - } catch (_err) { - const err = _err as {code: string}; - if (err.code !== 'ENOENT') { - // ENOENT === !exists() - throw err; - } - } - return null; + return fs.readFileSync(fileName, 'utf8'); }); -const getParsedFile = memoize(() => recast.parse(getFileContents()!)); +const getParsedFile = memoize(() => JSON.parse(getFileContents())); -const getProperties = memoize( - (): any[] => - ((getParsedFile()?.program?.body as any[]) || []).find( - (bodyItem) => - bodyItem.type === 'ExpressionStatement' && - bodyItem.expression.type === 'AssignmentExpression' && - bodyItem.expression.left.object.name === 'module' && - bodyItem.expression.left.property.name === 'exports' && - bodyItem.expression.right.type === 'ObjectExpression' - )?.expression?.right?.properties || [] -); - -const getPluginsByKey = (key: string): any[] => - getProperties().find((property) => property?.key?.name === key)?.value?.elements || []; +const getPluginsByKey = (key: string): any[] => getParsedFile()[key] || []; const getPlugins = memoize(() => { return getPluginsByKey('plugins'); @@ -87,13 +62,13 @@ function exists() { function isInstalled(plugin: string, locally?: boolean) { const array = locally ? getLocalPlugins() : getPlugins(); if (array && Array.isArray(array)) { - return array.some((entry) => entry.value === plugin); + return array.includes(plugin); } return false; } -function save() { - return pify(fs.writeFile)(fileName, recast.print(getParsedFile()).code, 'utf8'); +function save(config: any) { + return fs.writeFileSync(fileName, JSON.stringify(config, null, 2), 'utf8'); } function getPackageName(plugin: string) { @@ -135,26 +110,25 @@ function install(plugin: string, locally?: boolean) { return Promise.reject(`${plugin} is already installed`); } - array.push(recast.types.builders.literal(plugin)); - return save(); + const config = getParsedFile(); + config[locally ? 'localPlugins' : 'plugins'] = [...array, plugin]; + save(config); }); } -function uninstall(plugin: string) { +async function uninstall(plugin: string) { if (!isInstalled(plugin)) { return Promise.reject(`${plugin} is not installed`); } - const index = getPlugins().findIndex((entry) => entry.value === plugin); - getPlugins().splice(index, 1); - return save(); + const config = getParsedFile(); + config.plugins = getPlugins().filter((p) => p !== plugin); + save(config); } function list() { if (getPlugins().length > 0) { - return getPlugins() - .map((plugin) => plugin.value) - .join('\n'); + return getPlugins().join('\n'); } return false; } diff --git a/lib/config.d.ts b/lib/config.d.ts index 7d12c193..0ab031cf 100644 --- a/lib/config.d.ts +++ b/lib/config.d.ts @@ -20,60 +20,192 @@ export type ColorMap = { }; export type configOptions = { + /** + * if `true` (default), Hyper will update plugins every 5 hours + * you can also set it to a custom time e.g. `1d` or `2h` + */ autoUpdatePlugins: boolean | string; + /** + * terminal background color + * + * opacity is only supported on macOS + */ backgroundColor: string; + /** + * Supported Options: + * 1. 'SOUND' -> Enables the bell as a sound + * 2. false: turns off the bell + */ bell: string; + /** + * base64 encoded string of the sound file to use for the bell + * if null, the default bell will be used + * @nullable + */ bellSound: string | null; + /** + * An absolute file path to a sound file on the machine. + * @nullable + */ bellSoundURL: string | null; + /** border color (window, tabs) */ borderColor: string; + /** + * the full list. if you're going to provide the full color palette, + * including the 6 x 6 color cubes and the grayscale map, just provide + * an array here instead of a color map object + */ colors: ColorMap; + /** if `true` selected text will automatically be copied to the clipboard */ copyOnSelect: boolean; + /** custom CSS to embed in the main window */ css: string; + /** terminal text color under BLOCK cursor */ cursorAccentColor: string; + /** set to `true` for blinking cursor */ cursorBlink: boolean; + /** terminal cursor background color and opacity (hex, rgb, hsl, hsv, hwb or cmyk) */ cursorColor: string; + /** `'BEAM'` for |, `'UNDERLINE'` for _, `'BLOCK'` for █ */ cursorShape: 'BEAM' | 'UNDERLINE' | 'BLOCK'; + /** if `true` hyper will be set as the default protocol client for SSH */ defaultSSHApp: boolean; + /** if `true` hyper will not check for updates */ disableAutoUpdates: boolean; + /** if `false` Hyper will use ligatures provided by some fonts */ disableLigatures: boolean; - env: Record; + /** for environment variables */ + env: {[k: string]: string}; + /** font family with optional fallbacks */ fontFamily: string; + /** default font size in pixels for all tabs */ fontSize: number; + /** default font weight eg:'normal', '400', 'bold' */ fontWeight: FontWeight; + /** font weight for bold characters eg:'normal', '600', 'bold' */ fontWeightBold: FontWeight; + /** color of the text */ foregroundColor: string; + /** letter spacing as a relative unit */ letterSpacing: number; + /** line height as a relative unit */ lineHeight: number; + /** + * choose either `'vertical'`, if you want the column mode when Option key is hold during selection (Default) + * or `'force'`, if you want to force selection regardless of whether the terminal is in mouse events mode + * (inside tmux or vim with mouse mode enabled for example). + */ macOptionSelectionMode: string; - modifierKeys: { + modifierKeys?: { altIsMeta: boolean; cmdIsMeta: boolean; }; + /** custom padding (CSS format, i.e.: `top right bottom left` or `top horizontal bottom` or `vertical horizontal` or `all`) */ padding: string; + /** + * set to true to preserve working directory when creating splits or tabs + */ preserveCWD: boolean; + /** + * if `true` on right click selected text will be copied or pasted if no + * selection is present (`true` by default on Windows and disables the context menu feature) + */ quickEdit: boolean; + /** + * set to true to enable screen reading apps (like NVDA) to read the contents of the terminal + */ screenReaderMode: boolean; scrollback: number; + /** terminal selection color */ selectionColor: string; + /** + * the shell to run when spawning a new session (i.e. /usr/local/bin/fish) + * if left empty, your system's login shell will be used by default + * + * Windows + * - Make sure to use a full path if the binary name doesn't work + * - Remove `--login` in shellArgs + * + * Windows Subsystem for Linux (WSL) - previously Bash on Windows + * - Example: `C:\\Windows\\System32\\wsl.exe` + * + * Git-bash on Windows + * - Example: `C:\\Program Files\\Git\\bin\\bash.exe` + * + * PowerShell on Windows + * - Example: `C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe` + * + * Cygwin + * - Example: `C:\\cygwin64\\bin\\bash.exe` + * + * Git Bash + * - Example: `C:\\Program Files\\Git\\git-cmd.exe` + * Then Add `--command=usr/bin/bash.exe` to shellArgs + */ shell: string; + /** + * for setting shell arguments (i.e. for using interactive shellArgs: `['-i']`) + * by default `['--login']` will be used + */ shellArgs: string[]; + /** + * if you're using a Linux setup which show native menus, set to false + * + * default: `true` on Linux, `true` on Windows, ignored on macOS + */ showHamburgerMenu: boolean | ''; - showWindowControls: string; + /** + * set to `false` if you want to hide the minimize, maximize and close buttons + * + * additionally, set to `'left'` if you want them on the left, like in Ubuntu + * + * default: `true` on Windows and Linux, ignored on macOS + */ + showWindowControls: boolean | 'left' | ''; + /** custom CSS to embed in the terminal window */ termCSS: string; - uiFontFamily: string; + uiFontFamily?: string; + /** choose either `'stable'` for receiving highly polished, or `'canary'` for less polished but more frequent updates */ updateChannel: 'stable' | 'canary'; - useConpty: boolean; + useConpty?: boolean; + /** + * Whether to use the WebGL renderer. Set it to false to use canvas-based + * rendering (slower, but supports transparent backgrounds) + */ webGLRenderer: boolean; - webLinksActivationKey: 'ctrl' | 'alt' | 'meta' | 'shift'; - windowSize: [number, number]; + // TODO: does not pick up config changes automatically, need to restart terminal + /** + * keypress required for weblink activation: [ctrl | alt | meta | shift] + */ + webLinksActivationKey: 'ctrl' | 'alt' | 'meta' | 'shift' | ''; + /** Initial window size in pixels */ + windowSize?: [number, number]; + /** set custom startup directory (must be an absolute path) */ workingDirectory: string; }; export type rawConfig = { config?: configOptions; + /** + * a list of plugins to fetch and install from npm + * format: [@org/]project[#version] + * examples: + * `hyperpower` + * `@company/project` + * `project#1.0.1` + */ plugins?: string[]; + /** + * in development, you can create a directory under + * `plugins/local/` and include it here + * to load it and avoid it being `npm install`ed + */ localPlugins?: string[]; - keymaps?: Record; + /** + * Example + * 'window:devtools': 'cmd+alt+o', + */ + keymaps?: {[k: string]: string | string[]}; }; export type parsedConfig = { diff --git a/lib/hyper.d.ts b/lib/hyper.d.ts index 9b92194e..857880d0 100644 --- a/lib/hyper.d.ts +++ b/lib/hyper.d.ts @@ -94,7 +94,7 @@ export type uiState = Immutable<{ scrollback: number; selectionColor: string; showHamburgerMenu: boolean | ''; - showWindowControls: string; + showWindowControls: boolean | 'left' | ''; termCSS: string; uiFontFamily: string; updateCanInstall: null | boolean;