Migrate config to json

This commit is contained in:
Labhansh Agrawal 2022-12-26 10:31:04 +05:30
parent de9dd1433a
commit bafa44845a
9 changed files with 725 additions and 133 deletions

View file

@ -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": {}
}

View file

@ -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 {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 {_init, _extractDefault} from './init';
import notify from '../notify'; import notify from '../notify';
import {rawConfig} from '../../lib/config'; import {rawConfig} from '../../lib/config';
import _ from 'lodash';
import {resolve} from 'path';
let defaultConfig: rawConfig; let defaultConfig: rawConfig;
@ -18,62 +29,33 @@ const _write = (path: string, data: string) => {
writeFileSync(path, format, 'utf8'); writeFileSync(path, format, 'utf8');
}; };
// Saves a file as backup by appending '.backup' or '.backup2', '.backup3', etc. // Migrate Hyper3 config to Hyper4 but only if the user hasn't manually
// 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
// touched the new config and if the old config is not a symlink // touched the new config and if the old config is not a symlink
const migrateHyper2Config = () => { const migrateHyper3Config = () => {
if (cfgPath === legacyCfgPath) { copy(schemaPath, resolve(cfgDir, schemaFile), (err) => {
// No need to migrate if (err) {
return; console.error(err);
} }
if (!existsSync(legacyCfgPath)) { });
// Already migrated or user never used Hyper 2
return; if (existsSync(cfgPath)) {
}
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
return; return;
} }
if (existsNew) { if (!existsSync(legacyCfgPath)) {
const cfg1 = readFileSync(defaultCfg, 'utf8').replace(/\r|\n/g, ''); copySync(defaultCfg, cfgPath);
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; return;
} }
}
// Migrate // Migrate
copySync(legacyCfgPath, cfgPath); const defaultCfgData = JSON.parse(readFileSync(defaultCfg, 'utf8'));
saveAsBackup(legacyCfgPath); const legacyCfgData = _extractDefault(readFileSync(legacyCfgPath, 'utf8'));
const newCfgData = _.merge(defaultCfgData, legacyCfgData);
_write(cfgPath, JSON.stringify(newCfgData, null, 2));
notify( notify(
'Hyper 3', 'Hyper 4',
`Settings location has changed to ${cfgPath}.\nWe've automatically migrated your existing config!\nPlease restart Hyper now` `Settings location and format has changed.\nWe've automatically migrated your existing config to ${cfgPath}`
); );
}; };
@ -83,18 +65,18 @@ const _importConf = () => {
mkdirpSync(plugs.local); mkdirpSync(plugs.local);
try { try {
migrateHyper2Config(); migrateHyper3Config();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
let defaultCfgRaw = ''; let defaultCfgRaw = '{}';
try { try {
defaultCfgRaw = readFileSync(defaultCfg, 'utf8'); defaultCfgRaw = readFileSync(defaultCfg, 'utf8');
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
const _defaultCfg = _extractDefault(defaultCfgRaw) as rawConfig; const _defaultCfg = JSON.parse(defaultCfgRaw) as rawConfig;
// Importing platform specific keymap // Importing platform specific keymap
let content = '{}'; let content = '{}';
@ -107,12 +89,12 @@ const _importConf = () => {
_defaultCfg.keymaps = mapping; _defaultCfg.keymaps = mapping;
// Import user config // Import user config
let userCfg: string; let userCfg: rawConfig;
try { try {
userCfg = readFileSync(cfgPath, 'utf8'); userCfg = JSON.parse(readFileSync(cfgPath, 'utf8'));
} catch (err) { } catch (err) {
_write(cfgPath, defaultCfgRaw); _write(cfgPath, defaultCfgRaw);
userCfg = defaultCfgRaw; userCfg = JSON.parse(defaultCfgRaw);
} }
return {userCfg, defaultCfg: _defaultCfg}; return {userCfg, defaultCfg: _defaultCfg};
@ -121,7 +103,7 @@ const _importConf = () => {
export const _import = () => { export const _import = () => {
const imported = _importConf(); const imported = _importConf();
defaultConfig = imported.defaultCfg; defaultConfig = imported.defaultCfg;
const result = _init(imported); const result = _init(imported.userCfg, imported.defaultCfg);
return result; return result;
}; };

View file

@ -2,6 +2,7 @@ import vm from 'vm';
import notify from '../notify'; import notify from '../notify';
import mapKeys from '../utils/map-keys'; import mapKeys from '../utils/map-keys';
import {parsedConfig, rawConfig, configOptions} from '../../lib/config'; import {parsedConfig, rawConfig, configOptions} from '../../lib/config';
import _ from 'lodash';
const _extract = (script?: vm.Script): Record<string, any> => { const _extract = (script?: vm.Script): Record<string, any> => {
const module: Record<string, any> = {}; const module: Record<string, any> = {};
@ -27,23 +28,21 @@ const _extractDefault = (cfg: string) => {
}; };
// init config // init config
const _init = (cfg: {userCfg: string; defaultCfg: rawConfig}): parsedConfig => { const _init = (userCfg: rawConfig, defaultCfg: rawConfig): parsedConfig => {
const script = _syntaxValidation(cfg.userCfg);
const _cfg = script && (_extract(script) as rawConfig);
return { return {
config: (() => { config: (() => {
if (_cfg?.config) { if (userCfg?.config) {
return _cfg.config; return _.merge(defaultCfg.config, userCfg.config);
} else { } else {
notify('Error reading configuration: `config` key is missing'); 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 // 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 // Ignore undefined values in plugin and localPlugins array Issue #1862
plugins: (_cfg?.plugins && _cfg.plugins.filter(Boolean)) || [], plugins: (userCfg?.plugins && userCfg.plugins.filter(Boolean)) || [],
localPlugins: (_cfg?.localPlugins && _cfg.localPlugins.filter(Boolean)) || [] localPlugins: (userCfg?.localPlugins && userCfg.localPlugins.filter(Boolean)) || []
}; };
}; };

View file

@ -5,22 +5,30 @@ import {statSync} from 'fs';
import {resolve, join} from 'path'; import {resolve, join} from 'path';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
const cfgFile = '.hyper.js'; const cfgFile = 'hyper.json';
const defaultCfgFile = 'config-default.js'; const defaultCfgFile = 'config-default.json';
const schemaFile = 'schema.json';
const homeDirectory = homedir(); const homeDirectory = homedir();
// If the user defines XDG_CONFIG_HOME they definitely want their config there, // 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 // 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 process.env.XDG_CONFIG_HOME !== undefined
? join(process.env.XDG_CONFIG_HOME, 'hyper') ? join(process.env.XDG_CONFIG_HOME, 'hyper')
: process.platform == 'win32' : process.platform == 'win32'
? app.getPath('userData') ? app.getPath('userData')
: homedir(); : homedir(),
'.hyper.js'
);
let cfgDir = applicationDirectory; let cfgPath = join(cfgDir, cfgFile);
let cfgPath = join(applicationDirectory, cfgFile); const schemaPath = resolve(__dirname, schemaFile);
const legacyCfgPath = join(homeDirectory, cfgFile); // Hyper 2 config location
const devDir = resolve(__dirname, '../..'); const devDir = resolve(__dirname, '../..');
const devCfg = join(devDir, cfgFile); const devCfg = join(devDir, cfgFile);
@ -38,10 +46,8 @@ if (isDev) {
} }
} }
const plugins = resolve(cfgDir, '.hyper_plugins'); const plugins = resolve(cfgDir, 'plugins');
const plugs = { const plugs = {
legacyBase: resolve(homeDirectory, '.hyper_plugins'),
legacyLocal: resolve(homeDirectory, '.hyper_plugins', 'local'),
base: plugins, base: plugins,
local: resolve(plugins, 'local'), local: resolve(plugins, 'local'),
cache: resolve(plugins, 'cache') cache: resolve(plugins, 'cache')
@ -82,5 +88,7 @@ export {
yarn, yarn,
cliScriptPath, cliScriptPath,
cliLinkPath, cliLinkPath,
homeDirectory homeDirectory,
schemaFile,
schemaPath
}; };

427
app/config/schema.json Normal file
View file

@ -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"
}

View file

@ -17,6 +17,7 @@ import {decorateSessionOptions, decorateSessionClass} from '../plugins';
import {enable as remoteEnable} from '@electron/remote/main'; import {enable as remoteEnable} from '@electron/remote/main';
import {configOptions} from '../../lib/config'; import {configOptions} from '../../lib/config';
import {getWorkingDirectoryFromPID} from 'native-process-working-directory'; import {getWorkingDirectoryFromPID} from 'native-process-working-directory';
import {existsSync} from 'fs';
export function newWindow( export function newWindow(
options_: BrowserWindowConstructorOptions, options_: BrowserWindowConstructorOptions,
@ -138,7 +139,7 @@ export function newWindow(
} catch (error) { } catch (error) {
console.error(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 // remove the rows and cols, the wrong value of them will break layout when init create

View file

@ -5,25 +5,22 @@ import os from 'os';
import got from 'got'; import got from 'got';
import registryUrlModule from 'registry-url'; import registryUrlModule from 'registry-url';
const registryUrl = registryUrlModule(); const registryUrl = registryUrlModule();
import pify from 'pify';
import * as recast from 'recast';
import path from 'path'; import path from 'path';
// If the user defines XDG_CONFIG_HOME they definitely want their config there, // 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 // otherwise use the home directory in linux/mac and userdata in windows
const applicationDirectory = const applicationDirectory = process.env.XDG_CONFIG_HOME
process.env.XDG_CONFIG_HOME !== undefined ? path.join(process.env.XDG_CONFIG_HOME, 'Hyper')
? path.join(process.env.XDG_CONFIG_HOME, 'hyper') : process.platform === 'win32'
: process.platform == 'win32'
? path.join(process.env.APPDATA!, 'Hyper') ? path.join(process.env.APPDATA!, 'Hyper')
: os.homedir(); : path.join(os.homedir(), '.config', 'Hyper');
const devConfigFileName = path.join(__dirname, `../.hyper.js`); const devConfigFileName = path.join(__dirname, `../hyper.json`);
const fileName = const fileName =
process.env.NODE_ENV !== 'production' && fs.existsSync(devConfigFileName) process.env.NODE_ENV !== 'production' && fs.existsSync(devConfigFileName)
? 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 * We need to make sure the file reading and parsing is lazy so that failure to
@ -43,34 +40,12 @@ function memoize<T extends (...args: any[]) => any>(fn: T): T {
} }
const getFileContents = memoize(() => { const getFileContents = memoize(() => {
try {
return fs.readFileSync(fileName, 'utf8'); return fs.readFileSync(fileName, 'utf8');
} catch (_err) {
const err = _err as {code: string};
if (err.code !== 'ENOENT') {
// ENOENT === !exists()
throw err;
}
}
return null;
}); });
const getParsedFile = memoize(() => recast.parse(getFileContents()!)); const getParsedFile = memoize(() => JSON.parse(getFileContents()));
const getProperties = memoize( const getPluginsByKey = (key: string): any[] => getParsedFile()[key] || [];
(): 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 getPlugins = memoize(() => { const getPlugins = memoize(() => {
return getPluginsByKey('plugins'); return getPluginsByKey('plugins');
@ -87,13 +62,13 @@ function exists() {
function isInstalled(plugin: string, locally?: boolean) { function isInstalled(plugin: string, locally?: boolean) {
const array = locally ? getLocalPlugins() : getPlugins(); const array = locally ? getLocalPlugins() : getPlugins();
if (array && Array.isArray(array)) { if (array && Array.isArray(array)) {
return array.some((entry) => entry.value === plugin); return array.includes(plugin);
} }
return false; return false;
} }
function save() { function save(config: any) {
return pify(fs.writeFile)(fileName, recast.print(getParsedFile()).code, 'utf8'); return fs.writeFileSync(fileName, JSON.stringify(config, null, 2), 'utf8');
} }
function getPackageName(plugin: string) { function getPackageName(plugin: string) {
@ -135,26 +110,25 @@ function install(plugin: string, locally?: boolean) {
return Promise.reject(`${plugin} is already installed`); return Promise.reject(`${plugin} is already installed`);
} }
array.push(recast.types.builders.literal(plugin)); const config = getParsedFile();
return save(); config[locally ? 'localPlugins' : 'plugins'] = [...array, plugin];
save(config);
}); });
} }
function uninstall(plugin: string) { async function uninstall(plugin: string) {
if (!isInstalled(plugin)) { if (!isInstalled(plugin)) {
return Promise.reject(`${plugin} is not installed`); return Promise.reject(`${plugin} is not installed`);
} }
const index = getPlugins().findIndex((entry) => entry.value === plugin); const config = getParsedFile();
getPlugins().splice(index, 1); config.plugins = getPlugins().filter((p) => p !== plugin);
return save(); save(config);
} }
function list() { function list() {
if (getPlugins().length > 0) { if (getPlugins().length > 0) {
return getPlugins() return getPlugins().join('\n');
.map((plugin) => plugin.value)
.join('\n');
} }
return false; return false;
} }

148
lib/config.d.ts vendored
View file

@ -20,60 +20,192 @@ export type ColorMap = {
}; };
export type configOptions = { 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; autoUpdatePlugins: boolean | string;
/**
* terminal background color
*
* opacity is only supported on macOS
*/
backgroundColor: string; backgroundColor: string;
/**
* Supported Options:
* 1. 'SOUND' -> Enables the bell as a sound
* 2. false: turns off the bell
*/
bell: string; 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; bellSound: string | null;
/**
* An absolute file path to a sound file on the machine.
* @nullable
*/
bellSoundURL: string | null; bellSoundURL: string | null;
/** border color (window, tabs) */
borderColor: string; 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; colors: ColorMap;
/** if `true` selected text will automatically be copied to the clipboard */
copyOnSelect: boolean; copyOnSelect: boolean;
/** custom CSS to embed in the main window */
css: string; css: string;
/** terminal text color under BLOCK cursor */
cursorAccentColor: string; cursorAccentColor: string;
/** set to `true` for blinking cursor */
cursorBlink: boolean; cursorBlink: boolean;
/** terminal cursor background color and opacity (hex, rgb, hsl, hsv, hwb or cmyk) */
cursorColor: string; cursorColor: string;
/** `'BEAM'` for |, `'UNDERLINE'` for _, `'BLOCK'` for █ */
cursorShape: 'BEAM' | 'UNDERLINE' | 'BLOCK'; cursorShape: 'BEAM' | 'UNDERLINE' | 'BLOCK';
/** if `true` hyper will be set as the default protocol client for SSH */
defaultSSHApp: boolean; defaultSSHApp: boolean;
/** if `true` hyper will not check for updates */
disableAutoUpdates: boolean; disableAutoUpdates: boolean;
/** if `false` Hyper will use ligatures provided by some fonts */
disableLigatures: boolean; disableLigatures: boolean;
env: Record<string, string>; /** for environment variables */
env: {[k: string]: string};
/** font family with optional fallbacks */
fontFamily: string; fontFamily: string;
/** default font size in pixels for all tabs */
fontSize: number; fontSize: number;
/** default font weight eg:'normal', '400', 'bold' */
fontWeight: FontWeight; fontWeight: FontWeight;
/** font weight for bold characters eg:'normal', '600', 'bold' */
fontWeightBold: FontWeight; fontWeightBold: FontWeight;
/** color of the text */
foregroundColor: string; foregroundColor: string;
/** letter spacing as a relative unit */
letterSpacing: number; letterSpacing: number;
/** line height as a relative unit */
lineHeight: number; 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; macOptionSelectionMode: string;
modifierKeys: { modifierKeys?: {
altIsMeta: boolean; altIsMeta: boolean;
cmdIsMeta: boolean; cmdIsMeta: boolean;
}; };
/** custom padding (CSS format, i.e.: `top right bottom left` or `top horizontal bottom` or `vertical horizontal` or `all`) */
padding: string; padding: string;
/**
* set to true to preserve working directory when creating splits or tabs
*/
preserveCWD: boolean; 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; quickEdit: boolean;
/**
* set to true to enable screen reading apps (like NVDA) to read the contents of the terminal
*/
screenReaderMode: boolean; screenReaderMode: boolean;
scrollback: number; scrollback: number;
/** terminal selection color */
selectionColor: string; 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; shell: string;
/**
* for setting shell arguments (i.e. for using interactive shellArgs: `['-i']`)
* by default `['--login']` will be used
*/
shellArgs: string[]; 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 | ''; 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; 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'; 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; webGLRenderer: boolean;
webLinksActivationKey: 'ctrl' | 'alt' | 'meta' | 'shift'; // TODO: does not pick up config changes automatically, need to restart terminal
windowSize: [number, number]; /**
* 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; workingDirectory: string;
}; };
export type rawConfig = { export type rawConfig = {
config?: configOptions; 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[]; 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[]; localPlugins?: string[];
keymaps?: Record<string, string | string[]>; /**
* Example
* 'window:devtools': 'cmd+alt+o',
*/
keymaps?: {[k: string]: string | string[]};
}; };
export type parsedConfig = { export type parsedConfig = {

2
lib/hyper.d.ts vendored
View file

@ -94,7 +94,7 @@ export type uiState = Immutable<{
scrollback: number; scrollback: number;
selectionColor: string; selectionColor: string;
showHamburgerMenu: boolean | ''; showHamburgerMenu: boolean | '';
showWindowControls: string; showWindowControls: boolean | 'left' | '';
termCSS: string; termCSS: string;
uiFontFamily: string; uiFontFamily: string;
updateCanInstall: null | boolean; updateCanInstall: null | boolean;