diff --git a/.eslintignore b/.eslintignore index 5f94ba00..7c25c8ac 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,4 +9,6 @@ assets website bin dist -target \ No newline at end of file +target +cache +schema.json \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 02af8b96..1fa89a98 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,7 +3,9 @@ "react", "prettier", "@typescript-eslint", - "eslint-comments" + "eslint-comments", + "lodash", + "import" ], "extends": [ "eslint:recommended", @@ -13,7 +15,6 @@ ], "parser": "@typescript-eslint/parser", "parserOptions": { - "ecmaVersion": 8, "sourceType": "module", "ecmaFeatures": { "jsx": true, @@ -33,7 +34,11 @@ "settings": { "react": { "version": "detect" - } + }, + "import/resolver": { + "typescript": {} + }, + "import/internal-regex": "^(electron|react)$" }, "rules": { "func-names": [ @@ -57,34 +62,21 @@ "bracketSpacing": false, "semi": true, "useTabs": false, - "jsxBracketSameLine": false + "bracketSameLine": false } ], - "eslint-comments/no-unused-disable": "error" - }, - "overrides": [ - { - "files": [ - "app/config/config-default.js", - ".hyper.js" - ], - "rules": { - "prettier/prettier": [ - "error", - { - "printWidth": 120, - "tabWidth": 2, - "singleQuote": true, - "trailingComma": "es5", - "bracketSpacing": false, - "semi": true, - "useTabs": false, - "parser": "babel", - "jsxBracketSameLine": false - } + "eslint-comments/no-unused-disable": "error", + "react/no-unknown-property":[ + "error", + { + "ignore": [ + "jsx", + "global" ] } - }, + ] + }, + "overrides": [ { "files": [ "**.ts", @@ -106,7 +98,63 @@ "@typescript-eslint/no-shadow": ["error"], "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/restrict-template-expressions": "off" + "@typescript-eslint/restrict-template-expressions": "off", + "@typescript-eslint/consistent-type-imports": [ "error", { "disallowTypeAnnotations": false } ], + "lodash/prop-shorthand": [ "error", "always" ], + "lodash/import-scope": [ "error", "method" ], + "lodash/collection-return": "error", + "lodash/collection-method-value": "error", + "import/no-extraneous-dependencies": "error", + "import/no-anonymous-default-export": "error", + "import/order": [ + "error", + { + "groups": [ + "builtin", + "internal", + "external", + "parent", + "sibling", + "index" + ], + "newlines-between": "always", + "alphabetize": { + "order": "asc", + "orderImportKind": "desc", + "caseInsensitive": true + } + } + ] + } + }, + { + "extends": [ + "plugin:jsonc/recommended-with-json", + "plugin:json-schema-validator/recommended" + ], + "files": [ + "*.json" + ], + "parser": "jsonc-eslint-parser", + "plugins": [ + "jsonc", + "json-schema-validator" + ], + "rules": { + "jsonc/array-element-newline": [ + "error", + "consistent" + ], + "jsonc/array-bracket-newline": [ + "error", + "consistent" + ], + "jsonc/indent": [ + "error", + 2 + ], + "prettier/prettier": "off", + "json-schema-validator/no-invalid": "error" } } ] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3e772142..15db91f4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -26,7 +26,7 @@ assignees: '' - **OS version and name**: - **Hyper.app version**: -- **Link of a [Gist](https://gist.github.com/) with the contents of your .hyper.js**: +- **Link of a [Gist](https://gist.github.com/) with the contents of your hyper.json**: - **Relevant information from devtools** _(CMD+ALT+I on macOS, CTRL+SHIFT+I elsewhere)_: - **The issue is reproducible in vanilla Hyper.app**: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8bf3b55e..20a51554 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,7 @@ updates: time: '11:00' open-pull-requests-limit: 30 target-branch: canary + versioning-strategy: increase - package-ecosystem: npm directory: "/app" schedule: @@ -14,3 +15,11 @@ updates: time: '11:00' open-pull-requests-limit: 30 target-branch: canary + versioning-strategy: increase +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + time: '11:00' + open-pull-requests-limit: 30 + target-branch: canary diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 07532e5e..569dda76 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,11 +35,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,10 +50,10 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl + # 📚 https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/spectron_comment.yml b/.github/workflows/e2e_comment.yml similarity index 67% rename from .github/workflows/spectron_comment.yml rename to .github/workflows/e2e_comment.yml index d2f5ac4c..573a4d59 100644 --- a/.github/workflows/spectron_comment.yml +++ b/.github/workflows/e2e_comment.yml @@ -1,11 +1,11 @@ -name: Comment spectron screenshots on PR +name: Comment e2e test screenshots on PR on: workflow_run: workflows: ['Node CI'] types: - completed jobs: - spectron_comment: + e2e_comment: runs-on: ubuntu-latest if: github.event.workflow_run.event == 'pull_request' steps: @@ -14,14 +14,14 @@ jobs: WORKFLOW_RUN_INFO: ${{ toJSON(github.event.workflow_run) }} run: echo "$WORKFLOW_RUN_INFO" - name: Download Artifacts - uses: dawidd6/action-download-artifact@v2.11.0 + uses: dawidd6/action-download-artifact@v3.1.4 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: nodejs.yml run_id: ${{ github.event.workflow_run.id }} - name: spectron + name: e2e - name: Get PR number - uses: dawidd6/action-download-artifact@v2.11.0 + uses: dawidd6/action-download-artifact@v3.1.4 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: nodejs.yml @@ -29,14 +29,14 @@ jobs: name: pr_num - name: Read the pr_num file id: pr_num_reader - uses: juliangruber/read-file-action@v1.0.0 + uses: juliangruber/read-file-action@v1.1.7 with: path: ./pr_num.txt - name: List images run: ls -al - name: Upload images to imgur id: upload_screenshots - uses: devicons/public-upload-to-imgur@v2.2.1 + uses: devicons/public-upload-to-imgur@v2.2.2 with: path: ./*.png client_id: ${{ secrets.IMGUR_CLIENT_ID }} @@ -46,11 +46,18 @@ jobs: IMG_MARKDOWN: ${{ join(fromJSON(steps.upload_screenshots.outputs.markdown_urls), '') }} MESSAGE: | Hi there, + Thank you for contributing to Hyper! + You can get the build artifacts from [here](https://nightly.link/{1}/actions/runs/{2}). Here are screenshots of Hyper built from this pr. {0} - Thank you for contributing to Hyper! with: type: create issue_number: ${{ steps.pr_num_reader.outputs.content }} token: ${{ secrets.GITHUB_TOKEN }} - body: ${{ format(env.MESSAGE, env.IMG_MARKDOWN) }} + body: ${{ format(env.MESSAGE, env.IMG_MARKDOWN, github.repository, github.event.workflow_run.id) }} + - name: Hide older comments + uses: kanga333/comment-hider@v0.4.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + leave_visible: 1 + issue_number: ${{ steps.pr_num_reader.outputs.content }} diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 69b2aac6..d42e2995 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -8,51 +8,69 @@ on: defaults: run: shell: bash +env: + NODE_VERSION: 18.x jobs: build: runs-on: ${{matrix.os}} strategy: matrix: - node-version: [15.x] - os: [macos-11.0, ubuntu-latest, windows-latest] + os: + - macos-latest + - ubuntu-latest + - windows-latest fail-fast: false steps: - name: Checkout - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/checkout@v4 + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node-version }} + node-version: ${{ env.NODE_VERSION }} + - name: Fix node-gyp and Python + run: python3 -m pip install packaging setuptools + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + - uses: actions/cache/restore@v4 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock', 'app/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- - name: Install run: yarn install - - name: Test + env: + npm_config_node_gyp: ${{ github.workspace }}${{ runner.os == 'Windows' && '\node_modules\node-gyp\bin\node-gyp.js' || '/node_modules/node-gyp/bin/node-gyp.js' }} + - name: Install libarchive-tools + if: runner.os == 'Linux' + run: | + sudo apt update + sudo apt install libarchive-tools + - name: Lint and Run Unit Tests run: yarn run test - name: Getting Build Icon if: github.ref == 'refs/heads/canary' || github.base_ref == 'canary' run: | cp build/canary.ico build/icon.ico cp build/canary.icns build/icon.icns - - name: Build (pr) - if: github.event_name == 'pull_request' - run: yarn run dist --publish=never - - name: Build (push) - if: github.event_name == 'push' - run: yarn run dist + - name: Build + run: | + if [ -z "$CSC_LINK" ] ; then unset CSC_LINK ; fi + if [ -z "$CSC_KEY_PASSWORD" ] ; then unset CSC_KEY_PASSWORD ; fi + if [ -z "$WIN_CSC_LINK" ] ; then unset WIN_CSC_LINK ; fi + if [ -z "$WIN_CSC_KEY_PASSWORD" ] ; then unset WIN_CSC_KEY_PASSWORD ; fi + if [ -z "$APPLE_ID" ] ; then unset APPLE_ID ; fi + if [ -z "$APPLE_APP_SPECIFIC_PASSWORD" ] ; then unset APPLE_APP_SPECIFIC_PASSWORD ; fi + yarn run dist env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_LINK: ${{ secrets.MAC_CERT_P12_BASE64 }} CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_P12_PASSWORD }} WIN_CSC_LINK: ${{ secrets.WIN_CERT_P12_BASE64 }} WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CERT_P12_PASSWORD }} - - name: Test Spectron - if: runner.os != 'Linux' - run: yarn run test:spectron - - name: Archive Spectron test screenshot - if: runner.os != 'Linux' - uses: actions/upload-artifact@v2 - with: - name: spectron - path: dist/tmp/*.png + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - name: Archive Build Artifacts uses: LabhanshAgrawal/upload-artifact@v3 with: @@ -62,15 +80,111 @@ jobs: dist/*.AppImage dist/*.deb dist/*.rpm + dist/*.pacman dist/*.exe + - name: Run E2E Tests + if: runner.os != 'Linux' + run: yarn run test:e2e + - name: Run E2E Tests on Linux + if: runner.os == 'Linux' + uses: GabrielBB/xvfb-action@v1.6 + with: + run: yarn run test:e2e + env: + SHELL: /bin/bash + - name: Archive E2E test screenshot + uses: actions/upload-artifact@v3 + with: + name: e2e + path: dist/tmp/*.png - name: Save the pr number in an artifact if: github.event_name == 'pull_request' env: PR_NUM: ${{ github.event.number }} run: echo $PR_NUM > pr_num.txt - name: Upload the pr num - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: github.event_name == 'pull_request' with: name: pr_num path: ./pr_num.txt + - uses: actions/cache/save@v4 + if: github.event_name == 'push' + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock', 'app/yarn.lock') }} + + build-linux-arm: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: armv7l + cpu: cortex-a8 + image: raspios_lite:latest + - name: arm64 + cpu: cortex-a53 + image: raspios_lite_arm64:latest + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Fix node-gyp and Python + run: python3 -m pip install packaging setuptools + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + - uses: actions/cache/restore@v4 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock', 'app/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Install + run: | + yarn install + sudo apt update + sudo apt install libarchive-tools + - name: Compile + run: yarn run build + - name: rebuild node-pty + uses: pguyot/arm-runner-action@v2.5.2 + with: + image_additional_mb: 2000 + base_image: ${{ matrix.image }} + cpu: ${{ matrix.cpu }} + shell: bash + copy_artifact_path: target/node_modules/node-pty + copy_artifact_dest: target/node_modules + commands: | + wget https://nodejs.org/dist/v18.16.0/node-v18.16.0-linux-${{ matrix.name }}.tar.xz + tar -xJf node-v18.16.0-linux-${{ matrix.name }}.tar.xz + sudo cp node-v18.16.0-linux-${{ matrix.name }}/* /usr/local/ -R + npm run rebuild-node-pty + - name: chown node-pty + run: | + sudo chown -R $USER:$USER target/node_modules/node-pty + - name: Prepare v8 snapshot + if: matrix.name == 'armv7l' + run: | + sudo dpkg --add-architecture i386 + sudo apt update + sudo apt install -y libglib2.0-0:i386 libexpat1:i386 libgcc-s1:i386 + npm_config_arch=armv7l yarn run v8-snapshot:arch + - name: Build + run: yarn run electron-builder -l deb rpm AppImage pacman --${{ matrix.name }} -c electron-builder-linux-ci.json + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Archive Build Artifacts + uses: LabhanshAgrawal/upload-artifact@v3 + with: + path: | + dist/*.snap + dist/*.AppImage + dist/*.deb + dist/*.rpm + dist/*.pacman diff --git a/.gitignore b/.gitignore index ef95864f..9c5932fb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ dist app/renderer target bin/cli.* +cache # dependencies node_modules @@ -12,8 +13,9 @@ npm-debug.log yarn-error.log # optional dev config file and plugins directory -.hyper.js -.hyper_plugins +hyper.json +schema.json +plugins .DS_Store .vscode/* diff --git a/PLUGINS.md b/PLUGINS.md index d266854d..afc284a3 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -11,11 +11,11 @@ Prerequisites and steps are described in the ["Contributing" section of our READ Be sure to use the `canary` branch. ### Create a dev config file -Copy your config file `.hyper.js` to the root of your cloned repository. Hyper, in dev mode, will use this copied config file. That means that you can continue to use your main installation of Hyper with your day-to-day configuration. -After the first run, Hyper, in dev mode, will have created a new `.hyper_plugins` directory in your repository directory. +Copy your config file `hyper.json` to the root of your cloned repository. Hyper, in dev mode, will use this copied config file. That means that you can continue to use your main installation of Hyper with your day-to-day configuration. +After the first run, Hyper, in dev mode, will have created a new `plugins` directory in your repository directory. ### Setup your plugin -Go to your recently created `/.hyper_plugins/local` directory and create/clone your plugin repo. An even better method on macOS/Linux is to add a symlink to your plugin directory. +Go to your recently created `/plugins/local` directory and create/clone your plugin repo. An even better method on macOS/Linux is to add a symlink to your plugin directory. Edit your dev config file, and add your plugin name (directory name in your `local` directory) in the `localPlugins` array. ```js diff --git a/README.md b/README.md index 813120cb..8c11085e 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ make sure its build process is working correctly by running `yarn run rebuild-no If you are on macOS, this typically is related to Xcode issues (like not having agreed to the Terms of Service by running `sudo xcodebuild` after a fresh Xcode installation). -##### Error with `c++` on macOS when running `yarn` +##### Error with `C++` on macOS when running `yarn` If you are getting compiler errors when running `yarn` add the environment variable `export CXX=clang++` @@ -110,7 +110,6 @@ If you have issues in the `codesign` step when running `yarn run dist` on macOS, ## Related Repositories -- [Art](https://github.com/vercel/art/tree/master/hyper) - [Website](https://github.com/vercel/hyper-site) - [Sample Extension](https://github.com/vercel/hyperpower) - [Sample Theme](https://github.com/vercel/hyperyellow) diff --git a/app/auto-updater-linux.ts b/app/auto-updater-linux.ts index 08bff975..aa95c1d7 100644 --- a/app/auto-updater-linux.ts +++ b/app/auto-updater-linux.ts @@ -1,6 +1,7 @@ -import fetch from 'electron-fetch'; import {EventEmitter} from 'events'; +import fetch from 'electron-fetch'; + class AutoUpdater extends EventEmitter implements Electron.AutoUpdater { updateURL!: string; quitAndInstall() { @@ -26,13 +27,12 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater { this.emit('update-not-available'); return; } - return res.json().then(({name, notes, pub_date}) => { + return res.json().then(({name, notes, pub_date}: {name: string; notes: string; pub_date: string}) => { // Only name is mandatory, needed to construct release URL. if (!name) { throw new Error('Malformed server response: release name is missing.'); } - // If `null` is passed to Date constructor, current time will be used. This doesn't work with `undefined` - const date = new Date(pub_date || null); + const date = pub_date ? new Date(pub_date) : new Date(); this.emit('update-available', {}, notes, name, date); }); }) @@ -47,4 +47,6 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater { } } -export default new AutoUpdater(); +const autoUpdaterLinux = new AutoUpdater(); + +export default autoUpdaterLinux; diff --git a/app/commands.ts b/app/commands.ts index e285b163..469a5a3d 100644 --- a/app/commands.ts +++ b/app/commands.ts @@ -1,4 +1,6 @@ -import {app, Menu, BrowserWindow} from 'electron'; +import {app, Menu} from 'electron'; +import type {BrowserWindow} from 'electron'; + import {openConfig, getConfig} from './config'; import {updatePlugins} from './plugins'; import {installCLI} from './utils/cli-install'; @@ -144,6 +146,22 @@ const commands: Record void> = { }; }); +//Profile specific commands +getConfig().profiles.forEach((profile) => { + commands[`window:new:${profile.name}`] = () => { + setTimeout(() => app.createWindow(undefined, undefined, profile.name), 0); + }; + commands[`tab:new:${profile.name}`] = (focusedWindow) => { + focusedWindow?.rpc.emit('termgroup add req', {profile: profile.name}); + }; + commands[`pane:splitRight:${profile.name}`] = (focusedWindow) => { + focusedWindow?.rpc.emit('split request vertical', {profile: profile.name}); + }; + commands[`pane:splitDown:${profile.name}`] = (focusedWindow) => { + focusedWindow?.rpc.emit('split request horizontal', {profile: profile.name}); + }; +}); + export const execCommand = (command: string, focusedWindow?: BrowserWindow) => { const fn = commands[command]; if (fn) { diff --git a/app/config.ts b/app/config.ts index 949e1d0e..b4613d57 100644 --- a/app/config.ts +++ b/app/config.ts @@ -1,11 +1,14 @@ +import {app} from 'electron'; + import chokidar from 'chokidar'; -import notify from './notify'; + +import type {parsedConfig, configOptions} from '../typings/config'; + import {_import, getDefaultConfig} from './config/import'; import _openConfig from './config/open'; import {cfgPath, cfgDir} from './config/paths'; +import notify from './notify'; import {getColorMap} from './utils/colors'; -import {parsedConfig, configOptions} from '../lib/config'; -import {app} from 'electron'; const watchers: Function[] = []; let cfg: parsedConfig = {} as any; @@ -57,17 +60,11 @@ const _watch = () => { console.error('error watching config', error); }); - app.on('before-quit', (e) => { + app.on('before-quit', () => { if (Object.keys(_watcher.getWatched()).length > 0) { - e.preventDefault(); - _watcher - .close() - .catch((err) => { - console.warn(err); - }) - .finally(() => { - app.quit(); - }); + _watcher.close().catch((err) => { + console.warn(err); + }); } }); }; @@ -84,8 +81,30 @@ export const getConfigDir = () => { return cfgDir; }; +export const getDefaultProfile = () => { + return cfg.config.defaultProfile || cfg.config.profiles[0]?.name || 'default'; +}; + +// get config for the default profile, keeping it for backward compatibility export const getConfig = () => { - return cfg.config; + return getProfileConfig(getDefaultProfile()); +}; + +export const getProfiles = () => { + return cfg.config.profiles; +}; + +export const getProfileConfig = (profileName: string): configOptions => { + const {profiles, defaultProfile, ...baseConfig} = cfg.config; + const profileConfig = profiles.find((p) => p.name === profileName)?.config || {}; + for (const key in profileConfig) { + if (typeof baseConfig[key] === 'object' && !Array.isArray(baseConfig[key])) { + baseConfig[key] = {...baseConfig[key], ...profileConfig[key]}; + } else { + baseConfig[key] = profileConfig[key]; + } + } + return {...baseConfig, defaultProfile, profiles}; }; export const openConfig = () => { diff --git a/app/config/config-default.js b/app/config/config-default.js deleted file mode 100644 index 512471f8..00000000 --- a/app/config/config-default.js +++ /dev/null @@ -1,180 +0,0 @@ -// Future versions of Hyper may add additional config options, -// which will not automatically be merged into this file. -// See https://hyper.is#cfg for all currently supported options. - -module.exports = { - config: { - // choose either `'stable'` for receiving highly polished, - // or `'canary'` for less polished but more frequent updates - updateChannel: 'stable', - - // default font size in pixels for all tabs - fontSize: 12, - - // font family with optional fallbacks - fontFamily: 'Menlo, "DejaVu Sans Mono", Consolas, "Lucida Console", monospace', - - // default font weight: 'normal' or 'bold' - fontWeight: 'normal', - - // font weight for bold characters: 'normal' or 'bold' - fontWeightBold: 'bold', - - // line height as a relative unit - lineHeight: 1, - - // letter spacing as a relative unit - letterSpacing: 0, - - // terminal cursor background color and opacity (hex, rgb, hsl, hsv, hwb or cmyk) - cursorColor: 'rgba(248,28,229,0.8)', - - // terminal text color under BLOCK cursor - cursorAccentColor: '#000', - - // `'BEAM'` for |, `'UNDERLINE'` for _, `'BLOCK'` for █ - cursorShape: 'BLOCK', - - // set to `true` (without backticks and without quotes) for blinking cursor - cursorBlink: false, - - // color of the text - foregroundColor: '#fff', - - // terminal background color - // opacity is only supported on macOS - backgroundColor: '#000', - - // terminal selection color - selectionColor: 'rgba(248,28,229,0.3)', - - // border color (window, tabs) - borderColor: '#333', - - // custom CSS to embed in the main window - css: '', - - // custom CSS to embed in the terminal window - termCSS: '', - - // set custom startup directory (must be an absolute path) - workingDirectory: '', - - // 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: '', - - // set to `false` (without backticks and without quotes) 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` (without backticks and without quotes) on Windows and Linux, ignored on macOS - showWindowControls: '', - - // custom padding (CSS format, i.e.: `top right bottom left`) - padding: '12px 14px', - - // 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: { - 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', - }, - - // 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` - shell: '', - - // for setting shell arguments (i.e. for using interactive shellArgs: `['-i']`) - // by default `['--login']` will be used - shellArgs: ['--login'], - - // for environment variables - env: {}, - - // Supported Options: - // 1. 'SOUND' -> Enables the bell as a sound - // 2. false: turns off the bell - bell: 'SOUND', - - // An absolute file path to a sound file on the machine. - // bellSoundURL: '/path/to/sound/file', - - // if `true` (without backticks and without quotes), selected text will automatically be copied to the clipboard - copyOnSelect: false, - - // if `true` (without backticks and without quotes), hyper will be set as the default protocol client for SSH - defaultSSHApp: true, - - // if `true` (without backticks and without quotes), 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: false, - - // 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: 'vertical', - - // Whether to use the WebGL renderer. Set it to false to use canvas-based - // rendering (slower, but supports transparent backgrounds) - webGLRenderer: false, - - // keypress required for weblink activation: [ctrl|alt|meta|shift] - // todo: does not pick up config changes automatically, need to restart terminal :/ - webLinksActivationKey: '', - - // if `false` (without backticks and without quotes), Hyper will use ligatures provided by some fonts - disableLigatures: true, - - // for advanced config flags please refer to https://hyper.is/#cfg - }, - - // a list of plugins to fetch and install from npm - // format: [@org/]project[#version] - // examples: - // `hyperpower` - // `@company/project` - // `project#1.0.1` - plugins: [], - - // in development, you can create a directory under - // `~/.hyper_plugins/local/` and include it here - // to load it and avoid it being `npm install`ed - localPlugins: [], - - keymaps: { - // Example - // 'window:devtools': 'cmd+alt+o', - }, -}; diff --git a/app/config/config-default.json b/app/config/config-default.json new file mode 100644 index 00000000..2a6a66ff --- /dev/null +++ b/app/config/config-default.json @@ -0,0 +1,77 @@ +{ + "$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, + "imageSupport": true, + "defaultProfile": "default", + "profiles": [ + { + "name": "default", + "config": {} + } + ] + }, + "plugins": [], + "localPlugins": [], + "keymaps": {} +} diff --git a/app/config/import.ts b/app/config/import.ts index 613f57ff..b9965375 100644 --- a/app/config/import.ts +++ b/app/config/import.ts @@ -1,100 +1,32 @@ -import {moveSync, copySync, existsSync, writeFileSync, readFileSync, lstatSync} from 'fs-extra'; -import {sync as mkdirpSync} from 'mkdirp'; -import {defaultCfg, cfgPath, legacyCfgPath, plugs, defaultPlatformKeyPath} from './paths'; -import {_init, _extractDefault} from './init'; +import {readFileSync, mkdirpSync} from 'fs-extra'; + +import type {rawConfig} from '../../typings/config'; import notify from '../notify'; -import {rawConfig} from '../../lib/config'; + +import {_init} from './init'; +import {migrateHyper3Config} from './migrate'; +import {defaultCfg, cfgPath, plugs, defaultPlatformKeyPath} from './paths'; let defaultConfig: rawConfig; -const _write = (path: string, data: string) => { - // This method will take text formatted as Unix line endings and transform it - // to text formatted with DOS line endings. We do this because the default - // text editor on Windows (notepad) doesn't Deal with LF files. Still. In 2017. - const crlfify = (str: string) => { - return str.replace(/\r?\n/g, '\r\n'); - }; - const format = process.platform === 'win32' ? crlfify(data.toString()) : data; - 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 -// 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 - 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; - } - } - - // Migrate - copySync(legacyCfgPath, cfgPath); - saveAsBackup(legacyCfgPath); - - notify( - 'Hyper 3', - `Settings location has changed to ${cfgPath}.\nWe've automatically migrated your existing config!\nPlease restart Hyper now` - ); -}; - const _importConf = () => { // init plugin directories if not present mkdirpSync(plugs.base); 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 +39,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; + notify("Couldn't parse config file. Using default config instead."); + userCfg = JSON.parse(defaultCfgRaw); } return {userCfg, defaultCfg: _defaultCfg}; @@ -121,7 +53,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 39a62b51..a41b864c 100644 --- a/app/config/init.ts +++ b/app/config/init.ts @@ -1,11 +1,14 @@ import vm from 'vm'; + +import merge from 'lodash/merge'; + +import type {parsedConfig, rawConfig, configOptions} from '../../typings/config'; import notify from '../notify'; import mapKeys from '../utils/map-keys'; -import {parsedConfig, rawConfig, configOptions} from '../../lib/config'; const _extract = (script?: vm.Script): Record => { const module: Record = {}; - script?.runInNewContext({module}); + script?.runInNewContext({module}, {displayErrors: true}); if (!module.exports) { throw new Error('Error reading configuration: `module.exports` not set'); } @@ -15,9 +18,10 @@ const _extract = (script?: vm.Script): Record => { const _syntaxValidation = (cfg: string) => { try { - return new vm.Script(cfg, {filename: '.hyper.js', displayErrors: true}); - } catch (err) { - notify(`Error loading config: ${err.name}`, `${err}`, {error: err}); + return new vm.Script(cfg, {filename: '.hyper.js'}); + } catch (_err) { + const err = _err as {name: string}; + notify(`Error loading config: ${err.name}`, JSON.stringify(err), {error: err}); } }; @@ -26,23 +30,33 @@ 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) { + const conf = userCfg.config; + conf.defaultProfile = conf.defaultProfile || 'default'; + conf.profiles = conf.profiles || []; + conf.profiles = conf.profiles.length > 0 ? conf.profiles : [{name: 'default', config: {}}]; + conf.profiles = conf.profiles.map((p, i) => ({ + ...p, + name: p.name || `profile-${i + 1}`, + config: p.config || {} + })); + if (!conf.profiles.map((p) => p.name).includes(conf.defaultProfile)) { + conf.defaultProfile = conf.profiles[0].name; + } + return merge({}, defaultCfg.config, conf); } 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?.filter(Boolean) || [], + localPlugins: userCfg?.localPlugins?.filter(Boolean) || [] }; }; diff --git a/app/config/migrate.ts b/app/config/migrate.ts new file mode 100644 index 00000000..522da1e9 --- /dev/null +++ b/app/config/migrate.ts @@ -0,0 +1,190 @@ +import {dirname, resolve} from 'path'; + +import {builders, namedTypes} from 'ast-types'; +import type {ExpressionKind} from 'ast-types/lib/gen/kinds'; +import {copy, copySync, existsSync, readFileSync, writeFileSync} from 'fs-extra'; +import merge from 'lodash/merge'; +import {parse, prettyPrint} from 'recast'; +import * as babelParser from 'recast/parsers/babel'; + +import notify from '../notify'; + +import {_extractDefault} from './init'; +import {cfgDir, cfgPath, defaultCfg, legacyCfgPath, plugs, schemaFile, schemaPath} from './paths'; + +// function to remove all json serializable entries from an array expression +function removeElements(node: namedTypes.ArrayExpression): namedTypes.ArrayExpression { + const newElements = node.elements.filter((element) => { + if (namedTypes.ObjectExpression.check(element)) { + const newElement = removeProperties(element); + if (newElement.properties.length === 0) { + return false; + } + } else if (namedTypes.ArrayExpression.check(element)) { + const newElement = removeElements(element); + if (newElement.elements.length === 0) { + return false; + } + } else if (namedTypes.Literal.check(element)) { + return false; + } + return true; + }); + return {...node, elements: newElements}; +} + +// function to remove all json serializable properties from an object expression +function removeProperties(node: namedTypes.ObjectExpression): namedTypes.ObjectExpression { + const newProperties = node.properties.filter((property) => { + if ( + namedTypes.ObjectProperty.check(property) && + (namedTypes.Literal.check(property.key) || namedTypes.Identifier.check(property.key)) && + !property.computed + ) { + if (namedTypes.ObjectExpression.check(property.value)) { + const newValue = removeProperties(property.value); + if (newValue.properties.length === 0) { + return false; + } + } else if (namedTypes.ArrayExpression.check(property.value)) { + const newValue = removeElements(property.value); + if (newValue.elements.length === 0) { + return false; + } + } else if (namedTypes.Literal.check(property.value)) { + return false; + } + } + return true; + }); + return {...node, properties: newProperties}; +} + +export function configToPlugin(code: string): string { + const ast: namedTypes.File = parse(code, { + parser: babelParser + }); + const statements = ast.program.body; + let moduleExportsNode: namedTypes.AssignmentExpression | null = null; + let configNode: ExpressionKind | null = null; + + for (const statement of statements) { + if (namedTypes.ExpressionStatement.check(statement)) { + const expression = statement.expression; + if ( + namedTypes.AssignmentExpression.check(expression) && + expression.operator === '=' && + namedTypes.MemberExpression.check(expression.left) && + namedTypes.Identifier.check(expression.left.object) && + expression.left.object.name === 'module' && + namedTypes.Identifier.check(expression.left.property) && + expression.left.property.name === 'exports' + ) { + moduleExportsNode = expression; + if (namedTypes.ObjectExpression.check(expression.right)) { + const properties = expression.right.properties; + for (const property of properties) { + if ( + namedTypes.ObjectProperty.check(property) && + namedTypes.Identifier.check(property.key) && + property.key.name === 'config' + ) { + configNode = property.value as ExpressionKind; + if (namedTypes.ObjectExpression.check(property.value)) { + configNode = removeProperties(property.value); + } + } + } + } else { + configNode = builders.memberExpression(moduleExportsNode.right, builders.identifier('config')); + } + } + } + } + + if (!moduleExportsNode) { + console.log('No module.exports found in config'); + return ''; + } + if (!configNode) { + console.log('No config field found in module.exports'); + return ''; + } + if (namedTypes.ObjectExpression.check(configNode) && configNode.properties.length === 0) { + return ''; + } + + moduleExportsNode.right = builders.objectExpression([ + builders.property( + 'init', + builders.identifier('decorateConfig'), + builders.arrowFunctionExpression( + [builders.identifier('_config')], + builders.callExpression( + builders.memberExpression(builders.identifier('Object'), builders.identifier('assign')), + [builders.objectExpression([]), builders.identifier('_config'), configNode] + ) + ) + ) + ]); + + return prettyPrint(ast, {tabWidth: 2}).code; +} + +export const _write = (path: string, data: string) => { + // This method will take text formatted as Unix line endings and transform it + // to text formatted with DOS line endings. We do this because the default + // text editor on Windows (notepad) doesn't Deal with LF files. Still. In 2017. + const crlfify = (str: string) => { + return str.replace(/\r?\n/g, '\r\n'); + }; + const format = process.platform === 'win32' ? crlfify(data.toString()) : data; + writeFileSync(path, format, 'utf8'); +}; + +// 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 +export const migrateHyper3Config = () => { + copy(schemaPath, resolve(cfgDir, schemaFile), (err) => { + if (err) { + console.error(err); + } + }); + + if (existsSync(cfgPath)) { + return; + } + + if (!existsSync(legacyCfgPath)) { + copySync(defaultCfg, cfgPath); + return; + } + + // Migrate + copySync(resolve(dirname(legacyCfgPath), '.hyper_plugins', 'local'), plugs.local); + + const defaultCfgData = JSON.parse(readFileSync(defaultCfg, 'utf8')); + let newCfgData; + try { + const legacyCfgRaw = readFileSync(legacyCfgPath, 'utf8'); + const legacyCfgData = _extractDefault(legacyCfgRaw); + newCfgData = merge({}, defaultCfgData, legacyCfgData); + + const pluginCode = configToPlugin(legacyCfgRaw); + if (pluginCode) { + const pluginPath = resolve(plugs.local, 'migrated-hyper3-config.js'); + newCfgData.localPlugins = ['migrated-hyper3-config', ...(newCfgData.localPlugins || [])]; + _write(pluginPath, pluginCode); + } + } catch (e) { + console.error(e); + notify( + 'Hyper 4', + `Failed to migrate your config from Hyper 3.\nDefault config will be created instead at ${cfgPath}` + ); + newCfgData = defaultCfgData; + } + _write(cfgPath, JSON.stringify(newCfgData, null, 2)); + + notify('Hyper 4', `Settings location and format has changed to ${cfgPath}`); +}; diff --git a/app/config/open.ts b/app/config/open.ts index 1d5047c9..264c2292 100644 --- a/app/config/open.ts +++ b/app/config/open.ts @@ -1,8 +1,11 @@ -import {shell} from 'electron'; -import {cfgPath} from './paths'; -import * as Registry from 'native-reg'; import {exec} from 'child_process'; +import {shell} from 'electron'; + +import * as Registry from 'native-reg'; + +import {cfgPath} from './paths'; + const getUserChoiceKey = () => { try { // Load FileExts keys for .js files @@ -57,7 +60,7 @@ const openNotepad = (file: string) => }); }); -export default () => { +const openConfig = () => { // Windows opens .js files with WScript.exe by default // If the user hasn't set up an editor for .js files, we fallback to notepad. if (process.platform === 'win32') { @@ -73,3 +76,5 @@ export default () => { } return shell.openPath(cfgPath).then((error) => error === ''); }; + +export default openConfig; diff --git a/app/config/paths.ts b/app/config/paths.ts index 0b794542..2c2ce283 100644 --- a/app/config/paths.ts +++ b/app/config/paths.ts @@ -1,26 +1,36 @@ // This module exports paths, names, and other metadata that is referenced -import {homedir} from 'os'; -import {app} from 'electron'; import {statSync} from 'fs'; +import {homedir} from 'os'; import {resolve, join} from 'path'; + +import {app} from 'electron'; + 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(); + ? app.getPath('userData') + : 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 +48,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 +90,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..6bcf8500 --- /dev/null +++ b/app/config/schema.json @@ -0,0 +1,756 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "FontWeight": { + "anyOf": [ + { + "enum": [ + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "900", + "bold", + "normal" + ], + "type": "string" + }, + { + "type": "number" + } + ], + "description": "A string or number representing text font weight." + }, + "Partial": { + "properties": { + "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", + "enum": [ + "SOUND", + false + ] + }, + "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" + }, + "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": { + "$ref": "#/definitions/FontWeight", + "description": "default font weight eg:'normal', '400', 'bold'" + }, + "fontWeightBold": { + "$ref": "#/definitions/FontWeight", + "description": "font weight for bold characters eg:'normal', '600', 'bold'" + }, + "foregroundColor": { + "description": "color of the text", + "type": "string" + }, + "imageSupport": { + "description": "Whether to enable Sixel and iTerm2 inline image protocol support or not.", + "type": "boolean" + }, + "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 (e.g. /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 (e.g. 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" + }, + "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" + } + }, + "type": "object" + }, + "configOptions": { + "allOf": [ + { + "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" + ] + }, + "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" + }, + "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" + } + }, + "required": [ + "autoUpdatePlugins", + "defaultSSHApp", + "disableAutoUpdates", + "updateChannel" + ], + "type": "object" + }, + { + "properties": { + "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", + "enum": [ + "SOUND", + false + ] + }, + "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" + }, + "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": { + "$ref": "#/definitions/FontWeight", + "description": "default font weight eg:'normal', '400', 'bold'" + }, + "fontWeightBold": { + "$ref": "#/definitions/FontWeight", + "description": "font weight for bold characters eg:'normal', '600', 'bold'" + }, + "foregroundColor": { + "description": "color of the text", + "type": "string" + }, + "imageSupport": { + "description": "Whether to enable Sixel and iTerm2 inline image protocol support or not.", + "type": "boolean" + }, + "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 (e.g. /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 (e.g. 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" + }, + "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": [ + "backgroundColor", + "bell", + "bellSound", + "bellSoundURL", + "borderColor", + "colors", + "copyOnSelect", + "css", + "cursorAccentColor", + "cursorBlink", + "cursorColor", + "cursorShape", + "disableLigatures", + "env", + "fontFamily", + "fontSize", + "fontWeight", + "fontWeightBold", + "foregroundColor", + "imageSupport", + "letterSpacing", + "lineHeight", + "macOptionSelectionMode", + "padding", + "preserveCWD", + "quickEdit", + "screenReaderMode", + "scrollback", + "selectionColor", + "shell", + "shellArgs", + "showHamburgerMenu", + "showWindowControls", + "termCSS", + "webGLRenderer", + "webLinksActivationKey", + "workingDirectory" + ], + "type": "object" + }, + { + "properties": { + "defaultProfile": { + "description": "The default profile name to use when launching a new session", + "type": "string" + }, + "profiles": { + "description": "A list of profiles to use", + "items": { + "properties": { + "config": { + "$ref": "#/definitions/Partial", + "description": "Specify all the options you want to override for each profile.\nOptions set here override the defaults set in the root." + }, + "name": { + "type": "string" + } + }, + "required": [ + "config", + "name" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "defaultProfile", + "profiles" + ], + "type": "object" + } + ] + } + }, + "properties": { + "config": { + "$ref": "#/definitions/configOptions" + }, + "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/config/windows.ts b/app/config/windows.ts index c18518a7..4cd6d500 100644 --- a/app/config/windows.ts +++ b/app/config/windows.ts @@ -1,5 +1,6 @@ +import type {BrowserWindow} from 'electron'; + import Config from 'electron-store'; -import {BrowserWindow} from 'electron'; export const defaults = { windowPosition: [50, 50] as [number, number], diff --git a/app/ext-modules.d.ts b/app/ext-modules.d.ts deleted file mode 100644 index 443d58ee..00000000 --- a/app/ext-modules.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare module 'git-describe' { - export function gitDescribe(...args: any[]): void; -} - -declare module 'default-shell' { - const val: string; - export default val; -} diff --git a/app/extend-electron.d.ts b/app/extend-electron.d.ts deleted file mode 100644 index 0a9a2ef9..00000000 --- a/app/extend-electron.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type {Server} from './rpc'; - -declare module 'electron' { - interface App { - config: typeof import('./config'); - plugins: typeof import('./plugins'); - getWindows: () => Set; - getLastFocusedWindow: () => BrowserWindow | null; - windowCallback?: (win: BrowserWindow) => void; - createWindow: ( - fn?: (win: BrowserWindow) => void, - options?: {size?: [number, number]; position?: [number, number]} - ) => BrowserWindow; - setVersion: (version: string) => void; - } - - // type Server = import('./rpc').Server; - interface BrowserWindow { - uid: string; - sessions: Map; - focusTime: number; - clean: () => void; - rpc: Server; - } -} diff --git a/app/index.d.ts b/app/index.d.ts deleted file mode 100644 index 1349d7d9..00000000 --- a/app/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -// Dummy file, required by tsc diff --git a/app/index.ts b/app/index.ts index b9adc620..84a804a4 100644 --- a/app/index.ts +++ b/app/index.ts @@ -1,30 +1,40 @@ +// eslint-disable-next-line import/order +import {cfgPath} from './config/paths'; + // Print diagnostic information for a few arguments instead of running Hyper. if (['--help', '-v', '--version'].includes(process.argv[1])) { // eslint-disable-next-line @typescript-eslint/no-var-requires const {version} = require('./package'); - const configLocation = process.platform === 'win32' ? `${process.env.userprofile}\\.hyper.js` : '~/.hyper.js'; console.log(`Hyper version ${version}`); console.log('Hyper does not accept any command line arguments. Please modify the config file instead.'); - console.log(`Hyper configuration file located at: ${configLocation}`); + console.log(`Hyper configuration file located at: ${cfgPath}`); process.exit(); } +// Enable remote module +// eslint-disable-next-line import/order +import {initialize as remoteInitialize} from '@electron/remote/main'; +remoteInitialize(); + +// set up config +// eslint-disable-next-line import/order +import * as config from './config'; +config.setup(); + // Native import {resolve} from 'path'; // Packages -import {app, BrowserWindow, Menu} from 'electron'; -import {gitDescribe} from 'git-describe'; +import {app, BrowserWindow, Menu, screen} from 'electron'; + import isDev from 'electron-is-dev'; -import * as config from './config'; +import {gitDescribe} from 'git-describe'; +import parseUrl from 'parse-url'; -// set up config -config.setup(); - -import * as plugins from './plugins'; -import {installCLI} from './utils/cli-install'; import * as AppMenu from './menus/menu'; +import * as plugins from './plugins'; import {newWindow} from './ui/window'; +import {installCLI} from './utils/cli-install'; import * as windowUtils from './utils/window-utils'; const windowSet = new Set([]); @@ -52,7 +62,7 @@ if (isDev) { console.log('running in dev mode'); // Override default appVersion which is set from package.json - gitDescribe({customArguments: ['--tags']}, (error: any, gitInfo: any) => { + gitDescribe({customArguments: ['--tags']}, (error: any, gitInfo: {raw: string}) => { if (!error) { app.setVersion(gitInfo.raw); } @@ -68,15 +78,13 @@ async function installDevExtensions(isDev_: boolean) { if (!isDev_) { return []; } - const installer = await import('electron-devtools-installer'); + const {default: installer, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS} = await import('electron-devtools-installer'); - const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'] as const; + const extensions = [REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS]; const forceDownload = Boolean(process.env.UPGRADE_EXTENSIONS); return Promise.all( - extensions.map((name) => - installer.default(installer[name], {forceDownload, loadExtensionOptions: {allowFileAccess: true}}) - ) + extensions.map((extension) => installer(extension, {forceDownload, loadExtensionOptions: {allowFileAccess: true}})) ); } @@ -86,16 +94,15 @@ app.on('ready', () => .then(() => { function createWindow( fn?: (win: BrowserWindow) => void, - options: {size?: [number, number]; position?: [number, number]} = {} + options: {size?: [number, number]; position?: [number, number]} = {}, + profileName: string = config.getDefaultProfile() ) { - const cfg = plugins.getDecoratedConfig(); + const cfg = plugins.getDecoratedConfig(profileName); const winSet = config.getWin(); let [startX, startY] = winSet.position; const [width, height] = options.size ? options.size : cfg.windowSize || winSet.size; - // eslint-disable-next-line @typescript-eslint/no-var-requires - const {screen} = require('electron'); const winPos = options.position; @@ -132,7 +139,7 @@ app.on('ready', () => [startX, startY] = config.windowDefaults.windowPosition; } - const hwin = newWindow({width, height, x: startX, y: startY}, cfg, fn); + const hwin = newWindow({width, height, x: startX, y: startY}, cfg, fn, profileName); windowSet.add(hwin); void hwin.loadURL(url); @@ -231,6 +238,6 @@ app.on('open-file', (_event, path) => { app.on('open-url', (_event, sshUrl) => { GetWindow((win: BrowserWindow) => { - win.rpc.emit('open ssh', sshUrl); + win.rpc.emit('open ssh', parseUrl(sshUrl)); }); }); diff --git a/app/keymaps/linux.json b/app/keymaps/linux.json index c0771e6d..da66d671 100644 --- a/app/keymaps/linux.json +++ b/app/keymaps/linux.json @@ -3,7 +3,7 @@ "window:reload": "ctrl+shift+r", "window:reloadFull": "ctrl+shift+f5", "window:preferences": "ctrl+,", - "window:hamburgerMenu": "alt", + "window:hamburgerMenu": "alt+f", "zoom:reset": "ctrl+0", "zoom:in": "ctrl+=", "zoom:out": "ctrl+-", diff --git a/app/keymaps/win32.json b/app/keymaps/win32.json index 0ff8301b..8e7b1913 100644 --- a/app/keymaps/win32.json +++ b/app/keymaps/win32.json @@ -3,7 +3,7 @@ "window:reload": "ctrl+shift+r", "window:reloadFull": "ctrl+shift+f5", "window:preferences": "ctrl+,", - "window:hamburgerMenu": "alt", + "window:hamburgerMenu": "alt+f", "zoom:reset": "ctrl+0", "zoom:in": "ctrl+=", "zoom:out": "ctrl+-", @@ -16,6 +16,12 @@ "alt+f4" ], "tab:new": "ctrl+shift+t", + "tab:next": [ + "ctrl+tab" + ], + "tab:prev": [ + "ctrl+shift+tab" + ], "tab:jump:prefix": "ctrl", "pane:next": "ctrl+pageup", "pane:prev": "ctrl+pagedown", diff --git a/app/menus/menu.ts b/app/menus/menu.ts index 47c5fc47..df877e69 100644 --- a/app/menus/menu.ts +++ b/app/menus/menu.ts @@ -1,20 +1,22 @@ // Packages -import {app, dialog, Menu, BrowserWindow} from 'electron'; +import {app, dialog, Menu} from 'electron'; +import type {BrowserWindow} from 'electron'; // Utilities +import {execCommand} from '../commands'; import {getConfig} from '../config'; import {icon} from '../config/paths'; -import viewMenu from './menus/view'; -import shellMenu from './menus/shell'; -import editMenu from './menus/edit'; -import toolsMenu from './menus/tools'; -import windowMenu from './menus/window'; -import helpMenu from './menus/help'; -import darwinMenu from './menus/darwin'; import {getDecoratedKeymaps} from '../plugins'; -import {execCommand} from '../commands'; import {getRendererTypes} from '../utils/renderer-utils'; +import darwinMenu from './menus/darwin'; +import editMenu from './menus/edit'; +import helpMenu from './menus/help'; +import shellMenu from './menus/shell'; +import toolsMenu from './menus/tools'; +import viewMenu from './menus/view'; +import windowMenu from './menus/window'; + const appName = app.name; const appVersion = app.getVersion(); @@ -54,14 +56,18 @@ export const createMenu = ( void dialog.showMessageBox({ title: `About ${appName}`, message: `${appName} ${appVersion} (${updateChannel})`, - detail: `Renderers: ${renderers}\nPlugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright Š 2020 Vercel, Inc.`, + detail: `Renderers: ${renderers}\nPlugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright Š 2022 Vercel, Inc.`, buttons: [], icon: icon as any }); }; const menu = [ ...(process.platform === 'darwin' ? [darwinMenu(commandKeys, execCommand, showAbout)] : []), - shellMenu(commandKeys, execCommand), + shellMenu( + commandKeys, + execCommand, + getConfig().profiles.map((p) => p.name) + ), editMenu(commandKeys, execCommand), viewMenu(commandKeys, execCommand), toolsMenu(commandKeys, execCommand), diff --git a/app/menus/menus/darwin.ts b/app/menus/menus/darwin.ts index 1a1376d6..59e514d1 100644 --- a/app/menus/menus/darwin.ts +++ b/app/menus/menus/darwin.ts @@ -1,8 +1,9 @@ // This menu label is overrided by OSX to be the appName // The label is set to appName here so it matches actual behavior -import {app, BrowserWindow, MenuItemConstructorOptions} from 'electron'; +import {app} from 'electron'; +import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; -export default ( +const darwinMenu = ( commandKeys: Record, execCommand: (command: string, focusedWindow?: BrowserWindow) => void, showAbout: () => void @@ -54,3 +55,5 @@ export default ( ] }; }; + +export default darwinMenu; diff --git a/app/menus/menus/edit.ts b/app/menus/menus/edit.ts index 40387b51..a9040f6e 100644 --- a/app/menus/menus/edit.ts +++ b/app/menus/menus/edit.ts @@ -1,6 +1,6 @@ -import {BrowserWindow, MenuItemConstructorOptions} from 'electron'; +import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; -export default ( +const editMenu = ( commandKeys: Record, execCommand: (command: string, focusedWindow?: BrowserWindow) => void ) => { @@ -31,7 +31,8 @@ export default ( } as any, { role: 'paste', - accelerator: commandKeys['editor:paste'] + accelerator: commandKeys['editor:paste'], + registerAccelerator: true }, { label: 'Select All', @@ -146,3 +147,5 @@ export default ( submenu }; }; + +export default editMenu; diff --git a/app/menus/menus/help.ts b/app/menus/menus/help.ts index c0d95644..3c9214c1 100644 --- a/app/menus/menus/help.ts +++ b/app/menus/menus/help.ts @@ -1,10 +1,14 @@ import {release} from 'os'; -import {app, shell, MenuItemConstructorOptions, dialog, clipboard} from 'electron'; + +import {app, shell, dialog, clipboard} from 'electron'; +import type {MenuItemConstructorOptions} from 'electron'; + import {getConfig, getPlugins} from '../../config'; -const {arch, env, platform, versions} = process; import {version} from '../../package.json'; -export default (commands: Record, showAbout: () => void): MenuItemConstructorOptions => { +const {arch, env, platform, versions} = process; + +const helpMenu = (commands: Record, showAbout: () => void): MenuItemConstructorOptions => { const submenu: MenuItemConstructorOptions[] = [ { label: `${app.name} Website`, @@ -22,7 +26,7 @@ export default (commands: Record, showAbout: () => void): MenuIt If not, please try and fulfil these first. --> -- [ ] Your Hyper.app version is **${version}**. Please verify your using the [latest](https://github.com/vercel/hyper/releases/latest) Hyper.app version +- [ ] Your Hyper.app version is **${version}**. Please verify you're using the [latest](https://github.com/vercel/hyper/releases/latest) Hyper.app version - [ ] I have searched the [issues](https://github.com/vercel/hyper/issues) of this repo and believe that this is not a duplicate --- - **Any relevant information from devtools?** _(CMD+OPTION+I on macOS, CTRL+SHIFT+I elsewhere)_: @@ -39,12 +43,12 @@ export default (commands: Record, showAbout: () => void): MenuIt --- - + - **${app.name} version**: ${env.TERM_PROGRAM_VERSION} "${app.getVersion()}" - **OS ARCH VERSION:** ${platform} ${arch} ${release()} - **Electron:** ${versions.electron} **LANG:** ${env.LANG} - **SHELL:** ${env.SHELL} **TERM:** ${env.TERM} -
.hyper.js contents +
hyper.json contents \`\`\`json ${JSON.stringify(getConfig(), null, 2)} @@ -106,3 +110,5 @@ ${JSON.stringify(getPlugins(), null, 2)} submenu }; }; + +export default helpMenu; diff --git a/app/menus/menus/shell.ts b/app/menus/menus/shell.ts index ec8bde39..651f071c 100644 --- a/app/menus/menus/shell.ts +++ b/app/menus/menus/shell.ts @@ -1,8 +1,9 @@ -import {BrowserWindow, MenuItemConstructorOptions} from 'electron'; +import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; -export default ( +const shellMenu = ( commandKeys: Record, - execCommand: (command: string, focusedWindow?: BrowserWindow) => void + execCommand: (command: string, focusedWindow?: BrowserWindow) => void, + profiles: string[] ): MenuItemConstructorOptions => { const isMac = process.platform === 'darwin'; @@ -43,6 +44,47 @@ export default ( { type: 'separator' }, + ...profiles.map( + (profile): MenuItemConstructorOptions => ({ + label: profile, + submenu: [ + { + label: 'New Tab', + accelerator: commandKeys[`tab:new:${profile}`], + click(item, focusedWindow) { + execCommand(`tab:new:${profile}`, focusedWindow); + } + }, + { + label: 'New Window', + accelerator: commandKeys[`window:new:${profile}`], + click(item, focusedWindow) { + execCommand(`window:new:${profile}`, focusedWindow); + } + }, + { + type: 'separator' + }, + { + label: 'Split Down', + accelerator: commandKeys[`pane:splitDown:${profile}`], + click(item, focusedWindow) { + execCommand(`pane:splitDown:${profile}`, focusedWindow); + } + }, + { + label: 'Split Right', + accelerator: commandKeys[`pane:splitRight:${profile}`], + click(item, focusedWindow) { + execCommand(`pane:splitRight:${profile}`, focusedWindow); + } + } + ] + }) + ), + { + type: 'separator' + }, { label: 'Close', accelerator: commandKeys['pane:close'], @@ -58,3 +100,5 @@ export default ( ] }; }; + +export default shellMenu; diff --git a/app/menus/menus/tools.ts b/app/menus/menus/tools.ts index 843d3d35..ac377728 100644 --- a/app/menus/menus/tools.ts +++ b/app/menus/menus/tools.ts @@ -1,6 +1,6 @@ -import {BrowserWindow, MenuItemConstructorOptions} from 'electron'; +import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; -export default ( +const toolsMenu = ( commands: Record, execCommand: (command: string, focusedWindow?: BrowserWindow) => void ): MenuItemConstructorOptions => { @@ -45,3 +45,5 @@ export default ( ] }; }; + +export default toolsMenu; diff --git a/app/menus/menus/view.ts b/app/menus/menus/view.ts index 98f9ef1e..d0a2b071 100644 --- a/app/menus/menus/view.ts +++ b/app/menus/menus/view.ts @@ -1,6 +1,6 @@ -import {BrowserWindow, MenuItemConstructorOptions} from 'electron'; +import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; -export default ( +const viewMenu = ( commandKeys: Record, execCommand: (command: string, focusedWindow?: BrowserWindow) => void ): MenuItemConstructorOptions => { @@ -55,3 +55,5 @@ export default ( ] }; }; + +export default viewMenu; diff --git a/app/menus/menus/window.ts b/app/menus/menus/window.ts index 56db6fe5..7ae142a6 100644 --- a/app/menus/menus/window.ts +++ b/app/menus/menus/window.ts @@ -1,11 +1,11 @@ -import {BrowserWindow, MenuItemConstructorOptions} from 'electron'; +import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; -export default ( +const windowMenu = ( commandKeys: Record, execCommand: (command: string, focusedWindow?: BrowserWindow) => void ): MenuItemConstructorOptions => { // Generating tab:jump array - const tabJump = []; + const tabJump: MenuItemConstructorOptions[] = []; for (let i = 1; i <= 9; i++) { // 9 is a special number because it means 'last' const label = i === 9 ? 'Last' : `${i}`; @@ -94,3 +94,5 @@ export default ( ] }; }; + +export default windowMenu; diff --git a/app/notifications.ts b/app/notifications.ts index ad7aaabd..1a7abb06 100644 --- a/app/notifications.ts +++ b/app/notifications.ts @@ -1,7 +1,9 @@ -import ms from 'ms'; +import type {BrowserWindow} from 'electron'; + import fetch from 'electron-fetch'; +import ms from 'ms'; + import {version} from './package.json'; -import {BrowserWindow} from 'electron'; const NEWS_URL = 'https://hyper-news.now.sh'; @@ -22,7 +24,7 @@ export default function fetchNotifications(win: BrowserWindow) { }) .then((res) => res.json()) .then((data) => { - const {message} = data || {}; + const message: {text: string; url: string; dismissable: boolean} | '' = data.message || ''; if (typeof message !== 'object' && message !== '') { throw new Error('Bad response'); } diff --git a/app/notify.ts b/app/notify.ts index a31f0305..ce5544fc 100644 --- a/app/notify.ts +++ b/app/notify.ts @@ -1,4 +1,5 @@ import {app, Notification} from 'electron'; + import {icon} from './config/paths'; export default function notify(title: string, body = '', details: {error?: any} = {}) { diff --git a/app/package.json b/app/package.json index 0a8d26d8..a7c6f60f 100644 --- a/app/package.json +++ b/app/package.json @@ -2,41 +2,46 @@ "name": "hyper", "productName": "Hyper", "description": "A terminal built on web technologies", - "version": "3.1.0-canary.6", + "version": "4.0.0-canary.5", "license": "MIT", "author": { "name": "ZEIT, Inc.", "email": "team@zeit.co" }, "repository": "zeit/hyper", + "scripts": { + "postinstall": "npx patch-package" + }, "dependencies": { - "async-retry": "1.3.1", - "chokidar": "^3.5.2", - "color": "3.1.3", - "convert-css-color-name-to-hex": "0.1.1", + "@babel/parser": "7.24.4", + "@electron/remote": "2.1.2", + "ast-types": "^0.16.1", + "async-retry": "1.3.3", + "chokidar": "^3.6.0", + "color": "4.2.3", "default-shell": "1.0.1", - "electron-fetch": "1.7.3", + "electron-devtools-installer": "3.2.0", + "electron-fetch": "1.9.1", "electron-is-dev": "2.0.0", - "electron-store": "8.0.0", - "file-uri-to-path": "2.0.0", - "fs-extra": "10.0.0", - "git-describe": "4.0.4", + "electron-store": "8.2.0", + "fs-extra": "11.2.0", + "git-describe": "4.1.1", "lodash": "4.17.21", - "mkdirp": "1.0.4", "ms": "2.1.3", - "node-pty": "0.10.1", + "native-process-working-directory": "^1.0.2", + "node-pty": "1.0.0", "os-locale": "5.0.0", - "parse-url": "5.0.3", - "pify": "5.0.0", + "parse-url": "8.1.0", "queue": "6.0.2", - "react": "17.0.2", - "react-dom": "17.0.2", - "semver": "7.3.5", + "react": "18.2.0", + "react-dom": "18.2.0", + "recast": "0.23.6", + "semver": "7.6.0", "shell-env": "3.0.1", "sudo-prompt": "^9.2.1", - "uuid": "8.3.2" + "uuid": "9.0.1" }, "optionalDependencies": { - "native-reg": "0.3.5" + "native-reg": "1.1.1" } } diff --git a/app/patches/node-pty+1.0.0.patch b/app/patches/node-pty+1.0.0.patch new file mode 100644 index 00000000..51f70f2d --- /dev/null +++ b/app/patches/node-pty+1.0.0.patch @@ -0,0 +1,15 @@ +diff --git a/node_modules/node-pty/src/win/conpty.cc b/node_modules/node-pty/src/win/conpty.cc +index 47af75c..884d542 100644 +--- a/node_modules/node-pty/src/win/conpty.cc ++++ b/node_modules/node-pty/src/win/conpty.cc +@@ -472,10 +472,6 @@ static NAN_METHOD(PtyKill) { + } + } + +- DisconnectNamedPipe(handle->hIn); +- DisconnectNamedPipe(handle->hOut); +- CloseHandle(handle->hIn); +- CloseHandle(handle->hOut); + CloseHandle(handle->hShell); + } + diff --git a/app/plugins.ts b/app/plugins.ts index 012c83c4..8e460119 100644 --- a/app/plugins.ts +++ b/app/plugins.ts @@ -1,20 +1,28 @@ /* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ -import {app, dialog, BrowserWindow, App} from 'electron'; -import {resolve, basename} from 'path'; +import {exec, execFile} from 'child_process'; import {writeFileSync} from 'fs'; +import {resolve, basename} from 'path'; +import {promisify} from 'util'; + +import {app, dialog, ipcMain as _ipcMain} from 'electron'; +import type {BrowserWindow, App, MenuItemConstructorOptions} from 'electron'; +import React from 'react'; + import Config from 'electron-store'; import ms from 'ms'; -import React from 'react'; import ReactDom from 'react-dom'; + +import type {IpcMainWithCommands} from '../typings/common'; +import type {configOptions} from '../typings/config'; + import * as config from './config'; +import {plugs} from './config/paths'; import notify from './notify'; import {availableExtensions} from './plugins/extensions'; import {install} from './plugins/install'; -import {plugs} from './config/paths'; import mapKeys from './utils/map-keys'; -import {configOptions} from '../lib/config'; // local storage const cache = new Config(); @@ -200,7 +208,7 @@ function syncPackageJSON() { const dependencies = toDependencies(plugins); const pkg = { name: 'hyper-plugins', - description: 'Auto-generated from `~/.hyper.js`!', + description: 'Auto-generated from `hyper.json`!', private: true, version: '0.0.1', repository: 'vercel/hyper', @@ -232,7 +240,7 @@ function toDependencies(plugins_: {plugins: string[]}) { if (match) { const index = match.index + 1; - const pieces = []; + const pieces: string[] = []; pieces[0] = plugin.substring(0, index); pieces[1] = plugin.substring(index + 1, plugin.length); @@ -274,7 +282,7 @@ function requirePlugins(): any[] { const {plugins: plugins_, localPlugins} = paths; const load = (path_: string) => { - let mod: any; + let mod: Record; try { mod = require(path_); const exposed = mod && Object.keys(mod).some((key) => availableExtensions.has(key)); @@ -294,7 +302,8 @@ function requirePlugins(): any[] { console.log(`Plugin ${mod._name} (${mod._version}) loaded.`); return mod; - } catch (err) { + } catch (_err) { + const err = _err as {code: string; message: string}; if (err.code === 'MODULE_NOT_FOUND') { console.warn(`Plugin error while loading "${basename(path_)}" (${path_}): ${err.message}`); } else { @@ -303,10 +312,13 @@ function requirePlugins(): any[] { } }; - return plugins_ + return [ + ...localPlugins.filter((p) => basename(p) === 'migrated-hyper3-config'), + ...plugins_, + ...localPlugins.filter((p) => basename(p) !== 'migrated-hyper3-config') + ] .map(load) - .concat(localPlugins.map(load)) - .filter((v) => Boolean(v)); + .filter((v): v is Record => Boolean(v)); } export const onApp = (app_: App) => { @@ -409,7 +421,7 @@ export const getDeprecatedConfig = () => { return deprecated; }; -export const decorateMenu = (tpl: any) => { +export const decorateMenu = (tpl: MenuItemConstructorOptions[]) => { return decorateObject(tpl, 'decorateMenu'); }; @@ -417,8 +429,8 @@ export const getDecoratedEnv = (baseEnv: Record) => { return decorateObject(baseEnv, 'decorateEnv'); }; -export const getDecoratedConfig = () => { - const baseConfig = config.getConfig(); +export const getDecoratedConfig = (profile: string) => { + const baseConfig = config.getProfileConfig(profile); const decoratedConfig = decorateObject(baseConfig, 'decorateConfig'); const fixedConfig = config.fixConfigDefaults(decoratedConfig); const translatedConfig = config.htermConfigTranslate(fixedConfig); @@ -449,3 +461,20 @@ export const decorateSessionClass = (Session: T): T => { }; export {toDependencies as _toDependencies}; + +const ipcMain = _ipcMain as IpcMainWithCommands; + +ipcMain.handle('child_process.exec', (event, command, options) => { + return promisify(exec)(command, options); +}); + +ipcMain.handle('child_process.execFile', (event, file, args, options) => { + return promisify(execFile)(file, args, options); +}); + +ipcMain.handle('getLoadedPluginVersions', () => getLoadedPluginVersions()); +ipcMain.handle('getPaths', () => getPaths()); +ipcMain.handle('getBasePaths', () => getBasePaths()); +ipcMain.handle('getDeprecatedConfig', () => getDeprecatedConfig()); +ipcMain.handle('getDecoratedConfig', (e, profile) => getDecoratedConfig(profile)); +ipcMain.handle('getDecoratedKeymaps', () => getDecoratedKeymaps()); diff --git a/app/plugins/install.ts b/app/plugins/install.ts index 61fec231..35ed7245 100644 --- a/app/plugins/install.ts +++ b/app/plugins/install.ts @@ -1,6 +1,8 @@ import cp from 'child_process'; -import queue from 'queue'; + import ms from 'ms'; +import queue from 'queue'; + import {yarn, plugs} from '../config/paths'; export const install = (fn: (err: string | null) => void) => { diff --git a/app/rpc.ts b/app/rpc.ts index dd8661ed..aead6363 100644 --- a/app/rpc.ts +++ b/app/rpc.ts @@ -1,15 +1,22 @@ import {EventEmitter} from 'events'; -import {ipcMain, BrowserWindow} from 'electron'; + +import {ipcMain} from 'electron'; +import type {BrowserWindow, IpcMainEvent} from 'electron'; + import {v4 as uuidv4} from 'uuid'; -export class Server extends EventEmitter { +import type {TypedEmitter, MainEvents, RendererEvents, FilterNever} from '../typings/common'; + +export class Server { + emitter: TypedEmitter; destroyed = false; win: BrowserWindow; id!: string; + constructor(win: BrowserWindow) { - super(); + this.emitter = new EventEmitter(); this.win = win; - this.ipcListener = this.ipcListener.bind(this); + this.emit = this.emit.bind(this); if (this.destroyed) { return; @@ -18,14 +25,13 @@ export class Server extends EventEmitter { const uid = uuidv4(); this.id = uid; - // eslint-disable-next-line @typescript-eslint/unbound-method ipcMain.on(uid, this.ipcListener); // we intentionally subscribe to `on` instead of `once` // to support reloading the window and re-initializing // the channel this.wc.on('did-finish-load', () => { - this.wc.send('init', uid); + this.wc.send('init', uid, win.profileName); }); } @@ -33,23 +39,35 @@ export class Server extends EventEmitter { return this.win.webContents; } - ipcListener(event: any, {ev, data}: {ev: string; data: any}) { - super.emit(ev, data); - } + ipcListener = (event: IpcMainEvent, {ev, data}: {ev: U; data: MainEvents[U]}) => + this.emitter.emit(ev, data); - emit(ch: string, data: any = {}): any { + on = (ev: U, fn: (arg0: MainEvents[U]) => void) => { + this.emitter.on(ev, fn); + return this; + }; + + once = (ev: U, fn: (arg0: MainEvents[U]) => void) => { + this.emitter.once(ev, fn); + return this; + }; + + emit>>(ch: U): boolean; + emit>(ch: U, data: RendererEvents[U]): boolean; + emit(ch: U, data?: RendererEvents[U]) { // This check is needed because data-batching can cause extra data to be // emitted after the window has already closed if (!this.win.isDestroyed()) { this.wc.send(this.id, {ch, data}); + return true; } + return false; } destroy() { - this.removeAllListeners(); + this.emitter.removeAllListeners(); this.wc.removeAllListeners(); if (this.id) { - // eslint-disable-next-line @typescript-eslint/unbound-method ipcMain.removeListener(this.id, this.ipcListener); } else { // mark for `genUid` in constructor @@ -58,6 +76,8 @@ export class Server extends EventEmitter { } } -export default (win: BrowserWindow) => { +const createRPC = (win: BrowserWindow) => { return new Server(win); }; + +export default createRPC; diff --git a/app/session.ts b/app/session.ts index dcbcba2c..4b706b3d 100644 --- a/app/session.ts +++ b/app/session.ts @@ -1,12 +1,17 @@ import {EventEmitter} from 'events'; -import {StringDecoder} from 'string_decoder'; -import defaultShell from 'default-shell'; -import {getDecoratedEnv} from './plugins'; -import {productName, version} from './package.json'; -import * as config from './config'; -import {IPty, IWindowsPtyForkOptions, spawn as npSpawn} from 'node-pty'; -import {cliScriptPath} from './config/paths'; import {dirname} from 'path'; +import {StringDecoder} from 'string_decoder'; + +import defaultShell from 'default-shell'; +import type {IPty, IWindowsPtyForkOptions, spawn as npSpawn} from 'node-pty'; +import osLocale from 'os-locale'; +import shellEnv from 'shell-env'; + +import * as config from './config'; +import {cliScriptPath} from './config/paths'; +import {productName, version} from './package.json'; +import {getDecoratedEnv} from './plugins'; +import {getFallBackShellConfig} from './utils/shell-fallback'; const createNodePtyError = () => new Error( @@ -21,7 +26,6 @@ try { throw createNodePtyError(); } -const envFromConfig = config.getConfig().env || {}; const useConpty = config.getConfig().useConpty; // Max duration to batch session data before sending it to the renderer process. @@ -54,7 +58,7 @@ class DataBatcher extends EventEmitter { this.timeout = null; } - write(chunk: Buffer) { + write(chunk: Buffer | string) { if (this.data.length + chunk.length >= BATCH_MAX_SIZE) { // We've reached the max batch size. Flush it and start another one if (this.timeout) { @@ -64,7 +68,7 @@ class DataBatcher extends EventEmitter { this.flush(); } - this.data += this.decoder.write(chunk); + this.data += typeof chunk === 'string' ? chunk : this.decoder.write(chunk); if (!this.timeout) { this.timeout = setTimeout(() => this.flush(), BATCH_DURATION_MS); @@ -82,11 +86,12 @@ class DataBatcher extends EventEmitter { interface SessionOptions { uid: string; - rows: number; - cols: number; - cwd: string; - shell: string; - shellArgs: string[]; + rows?: number; + cols?: number; + cwd?: string; + shell?: string; + shellArgs?: string[]; + profile: string; } export default class Session extends EventEmitter { pty: IPty | null; @@ -94,6 +99,7 @@ export default class Session extends EventEmitter { shell: string | null; ended: boolean; initTimestamp: number; + profile!: string; constructor(options: SessionOptions) { super(); this.pty = null; @@ -104,22 +110,25 @@ export default class Session extends EventEmitter { this.init(options); } - init({uid, rows, cols: columns, cwd, shell: _shell, shellArgs: _shellArgs}: SessionOptions) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const osLocale = require('os-locale') as typeof import('os-locale'); - const baseEnv = Object.assign( - {}, - process.env, - { - LANG: `${osLocale.sync().replace(/-/, '_')}.UTF-8`, - TERM: 'xterm-256color', - COLORTERM: 'truecolor', - TERM_PROGRAM: productName, - TERM_PROGRAM_VERSION: version - }, - envFromConfig - ); + init({uid, rows, cols, cwd, shell: _shell, shellArgs: _shellArgs, profile}: SessionOptions) { + this.profile = profile; + const envFromConfig = config.getProfileConfig(profile).env || {}; + const defaultShellArgs = ['--login']; + const shell = _shell || defaultShell; + const shellArgs = _shellArgs || defaultShellArgs; + + const cleanEnv = + process.env['APPIMAGE'] && process.env['APPDIR'] ? shellEnv.sync(_shell || defaultShell) : process.env; + const baseEnv: Record = { + ...cleanEnv, + LANG: `${osLocale.sync().replace(/-/, '_')}.UTF-8`, + TERM: 'xterm-256color', + COLORTERM: 'truecolor', + TERM_PROGRAM: productName, + TERM_PROGRAM_VERSION: version, + ...envFromConfig + }; // path to AppImage mount point is added to PATH environment variable automatically // which conflicts with the cli if (baseEnv['APPIMAGE'] && baseEnv['APPDIR']) { @@ -135,10 +144,8 @@ export default class Session extends EventEmitter { delete baseEnv.GOOGLE_API_KEY; } - const defaultShellArgs = ['--login']; - const options: IWindowsPtyForkOptions = { - cols: columns, + cols, rows, cwd, env: getDecoratedEnv(baseEnv) @@ -149,12 +156,10 @@ export default class Session extends EventEmitter { options.useConpty = useConpty; } - const shell = _shell || defaultShell; - const shellArgs = _shellArgs || defaultShellArgs; - try { this.pty = spawn(shell, shellArgs, options); - } catch (err) { + } catch (_err) { + const err = _err as {message: string}; if (/is not a function/.test(err.message)) { throw createNodePtyError(); } else { @@ -167,7 +172,7 @@ export default class Session extends EventEmitter { if (this.ended) { return; } - this.batcher?.write(chunk as any); + this.batcher?.write(chunk); }); this.batcher.on('flush', (data: string) => { @@ -180,15 +185,32 @@ export default class Session extends EventEmitter { // this will inform users in case there are errors in the config instead of instant exit const runDuration = new Date().getTime() - this.initTimestamp; if (e.exitCode > 0 && runDuration < 1000) { - const defaultShellConfig = {shell: defaultShell, shellArgs: defaultShellArgs}; - const msg = ` + const fallBackShellConfig = getFallBackShellConfig(shell, shellArgs, defaultShell, defaultShellArgs); + if (fallBackShellConfig) { + const msg = ` shell exited in ${runDuration} ms with exit code ${e.exitCode} please check the shell config: ${JSON.stringify({shell, shellArgs}, undefined, 2)} -fallback to default shell config: ${JSON.stringify(defaultShellConfig, undefined, 2)} +using fallback shell config: ${JSON.stringify(fallBackShellConfig, undefined, 2)} `; - console.warn(msg); - this.batcher?.write(msg.replace(/\n/g, '\r\n') as any); - this.init({uid, rows, cols: columns, cwd, ...defaultShellConfig}); + console.warn(msg); + this.batcher?.write(msg.replace(/\n/g, '\r\n')); + this.init({ + uid, + rows, + cols, + cwd, + shell: fallBackShellConfig.shell, + shellArgs: fallBackShellConfig.shellArgs, + profile + }); + } else { + const msg = ` +shell exited in ${runDuration} ms with exit code ${e.exitCode} +No fallback available, please check the shell config. +`; + console.warn(msg); + this.batcher?.write(msg.replace(/\n/g, '\r\n')); + } } else { this.ended = true; this.emit('exit'); @@ -215,7 +237,8 @@ fallback to default shell config: ${JSON.stringify(defaultShellConfig, undefined if (this.pty) { try { this.pty.resize(cols, rows); - } catch (err) { + } catch (_err) { + const err = _err as {stack: any}; console.error(err.stack); } } else { @@ -227,7 +250,8 @@ fallback to default shell config: ${JSON.stringify(defaultShellConfig, undefined if (this.pty) { try { this.pty.kill(); - } catch (err) { + } catch (_err) { + const err = _err as {stack: any}; console.error('exit error', err.stack); } } else { diff --git a/app/tsconfig.json b/app/tsconfig.json index 467bd920..5c7450fd 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -2,10 +2,13 @@ "extends": "../tsconfig.base.json", "compilerOptions": { "declarationDir": "../dist/tmp/appdts/", - "outDir": "../target/" + "outDir": "../target/", + "noImplicitAny": false }, "include": [ "./**/*", - "./package.json" + "./package.json", + "../typings/extend-electron.d.ts", + "../typings/ext-modules.d.ts" ] } diff --git a/app/ui/contextmenu.ts b/app/ui/contextmenu.ts index f6819811..634ea882 100644 --- a/app/ui/contextmenu.ts +++ b/app/ui/contextmenu.ts @@ -1,8 +1,11 @@ +import type {MenuItemConstructorOptions, BrowserWindow} from 'electron'; + +import {execCommand} from '../commands'; +import {getProfiles} from '../config'; import editMenu from '../menus/menus/edit'; import shellMenu from '../menus/menus/shell'; -import {execCommand} from '../commands'; import {getDecoratedKeymaps} from '../plugins'; -import {MenuItemConstructorOptions, BrowserWindow} from 'electron'; + const separator: MenuItemConstructorOptions = {type: 'separator'}; const getCommandKeys = (keymaps: Record): Record => @@ -20,14 +23,20 @@ const filterCutCopy = (selection: string, menuItem: MenuItemConstructorOptions) return menuItem; }; -export default ( +const contextMenuTemplate = ( createWindow: (fn?: (win: BrowserWindow) => void, options?: Record) => BrowserWindow, selection: string ) => { const commandKeys = getCommandKeys(getDecoratedKeymaps()); - const _shell = shellMenu(commandKeys, execCommand).submenu as MenuItemConstructorOptions[]; + const _shell = shellMenu( + commandKeys, + execCommand, + getProfiles().map((p) => p.name) + ).submenu as MenuItemConstructorOptions[]; const _edit = editMenu(commandKeys, execCommand).submenu.filter(filterCutCopy.bind(null, selection)); return _edit .concat(separator, _shell) .filter((menuItem) => !Object.prototype.hasOwnProperty.call(menuItem, 'enabled') || menuItem.enabled); }; + +export default contextMenuTemplate; diff --git a/app/ui/window.ts b/app/ui/window.ts index 2f001460..87eba6d7 100644 --- a/app/ui/window.ts +++ b/app/ui/window.ts @@ -1,25 +1,36 @@ -import {app, BrowserWindow, shell, Menu, BrowserWindowConstructorOptions} from 'electron'; +import {existsSync} from 'fs'; import {isAbsolute, normalize, sep} from 'path'; -import {parse as parseUrl} from 'url'; -import {v4 as uuidv4} from 'uuid'; -import fileUriToPath from 'file-uri-to-path'; +import {URL, fileURLToPath} from 'url'; + +import {app, BrowserWindow, shell, Menu} from 'electron'; +import type {BrowserWindowConstructorOptions} from 'electron'; + +import {enable as remoteEnable} from '@electron/remote/main'; import isDev from 'electron-is-dev'; -import updater from '../updater'; -import toElectronBackgroundColor from '../utils/to-electron-background-color'; -import {icon, homeDirectory} from '../config/paths'; -import createRPC from '../rpc'; -import notify from '../notify'; -import fetchNotifications from '../notifications'; -import Session from '../session'; -import contextMenuTemplate from './contextmenu'; +import {getWorkingDirectoryFromPID} from 'native-process-working-directory'; +import {v4 as uuidv4} from 'uuid'; + +import type {sessionExtraOptions} from '../../typings/common'; +import type {configOptions} from '../../typings/config'; import {execCommand} from '../commands'; -import {setRendererType, unsetRendererType} from '../utils/renderer-utils'; +import {getDefaultProfile} from '../config'; +import {icon, homeDirectory} from '../config/paths'; +import fetchNotifications from '../notifications'; +import notify from '../notify'; import {decorateSessionOptions, decorateSessionClass} from '../plugins'; +import createRPC from '../rpc'; +import Session from '../session'; +import updater from '../updater'; +import {setRendererType, unsetRendererType} from '../utils/renderer-utils'; +import toElectronBackgroundColor from '../utils/to-electron-background-color'; + +import contextMenuTemplate from './contextmenu'; export function newWindow( options_: BrowserWindowConstructorOptions, - cfg: any, - fn?: (win: BrowserWindow) => void + cfg: configOptions, + fn?: (win: BrowserWindow) => void, + profileName: string = getDefaultProfile() ): BrowserWindow { const classOpts = Object.assign({uid: uuidv4()}); app.plugins.decorateWindowClass(classOpts); @@ -39,12 +50,17 @@ export function newWindow( webPreferences: { nodeIntegration: true, navigateOnDragDrop: true, - enableRemoteModule: true, contextIsolation: false }, ...options_ }; const window = new BrowserWindow(app.plugins.getDecoratedBrowserOptions(winOpts)); + + window.profileName = profileName; + + // Enable remote module on this window + remoteEnable(window.webContents); + window.uid = classOpts.uid; app.plugins.onWindowClass(window); @@ -54,28 +70,13 @@ export function newWindow( const sessions = new Map(); const updateBackgroundColor = () => { - const cfg_ = app.plugins.getDecoratedConfig(); + const cfg_ = app.plugins.getDecoratedConfig(profileName); window.setBackgroundColor(toElectronBackgroundColor(cfg_.backgroundColor || '#000')); }; - // set working directory - let argPath = process.argv[1]; - if (argPath && process.platform === 'win32') { - if (/[a-zA-Z]:"/.test(argPath)) { - argPath = argPath.replace('"', sep); - } - argPath = normalize(argPath + sep); - } - let workingDirectory = homeDirectory; - if (argPath && isAbsolute(argPath)) { - workingDirectory = argPath; - } else if (cfg.workingDirectory && isAbsolute(cfg.workingDirectory)) { - workingDirectory = cfg.workingDirectory; - } - // config changes const cfgUnsubscribe = app.config.subscribe(() => { - const cfg_ = app.plugins.getDecoratedConfig(); + const cfg_ = app.plugins.getDecoratedConfig(profileName); // notify renderer window.webContents.send('config change'); @@ -118,23 +119,58 @@ export function newWindow( } }); - function createSession(extraOptions: any = {}) { + function createSession(extraOptions: sessionExtraOptions = {}) { const uid = uuidv4(); - const extraOptionsFiltered: any = {}; + const extraOptionsFiltered: sessionExtraOptions = {}; Object.keys(extraOptions).forEach((key) => { if (extraOptions[key] !== undefined) extraOptionsFiltered[key] = extraOptions[key]; }); + const profile = extraOptionsFiltered.profile || profileName; + const activeSession = extraOptionsFiltered.activeUid ? sessions.get(extraOptionsFiltered.activeUid) : undefined; + let cwd = ''; + if (cfg.preserveCWD !== false && activeSession && activeSession.profile === profile) { + const activePID = activeSession.pty?.pid; + if (activePID !== undefined) { + try { + cwd = getWorkingDirectoryFromPID(activePID) || ''; + } catch (error) { + console.error(error); + } + } + cwd = cwd && isAbsolute(cwd) && existsSync(cwd) ? cwd : ''; + } + + const profileCfg = app.plugins.getDecoratedConfig(profile); + + // set working directory + let argPath = process.argv[1]; + if (argPath && process.platform === 'win32') { + if (/[a-zA-Z]:"/.test(argPath)) { + argPath = argPath.replace('"', sep); + } + argPath = normalize(argPath + sep); + } + let workingDirectory = homeDirectory; + if (argPath && isAbsolute(argPath)) { + workingDirectory = argPath; + } else if (profileCfg.workingDirectory && isAbsolute(profileCfg.workingDirectory)) { + workingDirectory = profileCfg.workingDirectory; + } + // remove the rows and cols, the wrong value of them will break layout when init create const defaultOptions = Object.assign( { - cwd: workingDirectory, + cwd: cwd || workingDirectory, splitDirection: undefined, - shell: cfg.shell, - shellArgs: cfg.shellArgs && Array.from(cfg.shellArgs) + shell: profileCfg.shell, + shellArgs: profileCfg.shellArgs && Array.from(profileCfg.shellArgs) }, extraOptionsFiltered, - {uid} + { + profile: extraOptionsFiltered.profile || profileName, + uid + } ); const options = decorateSessionOptions(defaultOptions); const DecoratedSession = decorateSessionClass(Session); @@ -154,7 +190,8 @@ export function newWindow( splitDirection: options.splitDirection, shell: session.shell, pid: session.pty ? session.pty.pid : null, - activeUid: options.activeUid + activeUid: options.activeUid ?? undefined, + profile: options.profile }); session.on('data', (data: string) => { @@ -189,8 +226,8 @@ export function newWindow( session.resize({cols, rows}); } }); - rpc.on('data', ({uid, data, escaped}: {uid: string; data: string; escaped: boolean}) => { - const session = sessions.get(uid); + rpc.on('data', ({uid, data, escaped}) => { + const session = uid && sessions.get(uid); if (session) { if (escaped) { const escapedData = session.shell?.endsWith('cmd.exe') @@ -220,11 +257,12 @@ export function newWindow( // Same deal as above, grabbing the window titlebar when the window // is maximized on Windows results in unmaximize, without hitting any // app buttons - for (const ev of ['maximize', 'unmaximize', 'minimize', 'restore'] as any) { - window.on(ev, () => { - rpc.emit('windowGeometry change', {}); - }); - } + const onGeometryChange = () => rpc.emit('windowGeometry change', {isMaximized: window.isMaximized()}); + window.on('maximize', onGeometryChange); + window.on('unmaximize', onGeometryChange); + window.on('minimize', onGeometryChange); + window.on('restore', onGeometryChange); + window.on('move', () => { const position = window.getPosition(); rpc.emit('move', {bounds: {x: position[0], y: position[1]}}); @@ -238,10 +276,10 @@ export function newWindow( }); // pass on the full screen events from the window to react rpc.win.on('enter-full-screen', () => { - rpc.emit('enter full screen', {}); + rpc.emit('enter full screen'); }); rpc.win.on('leave-full-screen', () => { - rpc.emit('leave full screen', {}); + rpc.emit('leave full screen'); }); const deleteSessions = () => { sessions.forEach((session, key) => { @@ -259,29 +297,32 @@ export function newWindow( } }); - // If file is dropped onto the terminal window, navigate event is prevented - // and his path is added to active session. - window.webContents.on('will-navigate', (event, url) => { - const protocol = typeof url === 'string' && parseUrl(url).protocol; + const handleDroppedURL = (url: string) => { + const protocol = typeof url === 'string' && new URL(url).protocol; if (protocol === 'file:') { - event.preventDefault(); - - const path = fileUriToPath(url); - - rpc.emit('session data send', {data: path, escaped: true}); + const path = fileURLToPath(url); + return {uid: null, data: path, escaped: true}; } else if (protocol === 'http:' || protocol === 'https:') { + return {uid: null, data: url}; + } + }; + + // If file is dropped onto the terminal window, navigate and new-window events are prevented + // and it's path is added to active session. + window.webContents.on('will-navigate', (event, url) => { + const data = handleDroppedURL(url); + if (data) { event.preventDefault(); - rpc.emit('session data send', {data: url}); + rpc.emit('session data send', data); } }); - - // xterm makes link clickable - window.webContents.on('new-window', (event, url) => { - const protocol = typeof url === 'string' && parseUrl(url).protocol; - if (protocol === 'http:' || protocol === 'https:') { - event.preventDefault(); - void shell.openExternal(url); + window.webContents.setWindowOpenHandler(({url}) => { + const data = handleDroppedURL(url); + if (data) { + rpc.emit('session data send', data); + return {action: 'deny'}; } + return {action: 'allow'}; }); // expose internals to extension authors diff --git a/app/updater.ts b/app/updater.ts index 5b50665f..c7d07d4d 100644 --- a/app/updater.ts +++ b/app/updater.ts @@ -1,25 +1,46 @@ // Packages -import electron, {app, BrowserWindow, AutoUpdater} from 'electron'; -import ms from 'ms'; +import electron, {app} from 'electron'; +import type {BrowserWindow, AutoUpdater} from 'electron'; + import retry from 'async-retry'; +import ms from 'ms'; // Utilities +import autoUpdaterLinux from './auto-updater-linux'; +import {getDefaultProfile} from './config'; import {version} from './package.json'; import {getDecoratedConfig} from './plugins'; -import autoUpdaterLinux from './auto-updater-linux'; const {platform} = process; const isLinux = platform === 'linux'; const autoUpdater: AutoUpdater = isLinux ? autoUpdaterLinux : electron.autoUpdater; +const getDecoratedConfigWithRetry = async () => { + return await retry(() => { + const content = getDecoratedConfig(getDefaultProfile()); + if (!content) { + throw new Error('No config content loaded'); + } + return content; + }); +}; + +const checkForUpdates = async () => { + const config = await getDecoratedConfigWithRetry(); + if (!config.disableAutoUpdates) { + autoUpdater.checkForUpdates(); + } +}; + let isInit = false; // Default to the "stable" update channel let canaryUpdates = false; const buildFeedUrl = (canary: boolean, currentVersion: string) => { const updatePrefix = canary ? 'releases-canary' : 'releases'; - return `https://${updatePrefix}.hyper.is/update/${isLinux ? 'deb' : platform}/${currentVersion}`; + const archSuffix = process.arch === 'arm64' || app.runningUnderARM64Translation ? '_arm64' : ''; + return `https://${updatePrefix}.hyper.is/update/${isLinux ? 'deb' : platform}${archSuffix}/${currentVersion}`; }; const isCanary = (updateChannel: string) => updateChannel === 'canary'; @@ -29,15 +50,7 @@ async function init() { console.error('Error fetching updates', `${err.message} (${err.stack})`); }); - const config = await retry(() => { - const content = getDecoratedConfig(); - - if (!content) { - throw new Error('No config content loaded'); - } - - return content; - }); + const config = await getDecoratedConfigWithRetry(); // If defined in the config, switch to the "canary" channel if (config.updateChannel && isCanary(config.updateChannel)) { @@ -49,58 +62,59 @@ async function init() { autoUpdater.setFeedURL({url: feedURL}); setTimeout(() => { - autoUpdater.checkForUpdates(); + void checkForUpdates(); }, ms('10s')); setInterval(() => { - autoUpdater.checkForUpdates(); + void checkForUpdates(); }, ms('30m')); isInit = true; } -export default (win: BrowserWindow) => { +const updater = (win: BrowserWindow) => { if (!isInit) { void init(); } const {rpc} = win; - const onupdate = ( - ev: Event, - releaseNotes: string, - releaseName: string, - date: Date, - updateUrl: string, - onQuitAndInstall: any - ) => { + const onupdate = (ev: Event, releaseNotes: string, releaseName: string, date: Date, updateUrl: string) => { const releaseUrl = updateUrl || `https://github.com/vercel/hyper/releases/tag/${releaseName}`; - rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !!onQuitAndInstall}); + rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !isLinux}); }; - const eventName: any = isLinux ? 'update-available' : 'update-downloaded'; - - autoUpdater.on(eventName, onupdate); + if (isLinux) { + autoUpdater.on('update-available', onupdate); + } else { + autoUpdater.on('update-downloaded', onupdate); + } rpc.once('quit and install', () => { autoUpdater.quitAndInstall(); }); - app.config.subscribe(() => { - const {updateChannel} = app.plugins.getDecoratedConfig(); + app.config.subscribe(async () => { + const {updateChannel} = await getDecoratedConfigWithRetry(); const newUpdateIsCanary = isCanary(updateChannel); if (newUpdateIsCanary !== canaryUpdates) { const feedURL = buildFeedUrl(newUpdateIsCanary, version); autoUpdater.setFeedURL({url: feedURL}); - autoUpdater.checkForUpdates(); + void checkForUpdates(); canaryUpdates = newUpdateIsCanary; } }); win.on('close', () => { - autoUpdater.removeListener(eventName, onupdate); + if (isLinux) { + autoUpdater.removeListener('update-available', onupdate); + } else { + autoUpdater.removeListener('update-downloaded', onupdate); + } }); }; + +export default updater; diff --git a/app/utils/cli-install.ts b/app/utils/cli-install.ts index 7cac74a6..3a5b9930 100644 --- a/app/utils/cli-install.ts +++ b/app/utils/cli-install.ts @@ -1,20 +1,23 @@ -import pify from 'pify'; -import fs from 'fs'; +import {existsSync, readlink, symlink} from 'fs'; import path from 'path'; -import notify from '../notify'; -import {cliScriptPath, cliLinkPath} from '../config/paths'; +import {promisify} from 'util'; + +import {clipboard, dialog} from 'electron'; + +import {mkdirpSync} from 'fs-extra'; import * as Registry from 'native-reg'; import type {ValueType} from 'native-reg'; import sudoPrompt from 'sudo-prompt'; -import {clipboard, dialog} from 'electron'; -import {mkdirpSync} from 'fs-extra'; -const readlink = pify(fs.readlink); -const symlink = pify(fs.symlink); -const sudoExec = pify(sudoPrompt.exec, {multiArgs: true}); +import {cliScriptPath, cliLinkPath} from '../config/paths'; +import notify from '../notify'; + +const readLink = promisify(readlink); +const symLink = promisify(symlink); +const sudoExec = promisify(sudoPrompt.exec); const checkInstall = () => { - return readlink(cliLinkPath) + return readLink(cliLinkPath) .then((link) => link === cliScriptPath) .catch((err) => { if (err.code === 'ENOENT') { @@ -32,15 +35,16 @@ const addSymlink = async (silent: boolean) => { return; } console.log('Linking HyperCLI'); - if (!fs.existsSync(path.dirname(cliLinkPath))) { + if (!existsSync(path.dirname(cliLinkPath))) { try { mkdirpSync(path.dirname(cliLinkPath)); } catch (err) { throw `Failed to create directory ${path.dirname(cliLinkPath)} - ${err}`; } } - await symlink(cliScriptPath, cliLinkPath); - } catch (err) { + await symLink(cliScriptPath, cliLinkPath); + } catch (_err) { + const err = _err as {code: string}; // 'EINVAL' is returned by readlink, // 'EEXIST' is returned by symlink let error = @@ -61,7 +65,7 @@ sudo ln -sf "${cliScriptPath}" "${cliLinkPath}"`, await sudoExec(`ln -sf "${cliScriptPath}" "${cliLinkPath}"`, {name: 'Hyper'}); return; } catch (_error) { - error = _error[0]; + error = (_error as any[])[0]; } } else if (result.response === 1) { clipboard.writeText(`sudo ln -sf "${cliScriptPath}" "${cliLinkPath}"`); diff --git a/app/utils/map-keys.ts b/app/utils/map-keys.ts index e9c79423..4bcb9a1d 100644 --- a/app/utils/map-keys.ts +++ b/app/utils/map-keys.ts @@ -11,7 +11,7 @@ const generatePrefixedCommand = (command: string, shortcuts: string[]) => { return result; }; -export default (config: Record) => { +const mapKeys = (config: Record) => { return Object.keys(config).reduce((keymap: Record, command: string) => { if (!command) { return keymap; @@ -39,3 +39,5 @@ export default (config: Record) => { return keymap; }, {}); }; + +export default mapKeys; diff --git a/app/utils/shell-fallback.ts b/app/utils/shell-fallback.ts new file mode 100644 index 00000000..0eb7df4b --- /dev/null +++ b/app/utils/shell-fallback.ts @@ -0,0 +1,25 @@ +export const getFallBackShellConfig = ( + shell: string, + shellArgs: string[], + defaultShell: string, + defaultShellArgs: string[] +): { + shell: string; + shellArgs: string[]; +} | null => { + if (shellArgs.length > 0) { + return { + shell, + shellArgs: [] + }; + } + + if (shell != defaultShell) { + return { + shell: defaultShell, + shellArgs: defaultShellArgs + }; + } + + return null; +}; diff --git a/app/utils/system-context-menu.ts b/app/utils/system-context-menu.ts index e0c2b94e..80522d0c 100644 --- a/app/utils/system-context-menu.ts +++ b/app/utils/system-context-menu.ts @@ -9,7 +9,7 @@ const regKeys = [ ]; const regParts = [ {key: 'command', name: '', value: `${appPath} "%V"`}, - {name: '', value: 'Open Hyper here'}, + {name: '', value: 'Open &Hyper here'}, {name: 'Icon', value: `${appPath}`} ]; diff --git a/app/utils/to-electron-background-color.ts b/app/utils/to-electron-background-color.ts index 1a526534..a758f528 100644 --- a/app/utils/to-electron-background-color.ts +++ b/app/utils/to-electron-background-color.ts @@ -4,7 +4,7 @@ import Color from 'color'; // returns a background color that's in hex // format including the alpha channel (e.g.: `#00000050`) // input can be any css value (rgb, hsl, stringâ€Ļ) -export default (bgColor: string) => { +const toElectronBackgroundColor = (bgColor: string) => { const color = Color(bgColor); if (color.alpha() === 1) { @@ -13,5 +13,7 @@ export default (bgColor: string) => { // http://stackoverflow.com/a/11019879/1202488 const alphaHex = Math.round(color.alpha() * 255).toString(16); - return `#${alphaHex}${color.hex().toString().substr(1)}`; + return `#${alphaHex}${color.hex().toString().slice(1)}`; }; + +export default toElectronBackgroundColor; diff --git a/app/yarn.lock b/app/yarn.lock index dee3d210..8e654f1d 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -2,17 +2,32 @@ # yarn lockfile v1 -ajv-formats@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.0.2.tgz#69875cb99d76c74be46e9c7a4444bc232354eba0" - integrity sha512-Brah4Uo5/U8v76c6euTwtjVFFaVishwnJrQBYpev1JRh4vjA1F4HY3UzQez41YUCszUCXKagG8v6eVRBHV1gkw== +"@babel/parser@7.24.4": + version "7.24.4" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz#234487a110d89ad5a3ed4a8a566c36b9453e8c88" + integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg== + +"@electron/remote@2.1.2": + version "2.1.2" + resolved "https://registry.npmjs.org/@electron/remote/-/remote-2.1.2.tgz#52a97c8faa5b769155b649ef262f2f8c851776e6" + integrity sha512-EPwNx+nhdrTBxyCqXt/pftoQg/ybtWDW3DUWHafejvnB1ZGGfMpv6e15D8KeempocjXe78T7WreyGGb3mlZxdA== + +"@types/semver@^7.3.8": + version "7.3.8" + resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.8.tgz#508a27995498d7586dcecd77c25e289bfaf90c59" + integrity sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now== + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== dependencies: ajv "^8.0.0" -ajv@^8.0.0, ajv@^8.1.0: - version "8.1.0" - resolved "https://registry.npmjs.org/ajv/-/ajv-8.1.0.tgz#45d5d3d36c7cdd808930cc3e603cf6200dbeb736" - integrity sha512-B/Sk2Ix7A36fs/ZkuGLIR86EdjbgR6fsAcbx9lOP/QBSXujDNbVmIS/U4Itz5k8fPFDeVZl/zQ/gJW4Jrq6XjQ== +ajv@^8.0.0, ajv@^8.6.3: + version "8.6.3" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764" + integrity sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -20,9 +35,9 @@ ajv@^8.0.0, ajv@^8.1.0: uri-js "^4.2.2" ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + version "4.1.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== anymatch@~3.1.2: version "3.1.2" @@ -32,23 +47,43 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -async-retry@1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/async-retry/-/async-retry-1.3.1.tgz#139f31f8ddce50c0870b0ba558a6079684aaed55" - integrity sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA== +ast-types@^0.16.1: + version "0.16.1" + resolved "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz#7a9da1617c9081bc121faafe91711b4c8bb81da2" + integrity sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg== dependencies: - retry "0.12.0" + tslib "^2.0.1" + +async-retry@1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" atomically@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe" integrity sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w== +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + binary-extensions@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + braces@~3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -56,10 +91,10 @@ braces@~3.0.2: dependencies: fill-range "^7.0.1" -chokidar@^3.5.2: - version "3.5.2" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" - integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== +chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== dependencies: anymatch "~3.1.2" braces "~3.0.2" @@ -71,46 +106,46 @@ chokidar@^3.5.2: optionalDependencies: fsevents "~2.3.2" -color-convert@^1.9.1: - version "1.9.3" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: - color-name "1.1.3" + color-name "~1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@^1.0.0: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.5.4: - version "1.5.4" - resolved "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" - integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== +color-string@^1.9.0: + version "1.9.0" + resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" + integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" -color@3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" - integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== +color@4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== dependencies: - color-convert "^1.9.1" - color-string "^1.5.4" + color-convert "^2.0.1" + color-string "^1.9.0" -conf@^10.0.0: - version "10.0.1" - resolved "https://registry.npmjs.org/conf/-/conf-10.0.1.tgz#038093e5cbddc0e59bc14f63382c4ce732a4781d" - integrity sha512-QClEoNcruwBL84QgMEPHibL3ERxWIrRKhbjJKG1VsFBadm5QpS0jsu4QjY/maxUvhyAKXeyrs+ws+lC6PajnEg== +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +conf@^10.2.0: + version "10.2.0" + resolved "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz#838e757be963f1a2386dfe048a98f8f69f7b55d6" + integrity sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg== dependencies: - ajv "^8.1.0" - ajv-formats "^2.0.2" + ajv "^8.6.3" + ajv-formats "^2.1.1" atomically "^1.7.0" debounce-fn "^4.0.0" dot-prop "^6.0.1" @@ -120,13 +155,10 @@ conf@^10.0.0: pkg-up "^3.1.0" semver "^7.3.5" -convert-css-color-name-to-hex@0.1.1: - version "0.1.1" - resolved "https://registry.npmjs.org/convert-css-color-name-to-hex/-/convert-css-color-name-to-hex-0.1.1.tgz#38ac4d27ca470593fd663b18a072a308926a35a2" - integrity sha1-OKxNJ8pHBZP9ZjsYoHKjCJJqNaI= - dependencies: - css-color-names "0.0.3" - is-css-color-name "^0.1.1" +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== cross-spawn@^6.0.0: version "6.0.5" @@ -148,16 +180,6 @@ cross-spawn@^7.0.0: shebang-command "^2.0.0" which "^2.0.1" -css-color-names@0.0.2: - version "0.0.2" - resolved "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.2.tgz#fba18e8cff86579572d749c146c47ee83f0ea955" - integrity sha1-+6GOjP+GV5Vy10nBRsR+6D8OqVU= - -css-color-names@0.0.3: - version "0.0.3" - resolved "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.3.tgz#de0cef16f4d8aa8222a320d5b6d7e9bbada7b9f6" - integrity sha1-3gzvFvTYqoIioyDVttfpu62nufY= - debounce-fn@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz#ed76d206d8a50e60de0dd66d494d82835ffe61c7" @@ -177,10 +199,20 @@ dot-prop@^6.0.1: dependencies: is-obj "^2.0.0" -electron-fetch@1.7.3: - version "1.7.3" - resolved "https://registry.npmjs.org/electron-fetch/-/electron-fetch-1.7.3.tgz#06cf363d7f64073ec00a37e9949ec9d29ce6b08a" - integrity sha512-1AVMaxrHXTTMqd7EK0MGWusdqNr07Rpj8Th6bG4at0oNgIi/1LBwa9CjT/0Zy+M0k/tSJPS04nFxHj0SXDVgVw== +electron-devtools-installer@3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/electron-devtools-installer/-/electron-devtools-installer-3.2.0.tgz#acc48d24eb7033fe5af284a19667e73b78d406d0" + integrity sha512-t3UczsYugm4OAbqvdImMCImIMVdFzJAHgbwHpkl5jmfu1izVgUcP/mnrPqJIpEeCK1uZGpt+yHgWEN+9EwoYhQ== + dependencies: + rimraf "^3.0.2" + semver "^7.2.1" + tslib "^2.1.0" + unzip-crx-3 "^0.2.0" + +electron-fetch@1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/electron-fetch/-/electron-fetch-1.9.1.tgz#e28bfe78d467de3f2dec884b1d72b8b05322f30f" + integrity sha512-M9qw6oUILGVrcENMSRRefE1MbHPIz0h79EKIeJWK9v563aT9Qkh8aEHPO1H5vi970wPirNY+jO9OpFoLiMsMGA== dependencies: encoding "^0.1.13" @@ -189,13 +221,13 @@ electron-is-dev@2.0.0: resolved "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-2.0.0.tgz#833487a069b8dad21425c67a19847d9064ab19bd" integrity sha512-3X99K852Yoqu9AcW50qz3ibYBWY79/pBhlMCab8ToEWS48R0T9tyxRiQhwylE7zQdXrMnx2JKqUJyMPmt5FBqA== -electron-store@8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/electron-store/-/electron-store-8.0.0.tgz#81a4e687958e2dae1c5c84cc099a8148be776337" - integrity sha512-ZgRPUZkfrrjWSqxZeaxu7lEvmYf6tgl49dLMqxXGnEmliSiwv3u4rJPG+mH3fBQP9PBqgSh4TCuxHZImMMUgWg== +electron-store@8.2.0: + version "8.2.0" + resolved "https://registry.npmjs.org/electron-store/-/electron-store-8.2.0.tgz#114e6e453e8bb746ab4ccb542424d8c881ad2ca1" + integrity sha512-ukLL5Bevdil6oieAOXz3CMy+OgaItMiVBg701MNlG6W5RaC0AHN7rvlqTCmeb6O7jP0Qa1KKYTE0xV0xbhF4Hw== dependencies: - conf "^10.0.0" - type-fest "^1.0.2" + conf "^10.2.0" + type-fest "^2.17.0" encoding@^0.1.13: version "0.1.13" @@ -216,6 +248,11 @@ env-paths@^2.2.1: resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== +esprima@~4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + execa@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" @@ -249,11 +286,6 @@ fast-deep-equal@^3.1.1: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -file-uri-to-path@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz#7b415aeba227d575851e0a5b0c640d7656403fba" - integrity sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg== - fill-range@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -268,15 +300,20 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -fs-extra@10.0.0: - version "10.0.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" - integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== +fs-extra@11.2.0: + version "11.2.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" universalify "^2.0.0" +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + fsevents@~2.3.2: version "2.3.2" resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" @@ -296,12 +333,13 @@ get-stream@^5.0.0: dependencies: pump "^3.0.0" -git-describe@4.0.4: - version "4.0.4" - resolved "https://registry.npmjs.org/git-describe/-/git-describe-4.0.4.tgz#f3d55bce309becf6dc27fed535d380a621967e8c" - integrity sha512-L1X9OO1e4MusB4PzG9LXeXCQifRvyuoHTpuuZ521Qyxn/B0kWHWEOtsT4LsSfSNacZz0h4ZdYDsDG7f+SrA3hg== +git-describe@4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/git-describe/-/git-describe-4.1.1.tgz#a2a2882e442aa68abd0b3cb467459c83ed2f96ef" + integrity sha512-JC8ganO5kO80G8+XE98TDDjnMXQN3Estk3qdJuG2EGRF/l6zuMTMcN+8OSfQZ5FWpqIRLB015anWX4aSRgnxAQ== dependencies: - lodash "^4.17.11" + "@types/semver" "^7.3.8" + lodash "^4.17.21" optionalDependencies: semver "^5.6.0" @@ -312,6 +350,18 @@ glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.4" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" @@ -329,7 +379,20 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -inherits@~2.0.3: +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -351,13 +414,6 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-css-color-name@^0.1.1: - version "0.1.3" - resolved "https://registry.npmjs.org/is-css-color-name/-/is-css-color-name-0.1.3.tgz#ea3b51bc901d8a243d32c9b7873d0680dbbef7f1" - integrity sha1-6jtRvJAdiiQ9Msm3hz0GgNu+9/E= - dependencies: - css-color-names "0.0.2" - is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -380,13 +436,6 @@ is-obj@^2.0.0: resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== -is-ssh@^1.3.0: - version "1.3.2" - resolved "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.2.tgz#a4b82ab63d73976fd8263cceee27f99a88bdae2b" - integrity sha512-elEw0/0c2UscLrNG+OAorbP539E3rhliKPg+hDMWN9VwrDXfYK+4PBEykDPfxlYYtQvl84TascnQyobfQLHEhQ== - dependencies: - protocols "^1.1.0" - is-stream@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -397,6 +446,11 @@ is-stream@^2.0.0: resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -426,6 +480,16 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jszip@^3.1.0: + version "3.10.1" + resolved "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + lcid@^3.0.0: version "3.1.1" resolved "https://registry.npmjs.org/lcid/-/lcid-3.1.1.tgz#9030ec479a058fc36b5e8243ebaac8b6ac582fd0" @@ -433,6 +497,13 @@ lcid@^3.0.0: dependencies: invert-kv "^3.0.0" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + locate-path@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -441,7 +512,7 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" -lodash@4.17.21, lodash@^4.17.11: +lodash@4.17.21, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -491,55 +562,76 @@ mimic-fn@^3.0.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== -mkdirp@1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.1: + version "0.5.6" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" ms@2.1.3: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nan@^2.14.0: - version "2.14.2" - resolved "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" - integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== +nan@^2.17.0: + version "2.17.0" + resolved "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" + integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== -native-reg@0.3.5: - version "0.3.5" - resolved "https://registry.npmjs.org/native-reg/-/native-reg-0.3.5.tgz#80f6601028f236be68ef45b5c17493a94abb1145" - integrity sha512-lwaSAbq02DZ2aFK/ZvHzDpaHQSo7fh0GjIol+juMe6iwmeBO+ZMXFSPLfpOksOhcxgEBhgCU1bVm1G0u9lkdpA== +native-process-working-directory@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/native-process-working-directory/-/native-process-working-directory-1.0.2.tgz#7843e2fa1490f53cf8d2c7d1913de8b275e8b89a" + integrity sha512-3a67QQV8r3YMUTSOgvtMOCjPDgCpb/8xjv93L8Cqb8bv3hOKsWis4/+8HCu3bgj8ADQV75SCYFSsAGM5G0cXmQ== dependencies: - node-gyp-build "^4" + node-addon-api "^3.1.0" + +native-reg@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/native-reg/-/native-reg-1.1.1.tgz#cc5e12218c1da2f71d7421f617fc133441d70c77" + integrity sha512-DmqwT6XC8MLwo8HaZey3bASf0aa/gHC7FAuKMjuf7fXa7FLXwz/khXGouKcmD1rXAfJME1XveKSM4+86wLkb1w== + dependencies: + node-gyp-build "4" nice-try@^1.0.4: version "1.0.5" resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-gyp-build@^4: - version "4.2.3" - resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739" - integrity sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg== +node-addon-api@^3.1.0: + version "3.2.1" + resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== -node-pty@0.10.1: - version "0.10.1" - resolved "https://registry.npmjs.org/node-pty/-/node-pty-0.10.1.tgz#cd05d03a2710315ec40221232ec04186f6ac2c6d" - integrity sha512-JTdtUS0Im/yRsWJSx7yiW9rtpfmxqxolrtnyKwPLI+6XqTAPW/O2MjS8FYL4I5TsMbH2lVgDb2VMjp+9LoQGNg== +node-gyp-build@4: + version "4.3.0" + resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" + integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== + +node-pty@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz#7daafc0aca1c4ca3de15c61330373af4af5861fd" + integrity sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA== dependencies: - nan "^2.14.0" + nan "^2.17.0" normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -normalize-url@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-6.0.1.tgz#a4f27f58cf8c7b287b440b8a8201f42d0b00d256" - integrity sha512-VU4pzAuh7Kip71XEmO9aNREYAdMHFGTVj/i+CaTImS8x0i1d3jUZkXhqluy/PRgjPLMgsLQulYY3PJ/aSbSjpQ== - npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -554,15 +646,10 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" -object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -once@^1.3.1, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" @@ -616,29 +703,35 @@ p-try@^2.0.0: resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -parse-path@^4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/parse-path/-/parse-path-4.0.2.tgz#ef14f0d3d77bae8dd4bc66563a4c151aac9e65aa" - integrity sha512-HSqVz6iuXSiL8C1ku5Gl1Z5cwDd9Wo0q8CoffdAghP6bz8pJa1tcMC+m4N+z6VAS8QdksnIGq1TB6EgR4vPR6w== - dependencies: - is-ssh "^1.3.0" - protocols "^1.4.0" +pako@~1.0.2: + version "1.0.11" + resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== -parse-url@5.0.3: - version "5.0.3" - resolved "https://registry.npmjs.org/parse-url/-/parse-url-5.0.3.tgz#c158560f14cb1560917e0b7fd8b01adc1e9d3cab" - integrity sha512-nrLCVMJpqo12X8uUJT4GJPd5AFaTOrGx/QpJy3HNcVtq0AZSstVIsnxS5fqNPuoqMUs3MyfBoOP6Zvu2Arok5A== +parse-path@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz#605a2d58d0a749c8594405d8cc3a2bf76d16099b" + integrity sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog== dependencies: - is-ssh "^1.3.0" - normalize-url "^6.0.1" - parse-path "^4.0.0" - protocols "^1.4.0" + protocols "^2.0.0" + +parse-url@8.1.0: + version "8.1.0" + resolved "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz#972e0827ed4b57fc85f0ea6b0d839f0d8a57a57d" + integrity sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w== + dependencies: + parse-path "^7.0.0" path-exists@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -654,11 +747,6 @@ picomatch@^2.0.4, picomatch@^2.2.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== -pify@5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" - integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== - pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" @@ -666,10 +754,15 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -protocols@^1.1.0, protocols@^1.4.0: - version "1.4.8" - resolved "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8" - integrity sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg== +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +protocols@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86" + integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q== pump@^3.0.0: version "3.0.0" @@ -691,22 +784,33 @@ queue@6.0.2: dependencies: inherits "~2.0.3" -react-dom@17.0.2: - version "17.0.2" - resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== +react-dom@18.2.0: + version "18.2.0" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" + scheduler "^0.23.0" -react@17.0.2: - version "17.0.2" - resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== +react@18.2.0: + version "18.2.0" + resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" + +readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" readdirp@~3.6.0: version "3.6.0" @@ -715,33 +819,55 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +recast@0.23.6: + version "0.23.6" + resolved "https://registry.npmjs.org/recast/-/recast-0.23.6.tgz#198fba74f66143a30acc81929302d214ce4e3bfa" + integrity sha512-9FHoNjX1yjuesMwuthAmPKabxYQdOgihFYmT5ebXfYGBcnqXZf3WOVz+5foEZ8Y83P4ZY6yQD5GMmtV+pgCCAQ== + dependencies: + ast-types "^0.16.1" + esprima "~4.0.0" + source-map "~0.6.1" + tiny-invariant "^1.3.3" + tslib "^2.0.1" + require-from-string@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -retry@0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= +retry@0.13.1: + version "0.13.1" + resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" -semver@7.3.5, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== +semver@7.6.0, semver@^7.2.1, semver@^7.3.5: + version "7.6.0" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== dependencies: lru-cache "^6.0.0" @@ -750,6 +876,11 @@ semver@^5.5.0, semver@^5.6.0: resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -783,7 +914,12 @@ shell-env@3.0.1: execa "^1.0.0" strip-ansi "^5.2.0" -signal-exit@^3.0.0, signal-exit@^3.0.2: +signal-exit@^3.0.0: + version "3.0.7" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== @@ -795,6 +931,18 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" @@ -817,6 +965,11 @@ sudo-prompt@^9.2.1: resolved "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz#77efb84309c9ca489527a4e749f287e6bdd52afd" integrity sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw== +tiny-invariant@^1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -824,16 +977,30 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -type-fest@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-1.0.2.tgz#3f9c39982859f385c77c38b7e5f1432b8a3661c6" - integrity sha512-a720oz3Kjbp3ll0zkeN9qjRhO7I34MKMhPGQiQJAmaZQZQ1lo+NWThK322f7sXV+kTg9B1Ybt16KgBXWgteT8w== +tslib@^2.0.1, tslib@^2.1.0: + version "2.6.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" + integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== + +type-fest@^2.17.0: + version "2.18.0" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-2.18.0.tgz#fdef3a74e0a9e68ebe46054836650fb91ac3881e" + integrity sha512-pRS+/yrW5TjPPHNOvxhbNZexr2bS63WjrMU8a+VzEBhUi9Tz1pZeD+vQz3ut0svZ46P+SRqMEPnJmk2XnvNzTw== universalify@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +unzip-crx-3@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/unzip-crx-3/-/unzip-crx-3-0.2.0.tgz#d5324147b104a8aed9ae8639c95521f6f7cda292" + integrity sha512-0+JiUq/z7faJ6oifVB5nSwt589v1KCduqIJupNVDoWSXZtWDmjDGO3RAEOvwJ07w90aoXoP4enKsR7ecMrJtWQ== + dependencies: + jszip "^3.1.0" + mkdirp "^0.5.1" + yaku "^0.16.6" + uri-js@^4.2.2: version "4.4.0" resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" @@ -841,10 +1008,15 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -uuid@8.3.2: - version "8.3.2" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +uuid@9.0.1: + version "9.0.1" + resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== which@^1.2.9: version "1.3.1" @@ -865,6 +1037,11 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +yaku@^0.16.6: + version "0.16.7" + resolved "https://registry.npmjs.org/yaku/-/yaku-0.16.7.tgz#1d195c78aa9b5bf8479c895b9504fd4f0847984e" + integrity sha512-Syu3IB3rZvKvYk7yTiyl1bo/jiEFaaStrgv1V2TIJTqYPStSMQVO8EQjg/z+DRzLq/4LIIharNT3iH1hylEIRw== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" diff --git a/assets/search-icons.svg b/assets/search-icons.svg deleted file mode 100644 index 48778e75..00000000 --- a/assets/search-icons.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - left arrow - - - - - - - - right arrow - - - - - - - - cancel - - - - - - - - - diff --git a/ava-spectron.config.js b/ava-e2e.config.js similarity index 50% rename from ava-spectron.config.js rename to ava-e2e.config.js index e7298452..0b2f46db 100644 --- a/ava-spectron.config.js +++ b/ava-e2e.config.js @@ -1,9 +1,5 @@ -export default { +module.exports = { files: ['test/*'], - babel: { - compileEnhancements: false, - compileAsTests: ['**/testUtils/**/*'] - }, extensions: ['ts'], require: ['ts-node/register/transpile-only'], timeout: '30s' diff --git a/ava.config.js b/ava.config.js index c8c5c9ae..a0237585 100644 --- a/ava.config.js +++ b/ava.config.js @@ -1,9 +1,5 @@ -export default { +module.exports = { files: ['test/unit/*'], - babel: { - compileEnhancements: false, - compileAsTests: ['**/testUtils/**/*'] - }, extensions: ['ts'], require: ['ts-node/register/transpile-only'] }; diff --git a/bin/cp-snapshot.js b/bin/cp-snapshot.js new file mode 100644 index 00000000..a4e29392 --- /dev/null +++ b/bin/cp-snapshot.js @@ -0,0 +1,53 @@ +const path = require('path'); +const fs = require('fs'); +const {Arch} = require('electron-builder'); + +function copySnapshot(pathToElectron, archToCopy) { + const snapshotFileName = 'snapshot_blob.bin'; + const v8ContextFileName = getV8ContextFileName(archToCopy); + const pathToBlob = path.resolve(__dirname, '..', 'cache', archToCopy, snapshotFileName); + const pathToBlobV8 = path.resolve(__dirname, '..', 'cache', archToCopy, v8ContextFileName); + + console.log('Copying v8 snapshots from', pathToBlob, 'to', pathToElectron); + fs.copyFileSync(pathToBlob, path.join(pathToElectron, snapshotFileName)); + fs.copyFileSync(pathToBlobV8, path.join(pathToElectron, v8ContextFileName)); +} + +function getPathToElectron() { + switch (process.platform) { + case 'darwin': + return path.resolve( + __dirname, + '..', + 'node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources' + ); + case 'win32': + case 'linux': + return path.resolve(__dirname, '..', 'node_modules', 'electron', 'dist'); + } +} + +function getV8ContextFileName(archToCopy) { + if (process.platform === 'darwin') { + return `v8_context_snapshot${archToCopy === 'arm64' ? '.arm64' : '.x86_64'}.bin`; + } else { + return `v8_context_snapshot.bin`; + } +} + +exports.default = async (context) => { + const archToCopy = Arch[context.arch]; + const pathToElectron = + process.platform === 'darwin' + ? `${context.appOutDir}/Hyper.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources` + : context.appOutDir; + copySnapshot(pathToElectron, archToCopy); +}; + +if (require.main === module) { + const archToCopy = process.env.npm_config_arch; + const pathToElectron = getPathToElectron(); + if ((process.arch.startsWith('arm') ? 'arm64' : 'x64') === archToCopy) { + copySnapshot(pathToElectron, archToCopy); + } +} diff --git a/bin/mk-snapshot.js b/bin/mk-snapshot.js new file mode 100644 index 00000000..209426cf --- /dev/null +++ b/bin/mk-snapshot.js @@ -0,0 +1,51 @@ +const childProcess = require('child_process'); +const vm = require('vm'); +const path = require('path'); +const fs = require('fs'); +const electronLink = require('electron-link'); +const {mkdirp} = require('fs-extra'); + +const excludedModules = {}; + +const crossArchDirs = ['clang_x86_v8_arm', 'clang_x64_v8_arm64', 'win_clang_x64']; + +async function main() { + const baseDirPath = path.resolve(__dirname, '..'); + + console.log('Creating a linked script..'); + const result = await electronLink({ + baseDirPath: baseDirPath, + mainPath: `${__dirname}/snapshot-libs.js`, + cachePath: `${baseDirPath}/cache`, + // eslint-disable-next-line no-prototype-builtins + shouldExcludeModule: (modulePath) => excludedModules.hasOwnProperty(modulePath) + }); + + const snapshotScriptPath = `${baseDirPath}/cache/snapshot-libs.js`; + fs.writeFileSync(snapshotScriptPath, result.snapshotScript); + + // Verify if we will be able to use this in `mksnapshot` + vm.runInNewContext(result.snapshotScript, undefined, {filename: snapshotScriptPath, displayErrors: true}); + + const outputBlobPath = `${baseDirPath}/cache/${process.env.npm_config_arch}`; + await mkdirp(outputBlobPath); + + if (process.platform !== 'darwin') { + const mksnapshotBinPath = `${baseDirPath}/node_modules/electron-mksnapshot/bin`; + const matchingDirs = crossArchDirs.map((dir) => `${mksnapshotBinPath}/${dir}`).filter((dir) => fs.existsSync(dir)); + for (const dir of matchingDirs) { + if (fs.existsSync(`${mksnapshotBinPath}/gen/v8/embedded.S`)) { + await mkdirp(`${dir}/gen/v8`); + fs.copyFileSync(`${mksnapshotBinPath}/gen/v8/embedded.S`, `${dir}/gen/v8/embedded.S`); + } + } + } + + console.log(`Generating startup blob in "${outputBlobPath}"`); + childProcess.execFileSync( + path.resolve(__dirname, '..', 'node_modules', '.bin', 'mksnapshot' + (process.platform === 'win32' ? '.cmd' : '')), + [snapshotScriptPath, '--output_dir', outputBlobPath] + ); +} + +main().catch((err) => console.error(err)); diff --git a/bin/notarize.js b/bin/notarize.js new file mode 100644 index 00000000..8cce22a3 --- /dev/null +++ b/bin/notarize.js @@ -0,0 +1,16 @@ +const { notarize } = require("@electron/notarize"); + +exports.default = async function notarizing(context) { + const { electronPlatformName, appOutDir } = context; + if (electronPlatformName !== "darwin" || !process.env.APPLE_ID || !process.env.APPLE_PASSWORD) { + return; + } + + const appName = context.packager.appInfo.productFilename; + return await notarize({ + appBundleId: "co.zeit.hyper", + appPath: `${appOutDir}/${appName}.app`, + appleId: process.env.APPLE_ID, + appleIdPassword: process.env.APPLE_PASSWORD + }); +}; diff --git a/bin/snapshot-libs.js b/bin/snapshot-libs.js new file mode 100644 index 00000000..791e8ec8 --- /dev/null +++ b/bin/snapshot-libs.js @@ -0,0 +1,31 @@ +require('color-convert'); +require('color-string'); +require('columnify'); +require('lodash'); +require('ms'); +require('normalize-url'); +require('parse-url'); +require('php-escape-shell'); +require('plist'); +require('redux-thunk'); +require('redux'); +require('reselect'); +require('seamless-immutable'); +require('stylis'); +require('xterm-addon-unicode11'); +// eslint-disable-next-line no-constant-condition +if (false) { + require('args'); + require('mousetrap'); + require('open'); + require('react-dom'); + require('react-redux'); + require('react'); + require('xterm-addon-fit'); + require('xterm-addon-image'); + require('xterm-addon-search'); + require('xterm-addon-web-links'); + require('xterm-addon-webgl'); + require('xterm-addon-canvas'); + require('xterm'); +} diff --git a/build/mac/entitlements.plist b/build/mac/entitlements.plist new file mode 100644 index 00000000..82c2f9a1 --- /dev/null +++ b/build/mac/entitlements.plist @@ -0,0 +1,26 @@ + + + + + com.apple.security.automation.apple-events + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + com.apple.security.personal-information.addressbook + + com.apple.security.personal-information.calendars + + com.apple.security.personal-information.location + + com.apple.security.personal-information.photos-library + + + diff --git a/build/mac/hyper b/build/mac/hyper index 3f17302c..8f53707a 100755 --- a/build/mac/hyper +++ b/build/mac/hyper @@ -1,8 +1,27 @@ #!/usr/bin/env bash -# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/darwin/bin/code.sh +# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.65.2/resources/darwin/bin/code.sh -function realpath() { /usr/bin/python -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$0"; } -CONTENTS="$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")" +# TODO: bash is deprecated on macOS and will be removed. +# Port this to /bin/sh or /bin/zsh + +function app_realpath() { + SOURCE=$1 + while [ -h "$SOURCE" ]; do + DIR=$(dirname "$SOURCE") + SOURCE=$(readlink "$SOURCE") + [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE + done + SOURCE_DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )" + echo "${SOURCE_DIR%%"${SOURCE_DIR#*.app}"}" +} + +APP_PATH="$(app_realpath "${BASH_SOURCE[0]}")" +if [ -z "$APP_PATH" ]; then + echo "Unable to determine app path from symlink : ${BASH_SOURCE[0]}" + exit 1 +fi + +CONTENTS="$APP_PATH/Contents" ELECTRON="$CONTENTS/MacOS/Hyper" CLI="$CONTENTS/Resources/bin/cli.js" ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@" diff --git a/build/win/installer.nsh b/build/win/installer.nsh index 48d68816..2a0b0a67 100644 --- a/build/win/installer.nsh +++ b/build/win/installer.nsh @@ -1,15 +1,15 @@ !macro customInstall - WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper" "" "Open Hyper here" - WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper" "Icon" "$appExe" - WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper\command" "" `$appExe "%V"` + WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper" "" "Open &Hyper here" + WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper" "Icon" `"$appExe"` + WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper\command" "" `"$appExe" "%V"` - WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper" "" "Open Hyper here" - WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper" "Icon" "$appExe" - WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper\command" "" `$appExe "%V"` + WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper" "" "Open &Hyper here" + WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper" "Icon" `"$appExe"` + WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper\command" "" `"$appExe" "%V"` - WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper" "" "Open Hyper here" - WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper" "Icon" "$appExe" - WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper\command" "" `$appExe "%V"` + WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper" "" "Open &Hyper here" + WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper" "Icon" `"$appExe"` + WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper\command" "" `"$appExe" "%V"` !macroend !macro customUnInstall @@ -25,4 +25,4 @@ !macro customInit IfFileExists $LOCALAPPDATA\Hyper\Update.exe 0 +2 nsExec::Exec '"$LOCALAPPDATA\Hyper\Update.exe" --uninstall -s' -!macroend \ No newline at end of file +!macroend diff --git a/cli/api.ts b/cli/api.ts index d99174f5..6c2a4347 100644 --- a/cli/api.ts +++ b/cli/api.ts @@ -2,28 +2,27 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ import fs from 'fs'; import os from 'os'; +import path from 'path'; + 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' +const applicationDirectory = process.env.XDG_CONFIG_HOME + ? path.join(process.env.XDG_CONFIG_HOME, 'Hyper') + : process.platform === 'win32' ? 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 = 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 @@ -33,7 +32,7 @@ const fileName = function memoize any>(fn: T): T { let hasResult = false; let result: any; - return ((...args: any[]) => { + return ((...args: Parameters) => { if (!hasResult) { result = fn(...args); hasResult = true; @@ -43,33 +42,12 @@ function memoize any>(fn: T): T { } const getFileContents = memoize(() => { - try { - return fs.readFileSync(fileName, 'utf8'); - } catch (err) { - 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'); @@ -86,13 +64,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) { @@ -108,13 +86,15 @@ function getPackageName(plugin: string) { function existsOnNpm(plugin: string) { const name = getPackageName(plugin); - return got.get(registryUrl + name.toLowerCase(), {timeout: 10000, responseType: 'json'}).then((res) => { - if (!res.body.versions) { - return Promise.reject(res); - } else { - return res; - } - }); + return got + .get(registryUrl + name.toLowerCase(), {timeout: {request: 10000}, responseType: 'json'}) + .then((res) => { + if (!res.body.versions) { + return Promise.reject(res); + } else { + return res; + } + }); } function install(plugin: string, locally?: boolean) { @@ -132,26 +112,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/cli/index.ts b/cli/index.ts index 99bbaa86..86dbc414 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -1,16 +1,20 @@ // This is a CLI tool, using console is OK /* eslint no-console: 0 */ -import {spawn, exec, SpawnOptions} from 'child_process'; -import {isAbsolute, resolve} from 'path'; +import {spawn, exec} from 'child_process'; +import type {SpawnOptions} from 'child_process'; import {existsSync} from 'fs'; -import {version} from '../app/package.json'; -import pify from 'pify'; +import {isAbsolute, resolve} from 'path'; +import {promisify} from 'util'; + import args from 'args'; import chalk from 'chalk'; -import open from 'open'; -import columnify from 'columnify'; +import _columnify from 'columnify'; import got from 'got'; +import open from 'open'; import ora from 'ora'; + +import {version} from '../app/package.json'; + import * as api from './api'; let commandPromise: Promise | undefined; @@ -32,6 +36,22 @@ const checkConfig = () => { process.exit(1); }; +const columnify = (data: {name: string; description: string}[]) => { + const maxNameLength = Math.max(...data.map((entry) => entry.name.length), 0); + const descriptionWidth = process.stdout.columns - maxNameLength - 1; + return _columnify(data, { + showHeaders: false, + config: { + description: { + maxWidth: descriptionWidth + }, + name: { + dataTransform: (nameValue) => chalk.green(nameValue) + } + } + }).replace(/\s+$/gm, ''); // remove padding from the end of all lines +}; + args.command( 'install', 'Install a plugin', @@ -57,7 +77,7 @@ args.command( commandPromise = api .uninstall(pluginName) .then(() => console.log(chalk.green(`${pluginName} uninstalled successfully!`))) - .catch((err) => console.log(chalk.red(err))); + .catch((err) => console.error(chalk.red(err))); }, ['u', 'rm', 'remove'] ); @@ -92,12 +112,6 @@ const lsRemote = (pattern?: string) => { entries.map(({name, description}) => { return {name, description}; }) - ) - .then((entries) => - entries.map((entry) => { - entry.name = chalk.green(entry.name); - return entry; - }) ); }; @@ -116,9 +130,8 @@ args.command( console.error(`${chalk.red('Try')} ${chalk.green('hyper ls-remote')}`); process.exit(1); } else { - let msg = columnify(entries); + const msg = columnify(entries); spinner.succeed(); - msg = msg.substring(msg.indexOf('\n') + 1); // remove header console.log(msg); } }) @@ -138,10 +151,8 @@ args.command( commandPromise = lsRemote() .then((entries) => { - let msg = columnify(entries); - + const msg = columnify(entries); spinner.succeed(); - msg = msg.substring(msg.indexOf('\n') + 1); // remove header console.log(msg); }) .catch((err) => { @@ -184,8 +195,10 @@ const main = (argv: string[]) => { version: false, mri: { boolean: ['v', 'verbose'] - } - } as any); + }, + mainColor: 'yellow', + subColor: 'dim' + }); if (commandPromise) { return commandPromise; @@ -225,7 +238,7 @@ const main = (argv: string[]) => { const opts = { env }; - return pify(exec)(cmd, opts); + return promisify(exec)(cmd, opts); } } diff --git a/electron-builder-linux-ci.json b/electron-builder-linux-ci.json new file mode 100644 index 00000000..a29fc86a --- /dev/null +++ b/electron-builder-linux-ci.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json.schemastore.org/electron-builder", + "extends": "electron-builder.json", + "afterSign": null, + "npmRebuild": false +} diff --git a/electron-builder.json b/electron-builder.json index 57923dd7..e5f0d691 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -1,6 +1,8 @@ { "$schema": "http://json.schemastore.org/electron-builder", "appId": "co.zeit.hyper", + "afterSign": "./bin/notarize.js", + "afterPack": "./bin/cp-snapshot.js", "directories": { "app": "target" }, @@ -15,42 +17,25 @@ ] } ], + "artifactName": "${productName}-${version}-${arch}.${ext}", "linux": { "category": "TerminalEmulator", "target": [ - { - "target": "deb", - "arch": [ - "x64", - "arm64" - ] - }, - { - "target": "AppImage", - "arch": [ - "x64", - "arm64" - ] - }, - { - "target": "rpm", - "arch": [ - "x64", - "arm64" - ] - }, - { - "target": "snap", - "arch": [ - "x64" - ] - } + "deb", + "AppImage", + "rpm", + "snap", + "pacman" ] }, "win": { - "target": [ - "nsis" - ], + "target": { + "target": "nsis", + "arch": [ + "x64", + "arm64" + ] + }, "rfc3161TimeStampServer": "http://timestamp.comodoca.com" }, "nsis": { @@ -69,6 +54,8 @@ }, "artifactName": "${productName}-${version}-${os}-${arch}.${ext}", "category": "public.app-category.developer-tools", + "entitlements": "./build/mac/entitlements.plist", + "entitlementsInherit": "./build/mac/entitlements.plist", "extendInfo": { "CFBundleDocumentTypes": [ { @@ -117,7 +104,11 @@ "afterInstall": "./build/linux/after-install.tpl" }, "rpm": { - "afterInstall": "./build/linux/after-install.tpl" + "afterInstall": "./build/linux/after-install.tpl", + "fpm": [ + "--rpm-rpmbuild-define", + "_build_id_links none" + ] }, "snap": { "confinement": "classic", diff --git a/lib/actions/config.ts b/lib/actions/config.ts index 4c68302e..7d1110cd 100644 --- a/lib/actions/config.ts +++ b/lib/actions/config.ts @@ -1,6 +1,6 @@ -import {CONFIG_LOAD, CONFIG_RELOAD} from '../constants/config'; -import {HyperActions} from '../hyper'; -import {configOptions} from '../config'; +import type {configOptions} from '../../typings/config'; +import {CONFIG_LOAD, CONFIG_RELOAD} from '../../typings/constants/config'; +import type {HyperActions} from '../../typings/hyper'; export function loadConfig(config: configOptions): HyperActions { return { diff --git a/lib/actions/header.ts b/lib/actions/header.ts index 9cc52cef..771efa03 100644 --- a/lib/actions/header.ts +++ b/lib/actions/header.ts @@ -1,14 +1,15 @@ -import {CLOSE_TAB, CHANGE_TAB} from '../constants/tabs'; +import {CLOSE_TAB, CHANGE_TAB} from '../../typings/constants/tabs'; import { UI_WINDOW_MAXIMIZE, UI_WINDOW_UNMAXIMIZE, UI_OPEN_HAMBURGER_MENU, UI_WINDOW_MINIMIZE, UI_WINDOW_CLOSE -} from '../constants/ui'; +} from '../../typings/constants/ui'; +import type {HyperDispatch} from '../../typings/hyper'; import rpc from '../rpc'; + import {userExitTermGroup, setActiveGroup} from './term-groups'; -import {HyperDispatch} from '../hyper'; export function closeTab(uid: string) { return (dispatch: HyperDispatch) => { @@ -39,7 +40,7 @@ export function maximize() { dispatch({ type: UI_WINDOW_MAXIMIZE, effect() { - rpc.emit('maximize', null); + rpc.emit('maximize'); } }); }; @@ -50,7 +51,7 @@ export function unmaximize() { dispatch({ type: UI_WINDOW_UNMAXIMIZE, effect() { - rpc.emit('unmaximize', null); + rpc.emit('unmaximize'); } }); }; @@ -72,7 +73,7 @@ export function minimize() { dispatch({ type: UI_WINDOW_MINIMIZE, effect() { - rpc.emit('minimize', null); + rpc.emit('minimize'); } }); }; @@ -83,7 +84,7 @@ export function close() { dispatch({ type: UI_WINDOW_CLOSE, effect() { - rpc.emit('close', null); + rpc.emit('close'); } }); }; diff --git a/lib/actions/index.ts b/lib/actions/index.ts index 68b89fbf..192dd18b 100644 --- a/lib/actions/index.ts +++ b/lib/actions/index.ts @@ -1,6 +1,6 @@ +import {INIT} from '../../typings/constants'; +import type {HyperDispatch} from '../../typings/hyper'; import rpc from '../rpc'; -import {INIT} from '../constants'; -import {HyperDispatch} from '../hyper'; export default function init() { return (dispatch: HyperDispatch) => { diff --git a/lib/actions/notifications.ts b/lib/actions/notifications.ts index 71a1b9cc..8433671f 100644 --- a/lib/actions/notifications.ts +++ b/lib/actions/notifications.ts @@ -1,5 +1,5 @@ -import {NOTIFICATION_MESSAGE, NOTIFICATION_DISMISS} from '../constants/notifications'; -import {HyperActions} from '../hyper'; +import {NOTIFICATION_MESSAGE, NOTIFICATION_DISMISS} from '../../typings/constants/notifications'; +import type {HyperActions} from '../../typings/hyper'; export function dismissNotification(id: string): HyperActions { return { diff --git a/lib/actions/sessions.ts b/lib/actions/sessions.ts index af77f765..4cb53f9d 100644 --- a/lib/actions/sessions.ts +++ b/lib/actions/sessions.ts @@ -1,6 +1,4 @@ -import rpc from '../rpc'; -import {keys} from '../utils/object'; -import findBySession from '../utils/term-groups'; +import type {Session} from '../../typings/common'; import { SESSION_ADD, SESSION_RESIZE, @@ -13,12 +11,14 @@ import { SESSION_CLEAR_ACTIVE, SESSION_USER_DATA, SESSION_SET_XTERM_TITLE, - SESSION_SEARCH, - SESSION_SEARCH_CLOSE -} from '../constants/sessions'; -import {HyperState, session, HyperDispatch, HyperActions} from '../hyper'; + SESSION_SEARCH +} from '../../typings/constants/sessions'; +import type {HyperState, HyperDispatch, HyperActions} from '../../typings/hyper'; +import rpc from '../rpc'; +import {keys} from '../utils/object'; +import findBySession from '../utils/term-groups'; -export function addSession({uid, shell, pid, cols, rows, splitDirection, activeUid}: session) { +export function addSession({uid, shell, pid, cols = null, rows = null, splitDirection, activeUid, profile}: Session) { return (dispatch: HyperDispatch, getState: () => HyperState) => { const {sessions} = getState(); const now = Date.now(); @@ -31,20 +31,20 @@ export function addSession({uid, shell, pid, cols, rows, splitDirection, activeU rows, splitDirection, activeUid: activeUid ? activeUid : sessions.activeUid, - now + now, + profile }); }; } -export function requestSession() { +export function requestSession(profile: string | undefined) { return (dispatch: HyperDispatch, getState: () => HyperState) => { dispatch({ type: SESSION_REQUEST, effect: () => { const {ui} = getState(); - // the cols and rows from preview session maybe not accurate. so remove. - const {/*cols, rows,*/ cwd} = ui; - rpc.emit('new', {cwd}); + const {cwd} = ui; + rpc.emit('new', {cwd, profile}); } }); }; @@ -135,12 +135,13 @@ export function resizeSession(uid: string, cols: number, rows: number) { }; } -export function onSearch(uid?: string) { +export function openSearch(uid?: string) { return (dispatch: HyperDispatch, getState: () => HyperState) => { const targetUid = uid || getState().sessions.activeUid!; dispatch({ type: SESSION_SEARCH, - uid: targetUid + uid: targetUid, + value: true }); }; } @@ -150,8 +151,9 @@ export function closeSearch(uid?: string, keyEvent?: any) { const targetUid = uid || getState().sessions.activeUid!; if (getState().sessions.sessions[targetUid]?.search) { dispatch({ - type: SESSION_SEARCH_CLOSE, - uid: targetUid + type: SESSION_SEARCH, + uid: targetUid, + value: false }); } else { if (keyEvent) { @@ -161,7 +163,7 @@ export function closeSearch(uid?: string, keyEvent?: any) { }; } -export function sendSessionData(uid: string | null, data: any, escaped?: boolean | null) { +export function sendSessionData(uid: string | null, data: string, escaped?: boolean) { return (dispatch: HyperDispatch, getState: () => HyperState) => { dispatch({ type: SESSION_USER_DATA, diff --git a/lib/actions/term-groups.ts b/lib/actions/term-groups.ts index 4696bb6d..6beeedd9 100644 --- a/lib/actions/term-groups.ts +++ b/lib/actions/term-groups.ts @@ -1,28 +1,32 @@ -import rpc from '../rpc'; +import {SESSION_REQUEST} from '../../typings/constants/sessions'; import { DIRECTION, TERM_GROUP_RESIZE, TERM_GROUP_REQUEST, TERM_GROUP_EXIT, TERM_GROUP_EXIT_ACTIVE -} from '../constants/term-groups'; -import {SESSION_REQUEST} from '../constants/sessions'; -import findBySession from '../utils/term-groups'; +} from '../../typings/constants/term-groups'; +import type {ITermState, ITermGroup, HyperState, HyperDispatch, HyperActions} from '../../typings/hyper'; +import rpc from '../rpc'; import {getRootGroups} from '../selectors'; +import findBySession from '../utils/term-groups'; + import {setActiveSession, ptyExitSession, userExitSession} from './sessions'; -import {ITermState, ITermGroup, HyperState, HyperDispatch, HyperActions} from '../hyper'; function requestSplit(direction: 'VERTICAL' | 'HORIZONTAL') { - return (activeUid: string) => + return (_activeUid: string | undefined, _profile: string | undefined) => (dispatch: HyperDispatch, getState: () => HyperState): void => { dispatch({ type: SESSION_REQUEST, effect: () => { const {ui, sessions} = getState(); + const activeUid = _activeUid ? _activeUid : sessions.activeUid; + const profile = _profile ? _profile : activeUid ? sessions.sessions[activeUid].profile : window.profileName; rpc.emit('new', { splitDirection: direction, cwd: ui.cwd, - activeUid: activeUid ? activeUid : sessions.activeUid + activeUid, + profile }); } }); @@ -40,17 +44,20 @@ export function resizeTermGroup(uid: string, sizes: number[]): HyperActions { }; } -export function requestTermGroup(activeUid: string) { +export function requestTermGroup(_activeUid: string | undefined, _profile: string | undefined) { return (dispatch: HyperDispatch, getState: () => HyperState) => { dispatch({ type: TERM_GROUP_REQUEST, effect: () => { - const {ui} = getState(); + const {ui, sessions} = getState(); const {cwd} = ui; + const activeUid = _activeUid ? _activeUid : sessions.activeUid; + const profile = _profile ? _profile : activeUid ? sessions.sessions[activeUid].profile : window.profileName; rpc.emit('new', { isNewGroup: true, cwd, - activeUid + activeUid, + profile }); } }); diff --git a/lib/actions/ui.ts b/lib/actions/ui.ts index af947222..92da4cee 100644 --- a/lib/actions/ui.ts +++ b/lib/actions/ui.ts @@ -1,10 +1,9 @@ +import {stat} from 'fs'; +import type {Stats} from 'fs'; + +import type parseUrl from 'parse-url'; import {php_escapeshellcmd as escapeShellCmd} from 'php-escape-shell'; -import {isExecutable} from '../utils/file'; -import {getRootGroups} from '../selectors'; -import findBySession from '../utils/term-groups'; -import notify from '../utils/notify'; -import rpc from '../rpc'; -import {requestSession, sendSessionData, setActiveSession} from './sessions'; + import { UI_FONT_SIZE_SET, UI_FONT_SIZE_INCR, @@ -24,14 +23,18 @@ import { UI_OPEN_SSH_URL, UI_CONTEXTMENU_OPEN, UI_COMMAND_EXEC -} from '../constants/ui'; +} from '../../typings/constants/ui'; +import type {HyperState, HyperDispatch, HyperActions, ITermGroups} from '../../typings/hyper'; +import rpc from '../rpc'; +import {getRootGroups} from '../selectors'; +import {isExecutable} from '../utils/file'; +import notify from '../utils/notify'; +import findBySession from '../utils/term-groups'; +import {requestSession, sendSessionData, setActiveSession} from './sessions'; import {setActiveGroup} from './term-groups'; -import parseUrl from 'parse-url'; -import {HyperState, HyperDispatch, HyperActions, ITermGroups} from '../hyper'; -import {stat, Stats} from 'fs'; -export function openContextMenu(uid: string, selection: any) { +export function openContextMenu(uid: string, selection: string) { return (dispatch: HyperDispatch, getState: () => HyperState) => { dispatch({ type: UI_CONTEXTMENU_OPEN, @@ -102,9 +105,10 @@ export function setFontSmoothing() { }; } -export function windowGeometryUpdated(): HyperActions { +export function windowGeometryUpdated({isMaximized}: {isMaximized: boolean}): HyperActions { return { - type: UI_WINDOW_GEOMETRY_CHANGED + type: UI_WINDOW_GEOMETRY_CHANGED, + isMaximized }; } @@ -268,11 +272,11 @@ export function openFile(path: string) { } rpc.once('session add', ({uid}) => { rpc.once('session data', () => { - dispatch(sendSessionData(uid, command, null)); + dispatch(sendSessionData(uid, command)); }); }); } - dispatch(requestSession()); + dispatch(requestSession(undefined)); }); } }); @@ -291,12 +295,11 @@ export function leaveFullScreen(): HyperActions { }; } -export function openSSH(url: string) { +export function openSSH(parsedUrl: ReturnType) { return (dispatch: HyperDispatch) => { dispatch({ type: UI_OPEN_SSH_URL, effect() { - const parsedUrl = parseUrl(url, true); let command = `${parsedUrl.protocol} ${parsedUrl.user ? `${parsedUrl.user}@` : ''}${parsedUrl.resource}`; if (parsedUrl.port) command += ` -p ${parsedUrl.port}`; @@ -305,11 +308,11 @@ export function openSSH(url: string) { rpc.once('session add', ({uid}) => { rpc.once('session data', () => { - dispatch(sendSessionData(uid, command, null)); + dispatch(sendSessionData(uid, command)); }); }); - dispatch(requestSession()); + dispatch(requestSession(undefined)); } }); }; diff --git a/lib/actions/updater.ts b/lib/actions/updater.ts index 17eef45b..45490f7e 100644 --- a/lib/actions/updater.ts +++ b/lib/actions/updater.ts @@ -1,12 +1,12 @@ -import {UPDATE_INSTALL, UPDATE_AVAILABLE} from '../constants/updater'; +import {UPDATE_INSTALL, UPDATE_AVAILABLE} from '../../typings/constants/updater'; +import type {HyperActions} from '../../typings/hyper'; import rpc from '../rpc'; -import {HyperActions} from '../hyper'; export function installUpdate(): HyperActions { return { type: UPDATE_INSTALL, effect: () => { - rpc.emit('quit and install', null); + rpc.emit('quit and install'); } }; } diff --git a/lib/command-registry.ts b/lib/command-registry.ts index 27b6e513..f72c08c9 100644 --- a/lib/command-registry.ts +++ b/lib/command-registry.ts @@ -1,18 +1,17 @@ -import {remote} from 'electron'; -import {HyperDispatch} from './hyper'; -import {closeSearch} from './actions/sessions'; -// TODO: Should be updates to new async API https://medium.com/@nornagon/electrons-remote-module-considered-harmful-70d69500f31 +import type {HyperDispatch} from '../typings/hyper'; -const {getDecoratedKeymaps} = remote.require('./plugins') as typeof import('../app/plugins'); +import {closeSearch} from './actions/sessions'; +import {ipcRenderer} from './utils/ipc'; let commands: Record void> = { 'editor:search-close': (e, dispatch) => { dispatch(closeSearch(undefined, e)); + window.focusActiveTerm(); } }; -export const getRegisteredKeys = () => { - const keymaps = getDecoratedKeymaps(); +export const getRegisteredKeys = async () => { + const keymaps = await ipcRenderer.invoke('getDecoratedKeymaps'); return Object.keys(keymaps).reduce((result: Record, actionName) => { const commandKeys = keymaps[actionName]; diff --git a/lib/components/header.tsx b/lib/components/header.tsx index faa131fb..416ecbc5 100644 --- a/lib/components/header.tsx +++ b/lib/components/header.tsx @@ -1,27 +1,27 @@ -import React from 'react'; +import React, {forwardRef, useState} from 'react'; +import type {HeaderProps} from '../../typings/hyper'; import {decorate, getTabsProps} from '../utils/plugins'; import Tabs_ from './tabs'; -import {HeaderProps} from '../hyper'; const Tabs = decorate(Tabs_, 'Tabs'); -export default class Header extends React.PureComponent { - headerMouseDownWindowX!: number; - headerMouseDownWindowY!: number; +const Header = forwardRef((props, ref) => { + const [headerMouseDownWindowX, setHeaderMouseDownWindowX] = useState(0); + const [headerMouseDownWindowY, setHeaderMouseDownWindowY] = useState(0); - onChangeIntent = (active: string) => { + const onChangeIntent = (active: string) => { // we ignore clicks if they're a byproduct of a drag // motion to move the window - if (window.screenX !== this.headerMouseDownWindowX || window.screenY !== this.headerMouseDownWindowY) { + if (window.screenX !== headerMouseDownWindowX || window.screenY !== headerMouseDownWindowY) { return; } - this.props.onChangeTab(active); + props.onChangeTab(active); }; - handleHeaderMouseDown = () => { + const handleHeaderMouseDown = () => { // the hack of all hacks, this prevents the term // iframe from losing focus, for example, when // the user drags the nav around @@ -30,43 +30,43 @@ export default class Header extends React.PureComponent { // persist start positions of a potential drag motion // to differentiate dragging from clicking - this.headerMouseDownWindowX = window.screenX; - this.headerMouseDownWindowY = window.screenY; + setHeaderMouseDownWindowX(window.screenX); + setHeaderMouseDownWindowY(window.screenY); }; - handleHamburgerMenuClick = (event: React.MouseEvent) => { + const handleHamburgerMenuClick = (event: React.MouseEvent) => { let {right: x, bottom: y} = event.currentTarget.getBoundingClientRect(); x -= 15; // to compensate padding y -= 12; // ^ same - this.props.openHamburgerMenu({x, y}); + props.openHamburgerMenu({x, y}); }; - handleMaximizeClick = () => { - if (this.props.maximized) { - this.props.unmaximize(); + const handleMaximizeClick = () => { + if (props.maximized) { + props.unmaximize(); } else { - this.props.maximize(); + props.maximize(); } }; - handleMinimizeClick = () => { - this.props.minimize(); + const handleMinimizeClick = () => { + props.minimize(); }; - handleCloseClick = () => { - this.props.close(); + const handleCloseClick = () => { + props.close(); }; - getWindowHeaderConfig() { - const {showHamburgerMenu, showWindowControls} = this.props; + const getWindowHeaderConfig = () => { + const {showHamburgerMenu, showWindowControls} = props; const defaults = { - hambMenu: !this.props.isMac, // show by default on windows and linux - winCtrls: !this.props.isMac // show by default on Windows and Linux + hambMenu: !props.isMac, // show by default on windows and linux + winCtrls: !props.isMac // show by default on Windows and Linux }; // don't allow the user to change defaults on macOS - if (this.props.isMac) { + if (props.isMac) { return defaults; } @@ -74,182 +74,187 @@ export default class Header extends React.PureComponent { hambMenu: showHamburgerMenu === '' ? defaults.hambMenu : showHamburgerMenu, winCtrls: showWindowControls === '' ? defaults.winCtrls : showWindowControls }; + }; + + const {isMac} = props; + const {borderColor} = props; + let title = 'Hyper'; + if (props.tabs.length === 1 && props.tabs[0].title) { + // if there's only one tab we use its title as the window title + title = props.tabs[0].title; } + const {hambMenu, winCtrls} = getWindowHeaderConfig(); + const left = winCtrls === 'left'; + const maxButtonHref = props.maximized + ? './renderer/assets/icons.svg#restore-window' + : './renderer/assets/icons.svg#maximize-window'; - render() { - const {isMac} = this.props; - const props = getTabsProps(this.props, { - tabs: this.props.tabs, - borderColor: this.props.borderColor, - onClose: this.props.onCloseTab, - onChange: this.onChangeIntent, - fullScreen: this.props.fullScreen - }); - const {borderColor} = props; - let title = 'Hyper'; - if (props.tabs.length === 1 && props.tabs[0].title) { - // if there's only one tab we use its title as the window title - title = props.tabs[0].title; - } - const {hambMenu, winCtrls} = this.getWindowHeaderConfig(); - const left = winCtrls === 'left'; - const maxButtonHref = this.props.maximized - ? './renderer/assets/icons.svg#restore-window' - : './renderer/assets/icons.svg#maximize-window'; - - return ( -
window.focusActiveTerm()} - onDoubleClick={this.handleMaximizeClick} - > - {!isMac && ( -
1 ? 'header_windowHeaderWithBorder' : ''}`} - style={{borderColor}} - > - {hambMenu && ( - - - - )} - {title} - {winCtrls && ( -
-
- - - -
-
- - - -
-
- - - -
+ return ( +
window.focusActiveTerm()} + onDoubleClick={handleMaximizeClick} + ref={ref} + > + {!isMac && ( +
1 ? 'header_windowHeaderWithBorder' : ''}`} + style={{borderColor}} + > + {hambMenu && ( + + + + )} + {title} + {winCtrls && ( +
+
+ + +
- )} -
- )} - {this.props.customChildrenBefore} - - {this.props.customChildren} +
+ + + +
+
+ + + +
+
+ )} +
+ )} + {props.customChildrenBefore} + + {props.customChildren} - -
- ); - } -} + .header_closeWindow:active { + color: #fe354e; + } + `} + + ); +}); + +Header.displayName = 'Header'; + +export default Header; diff --git a/lib/components/new-tab.tsx b/lib/components/new-tab.tsx new file mode 100644 index 00000000..3fe99543 --- /dev/null +++ b/lib/components/new-tab.tsx @@ -0,0 +1,149 @@ +import React, {useRef, useState} from 'react'; + +import {VscChevronDown} from '@react-icons/all-files/vsc/VscChevronDown'; +import useClickAway from 'react-use/lib/useClickAway'; + +import type {configOptions} from '../../typings/config'; + +interface Props { + defaultProfile: string; + profiles: configOptions['profiles']; + openNewTab: (name: string) => void; + backgroundColor: string; + borderColor: string; + tabsVisible: boolean; +} +const isMac = /Mac/.test(navigator.userAgent); + +const DropdownButton = ({defaultProfile, profiles, openNewTab, backgroundColor, borderColor, tabsVisible}: Props) => { + const [dropdownOpen, setDropdownOpen] = useState(false); + const ref = useRef(null); + + const toggleDropdown = () => { + setDropdownOpen(!dropdownOpen); + }; + + useClickAway(ref, () => { + setDropdownOpen(false); + }); + + return ( +
e.stopPropagation()} + onBlur={() => setDropdownOpen(false)} + > + + + {dropdownOpen && ( +
    + {profiles.map((profile) => ( +
  • { + openNewTab(profile.name); + setDropdownOpen(false); + }} + className={`profile_dropdown_item ${ + profile.name === defaultProfile && profiles.length > 1 ? 'profile_dropdown_item_default' : '' + }`} + > + {profile.name} +
  • + ))} +
+ )} + + +
+ ); +}; + +export default DropdownButton; diff --git a/lib/components/notification.tsx b/lib/components/notification.tsx index 12e33eb1..68b7a2df 100644 --- a/lib/components/notification.tsx +++ b/lib/components/notification.tsx @@ -1,115 +1,110 @@ -import React from 'react'; -import {NotificationProps, NotificationState} from '../hyper'; +import React, {forwardRef, useEffect, useRef, useState} from 'react'; -export default class Notification extends React.PureComponent { - dismissTimer!: NodeJS.Timeout; - constructor(props: NotificationProps) { - super(props); - this.state = { - dismissing: false - }; - } +import type {NotificationProps} from '../../typings/hyper'; - componentDidMount() { - if (this.props.dismissAfter) { - this.setDismissTimer(); - } - } +const Notification = forwardRef>((props, ref) => { + const dismissTimer = useRef(undefined); + const [dismissing, setDismissing] = useState(false); - componentDidUpdate(prevProps: NotificationProps, prevState: NotificationState) { + useEffect(() => { + setDismissTimer(); + }, []); + + useEffect(() => { // if we have a timer going and the notification text // changed we reset the timer - if (this.props.text !== prevProps.text) { - if (prevProps.dismissAfter) { - this.resetDismissTimer(); - } - if (prevState.dismissing) { - this.setState({dismissing: false}); - } - } - } + resetDismissTimer(); + setDismissing(false); + }, [props.text]); - handleDismiss = () => { - this.setState({dismissing: true}); + const handleDismiss = () => { + setDismissing(true); }; - onElement = (el: HTMLDivElement | null) => { + const onElement = (el: HTMLDivElement | null) => { if (el) { el.addEventListener('webkitTransitionEnd', () => { - if (this.state.dismissing) { - this.props.onDismiss(); + if (dismissing) { + props.onDismiss(); } }); - const {backgroundColor} = this.props; + const {backgroundColor} = props; if (backgroundColor) { el.style.setProperty('background-color', backgroundColor, 'important'); } + + if (ref) { + if (typeof ref === 'function') ref(el); + else ref.current = el; + } } }; - setDismissTimer() { - this.dismissTimer = setTimeout(() => { - this.handleDismiss(); - }, this.props.dismissAfter); - } + const setDismissTimer = () => { + if (typeof props.dismissAfter === 'number') { + dismissTimer.current = setTimeout(() => { + handleDismiss(); + }, props.dismissAfter); + } + }; - resetDismissTimer() { - clearTimeout(this.dismissTimer); - this.setDismissTimer(); - } + const resetDismissTimer = () => { + clearTimeout(dismissTimer.current); + setDismissTimer(); + }; - componentWillUnmount() { - clearTimeout(this.dismissTimer); - } + useEffect(() => { + return () => { + clearTimeout(dismissTimer.current); + }; + }, []); - render() { - const {backgroundColor, color} = this.props; - const opacity = this.state.dismissing ? 0 : 1; - return ( -
- {this.props.customChildrenBefore} - {this.props.children || this.props.text} - {this.props.userDismissable ? ( - - [x] - - ) : null} - {this.props.customChildren} + const {backgroundColor, color} = props; + const opacity = dismissing ? 0 : 1; + return ( +
+ {props.customChildrenBefore} + {props.children || props.text} + {props.userDismissable ? ( + + [x] + + ) : null} + {props.customChildren} - -
- ); - } -} + .notification_dismissLink:hover, + .notification_dismissLink:focus { + font-weight: 900; + } + `} +
+ ); +}); + +Notification.displayName = 'Notification'; + +export default Notification; diff --git a/lib/components/notifications.tsx b/lib/components/notifications.tsx index 4a7b00bd..79e6ede3 100644 --- a/lib/components/notifications.tsx +++ b/lib/components/notifications.tsx @@ -1,132 +1,132 @@ -import React from 'react'; +import React, {forwardRef} from 'react'; +import type {NotificationsProps} from '../../typings/hyper'; import {decorate} from '../utils/plugins'; import Notification_ from './notification'; -import {NotificationsProps} from '../hyper'; const Notification = decorate(Notification_, 'Notification'); -export default class Notifications extends React.PureComponent { - render() { - return ( -
- {this.props.customChildrenBefore} - {this.props.fontShowing && ( - - )} +const Notifications = forwardRef((props, ref) => { + return ( +
+ {props.customChildrenBefore} + {props.fontShowing && ( + + )} - {this.props.resizeShowing && ( - - )} + {props.resizeShowing && ( + + )} - {this.props.messageShowing && ( - - {this.props.messageURL - ? [ - this.props.messageText, - ' (', - { - void window.require('electron').shell.openExternal(ev.currentTarget.href); - ev.preventDefault(); - }} - href={this.props.messageURL} - > - more - , - ')' - ] - : null} - - )} - - {this.props.updateShowing && ( - - Version {this.props.updateVersion} ready. - {this.props.updateNote && ` ${this.props.updateNote.trim().replace(/\.$/, '')}`} ( - { - void window.require('electron').shell.openExternal(ev.currentTarget.href); - ev.preventDefault(); - }} - href={`https://github.com/vercel/hyper/releases/tag/${this.props.updateVersion}`} - > - notes - - ).{' '} - {this.props.updateCanInstall ? ( + {props.messageShowing && ( + + {props.messageURL ? ( + <> + {props.messageText} ( - Restart - - ) : ( - { void window.require('electron').shell.openExternal(ev.currentTarget.href); ev.preventDefault(); }} - href={this.props.updateReleaseUrl!} + href={props.messageURL} > - Download + more - )} - .{' '} - - )} - {this.props.customChildren} + ) + + ) : null} + + )} - -
- ); - } -} + {props.updateShowing && ( + + Version {props.updateVersion} ready. + {props.updateNote && ` ${props.updateNote.trim().replace(/\.$/, '')}`} ( + { + void window.require('electron').shell.openExternal(ev.currentTarget.href); + ev.preventDefault(); + }} + href={`https://github.com/vercel/hyper/releases/tag/${props.updateVersion}`} + > + notes + + ).{' '} + {props.updateCanInstall ? ( + + Restart + + ) : ( + { + void window.require('electron').shell.openExternal(ev.currentTarget.href); + ev.preventDefault(); + }} + href={props.updateReleaseUrl!} + > + Download + + )} + .{' '} + + )} + {props.customChildren} + + +
+ ); +}); + +Notifications.displayName = 'Notifications'; + +export default Notifications; diff --git a/lib/components/searchBox.tsx b/lib/components/searchBox.tsx index 1cc2fda7..55a72f16 100644 --- a/lib/components/searchBox.tsx +++ b/lib/components/searchBox.tsx @@ -1,80 +1,237 @@ -import React from 'react'; -import {SearchBoxProps} from '../hyper'; +import React, {useCallback, useRef, useEffect, forwardRef} from 'react'; -const searchBoxStyling: React.CSSProperties = { - float: 'right', - height: '28px', - backgroundColor: 'white', - position: 'absolute', - right: '10px', - top: '0px', - width: '224px', - zIndex: 9999 +import {VscArrowDown} from '@react-icons/all-files/vsc/VscArrowDown'; +import {VscArrowUp} from '@react-icons/all-files/vsc/VscArrowUp'; +import {VscCaseSensitive} from '@react-icons/all-files/vsc/VscCaseSensitive'; +import {VscClose} from '@react-icons/all-files/vsc/VscClose'; +import {VscRegex} from '@react-icons/all-files/vsc/VscRegex'; +import {VscWholeWord} from '@react-icons/all-files/vsc/VscWholeWord'; +import clsx from 'clsx'; + +import type {SearchBoxProps} from '../../typings/hyper'; + +type SearchButtonColors = { + foregroundColor: string; + selectionColor: string; + backgroundColor: string; }; -const enterKey = 13; +type SearchButtonProps = React.PropsWithChildren< + { + onClick: () => void; + active: boolean; + title: string; + } & SearchButtonColors +>; -export default class SearchBox extends React.PureComponent { - searchTerm: string; - constructor(props: SearchBoxProps) { - super(props); - this.searchTerm = ''; - } +const SearchButton = ({ + onClick, + active, + title, + foregroundColor, + backgroundColor, + selectionColor, + children +}: SearchButtonProps) => { + const handleKeyUp = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + onClick(); + } + }, + [onClick] + ); - handleChange = (event: React.KeyboardEvent) => { - this.searchTerm = event.currentTarget.value; - if (event.keyCode === enterKey) { - this.props.search(event.currentTarget.value); - } + return ( +
+ {children} + +
+ ); +}; + +const SearchBox = forwardRef((props, ref) => { + const { + caseSensitive, + wholeWord, + regex, + results, + toggleCaseSensitive, + toggleWholeWord, + toggleRegex, + next, + prev, + close, + backgroundColor, + foregroundColor, + borderColor, + selectionColor, + font + } = props; + + const searchTermRef = useRef(''); + const inputRef = useRef(null); + + const handleChange = useCallback( + (event: React.KeyboardEvent) => { + searchTermRef.current = event.currentTarget.value; + if (event.shiftKey && event.key === 'Enter') { + prev(searchTermRef.current); + } else if (event.key === 'Enter') { + next(searchTermRef.current); + } + }, + [prev, next] + ); + + useEffect(() => { + inputRef.current?.focus(); + }, [inputRef.current]); + + const searchButtonColors: SearchButtonColors = { + backgroundColor: borderColor, + selectionColor, + foregroundColor }; - render() { - return ( -
- input?.focus()} /> - this.props.prev(this.searchTerm)}> - - - this.props.next(this.searchTerm)}> - - - this.props.close()}> - - - + + + + + + +
- ); - } -} + + + {results === undefined + ? '' + : results.resultCount === 0 + ? 'No results' + : `${results.resultIndex + 1} of ${results.resultCount}`} + + +
+ prev(searchTermRef.current)} + active={false} + title="Previous Match" + {...searchButtonColors} + > + + + + next(searchTermRef.current)} + active={false} + title="Next Match" + {...searchButtonColors} + > + + + + + + +
+ + + + ); +}); + +SearchBox.displayName = 'SearchBox'; + +export default SearchBox; diff --git a/lib/components/split-pane.tsx b/lib/components/split-pane.tsx index 8ca6cd8c..aea53f6f 100644 --- a/lib/components/split-pane.tsx +++ b/lib/components/split-pane.tsx @@ -1,219 +1,191 @@ -import React from 'react'; -import _ from 'lodash'; -import {SplitPaneProps} from '../hyper'; +import React, {useState, useEffect, useRef, forwardRef} from 'react'; -export default class SplitPane extends React.PureComponent { - dragPanePosition!: number; - dragTarget!: Element; - panes!: Element[]; - paneIndex!: number; - d1!: 'height' | 'width'; - d2!: 'top' | 'left'; - d3!: 'clientX' | 'clientY'; - panesSize!: number; - dragging!: boolean; - constructor(props: SplitPaneProps) { - super(props); - this.state = {dragging: false}; - } +import sum from 'lodash/sum'; - componentDidUpdate(prevProps: SplitPaneProps) { - if (this.state.dragging && prevProps.sizes !== this.props.sizes) { - // recompute positions for ongoing dragging - this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2]; - } - } +import type {SplitPaneProps} from '../../typings/hyper'; - setupPanes(ev: any) { - this.panes = Array.from(ev.target.parentNode.childNodes); - this.paneIndex = this.panes.indexOf(ev.target); - this.paneIndex -= Math.ceil(this.paneIndex / 2); - } +const SplitPane = forwardRef((props, ref) => { + const dragPanePosition = useRef(0); + const dragTarget = useRef(null); + const paneIndex = useRef(0); + const d1 = props.direction === 'horizontal' ? 'height' : 'width'; + const d2 = props.direction === 'horizontal' ? 'top' : 'left'; + const d3 = props.direction === 'horizontal' ? 'clientY' : 'clientX'; + const panesSize = useRef(null); + const [dragging, setDragging] = useState(false); - handleAutoResize = (ev: React.MouseEvent) => { + const handleAutoResize = (ev: React.MouseEvent, index: number) => { ev.preventDefault(); - this.setupPanes(ev); + paneIndex.current = index; - const sizes_ = this.getSizes(); - sizes_[this.paneIndex] = 0; - sizes_[this.paneIndex + 1] = 0; + const sizes_ = getSizes(); + sizes_[paneIndex.current] = 0; + sizes_[paneIndex.current + 1] = 0; - const availableWidth = 1 - _.sum(sizes_); - sizes_[this.paneIndex] = availableWidth / 2; - sizes_[this.paneIndex + 1] = availableWidth / 2; + const availableWidth = 1 - sum(sizes_); + sizes_[paneIndex.current] = availableWidth / 2; + sizes_[paneIndex.current + 1] = availableWidth / 2; - this.props.onResize(sizes_); + props.onResize(sizes_); }; - handleDragStart = (ev: any) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const handleDragStart = (ev: React.MouseEvent, index: number) => { ev.preventDefault(); - this.setState({dragging: true}); - window.addEventListener('mousemove', this.onDrag); - window.addEventListener('mouseup', this.onDragEnd); + setDragging(true); + window.addEventListener('mousemove', onDrag); + window.addEventListener('mouseup', onDragEnd); - // dimensions to consider - if (this.props.direction === 'horizontal') { - this.d1 = 'height'; - this.d2 = 'top'; - this.d3 = 'clientY'; - } else { - this.d1 = 'width'; - this.d2 = 'left'; - this.d3 = 'clientX'; - } - - this.dragTarget = ev.target; - this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - this.panesSize = ev.target.parentNode.getBoundingClientRect()[this.d1]; - this.setupPanes(ev); + const target = ev.target as HTMLDivElement; + dragTarget.current = target; + dragPanePosition.current = dragTarget.current.getBoundingClientRect()[d2]; + panesSize.current = target.parentElement!.getBoundingClientRect()[d1]; + paneIndex.current = index; }; - getSizes() { - const {sizes} = this.props; + const getSizes = () => { + const {sizes} = props; let sizes_: number[]; if (sizes) { sizes_ = [...sizes.asMutable()]; } else { - const total = (this.props.children as React.ReactNodeArray).length; + const total = props.children.length; const count = new Array(total).fill(1 / total); sizes_ = count; } return sizes_; - } + }; - onDrag = (ev: MouseEvent) => { - const sizes_ = this.getSizes(); + const onDrag = (ev: MouseEvent) => { + const sizes_ = getSizes(); - const i = this.paneIndex; - const pos = ev[this.d3]; - const d = Math.abs(this.dragPanePosition - pos) / this.panesSize; - if (pos > this.dragPanePosition) { + const i = paneIndex.current; + const pos = ev[d3]; + const d = Math.abs(dragPanePosition.current - pos) / panesSize.current!; + if (pos > dragPanePosition.current) { sizes_[i] += d; sizes_[i + 1] -= d; } else { sizes_[i] -= d; sizes_[i + 1] += d; } - this.props.onResize(sizes_); + props.onResize(sizes_); }; - onDragEnd = () => { - if (this.state.dragging) { - window.removeEventListener('mousemove', this.onDrag); - window.removeEventListener('mouseup', this.onDragEnd); - this.setState({dragging: false}); - } + const onDragEnd = () => { + window.removeEventListener('mousemove', onDrag); + window.removeEventListener('mouseup', onDragEnd); + setDragging(false); }; - render() { - const children = this.props.children as React.ReactNodeArray; - const {direction, borderColor} = this.props; - const sizeProperty = direction === 'horizontal' ? 'height' : 'width'; - // workaround for the fact that if we don't specify - // sizes, sometimes flex fails to calculate the - // right height for the horizontal panes - const sizes = this.props.sizes || new Array(children.length).fill(1 / children.length); - return ( -
- {React.Children.map(children, (child, i) => { - const style = { - // flexBasis doesn't work for the first horizontal pane, height need to be specified - [sizeProperty]: `${sizes[i] * 100}%`, - flexBasis: `${sizes[i] * 100}%`, - flexGrow: 0 - }; - return [ -
+ useEffect(() => { + return () => { + onDragEnd(); + }; + }, []); + + const {children, direction, borderColor} = props; + const sizeProperty = direction === 'horizontal' ? 'height' : 'width'; + // workaround for the fact that if we don't specify + // sizes, sometimes flex fails to calculate the + // right height for the horizontal panes + const sizes = props.sizes || new Array(children.length).fill(1 / children.length); + return ( +
+ {children.map((child, i) => { + const style = { + // flexBasis doesn't work for the first horizontal pane, height need to be specified + [sizeProperty]: `${sizes[i] * 100}%`, + flexBasis: `${sizes[i] * 100}%`, + flexGrow: 0 + }; + + return ( + +
{child} -
, - i < children.length - 1 ? ( +
+ {i < children.length - 1 ? (
handleDragStart(e, i)} + onDoubleClick={(e) => handleAutoResize(e, i)} style={{backgroundColor: borderColor}} className={`splitpane_divider splitpane_divider_${direction}`} /> - ) : null - ]; - })} -
+ ) : null} + + ); + })} +
- -
- ); - } + /* + this shim is used to make sure mousemove events + trigger in all the draggable area of the screen + this is not the case due to hterm's