diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 0e2c0f9b..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,98 +0,0 @@ -version: 2 -jobs: - install: - macos: - xcode: "11.2.1" - working_directory: ~/repo - steps: - - checkout - - restore_cache: - key: cache-{{ checksum "yarn.lock" }} - - run: - name: Installing Dependencies - command: yarn --ignore-engines - - save_cache: - key: cache-{{ checksum "yarn.lock" }} - paths: - - node_modules - - run: - name: Getting build icon - command: if [[ $CIRCLE_BRANCH == canary ]]; then cp build/canary.icns build/icon.icns; fi - - persist_to_workspace: - root: . - paths: - - node_modules - - app/node_modules - - test: - macos: - xcode: "11.2.1" - steps: - - checkout - - attach_workspace: - at: . - - run: - name: Testing - command: yarn test - - build: - macos: - xcode: "11.2.1" - steps: - - checkout - - attach_workspace: - at: . - - run: - name: Building - command: yarn dist --publish 'never' - - store_artifacts: - path: dist - - persist_to_workspace: - root: . - paths: - - dist - - release: - macos: - xcode: "11.2.1" - steps: - - checkout - - attach_workspace: - at: . - - run: - name: Deploying to GitHub - command: yarn dist - - -workflows: - version: 2 - build: - jobs: - - install: - filters: - tags: - only: /.*/ - - test: - requires: - - install - filters: - tags: - only: /.*/ - - build: - requires: - - test - filters: - branches: - only: - - master - - canary - tags: - ignore: /.*/ - - release: - requires: - - test - filters: - tags: - only: /.*/ - branches: - ignore: /.*/ 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 3872c5e8..1fa89a98 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,23 +2,29 @@ "plugins": [ "react", "prettier", - "@typescript-eslint" + "@typescript-eslint", + "eslint-comments", + "lodash", + "import" ], "extends": [ "eslint:recommended", "plugin:react/recommended", - "plugin:prettier/recommended" + "plugin:prettier/recommended", + "plugin:eslint-comments/recommended" ], "parser": "@typescript-eslint/parser", "parserOptions": { - "ecmaVersion": 8, "sourceType": "module", "ecmaFeatures": { "jsx": true, "impliedStrict": true, "experimentalObjectRestSpread": true }, - "allowImportExportEverywhere": true + "allowImportExportEverywhere": true, + "project": [ + "./tsconfig.eslint.json" + ] }, "env": { "es6": true, @@ -28,7 +34,11 @@ "settings": { "react": { "version": "detect" - } + }, + "import/resolver": { + "typescript": {} + }, + "import/internal-regex": "^(electron|react)$" }, "rules": { "func-names": [ @@ -52,33 +62,21 @@ "bracketSpacing": false, "semi": true, "useTabs": false, - "jsxBracketSameLine": false + "bracketSameLine": false + } + ], + "eslint-comments/no-unused-disable": "error", + "react/no-unknown-property":[ + "error", + { + "ignore": [ + "jsx", + "global" + ] } ] }, "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 - } - ] - } - }, { "files": [ "**.ts", @@ -86,12 +84,77 @@ ], "extends": [ "plugin:@typescript-eslint/recommended", - "prettier/@typescript-eslint" + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "prettier" ], "rules": { "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-non-null-assertion": "off" + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/prefer-optional-chain": "error", + "@typescript-eslint/ban-types": "off", + "no-shadow": "off", + "@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/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.md b/.github/ISSUE_TEMPLATE/bug_report.md similarity index 74% rename from .github/issue_template.md rename to .github/ISSUE_TEMPLATE/bug_report.md index 9355fb4c..15db91f4 100644 --- a/.github/issue_template.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,3 +1,12 @@ +--- +name: Bug report +about: Create a report to help Hyper improve +title: '' +labels: '' +assignees: '' + +--- + -- [ ] I am on the [latest](https://github.com/zeit/hyper/releases/latest) Hyper.app version -- [ ] I have searched the [issues](https://github.com/zeit/hyper/issues) of this repo and believe that this is not a duplicate +- [ ] I am on 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 - **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/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..e28a3f4c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea/feature for Hyper +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..20a51554 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,25 @@ +version: 2 +updates: +- package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + time: '11:00' + open-pull-requests-limit: 30 + target-branch: canary + versioning-strategy: increase +- package-ecosystem: npm + directory: "/app" + schedule: + interval: weekly + 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/pull_request_template.md b/.github/pull_request_template.md index 9d8542bd..31a64d36 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,6 +3,6 @@ - To help whoever reviews your PR, it'd be extremely helpful for you to list whether your PR is ready to be merged, If there's anything left to do and if there are any related PRs - It'd also be extremely helpful to enable us to update your PR incase we need to rebase or what-not by checking `Allow edits from maintainers` -- If your PR changes some API, please make a PR for hyper website too: https://github.com/zeit/hyper-site. +- If your PR changes some API, please make a PR for hyper website too: https://github.com/vercel/hyper-site. Thanks, again! --> diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..569dda76 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,67 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ canary ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ canary ] + schedule: + - cron: '37 6 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + 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. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # 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@v3 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 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 + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/e2e_comment.yml b/.github/workflows/e2e_comment.yml new file mode 100644 index 00000000..573a4d59 --- /dev/null +++ b/.github/workflows/e2e_comment.yml @@ -0,0 +1,63 @@ +name: Comment e2e test screenshots on PR +on: + workflow_run: + workflows: ['Node CI'] + types: + - completed +jobs: + e2e_comment: + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + steps: + - name: Dump Workflow run info from GitHub context + env: + WORKFLOW_RUN_INFO: ${{ toJSON(github.event.workflow_run) }} + run: echo "$WORKFLOW_RUN_INFO" + - name: Download Artifacts + 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: e2e + - name: Get PR number + 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: pr_num + - name: Read the pr_num file + id: pr_num_reader + 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.2 + with: + path: ./*.png + client_id: ${{ secrets.IMGUR_CLIENT_ID }} + - name: Comment on the PR + uses: jungwinter/comment@v1 + env: + 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} + with: + type: create + issue_number: ${{ steps.pr_num_reader.outputs.content }} + token: ${{ secrets.GITHUB_TOKEN }} + 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 63f42524..d42e2995 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -5,21 +5,49 @@ on: - master - canary pull_request: +defaults: + run: + shell: bash +env: + NODE_VERSION: 18.x jobs: - ci_macos: - runs-on: macos-latest + build: + runs-on: ${{matrix.os}} strategy: matrix: - node-version: [12.x] + os: + - macos-latest + - ubuntu-latest + - windows-latest + fail-fast: false steps: - - uses: actions/checkout@v1 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + - name: Checkout + 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' @@ -27,112 +55,136 @@ jobs: cp build/canary.ico build/icon.ico cp build/canary.icns build/icon.icns - name: Build - run: yarn run dist --publish=never - env: - CI: true - - name: Get macOS Artifact Names - id: getmacosfilename run: | - echo "::set-output name=dmgName::$(ls dist/*.dmg | cut -d'/' -f2)" - echo "::set-output name=dmgPath::$(ls dist/*.dmg)" - - name: Archive macOS Build Artifacts - uses: actions/upload-artifact@v1 + 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 }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + - name: Archive Build Artifacts + uses: LabhanshAgrawal/upload-artifact@v3 with: - name: ${{ steps.getmacosfilename.outputs.dmgName }} - path: ${{ steps.getmacosfilename.outputs.dmgPath }} - ci_linux: + path: | + dist/*.dmg + dist/*.snap + 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@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: - node-version: [12.x] + include: + - name: armv7l + cpu: cortex-a8 + image: raspios_lite:latest + - name: arm64 + cpu: cortex-a53 + image: raspios_lite_arm64:latest + fail-fast: false steps: - - uses: actions/checkout@v1 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + - name: Checkout + 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 - 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 + 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 dist --publish=never + run: yarn run electron-builder -l deb rpm AppImage pacman --${{ matrix.name }} -c electron-builder-linux-ci.json env: - CI: true - - name: Get Linux Artifact Names - id: getlinuxfilename - run: | - echo "::set-output name=snapName::$(ls dist/*.snap | cut -d'/' -f2)" - echo "::set-output name=snapPath::$(ls dist/*.snap)" - echo "::set-output name=AppImageName::$(ls dist/*.AppImage | cut -d'/' -f2)" - echo "::set-output name=AppImagePath::$(ls dist/*.AppImage)" - echo "::set-output name=debName::$(ls dist/*.deb | cut -d'/' -f2)" - echo "::set-output name=debPath::$(ls dist/*.deb)" - echo "::set-output name=rpmName::$(ls dist/*.rpm | cut -d'/' -f2)" - echo "::set-output name=rpmPath::$(ls dist/*.rpm)" - - name: Archive Linux Build Artifacts (Snap) - uses: actions/upload-artifact@v1 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Archive Build Artifacts + uses: LabhanshAgrawal/upload-artifact@v3 with: - name: ${{ steps.getlinuxfilename.outputs.snapName }} - path: ${{ steps.getlinuxfilename.outputs.snapPath }} - - name: Archive Linux Build Artifacts (AppImage) - uses: actions/upload-artifact@v1 - with: - name: ${{ steps.getlinuxfilename.outputs.AppImageName }} - path: ${{ steps.getlinuxfilename.outputs.AppImagePath }} - - name: Archive Linux Build Artifacts (Deb) - uses: actions/upload-artifact@v1 - with: - name: ${{ steps.getlinuxfilename.outputs.debName }} - path: ${{ steps.getlinuxfilename.outputs.debPath }} - - name: Archive Linux Build Artifacts (RPM) - uses: actions/upload-artifact@v1 - with: - name: ${{ steps.getlinuxfilename.outputs.rpmName }} - path: ${{ steps.getlinuxfilename.outputs.rpmPath }} - ci_windows: - runs-on: windows-latest - strategy: - matrix: - node-version: [12.x] - steps: - - uses: actions/checkout@v1 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - name: Install - run: yarn install - - name: Test - run: yarn run test - - name: Getting Build Icon - if: github.ref == 'refs/heads/canary' || github.base_ref == 'canary' - run: | - Copy-Item .\build\canary.ico .\build\icon.ico - Copy-Item .\build\canary.icns .\build\icon.icns - - name: Build - run: yarn run dist --publish=never - env: - CI: true - - name: Get Windows Artifact Names - id: getwindowsfilename - run: | - Write-Host "::set-output name=exeName::$(Get-ChildItem -Name .\dist\squirrel-windows\ | Select-String exe)" - Write-Host "::set-output name=exePath::dist/squirrel-windows/$(Get-ChildItem -Name .\dist\squirrel-windows\ | Select-String exe)" - Write-Host "::set-output name=nupkgName::$(Get-ChildItem -Name .\dist\squirrel-windows\ | Select-String nupkg)" - Write-Host "::set-output name=nupkgPath::dist/squirrel-windows/$(Get-ChildItem -Name .\dist\squirrel-windows\ | Select-String nupkg)" - - name: Archive Windows Build Artifacts (exe) - uses: actions/upload-artifact@v1 - with: - name: ${{ steps.getwindowsfilename.outputs.exeName }} - path: ${{ steps.getwindowsfilename.outputs.exePath }} - - name: Archive Windows Build Artifacts (nupkg) - uses: actions/upload-artifact@v1 - with: - name: ${{ steps.getwindowsfilename.outputs.nupkgName }} - path: ${{ steps.getwindowsfilename.outputs.nupkgPath }} + path: | + dist/*.snap + dist/*.AppImage + dist/*.deb + dist/*.rpm + dist/*.pacman diff --git a/.gitignore b/.gitignore index 0fcb3422..9c5932fb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ dist app/renderer target bin/cli.* +cache # dependencies node_modules @@ -12,8 +13,11 @@ 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/settings.json +.vscode/* +!.vscode/launch.json +.idea diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 00000000..31354ec1 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 00000000..f077c917 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn test diff --git a/.huskyrc.json b/.huskyrc.json deleted file mode 100644 index 96a97ffa..00000000 --- a/.huskyrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/huskyrc", - "hooks": { - "pre-push": "yarn test" - } -} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6fd2647c..00000000 --- a/.travis.yml +++ /dev/null @@ -1,41 +0,0 @@ -sudo: required -dist: xenial - -language: node_js - -matrix: - include: - - os: linux - node_js: 12 - env: CC=clang CXX=clang++ npm_config_clang=1 - compiler: clang - -addons: - apt: - packages: - - gcc-multilib - - g++-multilib - - libgnome-keyring-dev - - icnsutils - - graphicsmagick - - xz-utils - - rpm - - bsdtar - - snapd - -before_install: - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo snap install snapcraft --classic; fi - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start; sleep 3; fi - -cache: yarn - -install: - - yarn - -after_success: - - (git branch --contains $TRAVIS_COMMIT | grep canary > /dev/null || [[ "$TRAVIS_BRANCH" == "canary" ]] ) && (cd build; cp canary.icns icon.icns; cp canary.ico icon.ico) - - yarn run dist - -branches: - except: - - "/^v\\d+\\.\\d+\\.\\d+$/" diff --git a/.yarnrc b/.yarnrc index 2659e9fb..45291c13 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,6 +1 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -child-concurrency "1" -lastUpdateCheck 1570388773781 +registry "https://registry.npmjs.org/" diff --git a/LICENSE b/LICENSE index 89491ddb..fe231dc9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ # MIT License -Copyright (c) 2018 ZEIT, Inc. +Copyright (c) 2018 Vercel, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/PLUGINS.md b/PLUGINS.md index 4b45b471..afc284a3 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -3,19 +3,19 @@ ## Workflow ### Run Hyper in dev mode -Hyper can be run in dev mode by cloning this repository and following the ["Contributing" section of our README](https://github.com/zeit/hyper#contribute). +Hyper can be run in dev mode by cloning this repository and following the ["Contributing" section of our README](https://github.com/vercel/hyper#contribute). In dev mode you'll get more ouput and access to React/Redux dev-tools in Electron. -Prerequisites and steps are described in the ["Contributing" section of our README](https://github.com/zeit/hyper#contribute). +Prerequisites and steps are described in the ["Contributing" section of our README](https://github.com/vercel/hyper#contribute). 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 @@ -30,7 +30,7 @@ module.exports = { ``` ### Running your plugin -To load, your plugin should expose at least one API method. All possible methods are listed [here](https://github.com/zeit/hyper/blob/canary/app/plugins/extensions.ts). +To load, your plugin should expose at least one API method. All possible methods are listed [here](https://github.com/vercel/hyper/blob/canary/app/plugins/extensions.ts). After launching Hyper in dev mode, run `yarn run app`, it should log that your plugin has been correcty loaded: `Plugin hyper-awesome-plugin (0.1.0) loaded.`. Name and version printed are the ones in your plugins `package.json` file. @@ -70,7 +70,7 @@ exports.decorateTerms = (Terms, {React}) => { // } } -``` +``` :warning: Note that you have to execute `this.props.onDecorated` to not break the handler chain. Without this, you could break other plugins that decorate the same component. ### Keymaps diff --git a/README.md b/README.md index 29a6d883..8c11085e 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,39 @@ -![](https://assets.zeit.co/image/upload/v1549723846/repositories/hyper/hyper-3-repo-banner.png) +![](https://assets.vercel.com/image/upload/v1549723846/repositories/hyper/hyper-3-repo-banner.png) -[![macOS CI Status](https://circleci.com/gh/zeit/hyper.svg?style=shield)](https://circleci.com/gh/zeit/hyper) -[![Windows CI status](https://ci.appveyor.com/api/projects/status/kqvb4oa772an58sc?svg=true)](https://ci.appveyor.com/project/zeit/hyper) -[![Linux CI status](https://travis-ci.org/zeit/hyper.svg?branch=master)](https://travis-ci.org/zeit/hyper) +

+ + + +

+ +[![Node CI](https://github.com/vercel/hyper/workflows/Node%20CI/badge.svg?event=push)](https://github.com/vercel/hyper/actions?query=workflow%3A%22Node+CI%22+branch%3Acanary+event%3Apush) [![Changelog #213](https://img.shields.io/badge/changelog-%23213-lightgrey.svg)](https://changelog.com/213) -[![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/zeit/hyper) For more details, head to: https://hyper.is +## Project goals + +The goal of the project is to create a beautiful and extensible experience for command-line interface users, built on open web standards. In the beginning, our focus will be primarily around speed, stability and the development of the correct API for extension authors. + +In the future, we anticipate the community will come up with innovative additions to enhance what could be the simplest, most powerful and well-tested interface for productivity. + ## Usage [Download the latest release!](https://hyper.is/#installation) ### Linux #### Arch and derivatives -Hyper is available in the [AUR](https://aur.archlinux.org/packages/hyper/). Use an AUR package manager like [aurman](https://github.com/polygamma/aurman) +Hyper is available in the [AUR](https://aur.archlinux.org/packages/hyper/). Use an AUR [package manager](https://wiki.archlinux.org/index.php/AUR_helpers) e.g. [paru](https://github.com/Morganamilo/paru) ```sh -aurman -S hyper +paru -S hyper +``` + +#### NixOS +Hyper is available as [Nix package](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/hyper/default.nix), to install the app run this command: + +```sh +nix-env -i hyper ``` ### macOS @@ -26,7 +42,7 @@ Use [Homebrew Cask](https://brew.sh) to download the app by running these comman ```bash brew update -brew cask install hyper +brew install --cask hyper ``` ### Windows @@ -83,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++` @@ -94,8 +110,7 @@ If you have issues in the `codesign` step when running `yarn run dist` on macOS, ## Related Repositories -- [Art](https://github.com/zeit/art/tree/master/hyper) -- [Website](https://github.com/zeit/hyper-site) -- [Sample Extension](https://github.com/zeit/hyperpower) -- [Sample Theme](https://github.com/zeit/hyperyellow) +- [Website](https://github.com/vercel/hyper-site) +- [Sample Extension](https://github.com/vercel/hyperpower) +- [Sample Theme](https://github.com/vercel/hyperyellow) - [Awesome Hyper](https://github.com/bnb/awesome-hyper) diff --git a/app/.yarnrc b/app/.yarnrc new file mode 100644 index 00000000..45291c13 --- /dev/null +++ b/app/.yarnrc @@ -0,0 +1 @@ +registry "https://registry.npmjs.org/" diff --git a/app/auto-updater-linux.js b/app/auto-updater-linux.ts similarity index 58% rename from app/auto-updater-linux.js rename to app/auto-updater-linux.ts index d3b73dd1..aa95c1d7 100644 --- a/app/auto-updater-linux.js +++ b/app/auto-updater-linux.ts @@ -1,7 +1,9 @@ -import fetch from 'electron-fetch'; import {EventEmitter} from 'events'; -class AutoUpdater extends EventEmitter { +import fetch from 'electron-fetch'; + +class AutoUpdater extends EventEmitter implements Electron.AutoUpdater { + updateURL!: string; quitAndInstall() { this.emitError('QuitAndInstall unimplemented'); } @@ -9,8 +11,8 @@ class AutoUpdater extends EventEmitter { return this.updateURL; } - setFeedURL(updateURL) { - this.updateURL = updateURL; + setFeedURL(options: Electron.FeedURLOptions) { + this.updateURL = options.url; } checkForUpdates() { @@ -20,29 +22,31 @@ class AutoUpdater extends EventEmitter { this.emit('checking-for-update'); fetch(this.updateURL) - .then(res => { + .then((res) => { if (res.status === 204) { - return this.emit('update-not-available'); + 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); }); }) .catch(this.emitError.bind(this)); } - emitError(error) { + emitError(error: string | Error) { if (typeof error === 'string') { error = new Error(error); } - this.emit('error', error, error.message); + this.emit('error', error); } } -export default new AutoUpdater(); +const autoUpdaterLinux = new AutoUpdater(); + +export default autoUpdaterLinux; diff --git a/app/commands.ts b/app/commands.ts index 4b5fdf15..469a5a3d 100644 --- a/app/commands.ts +++ b/app/commands.ts @@ -1,48 +1,51 @@ -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'; +import * as systemContextMenu from './utils/system-context-menu'; const commands: Record void> = { 'window:new': () => { // If window is created on the same tick, it will consume event too setTimeout(app.createWindow, 0); }, - 'tab:new': focusedWindow => { + 'tab:new': (focusedWindow) => { if (focusedWindow) { focusedWindow.rpc.emit('termgroup add req', {}); } else { setTimeout(app.createWindow, 0); } }, - 'pane:splitRight': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('split request vertical', {}); + 'pane:splitRight': (focusedWindow) => { + focusedWindow?.rpc.emit('split request vertical', {}); }, - 'pane:splitDown': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('split request horizontal', {}); + 'pane:splitDown': (focusedWindow) => { + focusedWindow?.rpc.emit('split request horizontal', {}); }, - 'pane:close': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('termgroup close req'); + 'pane:close': (focusedWindow) => { + focusedWindow?.rpc.emit('termgroup close req'); }, 'window:preferences': () => { - openConfig(); + void openConfig(); }, - 'editor:clearBuffer': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('session clear req'); + 'editor:clearBuffer': (focusedWindow) => { + focusedWindow?.rpc.emit('session clear req'); }, - 'editor:selectAll': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('term selectAll'); + 'editor:selectAll': (focusedWindow) => { + focusedWindow?.rpc.emit('term selectAll'); }, 'plugins:update': () => { updatePlugins(); }, - 'window:reload': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('reload'); + 'window:reload': (focusedWindow) => { + focusedWindow?.rpc.emit('reload'); }, - 'window:reloadFull': focusedWindow => { - focusedWindow && focusedWindow.reload(); + 'window:reloadFull': (focusedWindow) => { + focusedWindow?.reload(); }, - 'window:devtools': focusedWindow => { + 'window:devtools': (focusedWindow) => { if (!focusedWindow) { return; } @@ -53,75 +56,109 @@ const commands: Record void> = { webContents.openDevTools({mode: 'detach'}); } }, - 'zoom:reset': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('reset fontSize req'); + 'zoom:reset': (focusedWindow) => { + focusedWindow?.rpc.emit('reset fontSize req'); }, - 'zoom:in': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('increase fontSize req'); + 'zoom:in': (focusedWindow) => { + focusedWindow?.rpc.emit('increase fontSize req'); }, - 'zoom:out': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('decrease fontSize req'); + 'zoom:out': (focusedWindow) => { + focusedWindow?.rpc.emit('decrease fontSize req'); }, - 'tab:prev': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('move left req'); + 'tab:prev': (focusedWindow) => { + focusedWindow?.rpc.emit('move left req'); }, - 'tab:next': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('move right req'); + 'tab:next': (focusedWindow) => { + focusedWindow?.rpc.emit('move right req'); }, - 'pane:prev': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('prev pane req'); + 'pane:prev': (focusedWindow) => { + focusedWindow?.rpc.emit('prev pane req'); }, - 'pane:next': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('next pane req'); + 'pane:next': (focusedWindow) => { + focusedWindow?.rpc.emit('next pane req'); }, - 'editor:movePreviousWord': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('session move word left req'); + 'editor:movePreviousWord': (focusedWindow) => { + focusedWindow?.rpc.emit('session move word left req'); }, - 'editor:moveNextWord': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('session move word right req'); + 'editor:moveNextWord': (focusedWindow) => { + focusedWindow?.rpc.emit('session move word right req'); }, - 'editor:moveBeginningLine': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('session move line beginning req'); + 'editor:moveBeginningLine': (focusedWindow) => { + focusedWindow?.rpc.emit('session move line beginning req'); }, - 'editor:moveEndLine': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('session move line end req'); + 'editor:moveEndLine': (focusedWindow) => { + focusedWindow?.rpc.emit('session move line end req'); }, - 'editor:deletePreviousWord': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('session del word left req'); + 'editor:deletePreviousWord': (focusedWindow) => { + focusedWindow?.rpc.emit('session del word left req'); }, - 'editor:deleteNextWord': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('session del word right req'); + 'editor:deleteNextWord': (focusedWindow) => { + focusedWindow?.rpc.emit('session del word right req'); }, - 'editor:deleteBeginningLine': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('session del line beginning req'); + 'editor:deleteBeginningLine': (focusedWindow) => { + focusedWindow?.rpc.emit('session del line beginning req'); }, - 'editor:deleteEndLine': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('session del line end req'); + 'editor:deleteEndLine': (focusedWindow) => { + focusedWindow?.rpc.emit('session del line end req'); }, - 'editor:break': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('session break req'); + 'editor:break': (focusedWindow) => { + focusedWindow?.rpc.emit('session break req'); }, - 'editor:search': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('session search'); + 'editor:stop': (focusedWindow) => { + focusedWindow?.rpc.emit('session stop req'); }, - 'editor:search-close': focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('session search close'); + 'editor:quit': (focusedWindow) => { + focusedWindow?.rpc.emit('session quit req'); + }, + 'editor:tmux': (focusedWindow) => { + focusedWindow?.rpc.emit('session tmux req'); + }, + 'editor:search': (focusedWindow) => { + focusedWindow?.rpc.emit('session search'); + }, + 'editor:search-close': (focusedWindow) => { + focusedWindow?.rpc.emit('session search close'); }, 'cli:install': () => { - installCLI(true); + void installCLI(true); }, 'window:hamburgerMenu': () => { - if (getConfig().showHamburgerMenu) { - Menu.getApplicationMenu()!.popup({x: 15, y: 15}); + if (process.platform !== 'darwin' && ['', true].includes(getConfig().showHamburgerMenu)) { + Menu.getApplicationMenu()!.popup({x: 25, y: 22}); } + }, + 'systemContextMenu:add': () => { + systemContextMenu.add(); + }, + 'systemContextMenu:remove': () => { + systemContextMenu.remove(); + }, + 'window:toggleKeepOnTop': (focusedWindow) => { + focusedWindow?.setAlwaysOnTop(!focusedWindow.isAlwaysOnTop()); } }; //Special numeric command -([1, 2, 3, 4, 5, 6, 7, 8, 'last'] as const).forEach(cmdIndex => { +([1, 2, 3, 4, 5, 6, 7, 8, 'last'] as const).forEach((cmdIndex) => { const index = cmdIndex === 'last' ? cmdIndex : cmdIndex - 1; - commands[`tab:jump:${cmdIndex}`] = focusedWindow => { - focusedWindow && focusedWindow.rpc.emit('move jump req', index); + commands[`tab:jump:${cmdIndex}`] = (focusedWindow) => { + focusedWindow?.rpc.emit('move jump req', index); + }; +}); + +//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}); }; }); diff --git a/app/config.ts b/app/config.ts index dfb8d572..b4613d57 100644 --- a/app/config.ts +++ b/app/config.ts @@ -1,20 +1,24 @@ -import fs from 'fs'; -import notify from './notify'; +import {app} from 'electron'; + +import chokidar from 'chokidar'; + +import type {parsedConfig, configOptions} from '../typings/config'; + import {_import, getDefaultConfig} from './config/import'; import _openConfig from './config/open'; -import win from './config/windows'; import {cfgPath, cfgDir} from './config/paths'; +import notify from './notify'; import {getColorMap} from './utils/colors'; -const watchers: any[] = []; -let cfg: Record = {}; -let _watcher: fs.FSWatcher; +const watchers: Function[] = []; +let cfg: parsedConfig = {} as any; +let _watcher: chokidar.FSWatcher; -export const getDeprecatedCSS = (config: Record) => { +export const getDeprecatedCSS = (config: configOptions) => { const deprecated: string[] = []; const deprecatedCSS = ['x-screen', 'x-row', 'cursor-node', '::selection']; - deprecatedCSS.forEach(css => { - if ((config.css && config.css.includes(css)) || (config.termCSS && config.termCSS.includes(css))) { + deprecatedCSS.forEach((css) => { + if (config.css?.includes(css) || config.termCSS?.includes(css)) { deprecated.push(css); } }); @@ -35,7 +39,7 @@ const checkDeprecatedConfig = () => { const _watch = () => { if (_watcher) { - return _watcher; + return; } const onChange = () => { @@ -43,47 +47,26 @@ const _watch = () => { setTimeout(() => { cfg = _import(); notify('Configuration updated', 'Hyper configuration reloaded!'); - watchers.forEach(fn => fn()); + watchers.forEach((fn) => { + fn(); + }); checkDeprecatedConfig(); }, 100); }; - // Windows - if (process.platform === 'win32') { - // watch for changes on config every 2s on Windows - // https://github.com/zeit/hyper/pull/1772 - _watcher = fs.watchFile(cfgPath, {interval: 2000}, (curr, prev) => { - if (!curr.mtime || curr.mtime.getTime() === 0) { - //eslint-disable-next-line no-console - console.error('error watching config'); - } else if (curr.mtime.getTime() !== prev.mtime.getTime()) { - onChange(); - } - }) as any; - return; - } - // macOS/Linux - function setWatcher() { - try { - _watcher = fs.watch(cfgPath, eventType => { - if (eventType === 'rename') { - _watcher.close(); - // Ensure that new file has been written - setTimeout(() => setWatcher(), 500); - } + _watcher = chokidar.watch(cfgPath); + _watcher.on('change', onChange); + _watcher.on('error', (error) => { + console.error('error watching config', error); + }); + + app.on('before-quit', () => { + if (Object.keys(_watcher.getWatched()).length > 0) { + _watcher.close().catch((err) => { + console.warn(err); }); - } catch (e) { - //eslint-disable-next-line no-console - console.error('Failed to watch config file:', cfgPath, e); - return; } - _watcher.on('change', onChange); - _watcher.on('error', error => { - //eslint-disable-next-line no-console - console.error('error watching config', error); - }); - } - setWatcher(); + }); }; export const subscribe = (fn: Function) => { @@ -98,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 = () => { @@ -123,19 +128,17 @@ export const setup = () => { checkDeprecatedConfig(); }; -export const getWin = win.get; -export const winRecord = win.recordState; -export const windowDefaults = win.defaults; +export {get as getWin, recordState as winRecord, defaults as windowDefaults} from './config/windows'; -export const fixConfigDefaults = (decoratedConfig: any) => { - const defaultConfig = getDefaultConfig()?.config; +export const fixConfigDefaults = (decoratedConfig: configOptions) => { + const defaultConfig = getDefaultConfig().config!; decoratedConfig.colors = getColorMap(decoratedConfig.colors) || {}; // We must have default colors for xterm css. - decoratedConfig.colors = Object.assign({}, defaultConfig.colors, decoratedConfig.colors); + decoratedConfig.colors = {...defaultConfig.colors, ...decoratedConfig.colors}; return decoratedConfig; }; -export const htermConfigTranslate = (config: Record) => { +export const htermConfigTranslate = (config: configOptions) => { const cssReplacements: Record = { 'x-screen x-row([ {.[])': '.xterm-rows > div$1', '.cursor-node([ {.[])': '.terminal-cursor$1', @@ -143,11 +146,11 @@ export const htermConfigTranslate = (config: Record) => { 'x-screen a([ {.[])': '.terminal a$1', 'x-row a([ {.[])': '.terminal a$1' }; - Object.keys(cssReplacements).forEach(pattern => { + Object.keys(cssReplacements).forEach((pattern) => { const searchvalue = new RegExp(pattern, 'g'); const newvalue = cssReplacements[pattern]; - config.css = config.css && config.css.replace(searchvalue, newvalue); - config.termCSS = config.termCSS && config.termCSS.replace(searchvalue, newvalue); + config.css = config.css?.replace(searchvalue, newvalue); + config.termCSS = config.termCSS?.replace(searchvalue, newvalue); }); return config; }; diff --git a/app/config/config-default.js b/app/config/config-default.js deleted file mode 100644 index 9ed1e733..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` - // - // Git Bash - // - Example: `C:\\Program Files\\Git\\git-cmd.exe` - // Then Add `--command=usr/bin/bash.exe` to shellArgs - 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: true, - - // if `true` (without backticks and without quotes), Hyper will ignore ligatures provided by some fonts - disableLigatures: false, - - // 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 d98f4bfa..b9965375 100644 --- a/app/config/import.ts +++ b/app/config/import.ts @@ -1,85 +1,13 @@ -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'; -let defaultConfig: Record | undefined; +import {_init} from './init'; +import {migrateHyper3Config} from './migrate'; +import {defaultCfg, cfgPath, plugs, defaultPlatformKeyPath} from './paths'; -const _write = (path: string, data: any) => { - // 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) { - try { - const backupPath = `${src}.backup${attempt === 1 ? '' : attempt}`; - moveSync(src, backupPath); - return backupPath; - } catch (e) { - if (e.code === 'EEXIST') { - attempt++; - } else { - throw e; - } - } - } - 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` - ); -}; +let defaultConfig: rawConfig; const _importConf = () => { // init plugin directories if not present @@ -87,49 +15,51 @@ const _importConf = () => { mkdirpSync(plugs.local); try { - migrateHyper2Config(); + migrateHyper3Config(); } catch (err) { - //eslint-disable-next-line no-console console.error(err); } + let defaultCfgRaw = '{}'; try { - const defaultCfgRaw = readFileSync(defaultCfg, 'utf8'); - const _defaultCfg = _extractDefault(defaultCfgRaw); - // Importing platform specific keymap - try { - const content = readFileSync(defaultPlatformKeyPath(), 'utf8'); - const mapping = JSON.parse(content) as Record; - _defaultCfg.keymaps = mapping; - } catch (err) { - //eslint-disable-next-line no-console - console.error(err); - } - - // Import user config - try { - const userCfg = readFileSync(cfgPath, 'utf8'); - return {userCfg, defaultCfg: _defaultCfg}; - } catch (err) { - _write(cfgPath, defaultCfgRaw); - return {userCfg: defaultCfgRaw, defaultCfg: _defaultCfg}; - } + defaultCfgRaw = readFileSync(defaultCfg, 'utf8'); } catch (err) { - //eslint-disable-next-line no-console console.log(err); } + const _defaultCfg = JSON.parse(defaultCfgRaw) as rawConfig; + + // Importing platform specific keymap + let content = '{}'; + try { + content = readFileSync(defaultPlatformKeyPath(), 'utf8'); + } catch (err) { + console.error(err); + } + const mapping = JSON.parse(content) as Record; + _defaultCfg.keymaps = mapping; + + // Import user config + let userCfg: rawConfig; + try { + userCfg = JSON.parse(readFileSync(cfgPath, 'utf8')); + } catch (err) { + notify("Couldn't parse config file. Using default config instead."); + userCfg = JSON.parse(defaultCfgRaw); + } + + return {userCfg, defaultCfg: _defaultCfg}; }; export const _import = () => { const imported = _importConf(); - defaultConfig = imported?.defaultCfg; - const result = _init(imported!); + defaultConfig = imported.defaultCfg; + const result = _init(imported.userCfg, imported.defaultCfg); return result; }; export const getDefaultConfig = () => { if (!defaultConfig) { - defaultConfig = _importConf()?.defaultCfg; + defaultConfig = _importConf().defaultCfg; } return defaultConfig; }; diff --git a/app/config/init.ts b/app/config/init.ts index 561103f7..a41b864c 100644 --- a/app/config/init.ts +++ b/app/config/init.ts @@ -1,21 +1,27 @@ 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'; 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'); } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return module.exports; }; const _syntaxValidation = (cfg: string) => { try { - return new vm.Script(cfg, {filename: '.hyper.js', displayErrors: true}); - } catch (err) { - notify('Error loading config:', `${err.name}, see DevTools for more info`, {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}); } }; @@ -24,22 +30,34 @@ const _extractDefault = (cfg: string) => { }; // init config -const _init = (cfg: {userCfg: string; defaultCfg: Record}) => { - const script = _syntaxValidation(cfg.userCfg); - if (script) { - const _cfg = _extract(script); - if (!_cfg.config) { - notify('Error reading configuration: `config` key is missing'); - return cfg.defaultCfg; - } +const _init = (userCfg: rawConfig, defaultCfg: rawConfig): parsedConfig => { + return { + 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 defaultCfg.config || ({} as configOptions); + } + })(), // Merging platform specific keymaps with user defined keymaps - _cfg.keymaps = mapKeys(Object.assign({}, cfg.defaultCfg.keymaps, _cfg.keymaps)); + keymaps: mapKeys({...defaultCfg.keymaps, ...userCfg?.keymaps}), // Ignore undefined values in plugin and localPlugins array Issue #1862 - _cfg.plugins = (_cfg.plugins && _cfg.plugins.filter(Boolean)) || []; - _cfg.localPlugins = (_cfg.localPlugins && _cfg.localPlugins.filter(Boolean)) || []; - return _cfg; - } - return cfg.defaultCfg; + plugins: userCfg?.plugins?.filter(Boolean) || [], + localPlugins: userCfg?.localPlugins?.filter(Boolean) || [] + }; }; export {_init, _extractDefault}; 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 63779bce..264c2292 100644 --- a/app/config/open.ts +++ b/app/config/open.ts @@ -1,76 +1,80 @@ +import {exec} from 'child_process'; + import {shell} from 'electron'; + +import * as Registry from 'native-reg'; + import {cfgPath} from './paths'; -export default () => Promise.resolve(shell.openItem(cfgPath)); -// 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') { - const Registry = require('winreg') as typeof import('winreg'); - const {exec} = require('child_process') as typeof import('child_process'); - - const getUserChoiceKey = async () => { +const getUserChoiceKey = () => { + try { // Load FileExts keys for .js files - const keys: Winreg.Registry[] = await new Promise((resolve, reject) => { - new Registry({ - hive: Registry.HKCU, - key: '\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.js' - }).keys((error, items) => { - if (error) { - reject(error); - } else { - resolve(items || []); - } - }); - }); + const fileExtsKeys = Registry.openKey( + Registry.HKCU, + 'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.js', + Registry.Access.READ + ); + const keys = fileExtsKeys ? Registry.enumKeyNames(fileExtsKeys) : []; + Registry.closeKey(fileExtsKeys); // Find UserChoice key - const userChoice = keys.find(k => k.key.endsWith('UserChoice')); - return userChoice; - }; + const userChoice = keys.find((k) => k.endsWith('UserChoice')); + return userChoice + ? `Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.js\\${userChoice}` + : userChoice; + } catch (error) { + console.error(error); + return; + } +}; - const hasDefaultSet = async () => { - const userChoice = await getUserChoiceKey(); - if (!userChoice) return false; +const hasDefaultSet = () => { + const userChoice = getUserChoiceKey(); + if (!userChoice) return false; + try { // Load key values - const values: string[] = await new Promise((resolve, reject) => { - userChoice.values((error, items) => { - if (error) { - reject(error); - } - resolve(items.map(item => item.value || '') || []); - }); - }); + const userChoiceKey = Registry.openKey(Registry.HKCU, userChoice, Registry.Access.READ)!; + const values: string[] = Registry.enumValueNames(userChoiceKey).map( + (x) => (Registry.queryValue(userChoiceKey, x) as string) || '' + ); + Registry.closeKey(userChoiceKey); // Look for default program const hasDefaultProgramConfigured = values.every( - value => value && typeof value === 'string' && !value.includes('WScript.exe') && !value.includes('JSFile') + (value) => value && typeof value === 'string' && !value.includes('WScript.exe') && !value.includes('JSFile') ); return hasDefaultProgramConfigured; - }; + } catch (error) { + console.error(error); + return false; + } +}; - // This mimics shell.openItem, true if it worked, false if not. - const openNotepad = (file: string) => - new Promise(resolve => { - exec(`start notepad.exe ${file}`, error => { - resolve(!error); - }); +// This mimics shell.openItem, true if it worked, false if not. +const openNotepad = (file: string) => + new Promise((resolve) => { + exec(`start notepad.exe ${file}`, (error) => { + resolve(!error); }); + }); - module.exports = () => - hasDefaultSet() - .then(yes => { - if (yes) { - return shell.openItem(cfgPath); - } - //eslint-disable-next-line no-console - console.warn('No default app set for .js files, using notepad.exe fallback'); - return openNotepad(cfgPath); - }) - .catch(err => { - //eslint-disable-next-line no-console - console.error('Open config with default app error:', err); - return openNotepad(cfgPath); - }); -} +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') { + try { + if (hasDefaultSet()) { + return shell.openPath(cfgPath).then((error) => error === ''); + } + console.warn('No default app set for .js files, using notepad.exe fallback'); + } catch (err) { + console.error('Open config with default app error:', err); + } + return openNotepad(cfgPath); + } + return shell.openPath(cfgPath).then((error) => error === ''); +}; + +export default openConfig; diff --git a/app/config/paths.ts b/app/config/paths.ts index 000826b2..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); @@ -32,17 +42,14 @@ if (isDev) { statSync(devCfg); cfgPath = devCfg; cfgDir = devDir; - //eslint-disable-next-line no-console console.log('using config file:', cfgPath); } catch (err) { // ignore } } -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') @@ -83,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 0486fdf4..4cd6d500 100644 --- a/app/config/windows.ts +++ b/app/config/windows.ts @@ -1,23 +1,21 @@ -import Config from 'electron-store'; -import {BrowserWindow} from 'electron'; +import type {BrowserWindow} from 'electron'; -const defaults = { - windowPosition: [50, 50], - windowSize: [540, 380] +import Config from 'electron-store'; + +export const defaults = { + windowPosition: [50, 50] as [number, number], + windowSize: [540, 380] as [number, number] }; // local storage const cfg = new Config({defaults}); -export default { - defaults, - get() { - const position = cfg.get('windowPosition'); - const size = cfg.get('windowSize'); - return {position, size}; - }, - recordState(win: BrowserWindow) { - cfg.set('windowPosition', win.getPosition()); - cfg.set('windowSize', win.getSize()); - } -}; +export function get() { + const position = cfg.get('windowPosition', defaults.windowPosition); + const size = cfg.get('windowSize', defaults.windowSize); + return {position, size}; +} +export function recordState(win: BrowserWindow) { + cfg.set('windowPosition', win.getPosition()); + cfg.set('windowSize', win.getSize()); +} 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/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 5fba4d7e..84a804a4 100644 --- a/app/index.ts +++ b/app/index.ts @@ -1,89 +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'; - //eslint-disable-next-line no-console console.log(`Hyper version ${version}`); - //eslint-disable-next-line no-console console.log('Hyper does not accept any command line arguments. Please modify the config file instead.'); - //eslint-disable-next-line no-console - console.log(`Hyper configuration file located at: ${configLocation}`); + console.log(`Hyper configuration file located at: ${cfgPath}`); process.exit(); } -const checkSquirrel = () => { - let squirrel; +// Enable remote module +// eslint-disable-next-line import/order +import {initialize as remoteInitialize} from '@electron/remote/main'; +remoteInitialize(); - try { - squirrel = require('electron-squirrel-startup'); - //eslint-disable-next-line no-empty - } catch (err) {} - if (squirrel) { - process.exit(); - } -}; - -// handle startup squirrel events -if (process.platform === 'win32') { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const systemContextMenu = require('./system-context-menu'); - - switch (process.argv[1]) { - case '--squirrel-install': - case '--squirrel-updated': - systemContextMenu.add(() => { - checkSquirrel(); - }); - break; - case '--squirrel-uninstall': - systemContextMenu.remove(() => { - checkSquirrel(); - }); - break; - default: - checkSquirrel(); - } -} +// 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'; -// Hack - this declararion doesn't work when put into ./ext-modules.d.ts for some reason so it's in this file for the time being -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?: Record) => BrowserWindow; - setVersion: (version: string) => void; - } - - type Server = import('./rpc').Server; - interface BrowserWindow { - uid: string; - sessions: Map; - focusTime: number; - clean: () => void; - rpc: Server; - } -} - -// 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([]); @@ -104,54 +55,54 @@ app.getLastFocusedWindow = () => { }); }; -//eslint-disable-next-line no-console console.log('Disabling Chromium GPU blacklist'); app.commandLine.appendSwitch('ignore-gpu-blacklist'); if (isDev) { - //eslint-disable-next-line no-console 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); } }); } else { - //eslint-disable-next-line no-console console.log('running in prod mode'); } const url = `file://${resolve(isDev ? __dirname : app.getAppPath(), 'index.html')}`; -//eslint-disable-next-line no-console console.log('electron will open', url); -function installDevExtensions(isDev_: boolean) { +async function installDevExtensions(isDev_: boolean) { if (!isDev_) { - return Promise.resolve([]); + return []; } - // eslint-disable-next-line @typescript-eslint/no-var-requires - const installer = require('electron-devtools-installer') as typeof 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))); + return Promise.all( + extensions.map((extension) => installer(extension, {forceDownload, loadExtensionOptions: {allowFileAccess: true}})) + ); } +// eslint-disable-next-line @typescript-eslint/no-misused-promises app.on('ready', () => installDevExtensions(isDev) .then(() => { - function createWindow(fn?: (win: BrowserWindow) => void, options: Record = {}) { - const cfg = plugins.getDecoratedConfig(); + function createWindow( + fn?: (win: BrowserWindow) => void, + options: {size?: [number, number]; position?: [number, number]} = {}, + profileName: string = config.getDefaultProfile() + ) { + 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; @@ -188,9 +139,9 @@ 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); - hwin.loadURL(url); + void hwin.loadURL(url); // the window can be closed by the browser process itself hwin.on('close', () => { @@ -198,12 +149,6 @@ app.on('ready', () => windowSet.delete(hwin); }); - hwin.on('closed', () => { - if (process.platform !== 'darwin' && windowSet.size === 0) { - app.quit(); - } - }); - return hwin; } @@ -222,6 +167,12 @@ app.on('ready', () => } }); + app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } + }); + const makeMenu = () => { const menu = plugins.decorateMenu(AppMenu.createMenu(createWindow, plugins.getLoadedPluginVersions)); @@ -248,26 +199,26 @@ app.on('ready', () => if (!isDev) { // check if should be set/removed as default ssh protocol client if (config.getConfig().defaultSSHApp && !app.isDefaultProtocolClient('ssh')) { - //eslint-disable-next-line no-console console.log('Setting Hyper as default client for ssh:// protocol'); app.setAsDefaultProtocolClient('ssh'); } else if (!config.getConfig().defaultSSHApp && app.isDefaultProtocolClient('ssh')) { - //eslint-disable-next-line no-console console.log('Removing Hyper from default client for ssh:// protocol'); app.removeAsDefaultProtocolClient('ssh'); } - installCLI(false); + void installCLI(false); } }) - .catch(err => { - //eslint-disable-next-line no-console + .catch((err) => { console.error('Error while loading devtools extensions', err); }) ); -app.on('open-file', (event, path) => { +/** + * Get last focused BrowserWindow or create new if none and callback + * @param callback Function to call with the BrowserWindow + */ +function GetWindow(callback: (win: BrowserWindow) => void) { const lastWindow = app.getLastFocusedWindow(); - const callback = (win: BrowserWindow) => win.rpc.emit('open file', {path}); if (lastWindow) { callback(lastWindow); } else if (!lastWindow && {}.hasOwnProperty.call(app, 'createWindow')) { @@ -277,18 +228,16 @@ app.on('open-file', (event, path) => { // sets his callback to an app.windowCallback property. app.windowCallback = callback; } +} + +app.on('open-file', (_event, path) => { + GetWindow((win: BrowserWindow) => { + win.rpc.emit('open file', {path}); + }); }); -app.on('open-url', (event, sshUrl) => { - const lastWindow = app.getLastFocusedWindow(); - const callback = (win: BrowserWindow) => win.rpc.emit('open ssh', sshUrl); - if (lastWindow) { - callback(lastWindow); - } else if (!lastWindow && {}.hasOwnProperty.call(app, 'createWindow')) { - app.createWindow(callback); - } else { - // If createWindow doesn't exist yet ('ready' event was not fired), - // sets his callback to an app.windowCallback property. - app.windowCallback = callback; - } +app.on('open-url', (_event, sshUrl) => { + GetWindow((win: BrowserWindow) => { + win.rpc.emit('open ssh', parseUrl(sshUrl)); + }); }); diff --git a/app/keymaps/linux.json b/app/keymaps/linux.json index ab07a2fd..da66d671 100644 --- a/app/keymaps/linux.json +++ b/app/keymaps/linux.json @@ -3,6 +3,7 @@ "window:reload": "ctrl+shift+r", "window:reloadFull": "ctrl+shift+f5", "window:preferences": "ctrl+,", + "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 6a896a39..df877e69 100644 --- a/app/menus/menu.ts +++ b/app/menus/menu.ts @@ -1,21 +1,23 @@ // 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 pluginsMenu from './menus/plugins'; -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'; -const appName = app.getName(); +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(); let menu_: Menu; @@ -34,14 +36,14 @@ export const createMenu = ( let updateChannel = 'stable'; - if (config && config.updateChannel && config.updateChannel === 'canary') { + if (config?.updateChannel && config.updateChannel === 'canary') { updateChannel = 'canary'; } const showAbout = () => { const loadedPlugins = getLoadedPluginVersions(); const pluginList = - loadedPlugins.length === 0 ? 'none' : loadedPlugins.map(plugin => `\n ${plugin.name} (${plugin.version})`); + loadedPlugins.length === 0 ? 'none' : loadedPlugins.map((plugin) => `\n ${plugin.name} (${plugin.version})`); const rendererCounts = Object.values(getRendererTypes()).reduce((acc: Record, type) => { acc[type] = acc[type] ? acc[type] + 1 : 1; @@ -51,20 +53,24 @@ export const createMenu = ( .map(([type, count]) => type + (count > 1 ? ` (${count})` : '')) .join(', '); - dialog.showMessageBox({ + void dialog.showMessageBox({ title: `About ${appName}`, message: `${appName} ${appVersion} (${updateChannel})`, - detail: `Renderers: ${renderers}\nPlugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright © 2020 ZEIT, 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), - pluginsMenu(commandKeys, execCommand), + toolsMenu(commandKeys, execCommand), windowMenu(commandKeys, execCommand), helpMenu(commandKeys, showAbout) ]; diff --git a/app/menus/menus/darwin.ts b/app/menus/menus/darwin.ts index 6756ab20..59e514d1 100644 --- a/app/menus/menus/darwin.ts +++ b/app/menus/menus/darwin.ts @@ -1,14 +1,15 @@ // 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 ): MenuItemConstructorOptions => { return { - label: `${app.getName()}`, + label: `${app.name}`, submenu: [ { label: 'About Hyper', @@ -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 07d4414b..3c9214c1 100644 --- a/app/menus/menus/help.ts +++ b/app/menus/menus/help.ts @@ -1,36 +1,38 @@ import {release} from 'os'; -import {app, shell, MenuItemConstructorOptions} 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.getName()} Website`, + label: `${app.name} Website`, click() { - shell.openExternal('https://hyper.is'); + void shell.openExternal('https://hyper.is'); } }, { label: 'Report Issue', - click() { - const body = ` - - - [ ] Your Hyper.app version is **${version}**. Please verify your using the [latest](https://github.com/zeit/hyper/releases/latest) Hyper.app version - - [ ] I have searched the [issues](https://github.com/zeit/hyper/issues) of this repo and believe that this is not a duplicate - - --- - - **Any relevant information from devtools?** _(CMD+ALT+I on macOS, CTRL+SHIFT+I elsewhere)_: +- [ ] 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)_: - - **Is the issue reproducible in vanilla Hyper.app?** +- **Is the issue reproducible in vanilla Hyper.app?** ## Issue @@ -40,26 +42,54 @@ 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.json contents - - - **${app.getName()} version**: ${env.TERM_PROGRAM_VERSION} "${app.getVersion()}" +\`\`\`json +${JSON.stringify(getConfig(), null, 2)} +\`\`\` +
+
plugins - - **OS ARCH VERSION:** ${platform} ${arch} ${release()} - - **Electron:** ${versions.electron} **LANG:** ${env.LANG} - - **SHELL:** ${env.SHELL} **TERM:** ${env.TERM} +\`\`\`json +${JSON.stringify(getPlugins(), null, 2)} +\`\`\` +
`; -
- ~/.hyper.js contents -
-        
-          ${JSON.stringify(getConfig(), null, 2)}
-
-          ${JSON.stringify(getPlugins(), null, 2)}
-        
-      
-
`; - - shell.openExternal(`https://github.com/zeit/hyper/issues/new?body=${encodeURIComponent(body)}`); + const issueURL = `https://github.com/vercel/hyper/issues/new?body=${encodeURIComponent(body)}`; + const copyAndSend = () => { + clipboard.writeText(body); + void shell.openExternal( + `https://github.com/vercel/hyper/issues/new?body=${encodeURIComponent( + '\n' + )}` + ); + }; + if (!focusedWindow) { + copyAndSend(); + } else if (issueURL.length > 6144) { + void dialog + .showMessageBox(focusedWindow, { + message: + 'There is too much data to send to GitHub directly. The data will be copied to the clipboard, ' + + 'please paste it into the GitHub issue page that will open.', + type: 'warning', + buttons: ['OK', 'Cancel'] + }) + .then((result) => { + if (result.response === 0) { + copyAndSend(); + } + }); + } else { + void shell.openExternal(issueURL); + } } } ]; @@ -68,7 +98,7 @@ export default (commands: Record, showAbout: () => void): MenuIt submenu.push( {type: 'separator'}, { - role: 'about', + label: 'About Hyper', click() { showAbout(); } @@ -80,3 +110,5 @@ export default (commands: Record, showAbout: () => void): MenuIt submenu }; }; + +export default helpMenu; diff --git a/app/menus/menus/plugins.ts b/app/menus/menus/plugins.ts deleted file mode 100644 index 47a6a6c2..00000000 --- a/app/menus/menus/plugins.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {BrowserWindow, MenuItemConstructorOptions} from 'electron'; - -export default ( - commands: Record, - execCommand: (command: string, focusedWindow?: BrowserWindow) => void -): MenuItemConstructorOptions => { - return { - label: 'Plugins', - submenu: [ - { - label: 'Update', - accelerator: commands['plugins:update'], - click() { - execCommand('plugins:update'); - } - }, - { - label: 'Install Hyper CLI command in PATH', - click() { - execCommand('cli:install'); - } - }, - { - type: 'separator' - } - ] - }; -}; 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 new file mode 100644 index 00000000..ac377728 --- /dev/null +++ b/app/menus/menus/tools.ts @@ -0,0 +1,49 @@ +import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; + +const toolsMenu = ( + commands: Record, + execCommand: (command: string, focusedWindow?: BrowserWindow) => void +): MenuItemConstructorOptions => { + return { + label: 'Tools', + submenu: [ + { + label: 'Update plugins', + accelerator: commands['plugins:update'], + click() { + execCommand('plugins:update'); + } + }, + { + label: 'Install Hyper CLI command in PATH', + click() { + execCommand('cli:install'); + } + }, + { + type: 'separator' + }, + ...(process.platform === 'win32' + ? [ + { + label: 'Add Hyper to system context menu', + click() { + execCommand('systemContextMenu:add'); + } + }, + { + label: 'Remove Hyper from system context menu', + click() { + execCommand('systemContextMenu:remove'); + } + }, + { + type: 'separator' + } + ] + : []) + ] + }; +}; + +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 8ea018dc..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}`; @@ -81,6 +81,12 @@ export default ( { role: 'front' }, + { + label: 'Toggle Always on Top', + click: (item, focusedWindow) => { + execCommand('window:toggleKeepOnTop', focusedWindow); + } + }, { role: 'togglefullscreen', accelerator: commandKeys['window:toggleFullScreen'] @@ -88,3 +94,5 @@ export default ( ] }; }; + +export default windowMenu; diff --git a/app/notifications.ts b/app/notifications.ts index 0a92cc36..1a7abb06 100644 --- a/app/notifications.ts +++ b/app/notifications.ts @@ -1,20 +1,20 @@ -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'; export default function fetchNotifications(win: BrowserWindow) { const {rpc} = win; - const retry = (err?: any) => { + const retry = (err?: Error) => { setTimeout(() => fetchNotifications(win), ms('30m')); if (err) { - //eslint-disable-next-line no-console console.error('Notification messages fetch error', err.stack); } }; - //eslint-disable-next-line no-console console.log('Checking for notification messages'); fetch(NEWS_URL, { headers: { @@ -22,14 +22,13 @@ export default function fetchNotifications(win: BrowserWindow) { 'X-Hyper-Platform': process.platform } }) - .then(res => res.json()) - .then(data => { - const {message} = data || {}; + .then((res) => res.json()) + .then((data) => { + const message: {text: string; url: string; dismissable: boolean} | '' = data.message || ''; if (typeof message !== 'object' && message !== '') { throw new Error('Bad response'); } if (message === '') { - //eslint-disable-next-line no-console console.log('No matching notification messages'); } else { rpc.emit('add notification', message); diff --git a/app/notify.html b/app/notify.html deleted file mode 100644 index 6dd8af4e..00000000 --- a/app/notify.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/app/notify.ts b/app/notify.ts index 8c63a8e2..ce5544fc 100644 --- a/app/notify.ts +++ b/app/notify.ts @@ -1,46 +1,21 @@ -import {resolve} from 'path'; -import {app, BrowserWindow} from 'electron'; -import isDev from 'electron-is-dev'; +import {app, Notification} from 'electron'; -let win: BrowserWindow; +import {icon} from './config/paths'; -// the hack of all hacks -// electron doesn't have a built in notification thing, -// so we launch a window on which we can use the -// HTML5 `Notification` API :'( - -let buffer: string[][] = []; - -function notify(title: string, body = '', details: any = {}) { - //eslint-disable-next-line no-console +export default function notify(title: string, body = '', details: {error?: any} = {}) { console.log(`[Notification] ${title}: ${body}`); if (details.error) { - //eslint-disable-next-line no-console console.error(details.error); } - if (win) { - win.webContents.send('notification', {title, body}); + if (app.isReady()) { + _createNotification(title, body); } else { - buffer.push([title, body]); + app.on('ready', () => { + _createNotification(title, body); + }); } } -app.on('ready', () => { - const win_ = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true - } - }); - const url = `file://${resolve(isDev ? __dirname : app.getAppPath(), 'notify.html')}`; - win_.loadURL(url); - win_.webContents.on('dom-ready', () => { - win = win_; - buffer.forEach(([title, body]) => { - notify(title, body); - }); - buffer = []; - }); -}); - -export default notify; +const _createNotification = (title: string, body: string) => { + new Notification({title, body, ...(process.platform === 'linux' && {icon})}).show(); +}; diff --git a/app/package.json b/app/package.json index 629cb178..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.4", + "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", - "color": "3.1.2", - "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.4.0", - "electron-is-dev": "1.1.0", - "electron-squirrel-startup": "1.0.0", - "electron-store": "5.1.0", - "file-uri-to-path": "2.0.0", - "fs-extra": "8.1.0", - "git-describe": "4.0.4", - "lodash": "4.17.15", - "mkdirp": "1.0.3", - "ms": "2.1.2", - "node-pty": "0.9.0", - "os-locale": "4.0.0", - "parse-url": "5.0.1", - "pify": "5.0.0", - "queue": "6.0.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "semver": "7.1.3", - "shell-env": "3.0.0", - "uuid": "3.4.0", - "winreg": "1.2.4" + "electron-devtools-installer": "3.2.0", + "electron-fetch": "1.9.1", + "electron-is-dev": "2.0.0", + "electron-store": "8.2.0", + "fs-extra": "11.2.0", + "git-describe": "4.1.1", + "lodash": "4.17.21", + "ms": "2.1.3", + "native-process-working-directory": "^1.0.2", + "node-pty": "1.0.0", + "os-locale": "5.0.0", + "parse-url": "8.1.0", + "queue": "6.0.2", + "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": "9.0.1" }, "optionalDependencies": { - "native-reg": "0.3.3" + "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 da62f360..8e460119 100644 --- a/app/plugins.ts +++ b/app/plugins.ts @@ -1,16 +1,27 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ -import {app, dialog, BrowserWindow, App} from 'electron'; -import {resolve, basename} from 'path'; +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +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'; // local storage @@ -49,7 +60,7 @@ config.subscribe(() => { // patching Module._load // so plugins can `require` them without needing their own version -// https://github.com/zeit/hyper/issues/619 +// https://github.com/vercel/hyper/issues/619 function patchModuleLoad() { // eslint-disable-next-line @typescript-eslint/no-var-requires const Module = require('module'); @@ -81,7 +92,7 @@ function patchModuleLoad() { } function checkDeprecatedExtendKeymaps() { - modules.forEach(plugin => { + modules.forEach((plugin) => { if (plugin.extendKeymaps) { notify('Plugin warning!', `"${plugin._name}" use deprecated "extendKeymaps" handler`); return; @@ -98,11 +109,10 @@ function updatePlugins({force = false} = {}) { updating = true; syncPackageJSON(); const id_ = id; - install((err: any) => { + install((err) => { updating = false; if (err) { - //eslint-disable-next-line no-console notify('Error updating plugins.', err, {error: err}); } else { // flag successful plugin update @@ -124,7 +134,9 @@ function updatePlugins({force = false} = {}) { cache.set('hyper.plugin-versions', pluginVersions); // notify watchers - watchers.forEach(fn => fn(err, {force})); + watchers.forEach((fn) => { + fn(err, {force}); + }); if (force || changed) { if (changed) { @@ -140,9 +152,10 @@ function updatePlugins({force = false} = {}) { function getPluginVersions() { const paths_ = paths.plugins.concat(paths.localPlugins); - return paths_.map(path_ => { - let version = null; + return paths_.map((path_) => { + let version: string | null = null; try { + // eslint-disable-next-line @typescript-eslint/no-var-requires version = require(resolve(path_, 'package.json')).version; //eslint-disable-next-line no-empty } catch (err) {} @@ -152,7 +165,7 @@ function getPluginVersions() { function clearCache() { // trigger unload hooks - modules.forEach(mod => { + modules.forEach((mod) => { if (mod.onUnload) { mod.onUnload(app); } @@ -169,7 +182,7 @@ function clearCache() { export {updatePlugins}; export const getLoadedPluginVersions = () => { - return modules.map(mod => ({name: mod._name, version: mod._version})); + return modules.map((mod) => ({name: mod._name, version: mod._version})); }; // we schedule the initial plugins update @@ -177,7 +190,6 @@ export const getLoadedPluginVersions = () => { // to prevent slowness if (cache.get('hyper.plugins') !== id || process.env.HYPER_FORCE_UPDATE) { // install immediately if the user changed plugins - //eslint-disable-next-line no-console console.log('plugins have changed / not init, scheduling plugins installation'); setTimeout(() => { updatePlugins(); @@ -188,10 +200,7 @@ if (cache.get('hyper.plugins') !== id || process.env.HYPER_FORCE_UPDATE) { const baseConfig = config.getConfig(); if (baseConfig['autoUpdatePlugins']) { // otherwise update plugins every 5 hours - setInterval( - updatePlugins, - ms(baseConfig['autoUpdatePlugins'] === true ? '5h' : (baseConfig['autoUpdatePlugins'] as string)) - ); + setInterval(updatePlugins, ms(baseConfig['autoUpdatePlugins'] === true ? '5h' : baseConfig['autoUpdatePlugins'])); } })(); @@ -199,10 +208,10 @@ 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: 'zeit/hyper', + repository: 'vercel/hyper', license: 'MIT', homepage: 'https://hyper.is', dependencies @@ -217,7 +226,7 @@ function syncPackageJSON() { } function alert(message: string) { - dialog.showMessageBox({ + void dialog.showMessageBox({ message, buttons: ['Ok'] }); @@ -225,13 +234,13 @@ function alert(message: string) { function toDependencies(plugins_: {plugins: string[]}) { const obj: Record = {}; - plugins_.plugins.forEach(plugin => { + plugins_.plugins.forEach((plugin) => { const regex = /.(@|#)/; const match = regex.exec(plugin); 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); @@ -252,10 +261,10 @@ export const subscribe = (fn: Function) => { function getPaths() { return { - plugins: plugins.plugins.map(name => { + plugins: plugins.plugins.map((name) => { return resolve(path, 'node_modules', name.split('#')[0]); }), - localPlugins: plugins.localPlugins.map(name => { + localPlugins: plugins.localPlugins.map((name) => { return resolve(localPath, name); }) }; @@ -273,10 +282,10 @@ 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)); + const exposed = mod && Object.keys(mod).some((key) => availableExtensions.has(key)); if (!exposed) { notify('Plugin error!', `${`Plugin "${basename(path_)}" does not expose any `}Hyper extension API methods`); return; @@ -285,18 +294,17 @@ function requirePlugins(): any[] { // populate the name for internal errors here mod._name = basename(path_); try { + // eslint-disable-next-line @typescript-eslint/no-var-requires mod._version = require(resolve(path_, 'package.json')).version; } catch (err) { - //eslint-disable-next-line no-console console.warn(`No package.json found in ${path_}`); } - //eslint-disable-next-line no-console 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') { - //eslint-disable-next-line no-console console.warn(`Plugin error while loading "${basename(path_)}" (${path_}): ${err.message}`); } else { notify('Plugin error!', `Plugin "${basename(path_)}" failed to load (${err.message})`, {error: err}); @@ -304,14 +312,17 @@ 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) => { - modules.forEach(plugin => { + modules.forEach((plugin) => { if (plugin.onApp) { try { plugin.onApp(app_); @@ -325,7 +336,7 @@ export const onApp = (app_: App) => { }; export const onWindowClass = (win: BrowserWindow) => { - modules.forEach(plugin => { + modules.forEach((plugin) => { if (plugin.onWindowClass) { try { plugin.onWindowClass(win); @@ -339,7 +350,7 @@ export const onWindowClass = (win: BrowserWindow) => { }; export const onWindow = (win: BrowserWindow) => { - modules.forEach(plugin => { + modules.forEach((plugin) => { if (plugin.onWindow) { try { plugin.onWindow(win); @@ -356,7 +367,7 @@ export const onWindow = (win: BrowserWindow) => { // for all the available plugins function decorateEntity(base: any, key: string, type: 'object' | 'function') { let decorated = base; - modules.forEach(plugin => { + modules.forEach((plugin) => { if (plugin[key]) { let res; try { @@ -376,7 +387,7 @@ function decorateEntity(base: any, key: string, type: 'object' | 'function') { return decorated; } -function decorateObject(base: any, key: string) { +function decorateObject(base: T, key: string): T { return decorateEntity(base, key, 'object'); } @@ -385,14 +396,14 @@ function decorateClass(base: any, key: string) { } export const getDeprecatedConfig = () => { - const deprecated: Record = {}; + const deprecated: Record = {}; const baseConfig = config.getConfig(); - modules.forEach(plugin => { + modules.forEach((plugin) => { if (!plugin.decorateConfig) { return; } // We need to clone config in case of plugin modifies config directly. - let configTmp; + let configTmp: configOptions; try { configTmp = plugin.decorateConfig(JSON.parse(JSON.stringify(baseConfig))); } catch (e) { @@ -410,7 +421,7 @@ export const getDeprecatedConfig = () => { return deprecated; }; -export const decorateMenu = (tpl: any) => { +export const decorateMenu = (tpl: MenuItemConstructorOptions[]) => { return decorateObject(tpl, 'decorateMenu'); }; @@ -418,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); @@ -450,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 023fa111..35ed7245 100644 --- a/app/plugins/install.ts +++ b/app/plugins/install.ts @@ -1,18 +1,19 @@ 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: Function) => { +export const install = (fn: (err: string | null) => void) => { const spawnQueue = queue({concurrency: 1}); - function yarnFn(args: string[], cb: Function) { + function yarnFn(args: string[], cb: (err: string | null) => void) { const env = { NODE_ENV: 'production', ELECTRON_RUN_AS_NODE: 'true' }; - spawnQueue.push(end => { + spawnQueue.push((end) => { const cmd = [process.execPath, yarn].concat(args).join(' '); - //eslint-disable-next-line no-console console.log('Launching yarn:', cmd); cp.execFile( @@ -39,7 +40,7 @@ export const install = (fn: Function) => { spawnQueue.start(); } - yarnFn(['install', '--no-emoji', '--no-lockfile', '--cache-folder', plugs.cache], (err: any) => { + yarnFn(['install', '--no-emoji', '--no-lockfile', '--cache-folder', plugs.cache], (err) => { if (err) { return fn(err); } diff --git a/app/rpc.ts b/app/rpc.ts index 3b562830..aead6363 100644 --- a/app/rpc.ts +++ b/app/rpc.ts @@ -1,21 +1,28 @@ import {EventEmitter} from 'events'; -import {ipcMain, BrowserWindow} from 'electron'; -import uuid from 'uuid'; -export class Server extends EventEmitter { +import {ipcMain} from 'electron'; +import type {BrowserWindow, IpcMainEvent} from 'electron'; + +import {v4 as uuidv4} from 'uuid'; + +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; } - const uid = uuid.v4(); + const uid = uuidv4(); this.id = uid; ipcMain.on(uid, this.ipcListener); @@ -24,7 +31,7 @@ export class Server extends EventEmitter { // 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); }); } @@ -32,20 +39,33 @@ 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) { ipcMain.removeListener(this.id, this.ipcListener); @@ -56,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 7835795a..4b706b3d 100644 --- a/app/session.ts +++ b/app/session.ts @@ -1,10 +1,17 @@ import {EventEmitter} from 'events'; +import {dirname} from 'path'; import {StringDecoder} from 'string_decoder'; + import defaultShell from 'default-shell'; -import {getDecoratedEnv} from './plugins'; -import {productName, version} from './package.json'; +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 {IPty, IWindowsPtyForkOptions, spawn as npSpawn} from 'node-pty'; +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( @@ -13,12 +20,12 @@ const createNodePtyError = () => let spawn: typeof npSpawn; try { + // eslint-disable-next-line @typescript-eslint/no-var-requires spawn = require('node-pty').spawn; } catch (err) { 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. @@ -51,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) { @@ -61,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); @@ -79,53 +86,66 @@ 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; batcher: DataBatcher | null; shell: string | null; ended: boolean; + initTimestamp: number; + profile!: string; constructor(options: SessionOptions) { super(); this.pty = null; this.batcher = null; this.shell = null; this.ended = false; + this.initTimestamp = new Date().getTime(); this.init(options); } - init({uid, rows, cols: columns, cwd, shell, 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']) { + baseEnv['PATH'] = [dirname(cliScriptPath)] + .concat((baseEnv['PATH'] || '').split(':').filter((val) => !val.startsWith(baseEnv['APPDIR']))) + .join(':'); + } // Electron has a default value for process.env.GOOGLE_API_KEY // We don't want to leak this to the shell - // See https://github.com/zeit/hyper/issues/696 + // See https://github.com/vercel/hyper/issues/696 if (baseEnv.GOOGLE_API_KEY && process.env.GOOGLE_API_KEY === baseEnv.GOOGLE_API_KEY) { delete baseEnv.GOOGLE_API_KEY; } - const defaultShellArgs = ['--login']; - const options: IWindowsPtyForkOptions = { - cols: columns, + cols, rows, cwd, env: getDecoratedEnv(baseEnv) @@ -137,8 +157,9 @@ export default class Session extends EventEmitter { } try { - this.pty = spawn(shell || defaultShell, shellArgs || defaultShellArgs, options); - } catch (err) { + this.pty = spawn(shell, shellArgs, options); + } catch (_err) { + const err = _err as {message: string}; if (/is not a function/.test(err.message)) { throw createNodePtyError(); } else { @@ -147,25 +168,57 @@ export default class Session extends EventEmitter { } this.batcher = new DataBatcher(uid); - this.pty.onData(chunk => { + this.pty.onData((chunk) => { if (this.ended) { return; } - this.batcher?.write(chunk as any); + this.batcher?.write(chunk); }); - this.batcher.on('flush', data => { + this.batcher.on('flush', (data: string) => { this.emit('data', data); }); - this.pty.onExit(() => { + this.pty.onExit((e) => { if (!this.ended) { - this.ended = true; - this.emit('exit'); + // fall back to default shell config if the shell exits within 1 sec with non zero exit code + // 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 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)} +using fallback shell config: ${JSON.stringify(fallBackShellConfig, undefined, 2)} +`; + 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'); + } } }); - this.shell = shell || defaultShell; + this.shell = shell; } exit() { @@ -176,7 +229,6 @@ export default class Session extends EventEmitter { if (this.pty) { this.pty.write(data); } else { - //eslint-disable-next-line no-console console.warn('Warning: Attempted to write to a session with no pty'); } } @@ -185,12 +237,11 @@ export default class Session extends EventEmitter { if (this.pty) { try { this.pty.resize(cols, rows); - } catch (err) { - //eslint-disable-next-line no-console + } catch (_err) { + const err = _err as {stack: any}; console.error(err.stack); } } else { - //eslint-disable-next-line no-console console.warn('Warning: Attempted to resize a session with no pty'); } } @@ -199,12 +250,11 @@ export default class Session extends EventEmitter { if (this.pty) { try { this.pty.kill(); - } catch (err) { - //eslint-disable-next-line no-console + } catch (_err) { + const err = _err as {stack: any}; console.error('exit error', err.stack); } } else { - //eslint-disable-next-line no-console console.warn('Warning: Attempted to destroy a session with no pty'); } this.emit('exit'); diff --git a/app/system-context-menu.ts b/app/system-context-menu.ts deleted file mode 100644 index 96419769..00000000 --- a/app/system-context-menu.ts +++ /dev/null @@ -1,89 +0,0 @@ -import Registry from 'winreg'; - -const appPath = `"${process.execPath}"`; -const regKey = `\\Software\\Classes\\Directory\\background\\shell\\Hyper`; -const regParts = [ - {key: 'command', name: '', value: `${appPath} "%V"`}, - {name: '', value: 'Open Hyper here'}, - {name: 'Icon', value: `${appPath}`} -]; - -function addValues(hyperKey: Registry.Registry, commandKey: Registry.Registry, callback: Function) { - hyperKey.set(regParts[1].name, Registry.REG_SZ, regParts[1].value, error => { - if (error) { - //eslint-disable-next-line no-console - console.error(error.message); - } - hyperKey.set(regParts[2].name, Registry.REG_SZ, regParts[2].value, err => { - if (err) { - //eslint-disable-next-line no-console - console.error(err.message); - } - commandKey.set(regParts[0].name, Registry.REG_SZ, regParts[0].value, err_ => { - if (err_) { - //eslint-disable-next-line no-console - console.error(err_.message); - } - callback(); - }); - }); - }); -} - -export const add = (callback: Function) => { - const hyperKey = new Registry({hive: 'HKCU', key: regKey}); - const commandKey = new Registry({ - hive: 'HKCU', - key: `${regKey}\\${regParts[0].key}` - }); - - hyperKey.keyExists((error, exists) => { - if (error) { - //eslint-disable-next-line no-console - console.error(error.message); - } - if (exists) { - commandKey.keyExists((err_, exists_) => { - if (err_) { - //eslint-disable-next-line no-console - console.error(err_.message); - } - if (exists_) { - addValues(hyperKey, commandKey, callback); - } else { - commandKey.create(err => { - if (err) { - //eslint-disable-next-line no-console - console.error(err.message); - } - addValues(hyperKey, commandKey, callback); - }); - } - }); - } else { - hyperKey.create(err => { - if (err) { - //eslint-disable-next-line no-console - console.error(err.message); - } - commandKey.create(err_ => { - if (err_) { - //eslint-disable-next-line no-console - console.error(err_.message); - } - addValues(hyperKey, commandKey, callback); - }); - }); - } - }); -}; - -export const remove = (callback: Function) => { - new Registry({hive: 'HKCU', key: regKey}).destroy(err => { - if (err) { - //eslint-disable-next-line no-console - console.error(err.message); - } - callback(); - }); -}; 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/typings/native-reg.d.ts b/app/typings/native-reg.d.ts deleted file mode 100644 index ce1a902e..00000000 --- a/app/typings/native-reg.d.ts +++ /dev/null @@ -1,105 +0,0 @@ -/// -export declare enum HKEY { - CLASSES_ROOT = 2147483648, - CURRENT_USER = 2147483649, - LOCAL_MACHINE = 2147483650, - USERS = 2147483651, - PERFORMANCE_DATA = 2147483652, - PERFORMANCE_TEXT = 2147483728, - PERFORMANCE_NLSTEXT = 2147483744, - CURRENT_CONFIG = 2147483653, - DYN_DATA = 2147483654, - CURRENT_USER_LOCAL_SETTINGS = 2147483655 -} -export declare enum CreateKeyOptions { - NON_VOLATILE = 0, - VOLATILE = 1, - CREATE_LINK = 2, - BACKUP_RESTORE = 4 -} -export declare enum OpenKeyOptions { - OPEN_LINK = 8 -} -export declare enum Access { - QUERY_VALUE = 1, - SET_VALUE = 2, - CREATE_SUB_KEY = 4, - ENUMERATE_SUB_KEYS = 8, - NOTIFY = 16, - CREATE_LINK = 32, - WOW64_64KEY = 256, - WOW64_32KEY = 512, - READ = 131097, - WRITE = 131078, - EXECUTE = 131097, - ALL_ACCESS = 983103 -} -export declare enum ValueType { - NONE = 0, - SZ = 1, - EXPAND_SZ = 2, - BINARY = 3, - DWORD = 4, - DWORD_LITTLE_ENDIAN = 4, - DWORD_BIG_ENDIAN = 5, - LINK = 6, - MULTI_SZ = 7, - RESOURCE_LIST = 8, - FULL_RESOURCE_DESCRIPTOR = 9, - RESOURCE_REQUIREMENTS_LIST = 10, - QWORD = 11, - QWORD_LITTLE_ENDIAN = 11 -} -export declare enum GetValueFlags { - RT_ANY = 65535, - RT_REG_NONE = 1, - RT_REG_SZ = 2, - RT_REG_EXPAND_SZ = 4, - RT_REG_BINARY = 8, - RT_REG_DWORD = 16, - RT_REG_MULTI_SZ = 32, - RT_REG_QWORD = 64, - RT_DWORD = 24, - RT_QWORD = 72, - NO_EXPAND = 268435456, - SUBKEY_WOW6464KEY = 65536, - SUBKEY_WOW6432KEY = 131072 -} -export declare const HKCR = HKEY.CLASSES_ROOT; -export declare const HKCU = HKEY.CURRENT_USER; -export declare const HKLM = HKEY.LOCAL_MACHINE; -export declare const HKU = HKEY.USERS; -export declare type Value = Buffer & { - type: ValueType; -}; -export declare function isHKEY(hkey: any): hkey is HKEY; -export declare function createKey(hkey: HKEY, subKey: string, access: Access, options?: CreateKeyOptions): HKEY; -export declare function openKey(hkey: HKEY, subKey: string, access: Access, options?: OpenKeyOptions): HKEY | null; -export declare function openCurrentUser(access?: Access): HKEY; -export declare function loadAppKey(file: string, access: Access): HKEY | null; -export declare function enumKeyNames(hkey: HKEY): string[]; -export declare function enumValueNames(hkey: HKEY): string[]; -export declare function queryValueRaw(hkey: HKEY, valueName: string): Value | null; -export declare function getValueRaw(hkey: HKEY, subKey: string, valueName: string, flags?: GetValueFlags): Value | null; -export declare function setValueRaw(hkey: HKEY, valueName: string, valueType: ValueType, data: Buffer): void; -export declare function deleteKey(hkey: HKEY, subKey: string): boolean; -export declare function deleteTree(hkey: HKEY, subKey: string): boolean; -export declare function deleteKeyValue(hkey: HKEY, subKey: string, valueName: string): boolean; -export declare function deleteValue(hkey: HKEY, valueName: string): boolean; -export declare function closeKey(hkey: HKEY | null | undefined): void; -export declare type ParsedValue = number | string | string[] | Buffer; -export declare function parseValue(value: Value | null): ParsedValue | null; -export declare function parseString(value: Buffer): string; -export declare function parseMultiString(value: Buffer): string[]; -export declare function formatString(value: string): Buffer; -export declare function formatMultiString(values: string[]): Buffer; -export declare function formatDWORD(value: number): Buffer; -export declare function formatQWORD(value: number): Buffer; -export declare function setValueSZ(hkey: HKEY, valueName: string, value: string): void; -export declare function setValueEXPAND_SZ(hkey: HKEY, valueName: string, value: string): void; -export declare function setValueMULTI_SZ(hkey: HKEY, valueName: string, value: string[]): void; -export declare function setValueDWORD(hkey: HKEY, valueName: string, value: number): void; -export declare function setValueQWORD(hkey: HKEY, valueName: string, value: number): void; -export declare function getValue(hkey: HKEY, subKey: string, valueName: string, flags?: GetValueFlags): ParsedValue | null; -export declare function queryValue(hkey: HKEY, valueName: string): ParsedValue | null; -//# sourceMappingURL=index.d.ts.map diff --git a/app/ui/contextmenu.ts b/app/ui/contextmenu.ts index be004062..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); + .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 2599ca2c..87eba6d7 100644 --- a/app/ui/window.ts +++ b/app/ui/window.ts @@ -1,75 +1,82 @@ -import {app, BrowserWindow, shell, Menu, BrowserWindowConstructorOptions} from 'electron'; -import {isAbsolute} from 'path'; -import {parse as parseUrl} from 'url'; -import uuid from 'uuid'; -import fileUriToPath from 'file-uri-to-path'; +import {existsSync} from 'fs'; +import {isAbsolute, normalize, sep} from '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: uuid.v4()}); + const classOpts = Object.assign({uid: uuidv4()}); app.plugins.decorateWindowClass(classOpts); - const winOpts = Object.assign( - { - minWidth: 370, - minHeight: 190, - backgroundColor: toElectronBackgroundColor(cfg.backgroundColor || '#000'), - titleBarStyle: 'hiddenInset', - title: 'Hyper.app', - // we want to go frameless on Windows and Linux - frame: process.platform === 'darwin', - transparent: process.platform === 'darwin', - icon, - show: process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev, - acceptFirstMouse: true, - webPreferences: { - nodeIntegration: true, - navigateOnDragDrop: true - } + const winOpts: BrowserWindowConstructorOptions = { + minWidth: 370, + minHeight: 190, + backgroundColor: toElectronBackgroundColor(cfg.backgroundColor || '#000'), + titleBarStyle: 'hiddenInset', + title: 'Hyper.app', + // we want to go frameless on Windows and Linux + frame: process.platform === 'darwin', + transparent: process.platform === 'darwin', + icon, + show: Boolean(process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev), + acceptFirstMouse: true, + webPreferences: { + nodeIntegration: true, + navigateOnDragDrop: true, + contextIsolation: false }, - options_ - ); - + ...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); window.uid = classOpts.uid; const rpc = createRPC(window); - const sessions = new Map(); + 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 workingDirectory = homeDirectory; - if (process.argv[1] && isAbsolute(process.argv[1])) { - workingDirectory = process.argv[1]; - } 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'); @@ -92,7 +99,9 @@ export function newWindow( // If no callback is passed to createWindow, // a new session will be created by default. if (!fn) { - fn = (win: BrowserWindow) => win.rpc.emit('termgroup add req', {}); + fn = (win: BrowserWindow) => { + win.rpc.emit('termgroup add req', {}); + }; } // app.windowCallback is the createWindow callback @@ -100,30 +109,68 @@ export function newWindow( // and createWindow definition. It's executed in place of // the callback passed as parameter, and deleted right after. (app.windowCallback || fn)(window); - delete app.windowCallback; + app.windowCallback = undefined; fetchNotifications(window); // auto updates if (!isDev) { updater(window); } else { - //eslint-disable-next-line no-console console.log('ignoring auto updates during dev'); } }); - function createSession(extraOptions: any = {}) { - const uid = uuid.v4(); + function createSession(extraOptions: sessionExtraOptions = {}) { + const uid = uuidv4(); + 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) }, - extraOptions, - {uid} + extraOptionsFiltered, + { + profile: extraOptionsFiltered.profile || profileName, + uid + } ); const options = decorateSessionOptions(defaultOptions); const DecoratedSession = decorateSessionClass(Session); @@ -132,7 +179,7 @@ export function newWindow( return {session, options}; } - rpc.on('new', extraOptions => { + rpc.on('new', (extraOptions) => { const {session, options} = createSession(extraOptions); sessions.set(options.uid, session); @@ -143,10 +190,11 @@ 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: any) => { + session.on('data', (data: string) => { rpc.emit('session data', data); }); @@ -179,10 +227,10 @@ export function newWindow( } }); rpc.on('data', ({uid, data, escaped}) => { - const session = sessions.get(uid); + const session = uid && sessions.get(uid); if (session) { if (escaped) { - const escapedData = session.shell.endsWith('cmd.exe') + const escapedData = session.shell?.endsWith('cmd.exe') ? `"${data}"` // This is how cmd.exe does it : `'${data.replace(/'/g, `'\\''`)}'`; // Inside a single-quoted string nothing is interpreted @@ -197,12 +245,11 @@ export function newWindow( setRendererType(uid, type); }); rpc.on('open external', ({url}) => { - shell.openExternal(url); + void shell.openExternal(url); }); - rpc.on('open context menu', selection => { + rpc.on('open context menu', (selection) => { const {createWindow} = app; - const {buildFromTemplate} = Menu; - buildFromTemplate(contextMenuTemplate(createWindow, selection)).popup({window}); + Menu.buildFromTemplate(contextMenuTemplate(createWindow, selection)).popup({window}); }); rpc.on('open hamburger menu', ({x, y}) => { Menu.getApplicationMenu()!.popup({x: Math.ceil(x), y: Math.ceil(y)}); @@ -210,9 +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]}}); @@ -220,16 +270,16 @@ export function newWindow( rpc.on('close', () => { window.close(); }); - rpc.on('command', command => { + rpc.on('command', (command) => { const focusedWindow = BrowserWindow.getFocusedWindow(); execCommand(command, focusedWindow!); }); // 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) => { @@ -247,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(); - 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.js b/app/updater.js deleted file mode 100644 index aabcfa5b..00000000 --- a/app/updater.js +++ /dev/null @@ -1,101 +0,0 @@ -// Packages -import electron from 'electron'; -const {app} = electron; -import ms from 'ms'; -import retry from 'async-retry'; - -// Utilities -// eslint-disable-next-line no-unused-vars -import {version} from './package'; -import {getDecoratedConfig} from './plugins'; - -const {platform} = process; -const isLinux = platform === 'linux'; - -const autoUpdater = isLinux ? require('./auto-updater-linux').default : electron.autoUpdater; - -let isInit = false; -// Default to the "stable" update channel -let canaryUpdates = false; - -const buildFeedUrl = (canary, currentVersion) => { - const updatePrefix = canary ? 'releases-canary' : 'releases'; - return `https://${updatePrefix}.hyper.is/update/${isLinux ? 'deb' : platform}/${currentVersion}`; -}; - -const isCanary = updateChannel => updateChannel === 'canary'; - -async function init() { - autoUpdater.on('error', (err, msg) => { - //eslint-disable-next-line no-console - console.error('Error fetching updates', `${msg} (${err.stack})`); - }); - - const config = await retry(async () => { - const content = await getDecoratedConfig(); - - if (!content) { - throw new Error('No config content loaded'); - } - - return content; - }); - - // If defined in the config, switch to the "canary" channel - if (config.updateChannel && isCanary(config.updateChannel)) { - canaryUpdates = true; - } - - const feedURL = buildFeedUrl(canaryUpdates, version); - - autoUpdater.setFeedURL(feedURL); - - setTimeout(() => { - autoUpdater.checkForUpdates(); - }, ms('10s')); - - setInterval(() => { - autoUpdater.checkForUpdates(); - }, ms('30m')); - - isInit = true; -} - -export default win => { - if (!isInit) { - init(); - } - - const {rpc} = win; - - const onupdate = (ev, releaseNotes, releaseName, date, updateUrl, onQuitAndInstall) => { - const releaseUrl = updateUrl || `https://github.com/zeit/hyper/releases/tag/${releaseName}`; - rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !!onQuitAndInstall}); - }; - - const eventName = isLinux ? 'update-available' : 'update-downloaded'; - - autoUpdater.on(eventName, onupdate); - - rpc.once('quit and install', () => { - autoUpdater.quitAndInstall(); - }); - - app.config.subscribe(() => { - const {updateChannel} = app.plugins.getDecoratedConfig(); - const newUpdateIsCanary = isCanary(updateChannel); - - if (newUpdateIsCanary !== canaryUpdates) { - const feedURL = buildFeedUrl(newUpdateIsCanary, version); - - autoUpdater.setFeedURL(feedURL); - autoUpdater.checkForUpdates(); - - canaryUpdates = newUpdateIsCanary; - } - }); - - win.on('close', () => { - autoUpdater.removeListener(eventName, onupdate); - }); -}; diff --git a/app/updater.ts b/app/updater.ts new file mode 100644 index 00000000..c7d07d4d --- /dev/null +++ b/app/updater.ts @@ -0,0 +1,120 @@ +// Packages +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'; + +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'; + 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'; + +async function init() { + autoUpdater.on('error', (err) => { + console.error('Error fetching updates', `${err.message} (${err.stack})`); + }); + + const config = await getDecoratedConfigWithRetry(); + + // If defined in the config, switch to the "canary" channel + if (config.updateChannel && isCanary(config.updateChannel)) { + canaryUpdates = true; + } + + const feedURL = buildFeedUrl(canaryUpdates, version); + + autoUpdater.setFeedURL({url: feedURL}); + + setTimeout(() => { + void checkForUpdates(); + }, ms('10s')); + + setInterval(() => { + void checkForUpdates(); + }, ms('30m')); + + isInit = true; +} + +const updater = (win: BrowserWindow) => { + if (!isInit) { + void init(); + } + + const {rpc} = win; + + 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: !isLinux}); + }; + + if (isLinux) { + autoUpdater.on('update-available', onupdate); + } else { + autoUpdater.on('update-downloaded', onupdate); + } + + rpc.once('quit and install', () => { + autoUpdater.quitAndInstall(); + }); + + app.config.subscribe(async () => { + const {updateChannel} = await getDecoratedConfigWithRetry(); + const newUpdateIsCanary = isCanary(updateChannel); + + if (newUpdateIsCanary !== canaryUpdates) { + const feedURL = buildFeedUrl(newUpdateIsCanary, version); + + autoUpdater.setFeedURL({url: feedURL}); + void checkForUpdates(); + + canaryUpdates = newUpdateIsCanary; + } + }); + + win.on('close', () => { + 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 0f856c7b..3a5b9930 100644 --- a/app/utils/cli-install.ts +++ b/app/utils/cli-install.ts @@ -1,24 +1,25 @@ -import pify from 'pify'; -import fs from 'fs'; +import {existsSync, readlink, symlink} from 'fs'; import path from 'path'; -import notify from '../notify'; +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 {cliScriptPath, cliLinkPath} from '../config/paths'; +import notify from '../notify'; -import * as regTypes from '../typings/native-reg'; -try { - // eslint-disable-next-line no-var, @typescript-eslint/no-var-requires - var Registry: typeof regTypes = require('native-reg'); -} catch (err) { - console.log(err); -} - -const readlink = pify(fs.readlink); -const symlink = pify(fs.symlink); +const readLink = promisify(readlink); +const symLink = promisify(symlink); +const sudoExec = promisify(sudoPrompt.exec); const checkInstall = () => { - return readlink(cliLinkPath) - .then(link => link === cliScriptPath) - .catch(err => { + return readLink(cliLinkPath) + .then((link) => link === cliScriptPath) + .catch((err) => { if (err.code === 'ENOENT') { return false; } @@ -26,35 +27,70 @@ const checkInstall = () => { }); }; -const addSymlink = () => { - return checkInstall().then(isInstalled => { +const addSymlink = async (silent: boolean) => { + try { + const isInstalled = await checkInstall(); if (isInstalled) { - //eslint-disable-next-line no-console console.log('Hyper CLI already in PATH'); - return Promise.resolve(); + return; } - //eslint-disable-next-line no-console console.log('Linking HyperCLI'); - return symlink(cliScriptPath, 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) { + const err = _err as {code: string}; + // 'EINVAL' is returned by readlink, + // 'EEXIST' is returned by symlink + let error = + err.code === 'EEXIST' || err.code === 'EINVAL' + ? `File already exists: ${cliLinkPath}` + : `Symlink creation failed: ${err.code}`; + // Need sudo access to create symlink + if (err.code === 'EACCES' && !silent) { + const result = await dialog.showMessageBox({ + message: `You need to grant elevated privileges to add Hyper CLI to PATH +Or you can run +sudo ln -sf "${cliScriptPath}" "${cliLinkPath}"`, + type: 'info', + buttons: ['OK', 'Copy Command', 'Cancel'] + }); + if (result.response === 0) { + try { + await sudoExec(`ln -sf "${cliScriptPath}" "${cliLinkPath}"`, {name: 'Hyper'}); + return; + } catch (_error) { + error = (_error as any[])[0]; + } + } else if (result.response === 1) { + clipboard.writeText(`sudo ln -sf "${cliScriptPath}" "${cliLinkPath}"`); + } + } + throw error; + } }; const addBinToUserPath = () => { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { try { const envKey = Registry.openKey(Registry.HKCU, 'Environment', Registry.Access.ALL_ACCESS)!; - // C:\Users\\AppData\Local\hyper\app-\resources\bin + // C:\Users\\AppData\Local\Programs\hyper\resources\bin const binPath = path.dirname(cliScriptPath); // C:\Users\\AppData\Local\hyper - const basePath = path.resolve(binPath, '../../..'); + const oldPath = path.resolve(process.env.LOCALAPPDATA!, 'hyper'); const items = Registry.enumValueNames(envKey); - const pathItem = items.find(item => item.toUpperCase() === 'PATH'); + const pathItem = items.find((item) => item.toUpperCase() === 'PATH'); const pathItemName = pathItem || 'PATH'; let newPathValue = binPath; - let type: regTypes.ValueType = Registry.ValueType.SZ; + let type: ValueType = Registry.ValueType.SZ; if (pathItem) { type = Registry.queryValueRaw(envKey, pathItem)!.type; if (type !== Registry.ValueType.SZ && type !== Registry.ValueType.EXPAND_SZ) { @@ -62,22 +98,22 @@ const addBinToUserPath = () => { return; } const value = Registry.queryValue(envKey, pathItem) as string; - const pathParts = value.split(';'); + let pathParts = value.split(';'); const existingPath = pathParts.includes(binPath); - if (existingPath) { - //eslint-disable-next-line no-console + const existingOldPath = pathParts.some((pathPart) => pathPart.startsWith(oldPath)); + if (existingPath && !existingOldPath) { console.log('Hyper CLI already in PATH'); + Registry.closeKey(envKey); resolve(); return; } - // Because version is in path we need to remove old path if present and add current path - newPathValue = pathParts - .filter(pathPart => !pathPart.startsWith(basePath)) - .concat([binPath]) - .join(';'); + // Because nsis install path is different from squirrel we need to remove old path if present + // and add current path if absent + if (existingOldPath) pathParts = pathParts.filter((pathPart) => !pathPart.startsWith(oldPath)); + if (!pathParts.includes(binPath)) pathParts.push(binPath); + newPathValue = pathParts.join(';'); } - //eslint-disable-next-line no-console console.log('Adding HyperCLI path (registry)'); Registry.setValueRaw(envKey, pathItemName, type, Registry.formatString(newPathValue)); Registry.closeKey(envKey); @@ -88,41 +124,36 @@ const addBinToUserPath = () => { }); }; -const logNotify = (withNotification: boolean, title: string, body: string, details?: any) => { +const logNotify = (withNotification: boolean, title: string, body: string, details?: {error?: any}) => { console.log(title, body, details); withNotification && notify(title, body, details); }; -export const installCLI = (withNotification: boolean) => { +export const installCLI = async (withNotification: boolean) => { if (process.platform === 'win32') { - addBinToUserPath() - .then(() => - logNotify( - withNotification, - 'Hyper CLI installed', - 'You may need to restart your computer to complete this installation process.' - ) - ) - .catch(err => - logNotify(withNotification, 'Hyper CLI installation failed', `Failed to add Hyper CLI path to user PATH ${err}`) + try { + await addBinToUserPath(); + logNotify( + withNotification, + 'Hyper CLI installed', + 'You may need to restart your computer to complete this installation process.' ); - } else if (process.platform === 'darwin') { - addSymlink() - .then(() => logNotify(withNotification, 'Hyper CLI installed', `Symlink created at ${cliLinkPath}`)) - .catch(err => { - // 'EINVAL' is returned by readlink, - // 'EEXIST' is returned by symlink - const error = - err.code === 'EEXIST' || err.code === 'EINVAL' - ? `File already exists: ${cliLinkPath}` - : `Symlink creation failed: ${err.code}`; - - //eslint-disable-next-line no-console - console.error(err); - logNotify(withNotification, 'Hyper CLI installation failed', error); - }); + } catch (err) { + logNotify(withNotification, 'Hyper CLI installation failed', `Failed to add Hyper CLI path to user PATH ${err}`); + } + } else if (process.platform === 'darwin' || process.platform === 'linux') { + // AppImages are mounted on run at a temporary path, don't create symlink + if (process.env['APPIMAGE']) { + console.log('Skipping CLI symlink creation as it is an AppImage install'); + return; + } + try { + await addSymlink(!withNotification); + logNotify(withNotification, 'Hyper CLI installed', `Symlink created at ${cliLinkPath}`); + } catch (error) { + logNotify(withNotification, 'Hyper CLI installation failed', `${error}`); + } } else { - withNotification && - notify('Hyper CLI installation', 'Command is added in PATH only at package installation. Please reinstall.'); + logNotify(withNotification, 'Hyper CLI installation failed', `Unsupported platform ${process.platform}`); } }; diff --git a/app/utils/colors.ts b/app/utils/colors.ts index d4adc882..04a92397 100644 --- a/app/utils/colors.ts +++ b/app/utils/colors.ts @@ -21,14 +21,16 @@ const colorList = [ export const getColorMap: { (colors: T): T extends (infer U)[] ? {[k: string]: U} : T; -} = colors => { +} = (colors) => { if (!Array.isArray(colors)) { return colors; } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return colors.reduce((result, color, index) => { if (index < colorList.length) { result[colorList[index]] = color; } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return result; }, {}); }; diff --git a/app/utils/map-keys.ts b/app/utils/map-keys.ts index 52e097c9..4bcb9a1d 100644 --- a/app/utils/map-keys.ts +++ b/app/utils/map-keys.ts @@ -4,14 +4,14 @@ const generatePrefixedCommand = (command: string, shortcuts: string[]) => { for (let i = 1; i <= 9; i++) { // 9 is a special number because it means 'last' const index = i === 9 ? 'last' : i; - const prefixedShortcuts = shortcuts.map(shortcut => `${shortcut}+${i}`); + const prefixedShortcuts = shortcuts.map((shortcut) => `${shortcut}+${i}`); result[`${baseCmd}:${index}`] = prefixedShortcuts; } return result; }; -export default (config: Record) => { +const mapKeys = (config: Record) => { return Object.keys(config).reduce((keymap: Record, command: string) => { if (!command) { return keymap; @@ -20,11 +20,10 @@ export default (config: Record) => { const _shortcuts = config[command]; const shortcuts = Array.isArray(_shortcuts) ? _shortcuts : [_shortcuts]; const fixedShortcuts: string[] = []; - shortcuts.forEach(shortcut => { + shortcuts.forEach((shortcut) => { let newShortcut = shortcut; if (newShortcut.indexOf('cmd') !== -1) { // Mousetrap use `command` and not `cmd` - //eslint-disable-next-line no-console console.warn('Your config use deprecated `cmd` in key combination. Please use `command` instead.'); newShortcut = newShortcut.replace('cmd', 'command'); } @@ -40,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 new file mode 100644 index 00000000..80522d0c --- /dev/null +++ b/app/utils/system-context-menu.ts @@ -0,0 +1,60 @@ +import * as Registry from 'native-reg'; +import type {HKEY} from 'native-reg'; + +const appPath = `"${process.execPath}"`; +const regKeys = [ + `Software\\Classes\\Directory\\Background\\shell\\Hyper`, + `Software\\Classes\\Directory\\shell\\Hyper`, + `Software\\Classes\\Drive\\shell\\Hyper` +]; +const regParts = [ + {key: 'command', name: '', value: `${appPath} "%V"`}, + {name: '', value: 'Open &Hyper here'}, + {name: 'Icon', value: `${appPath}`} +]; + +function addValues(hyperKey: HKEY, commandKey: HKEY) { + try { + Registry.setValueSZ(hyperKey, regParts[1].name, regParts[1].value); + } catch (error) { + console.error(error); + } + try { + Registry.setValueSZ(hyperKey, regParts[2].name, regParts[2].value); + } catch (err) { + console.error(err); + } + try { + Registry.setValueSZ(commandKey, regParts[0].name, regParts[0].value); + } catch (err_) { + console.error(err_); + } +} + +export const add = () => { + regKeys.forEach((regKey) => { + try { + const hyperKey = + Registry.openKey(Registry.HKCU, regKey, Registry.Access.ALL_ACCESS) || + Registry.createKey(Registry.HKCU, regKey, Registry.Access.ALL_ACCESS); + const commandKey = + Registry.openKey(Registry.HKCU, `${regKey}\\${regParts[0].key}`, Registry.Access.ALL_ACCESS) || + Registry.createKey(Registry.HKCU, `${regKey}\\${regParts[0].key}`, Registry.Access.ALL_ACCESS); + addValues(hyperKey, commandKey); + Registry.closeKey(hyperKey); + Registry.closeKey(commandKey); + } catch (error) { + console.error(error); + } + }); +}; + +export const remove = () => { + regKeys.forEach((regKey) => { + try { + Registry.deleteTree(Registry.HKCU, regKey); + } catch (err) { + console.error(err); + } + }); +}; diff --git a/app/utils/to-electron-background-color.ts b/app/utils/to-electron-background-color.ts index d9a5c988..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,8 +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 4e5db5d8..8e654f1d 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -2,88 +2,167 @@ # yarn lockfile v1 -ajv@^6.10.2: - version "6.11.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9" - integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA== +"@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.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" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" uri-js "^4.2.2" ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/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== -async-retry@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.1.tgz#139f31f8ddce50c0870b0ba558a6079684aaed55" - integrity sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA== +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== dependencies: - retry "0.12.0" + normalize-path "^3.0.0" + picomatch "^2.0.4" -color-convert@^1.9.1: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== +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: - color-name "1.1.3" + tslib "^2.0.1" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= +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" -color-name@^1.0.0: +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" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +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" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +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.4" + +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.5.2: - version "1.5.3" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" - integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== +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.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" - integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg== +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.2" + color-convert "^2.0.1" + color-string "^1.9.0" -conf@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/conf/-/conf-6.2.0.tgz#274d37a0a2e50757ffb89336e954d08718eb359a" - integrity sha512-fvl40R6YemHrFsNiyP7TD0tzOe3pQD2dfT2s20WvCaq57A1oV+RImbhn2Y4sQGDz1lB0wNSb7dPcPIvQB69YNA== - dependencies: - ajv "^6.10.2" - debounce-fn "^3.0.1" - dot-prop "^5.0.0" - env-paths "^2.2.0" - json-schema-typed "^7.0.1" - make-dir "^3.0.0" - onetime "^5.1.0" - pkg-up "^3.0.1" - semver "^6.2.0" - write-file-atomic "^3.0.0" +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== -convert-css-color-name-to-hex@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/convert-css-color-name-to-hex/-/convert-css-color-name-to-hex-0.1.1.tgz#38ac4d27ca470593fd663b18a072a308926a35a2" - integrity sha1-OKxNJ8pHBZP9ZjsYoHKjCJJqNaI= +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: - css-color-names "0.0.3" - is-css-color-name "^0.1.1" + ajv "^8.6.3" + ajv-formats "^2.1.1" + atomically "^1.7.0" + debounce-fn "^4.0.0" + dot-prop "^6.0.1" + env-paths "^2.2.1" + json-schema-typed "^7.0.3" + onetime "^5.1.2" + pkg-up "^3.1.0" + semver "^7.3.5" + +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" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== dependencies: nice-try "^1.0.4" @@ -92,91 +171,91 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -css-color-names@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/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.yarnpkg.com/css-color-names/-/css-color-names-0.0.3.tgz#de0cef16f4d8aa8222a320d5b6d7e9bbada7b9f6" - integrity sha1-3gzvFvTYqoIioyDVttfpu62nufY= - -debounce-fn@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/debounce-fn/-/debounce-fn-3.0.1.tgz#034afe8b904d985d1ec1aa589cd15f388741d680" - integrity sha512-aBoJh5AhpqlRoHZjHmOzZlRx+wz2xVwGL9rjs+Kj0EWUrL4/h4K7OD176thl2Tdoqui/AaA4xhHrNArGLAaI3Q== +cross-spawn@^7.0.0: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: - mimic-fn "^2.1.0" + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" -debug@^2.2.0: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== +debounce-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz#ed76d206d8a50e60de0dd66d494d82835ffe61c7" + integrity sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ== dependencies: - ms "2.0.0" + mimic-fn "^3.0.0" default-shell@1.0.1, default-shell@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/default-shell/-/default-shell-1.0.1.tgz#752304bddc6174f49eb29cb988feea0b8813c8bc" + resolved "https://registry.npmjs.org/default-shell/-/default-shell-1.0.1.tgz#752304bddc6174f49eb29cb988feea0b8813c8bc" integrity sha1-dSMEvdxhdPSespy5iP7qC4gTyLw= -dot-prop@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" - integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== +dot-prop@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" + integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== dependencies: is-obj "^2.0.0" -electron-fetch@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/electron-fetch/-/electron-fetch-1.4.0.tgz#a830d400f8ad358acba9b3c591e6ed477916bac5" - integrity sha512-rednYIpMbuzekTroNndQOFl95c4I/wMEbH9jxGoDEoKrM07b7FWydy6I3pbiAbCxDcYpmHtzMY6ykyLagR7JHw== +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: - encoding "^0.1.12" + rimraf "^3.0.2" + semver "^7.2.1" + tslib "^2.1.0" + unzip-crx-3 "^0.2.0" -electron-is-dev@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-1.1.0.tgz#b15a2a600bdc48a51a857d460e05f15b19a2522c" - integrity sha512-Z1qA/1oHNowGtSBIcWk0pcLEqYT/j+13xUw/MYOrBUOL4X7VN0i0KCTf5SqyvMPmW5pSPKbo28wkxMxzZ20YnQ== - -electron-squirrel-startup@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/electron-squirrel-startup/-/electron-squirrel-startup-1.0.0.tgz#19b4e55933fa0ef8f556784b9c660f772546a0b8" - integrity sha1-GbTlWTP6Dvj1VnhLnGYPdyVGoLg= +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: - debug "^2.2.0" + encoding "^0.1.13" -electron-store@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-5.1.0.tgz#0b3cb66b15d0002678fc5c13e8b0c38a8678d670" - integrity sha512-uhAF/4+zDb+y0hWqlBirEPEAR4ciCZDp4fRWGFNV62bG+ArdQPpXk7jS0MEVj3CfcG5V7hx7Dpq5oD+1j6GD8Q== - dependencies: - conf "^6.2.0" - type-fest "^0.7.1" +electron-is-dev@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-2.0.0.tgz#833487a069b8dad21425c67a19847d9064ab19bd" + integrity sha512-3X99K852Yoqu9AcW50qz3ibYBWY79/pBhlMCab8ToEWS48R0T9tyxRiQhwylE7zQdXrMnx2JKqUJyMPmt5FBqA== -encoding@^0.1.12: - version "0.1.12" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" - integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= +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: - iconv-lite "~0.4.13" + conf "^10.2.0" + type-fest "^2.17.0" + +encoding@^0.1.13: + version "0.1.13" + resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" end-of-stream@^1.1.0: version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" -env-paths@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" - integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA== +env-paths@^2.2.1: + version "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.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== dependencies: cross-spawn "^6.0.0" @@ -187,371 +266,507 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + fast-deep-equal@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" - integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -file-uri-to-path@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/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" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" find-up@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + resolved "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== dependencies: locate-path "^3.0.0" -fs-extra@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== +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 "^4.0.0" - universalify "^0.1.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" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== get-stream@^4.0.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== dependencies: pump "^3.0.0" -git-describe@4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/git-describe/-/git-describe-4.0.4.tgz#f3d55bce309becf6dc27fed535d380a621967e8c" - integrity sha512-L1X9OO1e4MusB4PzG9LXeXCQifRvyuoHTpuuZ521Qyxn/B0kWHWEOtsT4LsSfSNacZz0h4ZdYDsDG7f+SrA3hg== +get-stream@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== dependencies: - lodash "^4.17.11" + pump "^3.0.0" + +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: + "@types/semver" "^7.3.8" + lodash "^4.17.21" optionalDependencies: semver "^5.6.0" -graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== - -iconv-lite@~0.4.13: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: - safer-buffer ">= 2.1.2 < 3" + is-glob "^4.0.1" -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +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" -inherits@~2.0.3: +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" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + +iconv-lite@^0.6.2: + version "0.6.2" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" + integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +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.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== invert-kv@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-3.0.0.tgz#9db0c4817a1ec754df8067df6acf6828286f6a84" - integrity sha512-JzF8q2BeZA1ZkE3XROwRpoMQ9ObMgTtp0JH8EXewlbkikuOj2GPLIpUipdO+VL8QsTr2teAJD02EFGGL5cO7uw== + version "3.0.1" + resolved "https://registry.npmjs.org/invert-kv/-/invert-kv-3.0.1.tgz#a93c7a3d4386a1dc8325b97da9bb1620c0282523" + integrity sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw== is-arrayish@^0.3.1: version "0.3.2" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== -is-css-color-name@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-css-color-name/-/is-css-color-name-0.1.3.tgz#ea3b51bc901d8a243d32c9b7873d0680dbbef7f1" - integrity sha1-6jtRvJAdiiQ9Msm3hz0GgNu+9/E= +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== dependencies: - css-color-names "0.0.2" + binary-extensions "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== is-obj@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + 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.1" - resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.1.tgz#f349a8cadd24e65298037a522cf7520f2e81a0f3" - integrity sha512-0eRIASHZt1E68/ixClI8bp2YK2wmBPVWEismTs6M+M099jKgrzl/3E976zIbImSIob48N2/XGe9y7ZiYdImSlg== - dependencies: - protocols "^1.1.0" - is-stream@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= -is-typedarray@^1.0.0: +is-stream@^2.0.0: + version "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.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + 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.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= "js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema-typed@^7.0.1: +json-schema-typed@^7.0.3: version "7.0.3" - resolved "https://registry.yarnpkg.com/json-schema-typed/-/json-schema-typed-7.0.3.tgz#23ff481b8b4eebcd2ca123b4fa0409e66469a2d9" + resolved "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz#23ff481b8b4eebcd2ca123b4fa0409e66469a2d9" integrity sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A== -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" 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.yarnpkg.com/lcid/-/lcid-3.1.1.tgz#9030ec479a058fc36b5e8243ebaac8b6ac582fd0" + resolved "https://registry.npmjs.org/lcid/-/lcid-3.1.1.tgz#9030ec479a058fc36b5e8243ebaac8b6ac582fd0" integrity sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg== 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.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== dependencies: p-locate "^3.0.0" path-exists "^3.0.0" -lodash@4.17.15, lodash@^4.17.11: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +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== -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.1.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== dependencies: js-tokens "^3.0.0 || ^4.0.0" -make-dir@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392" - integrity sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w== +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: - semver "^6.0.0" + yallist "^4.0.0" map-age-cleaner@^0.1.3: version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + resolved "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== dependencies: p-defer "^1.0.0" mem@^5.0.0: version "5.1.1" - resolved "https://registry.yarnpkg.com/mem/-/mem-5.1.1.tgz#7059b67bf9ac2c924c9f1cff7155a064394adfb3" + resolved "https://registry.npmjs.org/mem/-/mem-5.1.1.tgz#7059b67bf9ac2c924c9f1cff7155a064394adfb3" integrity sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw== dependencies: map-age-cleaner "^0.1.3" mimic-fn "^2.1.0" p-is-promise "^2.1.0" +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mkdirp@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea" - integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g== +mimic-fn@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" + integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -nan@^2.14.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" - integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== - -native-reg@0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/native-reg/-/native-reg-0.3.3.tgz#820bcd6246029ab49cb10c9258b6ba3cb6d22ebe" - integrity sha512-zt7A29wK2yvPcSsRfx7snWjo2AUTWRVIE3QMuNfKqtwNhwSpo3V3FDQekja7amH0/rjDe8u2MKOkFSV8+I4i/A== +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: - node-gyp-build "^4.2.0" + 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.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-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-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.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-gyp-build@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.0.tgz#2c2b05f461f4178641a6ce2d7159f04094e9376d" - integrity sha512-4oiumOLhCDU9Rronz8PZ5S4IvT39H5+JEv/hps9V8s7RSLhsac0TCP78ulnHXOo8X1wdpPiTayGlM1jr4IbnaQ== +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.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.9.0.tgz#8f9bcc0d1c5b970a3184ffd533d862c7eb6590a6" - integrity sha512-MBnCQl83FTYOu7B4xWw10AW77AAh7ThCE1VXEv+JeWj8mSpGo+0bwgsV+b23ljBFwEM9OmsOv3kM27iUPPm84g== +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-url@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" - integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== +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== npm-run-path@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= dependencies: path-key "^2.0.0" -object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= +npm-run-path@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" -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.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" -onetime@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" - integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" -os-locale@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-4.0.0.tgz#06e4fb102f38f33e9e904f41af3c34a5aa3b2b7b" - integrity sha512-HsSR1+2l6as4Wp2SGZxqLnuFHxVvh1Ir9pvZxyujsC13egZVe7P0YeBLN0ijQzM/twrO5To3ia3jzBXAvpMTEA== +os-locale@5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/os-locale/-/os-locale-5.0.0.tgz#6d26c1d95b6597c5d5317bf5fba37eccec3672e0" + integrity sha512-tqZcNEDAIZKBEPnHPlVDvKrp7NzgLi7jRmhKiUoa2NUmhl13FtkAGLUVR+ZsYvApBQdBfYm43A4tXXQ4IrYLBA== dependencies: - execa "^1.0.0" + execa "^4.0.0" lcid "^3.0.0" mem "^5.0.0" p-defer@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + resolved "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= p-finally@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= p-is-promise@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" + resolved "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== p-limit@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" - integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== + version "2.3.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" p-locate@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== dependencies: p-limit "^2.0.0" p-try@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + 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.1" - resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-4.0.1.tgz#0ec769704949778cb3b8eda5e994c32073a1adff" - integrity sha512-d7yhga0Oc+PwNXDvQ0Jv1BuWkLVPXcAoQ/WREgd6vNNoKYaW52KI+RdOFjI63wjkmps9yUE8VS4veP+AgpQ/hA== - 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.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-5.0.1.tgz#99c4084fc11be14141efa41b3d117a96fcb9527f" - integrity sha512-flNUPP27r3vJpROi0/R3/2efgKkyXqnXwyP1KQ2U0SfFRgdizOdWfvrrvJg1LuOoxs7GQhmxJlq23IpQ/BkByg== +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 "^3.3.0" - 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.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + 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.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + resolved "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= -pify@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" - integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -pkg-up@^3.0.1: +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +pkg-up@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" + resolved "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== dependencies: find-up "^3.0.0" -prop-types@^15.6.2: - version "15.7.2" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" - integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.8.1" +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@^1.1.0, protocols@^1.4.0: - version "1.4.7" - resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.7.tgz#95f788a4f0e979b291ffefcf5636ad113d037d32" - integrity sha512-Fx65lf9/YDn3hUX08XUc0J8rSux36rEsyiv21ZGUC1mOyeM3lTRpZLcrm8aAolzS4itwVfm7TAPyxC2E5zd6xg== +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" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== dependencies: end-of-stream "^1.1.0" @@ -559,170 +774,275 @@ pump@^3.0.0: punycode@^2.1.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -queue@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.1.tgz#abd5a5b0376912f070a25729e0b6a7d565683791" - integrity sha512-AJBQabRCCNr9ANq8v77RJEv73DPbn55cdTb+Giq4X0AVnNVZvMHlYp7XlQiN+1npCZj1DuSmaA2hYVUUDgxFDg== +queue@6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" + integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== dependencies: inherits "~2.0.3" -react-dom@16.12.0: - version "16.12.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.12.0.tgz#0da4b714b8d13c2038c9396b54a92baea633fe11" - integrity sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw== +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" - prop-types "^15.6.2" - scheduler "^0.18.0" + scheduler "^0.23.0" -react-is@^16.8.1: - version "16.12.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" - integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== - -react@16.12.0: - version "16.12.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83" - integrity sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA== +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" - prop-types "^15.6.2" -retry@0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= +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" -"safer-buffer@>= 2.1.2 < 3": +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + 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.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.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -scheduler@^0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4" - integrity sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ== +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.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.3.tgz#e4345ce73071c53f336445cfc19efb1c311df2a6" - integrity sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA== +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" semver@^5.5.0, semver@^5.6.0: version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@^6.0.0, semver@^6.2.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +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.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= dependencies: shebang-regex "^1.0.0" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + shebang-regex@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= -shell-env@3.0.0: +shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/shell-env/-/shell-env-3.0.0.tgz#42484ebd0798ee321ba69f6151f2aeab13fde1d4" - integrity sha512-zE0lGldowbCLnnorLnOUO6gLSwEoW4u+qWcEV1HH2qje5sIg0PvBd+8ro74EgSZv0MBEP2dROD6vSKhGDbUIMQ== + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-env@3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/shell-env/-/shell-env-3.0.1.tgz#515a62f6cbd5e139365be2535745e8e53438ce77" + integrity sha512-b09fpMipAQ9ObwvIeKoQFLDXcEcCpYUUZanlad4OYQscw2I49C/u97OPQg9jWYo36bRDn62fbe07oWYqovIvKA== dependencies: default-shell "^1.0.1" execa "^1.0.0" strip-ansi "^5.2.0" -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= +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== simple-swizzle@^0.2.2: version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= 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.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== dependencies: ansi-regex "^4.1.0" strip-eof@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + resolved "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= -type-fest@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" - integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== +sudo-prompt@^9.2.1: + version "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" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: - is-typedarray "^1.0.0" + is-number "^7.0.0" -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +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.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + version "4.4.0" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" + integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== dependencies: punycode "^2.1.0" -uuid@3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +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" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" -winreg@1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b" - integrity sha1-ugZWKbepJRMOFXeRCM9UCZDpjRs= +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" wrappy@1: version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write-file-atomic@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.1.tgz#558328352e673b5bb192cf86500d60b230667d4b" - integrity sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" +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" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 72776eb4..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,29 +0,0 @@ -# https://github.com/sindresorhus/appveyor-node/blob/master/appveyor.yml - -environment: - matrix: - - platform: x64 - -image: Visual Studio 2019 - -install: - - ps: Install-Product node 12 x64 - - set CI=true - - yarn - -build: off - -matrix: - fast_finish: true - -shallow_clone: true - -test_script: - - node --version - - yarn --version - - yarn run test - -on_success: - - IF %APPVEYOR_REPO_BRANCH%==canary cp build\canary.ico build\icon.ico - - yarn run dist - - ps: Get-ChildItem .\dist\squirrel-windows\*.exe | % { Push-AppveyorArtifact $_.FullName } diff --git a/ava-e2e.config.js b/ava-e2e.config.js new file mode 100644 index 00000000..0b2f46db --- /dev/null +++ b/ava-e2e.config.js @@ -0,0 +1,6 @@ +module.exports = { + files: ['test/*'], + 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/Info.plist b/build/Info.plist deleted file mode 100644 index 4d91015b..00000000 --- a/build/Info.plist +++ /dev/null @@ -1,34 +0,0 @@ - - - CFBundleDocumentTypes - - - CFBundleTypeName - Folders - CFBundleTypeRole - Viewer - LSItemContentTypes - - public.folder - com.apple.bundle - com.apple.package - com.apple.resolvable - - LSHandlerRank - Alternate - - - CFBundleTypeName - UnixExecutables - CFBundleTypeRole - Shell - LSItemContentTypes - - public.unix-executable - - LSHandlerRank - Alternate - - - - diff --git a/build/canary.icns b/build/canary.icns index fa1a666f..d3a565a0 100644 Binary files a/build/canary.icns and b/build/canary.icns differ diff --git a/build/icon.fig b/build/icon.fig new file mode 100644 index 00000000..31370e7b Binary files /dev/null and b/build/icon.fig differ diff --git a/build/icon.icns b/build/icon.icns index bcdd6201..99692b86 100644 Binary files a/build/icon.icns and b/build/icon.icns differ 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 new file mode 100644 index 00000000..2a0b0a67 --- /dev/null +++ b/build/win/installer.nsh @@ -0,0 +1,28 @@ +!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\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"` +!macroend + +!macro customUnInstall + DeleteRegKey HKCU "Software\Classes\Directory\Background\shell\Hyper" + DeleteRegKey HKCU "Software\Classes\Directory\shell\Hyper" + DeleteRegKey HKCU "Software\Classes\Drive\shell\Hyper" +!macroend + +!macro customInstallMode + StrCpy $isForceCurrentInstall "1" +!macroend + +!macro customInit + IfFileExists $LOCALAPPDATA\Hyper\Update.exe 0 +2 + nsExec::Exec '"$LOCALAPPDATA\Hyper\Update.exe" --uninstall -s' +!macroend diff --git a/cli/api.ts b/cli/api.ts index ec4932e1..6c2a4347 100644 --- a/cli/api.ts +++ b/cli/api.ts @@ -1,27 +1,28 @@ +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* 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 @@ -31,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; @@ -41,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(() => ((getParsedFile()?.program?.body as any[]) || []).map(obj => obj)); - -const getPluginsByKey = (key: string) => { - const properties = getProperties(); - for (let i = 0; i < properties.length; i++) { - const rightProperties = Object.values(properties[i]?.expression?.right?.properties || {}); - for (let j = 0; j < rightProperties.length; j++) { - const plugin = rightProperties[j]; - if (plugin?.key?.name === key) { - return (plugin?.value?.elements as any[]) || []; - } - } - } -}; +const getPluginsByKey = (key: string): any[] => getParsedFile()[key] || []; const getPlugins = memoize(() => { return getPluginsByKey('plugins'); @@ -82,15 +62,15 @@ function exists() { } function isInstalled(plugin: string, locally?: boolean) { - const array = (locally ? getLocalPlugins() : getPlugins()) || []; + 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) { @@ -107,8 +87,8 @@ function getPackageName(plugin: string) { function existsOnNpm(plugin: string) { const name = getPackageName(plugin); return got - .get(registryUrl + name.toLowerCase(), {timeout: 10000, responseType: 'json'}) - .then(res => { + .get(registryUrl + name.toLowerCase(), {timeout: {request: 10000}, responseType: 'json'}) + .then((res) => { if (!res.body.versions) { return Promise.reject(res); } else { @@ -118,7 +98,7 @@ function existsOnNpm(plugin: string) { } function install(plugin: string, locally?: boolean) { - const array = (locally ? getLocalPlugins() : getPlugins()) || []; + const array = locally ? getLocalPlugins() : getPlugins(); return existsOnNpm(plugin) .catch((err: any) => { const {statusCode} = err; @@ -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 (Array.isArray(getPlugins())) { - return getPlugins()! - .map(plugin => plugin.value) - .join('\n'); + if (getPlugins().length > 0) { + return getPlugins().join('\n'); } return false; } diff --git a/cli/index.ts b/cli/index.ts index 7701a075..86dbc414 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -1,21 +1,23 @@ // This is a CLI tool, using console is OK /* eslint no-console: 0 */ import {spawn, exec} from 'child_process'; -import {isAbsolute, resolve} from 'path'; +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'; -const PLUGIN_PREFIX = 'hyper-'; - -let commandPromise: Promise; +let commandPromise: Promise | undefined; const assertPluginName = (pluginName: string) => { if (!pluginName) { @@ -34,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', @@ -44,7 +62,7 @@ args.command( commandPromise = api .install(pluginName) .then(() => console.log(chalk.green(`${pluginName} installed successfully!`))) - .catch((err: any) => console.error(chalk.red(err))); + .catch((err) => console.error(chalk.red(err))); }, ['i'] ); @@ -59,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'] ); @@ -83,21 +101,17 @@ args.command( const lsRemote = (pattern?: string) => { // note that no errors are catched by this function - const URL = `https://api.npms.io/v2/search?q=${(pattern && `${pattern}+`) || ''}keywords:hyper-plugin,hyper-theme`; + const URL = `https://api.npms.io/v2/search?q=${ + (pattern && `${pattern}+`) || '' + }keywords:hyper-plugin,hyper-theme&size=250`; + type npmResult = {package: {name: string; description: string}}; return got(URL) - .then(response => JSON.parse(response.body).results as any[]) - .then(entries => entries.map(entry => entry.package)) - .then(entries => entries.filter(entry => entry.name.indexOf(PLUGIN_PREFIX) === 0)) - .then(entries => + .then((response) => JSON.parse(response.body).results as npmResult[]) + .then((entries) => entries.map((entry) => entry.package)) + .then((entries) => entries.map(({name, description}) => { return {name, description}; }) - ) - .then(entries => - entries.map(entry => { - entry.name = chalk.green(entry.name); - return entry; - }) ); }; @@ -109,20 +123,19 @@ args.command( const query = args_[0] ? args_[0].toLowerCase() : ''; commandPromise = lsRemote(query) - .then(entries => { + .then((entries) => { if (entries.length === 0) { spinner.fail(); console.error(chalk.red(`Your search '${query}' did not match any plugins`)); 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); } }) - .catch(err => { + .catch((err) => { spinner.fail(); console.error(chalk.red(err)); // TODO }); @@ -137,14 +150,12 @@ args.command( const spinner = ora('Searching').start(); commandPromise = lsRemote() - .then(entries => { - let msg = columnify(entries); - + .then((entries) => { + const msg = columnify(entries); spinner.succeed(); - msg = msg.substring(msg.indexOf('\n') + 1); // remove header console.log(msg); }) - .catch(err => { + .catch((err) => { spinner.fail(); console.error(chalk.red(err)); // TODO }); @@ -158,7 +169,7 @@ args.command( (name, args_) => { const pluginName = args_[0]; assertPluginName(pluginName); - open(`http://ghub.io/${pluginName}`, {wait: false, url: true}); + void open(`http://ghub.io/${pluginName}`, {wait: false}); process.exit(0); }, ['d', 'h', 'home'] @@ -184,8 +195,10 @@ const main = (argv: string[]) => { version: false, mri: { boolean: ['v', 'verbose'] - } - } as any); + }, + mainColor: 'yellow', + subColor: 'dim' + }); if (commandPromise) { return commandPromise; @@ -203,12 +216,12 @@ const main = (argv: string[]) => { env['ELECTRON_ENABLE_LOGGING'] = '1'; } - const options: any = { + const options: SpawnOptions = { detached: true, env }; - const args_ = args.sub.map(arg => { + const args_ = args.sub.map((arg) => { const cwd = isAbsolute(arg) ? arg : resolve(process.cwd(), arg); if (!existsSync(cwd)) { console.error(chalk.red(`Error! Directory or file does not exist: ${cwd}`)); @@ -225,18 +238,20 @@ const main = (argv: string[]) => { const opts = { env }; - return pify(exec)(cmd, opts); + return promisify(exec)(cmd, opts); } } const child = spawn(process.execPath, args_, options); if (flags.verbose) { - child.stdout.on('data', data => console.log(data.toString('utf8'))); - child.stderr.on('data', data => console.error(data.toString('utf8'))); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + child.stdout?.on('data', (data) => console.log(data.toString('utf8'))); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + child.stderr?.on('data', (data) => console.error(data.toString('utf8'))); } if (flags.verbose) { - return new Promise(c => child.once('exit', () => c(null))); + return new Promise((c) => child.once('exit', () => c(null))); } child.unref(); return Promise.resolve(); @@ -248,7 +263,7 @@ function eventuallyExit(code: number) { main(process.argv) .then(() => eventuallyExit(0)) - .catch((err: any) => { + .catch((err) => { console.error(err.stack ? err.stack : err); eventuallyExit(1); }); 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 958cb668..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,54 +17,102 @@ ] } ], + "artifactName": "${productName}-${version}-${arch}.${ext}", "linux": { "category": "TerminalEmulator", "target": [ - { - "target": "deb", - "arch": [ - "x64" - ] - }, - { - "target": "AppImage", - "arch": [ - "x64" - ] - }, - { - "target": "rpm", - "arch": [ - "x64" - ] - }, - { - "target": "snap", - "arch": [ - "x64" - ] - } + "deb", + "AppImage", + "rpm", + "snap", + "pacman" ] }, "win": { - "target": [ - "squirrel" - ], + "target": { + "target": "nsis", + "arch": [ + "x64", + "arm64" + ] + }, "rfc3161TimeStampServer": "http://timestamp.comodoca.com" }, + "nsis": { + "include": "build/win/installer.nsh", + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true + }, "mac": { + "target": { + "target": "default", + "arch": [ + "x64", + "arm64" + ] + }, + "artifactName": "${productName}-${version}-${os}-${arch}.${ext}", "category": "public.app-category.developer-tools", - "extendInfo": "build/Info.plist", + "entitlements": "./build/mac/entitlements.plist", + "entitlementsInherit": "./build/mac/entitlements.plist", + "extendInfo": { + "CFBundleDocumentTypes": [ + { + "CFBundleTypeName": "Folders", + "CFBundleTypeRole": "Viewer", + "LSHandlerRank": "Alternate", + "LSItemContentTypes": [ + "public.folder", + "com.apple.bundle", + "com.apple.package", + "com.apple.resolvable" + ] + }, + { + "CFBundleTypeName": "UnixExecutables", + "CFBundleTypeRole": "Shell", + "LSHandlerRank": "Alternate", + "LSItemContentTypes": [ + "public.unix-executable" + ] + } + ], + "NSAppleEventsUsageDescription": "An application in Hyper wants to use AppleScript.", + "NSCalendarsUsageDescription": "An application in Hyper wants to access Calendar data.", + "NSCameraUsageDescription": "An application in Hyper wants to use the Camera.", + "NSContactsUsageDescription": "An application in Hyper wants to access your Contacts.", + "NSDesktopFolderUsageDescription": "An application in Hyper wants to access the Desktop folder.", + "NSDocumentsFolderUsageDescription": "An application in Hyper wants to access the Documents folder.", + "NSDownloadsFolderUsageDescription": "An application in Hyper wants to access the Downloads folder.", + "NSFileProviderDomainUsageDescription": "An application in Hyper wants to access files managed by a file provider.", + "NSFileProviderPresenceUsageDescription": "An application in Hyper wants to be informed when other apps access files that it manages.", + "NSLocationUsageDescription": "An application in Hyper wants to access your location information.", + "NSMicrophoneUsageDescription": "An application in Hyper wants to use your microphone.", + "NSMotionUsageDescription": "An application in Hyper wants to use the device’s accelerometer.", + "NSNetworkVolumesUsageDescription": "An application in Hyper wants to access files on a network volume.", + "NSPhotoLibraryUsageDescription": "An application in Hyper wants to access the photo library.", + "NSRemindersUsageDescription": "An application in Hyper wants to access your reminders.", + "NSRemovableVolumesUsageDescription": "An application in Hyper wants to access files on a removable volume.", + "NSSpeechRecognitionUsageDescription": "An application in Hyper wants to send user data to Apple’s speech recognition servers.", + "NSSystemAdministrationUsageDescription": "The operation being performed by an application in Hyper requires elevated permission." + }, "darkModeSupport": true }, "deb": { + "compression": "bzip2", "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" + "confinement": "classic", + "publish": "github" }, "protocols": { "name": "ssh URL", diff --git a/lib/actions/config.ts b/lib/actions/config.ts index a237cce0..7d1110cd 100644 --- a/lib/actions/config.ts +++ b/lib/actions/config.ts @@ -1,14 +1,15 @@ -import {CONFIG_LOAD, CONFIG_RELOAD} from '../constants/config'; -import {HyperActions} from '../hyper'; +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: any): HyperActions { +export function loadConfig(config: configOptions): HyperActions { return { type: CONFIG_LOAD, config }; } -export function reloadConfig(config: any): HyperActions { +export function reloadConfig(config: configOptions): HyperActions { const now = Date.now(); return { type: CONFIG_RELOAD, 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 fa8ddc0b..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,26 +31,26 @@ 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}); } }); }; } -export function addSessionData(uid: string, data: any) { +export function addSessionData(uid: string, data: string) { return (dispatch: HyperDispatch) => { dispatch({ type: SESSION_ADD_DATA, @@ -135,27 +135,35 @@ 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 }); }; } -export function closeSearch(uid?: string) { +export function closeSearch(uid?: string, keyEvent?: any) { return (dispatch: HyperDispatch, getState: () => HyperState) => { const targetUid = uid || getState().sessions.activeUid!; - dispatch({ - type: SESSION_SEARCH_CLOSE, - uid: targetUid - }); + if (getState().sessions.sessions[targetUid]?.search) { + dispatch({ + type: SESSION_SEARCH, + uid: targetUid, + value: false + }); + } else { + if (keyEvent) { + keyEvent.catched = false; + } + } }; } -export function sendSessionData(uid: string | null, data: any, escaped?: any) { +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 3070ca0a..6beeedd9 100644 --- a/lib/actions/term-groups.ts +++ b/lib/actions/term-groups.ts @@ -1,38 +1,42 @@ -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 {setActiveSession, ptyExitSession, userExitSession} from './sessions'; -import {ITermState, ITermGroup, HyperState, HyperDispatch} from '../hyper'; -import {Immutable} from 'seamless-immutable'; +import findBySession from '../utils/term-groups'; -function requestSplit(direction: string) { - return (activeUid: string) => (dispatch: HyperDispatch, getState: () => HyperState): void => { - dispatch({ - type: SESSION_REQUEST, - effect: () => { - const {ui, sessions} = getState(); - rpc.emit('new', { - splitDirection: direction, - cwd: ui.cwd, - activeUid: activeUid ? activeUid : sessions.activeUid - }); - } - }); - }; +import {setActiveSession, ptyExitSession, userExitSession} from './sessions'; + +function requestSplit(direction: 'VERTICAL' | 'HORIZONTAL') { + 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, + profile + }); + } + }); + }; } export const requestVerticalSplit = requestSplit(DIRECTION.VERTICAL); export const requestHorizontalSplit = requestSplit(DIRECTION.HORIZONTAL); -export function resizeTermGroup(uid: string, sizes: number[]) { +export function resizeTermGroup(uid: string, sizes: number[]): HyperActions { return { uid, type: TERM_GROUP_RESIZE, @@ -40,17 +44,20 @@ export function resizeTermGroup(uid: string, sizes: number[]) { }; } -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 }); } }); @@ -67,7 +74,7 @@ export function setActiveGroup(uid: string) { // When we've found the next group which we want to // set as active (after closing something), we also need // to find the first child group which has a sessionUid. -const findFirstSession = (state: Immutable, group: Immutable): string | undefined => { +const findFirstSession = (state: ITermState, group: ITermGroup): string | undefined => { if (group.sessionUid) { return group.sessionUid; } @@ -90,7 +97,7 @@ const findPrevious = (list: T[], old: T) => { return index ? list[index - 1] : list[1]; }; -const findNextSessionUid = (state: Immutable, group: Immutable) => { +const findNextSessionUid = (state: ITermState, group: ITermGroup) => { // If we're closing a root group (i.e. a whole tab), // the next group needs to be a root group as well: if (state.activeRootGroup === group.uid) { @@ -101,7 +108,7 @@ const findNextSessionUid = (state: Immutable, group: Immutable { + group.children.forEach((childUid) => { dispatch(userExitTermGroup(childUid)); }); } @@ -168,7 +175,7 @@ export function exitActiveTermGroup() { effect() { const {sessions, termGroups} = getState(); const {uid} = findBySession(termGroups, sessions.activeUid!)!; - dispatch(userExitTermGroup(uid!)); + dispatch(userExitTermGroup(uid)); } }); }; diff --git a/lib/actions/ui.ts b/lib/actions/ui.ts index fbbab767..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,16 +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} from '../hyper'; -import {Stats} from 'fs'; -const {stat} = window.require('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, @@ -104,24 +105,24 @@ 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 }; } // Find all sessions that are below the given // termGroup uid in the hierarchy: -const findChildSessions = (termGroups: any, uid: string): string[] => { +const findChildSessions = (termGroups: ITermGroups, uid: string): string[] => { const group = termGroups[uid]; if (group.sessionUid) { return [uid]; } - return group.children.reduce( - (total: string[], childUid: string) => total.concat(findChildSessions(termGroups, childUid)), - [] - ); + return group.children + .asMutable() + .reduce((total: string[], childUid: string) => total.concat(findChildSessions(termGroups, childUid)), []); }; // Get the index of the next or previous group, @@ -143,10 +144,9 @@ function moveToNeighborPane(type: typeof UI_MOVE_NEXT_PANE | typeof UI_MOVE_PREV const {uid} = findBySession(termGroups, sessions.activeUid!)!; const childGroups = findChildSessions(termGroups.termGroups, termGroups.activeRootGroup!); if (childGroups.length === 1) { - //eslint-disable-next-line no-console console.log('ignoring move for single group'); } else { - const index = getNeighborIndex(childGroups, uid!, type); + const index = getNeighborIndex(childGroups, uid, type); const {sessionUid} = termGroups.termGroups[childGroups[index]]; dispatch(setActiveSession(sessionUid!)); } @@ -174,7 +174,6 @@ export function moveLeft() { const index = groupUids.indexOf(uid); const next = groupUids[index - 1] || groupUids[groupUids.length - 1]; if (!next || uid === next) { - //eslint-disable-next-line no-console console.log('ignoring left move action'); } else { dispatch(setActiveGroup(next)); @@ -195,7 +194,6 @@ export function moveRight() { const index = groupUids.indexOf(uid); const next = groupUids[index + 1] || groupUids[0]; if (!next || uid === next) { - //eslint-disable-next-line no-console console.log('ignoring right move action'); } else { dispatch(setActiveGroup(next)); @@ -212,7 +210,7 @@ export function moveTo(i: number | 'last') { const {termGroups} = getState().termGroups; i = Object.keys(termGroups) - .map(uid => termGroups[uid]) + .map((uid) => termGroups[uid]) .filter(({parentUid}) => !parentUid).length - 1; } dispatch({ @@ -223,12 +221,10 @@ export function moveTo(i: number | 'last') { const groupUids = getGroupUids(state); const uid = state.termGroups.activeRootGroup; if (uid === groupUids[i as number]) { - //eslint-disable-next-line no-console console.log('ignoring same uid'); } else if (groupUids[i as number]) { dispatch(setActiveGroup(groupUids[i as number])); } else { - //eslint-disable-next-line no-console console.log('ignoring inexistent index', i); } } @@ -276,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)); }); } }); @@ -299,38 +295,37 @@ 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; + let command = `${parsedUrl.protocol} ${parsedUrl.user ? `${parsedUrl.user}@` : ''}${parsedUrl.resource}`; - if (parsedUrl.port) command += ' -p ' + parsedUrl.port; + if (parsedUrl.port) command += ` -p ${parsedUrl.port}`; command += '\n'; rpc.once('session add', ({uid}) => { rpc.once('session data', () => { - dispatch(sendSessionData(uid, command, null)); + dispatch(sendSessionData(uid, command)); }); }); - dispatch(requestSession()); + dispatch(requestSession(undefined)); } }); }; } -export function execCommand(command: any, fn: any, e: any) { +export function execCommand(command: string, fn: (e: any, dispatch: HyperDispatch) => void, e: any) { return (dispatch: HyperDispatch) => dispatch({ type: UI_COMMAND_EXEC, command, effect() { if (fn) { - fn(e); + fn(e, dispatch); } else { rpc.emit('command', command); } 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 d585f1f4..f72c08c9 100644 --- a/lib/command-registry.ts +++ b/lib/command-registry.ts @@ -1,16 +1,21 @@ -import {remote} from 'electron'; -// 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'); +import {closeSearch} from './actions/sessions'; +import {ipcRenderer} from './utils/ipc'; -let commands: Record = {}; +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]; - commandKeys.forEach((shortcut: string) => { + commandKeys.forEach((shortcut) => { result[shortcut] = actionName; }); return result; diff --git a/lib/components/header.js b/lib/components/header.js deleted file mode 100644 index 8fd3ed06..00000000 --- a/lib/components/header.js +++ /dev/null @@ -1,256 +0,0 @@ -import React from 'react'; - -import {decorate, getTabsProps} from '../utils/plugins'; - -import Tabs_ from './tabs'; - -const Tabs = decorate(Tabs_, 'Tabs'); - -export default class Header extends React.PureComponent { - onChangeIntent = active => { - // 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) { - return; - } - - this.props.onChangeTab(active); - }; - - handleHeaderMouseDown = () => { - // the hack of all hacks, this prevents the term - // iframe from losing focus, for example, when - // the user drags the nav around - // Fixed by calling window.focusActiveTerm(), thus we can support drag tab - // ev.preventDefault(); - - // persist start positions of a potential drag motion - // to differentiate dragging from clicking - this.headerMouseDownWindowX = window.screenX; - this.headerMouseDownWindowY = window.screenY; - }; - - handleHamburgerMenuClick = event => { - let {right: x, bottom: y} = event.currentTarget.getBoundingClientRect(); - x -= 15; // to compensate padding - y -= 12; // ^ same - this.props.openHamburgerMenu({x, y}); - }; - - handleMaximizeClick = () => { - if (this.props.maximized) { - this.props.unmaximize(); - } else { - this.props.maximize(); - } - }; - - handleMinimizeClick = () => { - this.props.minimize(); - }; - - handleCloseClick = () => { - this.props.close(); - }; - - componentWillUnmount() { - delete this.clicks; - clearTimeout(this.clickTimer); - } - - getWindowHeaderConfig() { - const {showHamburgerMenu, showWindowControls} = this.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 - }; - - // don't allow the user to change defaults on macOS - if (this.props.isMac) { - return defaults; - } - - return { - hambMenu: showHamburgerMenu === '' ? defaults.hambMenu : showHamburgerMenu, - winCtrls: showWindowControls === '' ? defaults.winCtrls : showWindowControls - }; - } - - 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 && ( -
-
- - - -
-
- - - -
-
- - - -
-
- )} -
- )} - {this.props.customChildrenBefore} - - {this.props.customChildren} - - -
- ); - } -} diff --git a/lib/components/header.tsx b/lib/components/header.tsx new file mode 100644 index 00000000..416ecbc5 --- /dev/null +++ b/lib/components/header.tsx @@ -0,0 +1,260 @@ +import React, {forwardRef, useState} from 'react'; + +import type {HeaderProps} from '../../typings/hyper'; +import {decorate, getTabsProps} from '../utils/plugins'; + +import Tabs_ from './tabs'; + +const Tabs = decorate(Tabs_, 'Tabs'); + +const Header = forwardRef((props, ref) => { + const [headerMouseDownWindowX, setHeaderMouseDownWindowX] = useState(0); + const [headerMouseDownWindowY, setHeaderMouseDownWindowY] = useState(0); + + const onChangeIntent = (active: string) => { + // we ignore clicks if they're a byproduct of a drag + // motion to move the window + if (window.screenX !== headerMouseDownWindowX || window.screenY !== headerMouseDownWindowY) { + return; + } + + props.onChangeTab(active); + }; + + const handleHeaderMouseDown = () => { + // the hack of all hacks, this prevents the term + // iframe from losing focus, for example, when + // the user drags the nav around + // Fixed by calling window.focusActiveTerm(), thus we can support drag tab + // ev.preventDefault(); + + // persist start positions of a potential drag motion + // to differentiate dragging from clicking + setHeaderMouseDownWindowX(window.screenX); + setHeaderMouseDownWindowY(window.screenY); + }; + + const handleHamburgerMenuClick = (event: React.MouseEvent) => { + let {right: x, bottom: y} = event.currentTarget.getBoundingClientRect(); + x -= 15; // to compensate padding + y -= 12; // ^ same + props.openHamburgerMenu({x, y}); + }; + + const handleMaximizeClick = () => { + if (props.maximized) { + props.unmaximize(); + } else { + props.maximize(); + } + }; + + const handleMinimizeClick = () => { + props.minimize(); + }; + + const handleCloseClick = () => { + props.close(); + }; + + const getWindowHeaderConfig = () => { + const {showHamburgerMenu, showWindowControls} = props; + + const defaults = { + 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 (props.isMac) { + return defaults; + } + + return { + 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'; + + return ( +
window.focusActiveTerm()} + onDoubleClick={handleMaximizeClick} + ref={ref} + > + {!isMac && ( +
1 ? 'header_windowHeaderWithBorder' : ''}`} + style={{borderColor}} + > + {hambMenu && ( + + + + )} + {title} + {winCtrls && ( +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ )} +
+ )} + {props.customChildrenBefore} + + {props.customChildren} + + +
+ ); +}); + +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.js b/lib/components/notification.js deleted file mode 100644 index 88badb7b..00000000 --- a/lib/components/notification.js +++ /dev/null @@ -1,113 +0,0 @@ -import React from 'react'; - -export default class Notification extends React.PureComponent { - constructor() { - super(); - this.state = { - dismissing: false - }; - } - - componentDidMount() { - if (this.props.dismissAfter) { - this.setDismissTimer(); - } - } - //TODO: Remove usage of legacy and soon deprecated lifecycle methods - UNSAFE_componentWillReceiveProps(next) { - // if we have a timer going and the notification text - // changed we reset the timer - if (next.text !== this.props.text) { - if (this.props.dismissAfter) { - this.resetDismissTimer(); - } - if (this.state.dismissing) { - this.setState({dismissing: false}); - } - } - } - - handleDismiss = () => { - this.setState({dismissing: true}); - }; - - onElement = el => { - if (el) { - el.addEventListener('webkitTransitionEnd', () => { - if (this.state.dismissing) { - this.props.onDismiss(); - } - }); - const {backgroundColor} = this.props; - if (backgroundColor) { - el.style.setProperty('background-color', backgroundColor, 'important'); - } - } - }; - - setDismissTimer() { - this.dismissTimer = setTimeout(() => { - this.handleDismiss(); - }, this.props.dismissAfter); - } - - resetDismissTimer() { - clearTimeout(this.dismissTimer); - this.setDismissTimer(); - } - - componentWillUnmount() { - clearTimeout(this.dismissTimer); - } - - 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} - - -
- ); - } -} diff --git a/lib/components/notification.tsx b/lib/components/notification.tsx new file mode 100644 index 00000000..68b7a2df --- /dev/null +++ b/lib/components/notification.tsx @@ -0,0 +1,110 @@ +import React, {forwardRef, useEffect, useRef, useState} from 'react'; + +import type {NotificationProps} from '../../typings/hyper'; + +const Notification = forwardRef>((props, ref) => { + const dismissTimer = useRef(undefined); + const [dismissing, setDismissing] = useState(false); + + useEffect(() => { + setDismissTimer(); + }, []); + + useEffect(() => { + // if we have a timer going and the notification text + // changed we reset the timer + resetDismissTimer(); + setDismissing(false); + }, [props.text]); + + const handleDismiss = () => { + setDismissing(true); + }; + + const onElement = (el: HTMLDivElement | null) => { + if (el) { + el.addEventListener('webkitTransitionEnd', () => { + if (dismissing) { + props.onDismiss(); + } + }); + const {backgroundColor} = props; + if (backgroundColor) { + el.style.setProperty('background-color', backgroundColor, 'important'); + } + + if (ref) { + if (typeof ref === 'function') ref(el); + else ref.current = el; + } + } + }; + + const setDismissTimer = () => { + if (typeof props.dismissAfter === 'number') { + dismissTimer.current = setTimeout(() => { + handleDismiss(); + }, props.dismissAfter); + } + }; + + const resetDismissTimer = () => { + clearTimeout(dismissTimer.current); + setDismissTimer(); + }; + + useEffect(() => { + return () => { + clearTimeout(dismissTimer.current); + }; + }, []); + + const {backgroundColor, color} = props; + const opacity = dismissing ? 0 : 1; + return ( +
+ {props.customChildrenBefore} + {props.children || props.text} + {props.userDismissable ? ( + + [x] + + ) : null} + {props.customChildren} + + +
+ ); +}); + +Notification.displayName = 'Notification'; + +export default Notification; diff --git a/lib/components/notifications.js b/lib/components/notifications.js deleted file mode 100644 index 8a68067a..00000000 --- a/lib/components/notifications.js +++ /dev/null @@ -1,131 +0,0 @@ -import React from 'react'; - -import {decorate} from '../utils/plugins'; - -import Notification_ from './notification'; - -const Notification = decorate(Notification_, 'Notification'); - -export default class Notifications extends React.PureComponent { - render() { - return ( -
- {this.props.customChildrenBefore} - {this.props.fontShowing && ( - - )} - - {this.props.resizeShowing && ( - - )} - - {this.props.messageShowing && ( - - {this.props.messageURL - ? [ - this.props.messageText, - ' (', - { - window.require('electron').shell.openExternal(ev.target.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(/\.$/, '')}`} ( - { - window.require('electron').shell.openExternal(ev.target.href); - ev.preventDefault(); - }} - href={`https://github.com/zeit/hyper/releases/tag/${this.props.updateVersion}`} - > - notes - - ).{' '} - {this.props.updateCanInstall ? ( - - Restart - - ) : ( - { - window.require('electron').shell.openExternal(ev.target.href); - ev.preventDefault(); - }} - href={this.props.updateReleaseUrl} - > - Download - - )} - .{' '} - - )} - {this.props.customChildren} - - -
- ); - } -} diff --git a/lib/components/notifications.tsx b/lib/components/notifications.tsx new file mode 100644 index 00000000..79e6ede3 --- /dev/null +++ b/lib/components/notifications.tsx @@ -0,0 +1,132 @@ +import React, {forwardRef} from 'react'; + +import type {NotificationsProps} from '../../typings/hyper'; +import {decorate} from '../utils/plugins'; + +import Notification_ from './notification'; + +const Notification = decorate(Notification_, 'Notification'); + +const Notifications = forwardRef((props, ref) => { + return ( +
+ {props.customChildrenBefore} + {props.fontShowing && ( + + )} + + {props.resizeShowing && ( + + )} + + {props.messageShowing && ( + + {props.messageURL ? ( + <> + {props.messageText} ( + { + void window.require('electron').shell.openExternal(ev.currentTarget.href); + ev.preventDefault(); + }} + href={props.messageURL} + > + more + + ) + + ) : 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.js b/lib/components/searchBox.js deleted file mode 100644 index a172cbce..00000000 --- a/lib/components/searchBox.js +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; - -const searchBoxStyling = { - float: 'right', - height: '28px', - backgroundColor: 'white', - position: 'absolute', - right: '10px', - top: '25px', - width: '224px', - zIndex: '9999' -}; - -const enterKey = 13; - -export default class SearchBox extends React.PureComponent { - constructor(props) { - super(props); - this.searchTerm = ''; - } - - handleChange = event => { - this.searchTerm = event.target.value; - if (event.keyCode === enterKey) { - this.props.search(event.target.value); - } - }; - - render() { - return ( -
- input && input.focus()} /> - this.props.prev(this.searchTerm)}> - {' '} - ←{' '} - - this.props.next(this.searchTerm)}> - {' '} - →{' '} - - this.props.close()}> - {' '} - x{' '} - - -
- ); - } -} diff --git a/lib/components/searchBox.tsx b/lib/components/searchBox.tsx new file mode 100644 index 00000000..55a72f16 --- /dev/null +++ b/lib/components/searchBox.tsx @@ -0,0 +1,237 @@ +import React, {useCallback, useRef, useEffect, forwardRef} from 'react'; + +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; +}; + +type SearchButtonProps = React.PropsWithChildren< + { + onClick: () => void; + active: boolean; + title: string; + } & SearchButtonColors +>; + +const SearchButton = ({ + onClick, + active, + title, + foregroundColor, + backgroundColor, + selectionColor, + children +}: SearchButtonProps) => { + const handleKeyUp = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + onClick(); + } + }, + [onClick] + ); + + 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 + }; + + return ( +
+
+ + + + + + + + + + + + + +
+ + + {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.js b/lib/components/split-pane.js deleted file mode 100644 index e3d8fa0d..00000000 --- a/lib/components/split-pane.js +++ /dev/null @@ -1,211 +0,0 @@ -/* eslint-disable quote-props */ -import React from 'react'; -import _ from 'lodash'; - -export default class SplitPane extends React.PureComponent { - constructor(props) { - super(props); - this.state = {dragging: false}; - } - - componentDidUpdate(prevProps) { - if (this.state.dragging && prevProps.sizes !== this.props.sizes) { - // recompute positions for ongoing dragging - this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2]; - } - } - - setupPanes(ev) { - this.panes = Array.from(ev.target.parentNode.childNodes); - this.paneIndex = this.panes.indexOf(ev.target); - this.paneIndex -= Math.ceil(this.paneIndex / 2); - } - - handleAutoResize = ev => { - ev.preventDefault(); - - this.setupPanes(ev); - - const sizes_ = this.getSizes(); - sizes_[this.paneIndex] = 0; - sizes_[this.paneIndex + 1] = 0; - - const availableWidth = 1 - _.sum(sizes_); - sizes_[this.paneIndex] = availableWidth / 2; - sizes_[this.paneIndex + 1] = availableWidth / 2; - - this.props.onResize(sizes_); - }; - - handleDragStart = ev => { - ev.preventDefault(); - this.setState({dragging: true}); - window.addEventListener('mousemove', this.onDrag); - window.addEventListener('mouseup', this.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]; - this.panesSize = ev.target.parentNode.getBoundingClientRect()[this.d1]; - this.setupPanes(ev); - }; - - getSizes() { - const {sizes} = this.props; - let sizes_; - - if (sizes) { - sizes_ = [].concat(sizes); - } else { - const total = this.props.children.length; - const count = new Array(total).fill(1 / total); - - sizes_ = count; - } - return sizes_; - } - - onDrag = ev => { - const sizes_ = this.getSizes(); - - const i = this.paneIndex; - const pos = ev[this.d3]; - const d = Math.abs(this.dragPanePosition - pos) / this.panesSize; - if (pos > this.dragPanePosition) { - sizes_[i] += d; - sizes_[i + 1] -= d; - } else { - sizes_[i] -= d; - sizes_[i + 1] += d; - } - this.props.onResize(sizes_); - }; - - onDragEnd = () => { - if (this.state.dragging) { - window.removeEventListener('mousemove', this.onDrag); - window.removeEventListener('mouseup', this.onDragEnd); - this.setState({dragging: false}); - } - }; - - render() { - const children = this.props.children; - const {direction, borderColor} = this.props; - const sizeProperty = direction === 'horizontal' ? 'height' : 'width'; - let {sizes} = this.props; - if (!sizes) { - // workaround for the fact that if we don't specify - // sizes, sometimes flex fails to calculate the - // right height for the horizontal panes - 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 [ -
- {child} -
, - i < children.length - 1 ? ( -
- ) : null - ]; - })} -
- - -
- ); - } - - componentWillUnmount() { - // ensure drag end - if (this.dragging) { - this.onDragEnd(); - } - } -} diff --git a/lib/components/split-pane.tsx b/lib/components/split-pane.tsx new file mode 100644 index 00000000..aea53f6f --- /dev/null +++ b/lib/components/split-pane.tsx @@ -0,0 +1,191 @@ +import React, {useState, useEffect, useRef, forwardRef} from 'react'; + +import sum from 'lodash/sum'; + +import type {SplitPaneProps} from '../../typings/hyper'; + +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); + + const handleAutoResize = (ev: React.MouseEvent, index: number) => { + ev.preventDefault(); + + paneIndex.current = index; + + const sizes_ = getSizes(); + sizes_[paneIndex.current] = 0; + sizes_[paneIndex.current + 1] = 0; + + const availableWidth = 1 - sum(sizes_); + sizes_[paneIndex.current] = availableWidth / 2; + sizes_[paneIndex.current + 1] = availableWidth / 2; + + props.onResize(sizes_); + }; + + const handleDragStart = (ev: React.MouseEvent, index: number) => { + ev.preventDefault(); + setDragging(true); + window.addEventListener('mousemove', onDrag); + window.addEventListener('mouseup', onDragEnd); + + const target = ev.target as HTMLDivElement; + dragTarget.current = target; + dragPanePosition.current = dragTarget.current.getBoundingClientRect()[d2]; + panesSize.current = target.parentElement!.getBoundingClientRect()[d1]; + paneIndex.current = index; + }; + + const getSizes = () => { + const {sizes} = props; + let sizes_: number[]; + + if (sizes) { + sizes_ = [...sizes.asMutable()]; + } else { + const total = props.children.length; + const count = new Array(total).fill(1 / total); + + sizes_ = count; + } + return sizes_; + }; + + const onDrag = (ev: MouseEvent) => { + const sizes_ = getSizes(); + + 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; + } + props.onResize(sizes_); + }; + + const onDragEnd = () => { + window.removeEventListener('mousemove', onDrag); + window.removeEventListener('mouseup', onDragEnd); + setDragging(false); + }; + + 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 ? ( +
handleDragStart(e, i)} + onDoubleClick={(e) => handleAutoResize(e, i)} + style={{backgroundColor: borderColor}} + className={`splitpane_divider splitpane_divider_${direction}`} + /> + ) : null} + + ); + })} +
+ + +
+ ); +}); + +SplitPane.displayName = 'SplitPane'; + +export default SplitPane; diff --git a/lib/components/style-sheet.js b/lib/components/style-sheet.js deleted file mode 100644 index 1dc02b72..00000000 --- a/lib/components/style-sheet.js +++ /dev/null @@ -1,153 +0,0 @@ -import React from 'react'; - -export default class StyleSheet extends React.PureComponent { - render() { - const {backgroundColor, fontFamily, foregroundColor, borderColor} = this.props; - - return ( - - ); - } -} diff --git a/lib/components/style-sheet.tsx b/lib/components/style-sheet.tsx new file mode 100644 index 00000000..86d89424 --- /dev/null +++ b/lib/components/style-sheet.tsx @@ -0,0 +1,27 @@ +import React, {forwardRef} from 'react'; + +import type {StyleSheetProps} from '../../typings/hyper'; + +const StyleSheet = forwardRef((props, ref) => { + const {borderColor} = props; + + return ( + + ); +}); + +StyleSheet.displayName = 'StyleSheet'; + +export default StyleSheet; diff --git a/lib/components/tab.js b/lib/components/tab.js deleted file mode 100644 index 9d3509db..00000000 --- a/lib/components/tab.js +++ /dev/null @@ -1,182 +0,0 @@ -import React from 'react'; - -export default class Tab extends React.PureComponent { - constructor() { - super(); - - this.state = { - hovered: false - }; - } - - handleHover = () => { - this.setState({ - hovered: true - }); - }; - - handleBlur = () => { - this.setState({ - hovered: false - }); - }; - - handleClick = event => { - const isLeftClick = event.nativeEvent.which === 1; - - if (isLeftClick && !this.props.isActive) { - this.props.onSelect(); - } - }; - - handleMouseUp = event => { - const isMiddleClick = event.nativeEvent.which === 2; - - if (isMiddleClick) { - this.props.onClose(); - } - }; - - render() { - const {isActive, isFirst, isLast, borderColor, hasActivity} = this.props; - const {hovered} = this.state; - - return ( - -
  • - {this.props.customChildrenBefore} - - - {this.props.text} - - - - - - - - {this.props.customChildren} -
  • - - -
    - ); - } -} diff --git a/lib/components/tab.tsx b/lib/components/tab.tsx new file mode 100644 index 00000000..c977c039 --- /dev/null +++ b/lib/components/tab.tsx @@ -0,0 +1,168 @@ +import React, {forwardRef} from 'react'; + +import type {TabProps} from '../../typings/hyper'; + +const Tab = forwardRef((props, ref) => { + const handleClick = (event: React.MouseEvent) => { + const isLeftClick = event.nativeEvent.which === 1; + + if (isLeftClick && !props.isActive) { + props.onSelect(); + } + }; + + const handleMouseUp = (event: React.MouseEvent) => { + const isMiddleClick = event.nativeEvent.which === 2; + + if (isMiddleClick) { + props.onClose(); + } + }; + + const {isActive, isFirst, isLast, borderColor, hasActivity} = props; + + return ( + <> +
  • + {props.customChildrenBefore} + + + {props.text} + + + + + + + + {props.customChildren} +
  • + + + + ); +}); + +Tab.displayName = 'Tab'; + +export default Tab; diff --git a/lib/components/tabs.js b/lib/components/tabs.js deleted file mode 100644 index 3b521619..00000000 --- a/lib/components/tabs.js +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; - -import {decorate, getTabProps} from '../utils/plugins'; - -import Tab_ from './tab'; - -const Tab = decorate(Tab_, 'Tab'); -const isMac = /Mac/.test(navigator.userAgent); - -export default class Tabs extends React.PureComponent { - render() { - const {tabs = [], borderColor, onChange, onClose, fullScreen} = this.props; - - const hide = !isMac && tabs.length === 1; - - return ( - - ); - } -} diff --git a/lib/components/tabs.tsx b/lib/components/tabs.tsx new file mode 100644 index 00000000..49043e7d --- /dev/null +++ b/lib/components/tabs.tsx @@ -0,0 +1,113 @@ +import React, {forwardRef} from 'react'; + +import type {TabsProps} from '../../typings/hyper'; +import {decorate, getTabProps} from '../utils/plugins'; + +import DropdownButton from './new-tab'; +import Tab_ from './tab'; + +const Tab = decorate(Tab_, 'Tab'); +const isMac = /Mac/.test(navigator.userAgent); + +const Tabs = forwardRef((props, ref) => { + const {tabs = [], borderColor, onChange, onClose, fullScreen} = props; + + const hide = !isMac && tabs.length === 1; + + return ( + + ); +}); + +Tabs.displayName = 'Tabs'; + +export default Tabs; diff --git a/lib/components/term-group.js b/lib/components/term-group.tsx similarity index 68% rename from lib/components/term-group.js rename to lib/components/term-group.tsx index c04f42d8..5e6e66ec 100644 --- a/lib/components/term-group.js +++ b/lib/components/term-group.tsx @@ -1,38 +1,47 @@ import React from 'react'; + import {connect} from 'react-redux'; -import {decorate, getTermProps, getTermGroupProps} from '../utils/plugins'; + +import type {HyperState, HyperDispatch, TermGroupProps, TermGroupOwnProps} from '../../typings/hyper'; import {resizeTermGroup} from '../actions/term-groups'; -import Term_ from './term'; +import {decorate, getTermProps, getTermGroupProps} from '../utils/plugins'; + import SplitPane_ from './split-pane'; +import Term_ from './term'; const Term = decorate(Term_, 'Term'); const SplitPane = decorate(SplitPane_, 'SplitPane'); -class TermGroup_ extends React.PureComponent { - constructor(props, context) { +class TermGroup_ extends React.PureComponent { + bound: WeakMap<(uid: string, ...args: any[]) => any, Record any>>; + term?: Term_ | null; + constructor(props: TermGroupProps, context: any) { super(props, context); this.bound = new WeakMap(); - this.termRefs = {}; } - bind(fn, thisObj, uid) { + bind any>( + fn: T, + thisObj: any, + uid: string + ): (...args: T extends (uid: string, ..._args: infer I) => any ? I : never) => ReturnType { if (!this.bound.has(fn)) { this.bound.set(fn, {}); } - const map = this.bound.get(fn); + const map = this.bound.get(fn)!; if (!map[uid]) { map[uid] = fn.bind(thisObj, uid); } return map[uid]; } - renderSplit(groups) { + renderSplit(groups: JSX.Element[]) { const [first, ...rest] = groups; if (rest.length === 0) { return first; } - const direction = this.props.termGroup.direction.toLowerCase(); + const direction = this.props.termGroup.direction!.toLowerCase() as 'horizontal' | 'vertical'; return ( { + onTermRef = (uid: string, term: Term_ | null) => { this.term = term; this.props.ref_(uid, term); }; - renderTerm(uid) { + renderTerm(uid: string) { const session = this.props.sessions[uid]; const termRef = this.props.terms[uid]; const props = getTermProps(uid, this.props, { @@ -76,7 +85,6 @@ class TermGroup_ extends React.PureComponent { letterSpacing: this.props.letterSpacing, modifierKeys: this.props.modifierKeys, padding: this.props.padding, - url: session.url, cleared: session.cleared, search: session.search, cols: session.cols, @@ -89,14 +97,19 @@ class TermGroup_ extends React.PureComponent { onResize: this.bind(this.props.onResize, null, uid), onTitle: this.bind(this.props.onTitle, null, uid), onData: this.bind(this.props.onData, null, uid), - toggleSearch: this.bind(this.props.toggleSearch, null, uid), + onOpenSearch: this.bind(this.props.onOpenSearch, null, uid), + onCloseSearch: this.bind(this.props.onCloseSearch, null, uid), onContextMenu: this.bind(this.props.onContextMenu, null, uid), borderColor: this.props.borderColor, selectionColor: this.props.selectionColor, quickEdit: this.props.quickEdit, webGLRenderer: this.props.webGLRenderer, + webLinksActivationKey: this.props.webLinksActivationKey, macOptionSelectionMode: this.props.macOptionSelectionMode, disableLigatures: this.props.disableLigatures, + screenReaderMode: this.props.screenReaderMode, + windowsPty: this.props.windowsPty, + imageSupport: this.props.imageSupport, uid }); @@ -112,7 +125,7 @@ class TermGroup_ extends React.PureComponent { return this.renderTerm(termGroup.sessionUid); } - const groups = childGroups.map(child => { + const groups = childGroups.asMutable().map((child) => { const props = getTermGroupProps( child.uid, this.props.parentProps, @@ -126,19 +139,20 @@ class TermGroup_ extends React.PureComponent { } } -const TermGroup = connect( - (state, ownProps) => ({ - childGroups: ownProps.termGroup.children.map(uid => state.termGroups.termGroups[uid]) - }), - (dispatch, ownProps) => ({ - onTermGroupResize(splitSizes) { - dispatch(resizeTermGroup(ownProps.termGroup.uid, splitSizes)); - } - }), - null, - {forwardRef: true} -)(TermGroup_); +const mapStateToProps = (state: HyperState, ownProps: TermGroupOwnProps) => ({ + childGroups: ownProps.termGroup.children.map((uid) => state.termGroups.termGroups[uid]) +}); + +const mapDispatchToProps = (dispatch: HyperDispatch, ownProps: TermGroupOwnProps) => ({ + onTermGroupResize(splitSizes: number[]) { + dispatch(resizeTermGroup(ownProps.termGroup.uid, splitSizes)); + } +}); + +const TermGroup = connect(mapStateToProps, mapDispatchToProps, null, {forwardRef: true})(TermGroup_); const DecoratedTermGroup = decorate(TermGroup, 'TermGroup'); export default TermGroup; + +export type TermGroupConnectedProps = ReturnType & ReturnType; diff --git a/lib/components/term.js b/lib/components/term.tsx similarity index 50% rename from lib/components/term.js rename to lib/components/term.tsx index 0aa8844e..45c1464c 100644 --- a/lib/components/term.js +++ b/lib/components/term.tsx @@ -1,24 +1,40 @@ +import {clipboard, shell} from 'electron'; import React from 'react'; + +import Color from 'color'; +import isEqual from 'lodash/isEqual'; +import pickBy from 'lodash/pickBy'; import {Terminal} from 'xterm'; +import type {ITerminalOptions, IDisposable} from 'xterm'; +import {CanvasAddon} from 'xterm-addon-canvas'; import {FitAddon} from 'xterm-addon-fit'; -import {WebLinksAddon} from 'xterm-addon-web-links'; -import {SearchAddon} from 'xterm-addon-search'; -import {WebglAddon} from 'xterm-addon-webgl'; +import {ImageAddon} from 'xterm-addon-image'; import {LigaturesAddon} from 'xterm-addon-ligatures'; -import {clipboard} from 'electron'; -import * as Color from 'color'; +import {SearchAddon} from 'xterm-addon-search'; +import type {ISearchDecorationOptions} from 'xterm-addon-search'; +import {Unicode11Addon} from 'xterm-addon-unicode11'; +import {WebLinksAddon} from 'xterm-addon-web-links'; +import {WebglAddon} from 'xterm-addon-webgl'; + +import type {TermProps} from '../../typings/hyper'; import terms from '../terms'; import processClipboard from '../utils/paste'; -import SearchBox from './searchBox'; +import {decorate} from '../utils/plugins'; -const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(navigator.platform); +import _SearchBox from './searchBox'; + +import 'xterm/css/xterm.css'; + +const SearchBox = decorate(_SearchBox, 'SearchBox'); + +const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(navigator.platform) || process.platform === 'win32'; // map old hterm constants to xterm.js const CURSOR_STYLES = { BEAM: 'bar', UNDERLINE: 'underline', BLOCK: 'block' -}; +} as const; const isWebgl2Supported = (() => { let isSupported = window.WebGL2RenderingContext ? undefined : false; @@ -32,10 +48,10 @@ const isWebgl2Supported = (() => { }; })(); -const getTermOptions = props => { +const getTermOptions = (props: TermProps): ITerminalOptions => { // Set a background color only if it is opaque const needTransparency = Color(props.backgroundColor).alpha() < 1; - const backgroundColor = needTransparency ? 'transparent' : props.backgroundColor; + const backgroundColor = needTransparency ? 'rgba(0,0,0,0)' : props.backgroundColor; return { macOptionIsMeta: props.modifierKeys.altIsMeta, @@ -50,14 +66,14 @@ const getTermOptions = props => { letterSpacing: props.letterSpacing, allowTransparency: needTransparency, macOptionClickForcesSelection: props.macOptionSelectionMode === 'force', - bellStyle: props.bell === 'SOUND' ? 'sound' : 'none', windowsMode: isWindows, + ...(isWindows && props.windowsPty && {windowsPty: props.windowsPty}), theme: { foreground: props.foregroundColor, background: backgroundColor, cursor: props.cursorColor, cursorAccent: props.cursorAccentColor, - selection: props.selectionColor, + selectionBackground: props.selectionColor, black: props.colors.black, red: props.colors.red, green: props.colors.green, @@ -74,26 +90,73 @@ const getTermOptions = props => { brightMagenta: props.colors.lightMagenta, brightCyan: props.colors.lightCyan, brightWhite: props.colors.lightWhite - } + }, + screenReaderMode: props.screenReaderMode, + overviewRulerWidth: 20, + allowProposedApi: true }; }; -export default class Term extends React.PureComponent { - constructor(props) { +export default class Term extends React.PureComponent< + TermProps, + { + searchOptions: { + caseSensitive: boolean; + wholeWord: boolean; + regex: boolean; + }; + searchResults: + | { + resultIndex: number; + resultCount: number; + } + | undefined; + } +> { + termRef: HTMLElement | null; + termWrapperRef: HTMLElement | null; + termOptions: ITerminalOptions; + disposableListeners: IDisposable[]; + defaultBellSound: HTMLAudioElement | null; + bellSound: HTMLAudioElement | null; + fitAddon: FitAddon; + searchAddon: SearchAddon; + static rendererTypes: Record; + term!: Terminal; + resizeObserver!: ResizeObserver; + resizeTimeout!: NodeJS.Timeout; + searchDecorations: ISearchDecorationOptions; + state = { + searchOptions: { + caseSensitive: false, + wholeWord: false, + regex: false + }, + searchResults: undefined + }; + + constructor(props: TermProps) { super(props); props.ref_(props.uid, this); this.termRef = null; this.termWrapperRef = null; - this.termRect = null; this.termOptions = {}; this.disposableListeners = []; - this.termDefaultBellSound = null; + this.defaultBellSound = null; + this.bellSound = null; this.fitAddon = new FitAddon(); this.searchAddon = new SearchAddon(); + this.searchDecorations = { + activeMatchColorOverviewRuler: Color(this.props.cursorColor).hex(), + matchOverviewRuler: Color(this.props.borderColor).hex(), + activeMatchBackground: Color(this.props.cursorColor).hex(), + activeMatchBorder: Color(this.props.cursorColor).hex(), + matchBorder: Color(this.props.cursorColor).hex() + }; } // The main process shows this in the About dialog - static reportRenderer(uid, type) { + static reportRenderer(uid: string, type: string) { const rendererTypes = Term.rendererTypes || {}; if (rendererTypes[uid] !== type) { rendererTypes[uid] = type; @@ -107,51 +170,89 @@ export default class Term extends React.PureComponent { this.termOptions = getTermOptions(props); this.term = props.term || new Terminal(this.termOptions); - this.termDefaultBellSound = this.term.getOption('bellSound'); + this.defaultBellSound = new Audio( + // Source: https://freesound.org/people/altemark/sounds/45759/ + // This sound is released under the Creative Commons Attribution 3.0 Unported + // (CC BY 3.0) license. It was created by 'altemark'. No modifications have been + // made, apart from the conversion to base64. + 'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjMyLjEwNAAAAAAAAAAAAAAA//tQxAADB8AhSmxhIIEVCSiJrDCQBTcu3UrAIwUdkRgQbFAZC1CQEwTJ9mjRvBA4UOLD8nKVOWfh+UlK3z/177OXrfOdKl7pyn3Xf//WreyTRUoAWgBgkOAGbZHBgG1OF6zM82DWbZaUmMBptgQhGjsyYqc9ae9XFz280948NMBWInljyzsNRFLPWdnZGWrddDsjK1unuSrVN9jJsK8KuQtQCtMBjCEtImISdNKJOopIpBFpNSMbIHCSRpRR5iakjTiyzLhchUUBwCgyKiweBv/7UsQbg8isVNoMPMjAAAA0gAAABEVFGmgqK////9bP/6XCykxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq' + ); + this.setBellSound(props.bell, props.bellSound); // The parent element for the terminal is attached and removed manually so // that we can preserve it across mounts and unmounts of the component - this.termRef = props.term ? props.term.element.parentElement : document.createElement('div'); + this.termRef = props.term ? props.term.element!.parentElement! : document.createElement('div'); this.termRef.className = 'term_fit term_term'; - this.termWrapperRef.appendChild(this.termRef); + this.termWrapperRef?.appendChild(this.termRef); if (!props.term) { - let needTransparency = Color(props.backgroundColor).alpha() < 1; + const needTransparency = Color(props.backgroundColor).alpha() < 1; let useWebGL = false; if (props.webGLRenderer) { if (needTransparency) { - // eslint-disable-next-line no-console console.warn( 'WebGL Renderer has been disabled since it does not support transparent backgrounds yet. ' + 'Falling back to canvas-based rendering.' ); } else if (!isWebgl2Supported()) { - // eslint-disable-next-line no-console console.warn('WebGL2 is not supported on your machine. Falling back to canvas-based rendering.'); } else { // Experimental WebGL renderer needs some more glue-code to make it work on Hyper. // If you're working on enabling back WebGL, you will also need to look into `xterm-addon-ligatures` support for that renderer. - // useWebGL = true; + useWebGL = true; } } Term.reportRenderer(props.uid, useWebGL ? 'WebGL' : 'Canvas'); + const shallActivateWebLink = (event: MouseEvent): boolean => { + if (!event) return false; + return props.webLinksActivationKey ? event[`${props.webLinksActivationKey}Key`] : true; + }; + + // eslint-disable-next-line @typescript-eslint/unbound-method this.term.attachCustomKeyEventHandler(this.keyboardHandler); this.term.loadAddon(this.fitAddon); this.term.loadAddon(this.searchAddon); - this.term.loadAddon(new WebLinksAddon()); + this.term.loadAddon( + new WebLinksAddon((event, uri) => { + if (shallActivateWebLink(event)) void shell.openExternal(uri); + }) + ); this.term.open(this.termRef); + if (useWebGL) { - this.term.loadAddon(new WebglAddon()); + const webglAddon = new WebglAddon(); + this.term.loadAddon(webglAddon); + webglAddon.onContextLoss(() => { + console.warn('WebGL context lost. Falling back to canvas-based rendering.'); + webglAddon.dispose(); + this.term.loadAddon(new CanvasAddon()); + }); + } else { + this.term.loadAddon(new CanvasAddon()); } - if (props.disableLigatures !== true) { + + if (props.disableLigatures !== true && !useWebGL) { this.term.loadAddon(new LigaturesAddon()); } + + this.term.loadAddon(new Unicode11Addon()); + this.term.unicode.activeVersion = '11'; + + if (props.imageSupport) { + this.term.loadAddon(new ImageAddon()); + } } else { // get the cached plugins - this.fitAddon = props.fitAddon; - this.searchAddon = props.searchAddon; + this.fitAddon = props.fitAddon!; + this.searchAddon = props.searchAddon!; + } + + try { + this.term.element!.style.padding = props.padding; + } catch (error) { + console.log(error); } this.fitAddon.fit(); @@ -165,9 +266,9 @@ export default class Term extends React.PureComponent { } if (props.onActive) { - this.term.textarea.addEventListener('focus', props.onActive); + this.term.textarea?.addEventListener('focus', props.onActive); this.disposableListeners.push({ - dispose: () => this.term.textarea.removeEventListener('focus', this.props.onActive) + dispose: () => this.term.textarea?.removeEventListener('focus', this.props.onActive) }); } @@ -175,6 +276,10 @@ export default class Term extends React.PureComponent { this.disposableListeners.push(this.term.onData(props.onData)); } + this.term.onBell(() => { + this.ringBell(); + }); + if (props.onResize) { this.disposableListeners.push( this.term.onResize(({cols, rows}) => { @@ -190,18 +295,27 @@ export default class Term extends React.PureComponent { this.disposableListeners.push( this.term.onCursorMove(() => { const cursorFrame = { - x: this.term.buffer.cursorX * this.term._core._renderService.dimensions.actualCellWidth, - y: this.term.buffer.cursorY * this.term._core._renderService.dimensions.actualCellHeight, - width: this.term._core._renderService.dimensions.actualCellWidth, - height: this.term._core._renderService.dimensions.actualCellHeight, - col: this.term.buffer.cursorX, - row: this.term.buffer.cursorY + x: this.term.buffer.active.cursorX * (this.term as any)._core._renderService.dimensions.actualCellWidth, + y: this.term.buffer.active.cursorY * (this.term as any)._core._renderService.dimensions.actualCellHeight, + width: (this.term as any)._core._renderService.dimensions.actualCellWidth, + height: (this.term as any)._core._renderService.dimensions.actualCellHeight, + col: this.term.buffer.active.cursorX, + row: this.term.buffer.active.cursorY }; - props.onCursorMove(cursorFrame); + props.onCursorMove?.(cursorFrame); }) ); } + this.disposableListeners.push( + this.searchAddon.onDidChangeResults((results) => { + this.setState((state) => ({ + ...state, + searchResults: results + })); + }) + ); + window.addEventListener('paste', this.onWindowPaste, { capture: true }); @@ -210,7 +324,6 @@ export default class Term extends React.PureComponent { } getTermDocument() { - // eslint-disable-next-line no-console console.warn( 'The underlying terminal engine of Hyper no longer ' + 'uses iframes with individual `document` objects for each ' + @@ -223,18 +336,18 @@ export default class Term extends React.PureComponent { // intercepting paste event for any necessary processing of // clipboard data, if result is falsy, paste event continues - onWindowPaste = e => { + onWindowPaste = (e: Event) => { if (!this.props.isTermActive) return; const processed = processClipboard(); if (processed) { e.preventDefault(); e.stopPropagation(); - this.term._core.handler(processed); + this.term.paste(processed); } }; - onMouseUp = e => { + onMouseUp = (e: React.MouseEvent) => { if (this.props.quickEdit && e.button === 2) { if (this.term.hasSelection()) { clipboard.writeText(this.term.getSelection()); @@ -247,13 +360,13 @@ export default class Term extends React.PureComponent { } }; - write(data) { + write(data: string | Uint8Array) { this.term.write(data); } - focus() { + focus = () => { this.term.focus(); - } + }; clear() { this.term.clear(); @@ -263,23 +376,32 @@ export default class Term extends React.PureComponent { this.term.reset(); } - search = searchTerm => { - this.searchAddon.findNext(searchTerm); + searchNext = (searchTerm: string) => { + this.searchAddon.findNext(searchTerm, { + ...this.state.searchOptions, + decorations: this.searchDecorations + }); }; - searchNext = searchTerm => { - this.searchAddon.findNext(searchTerm); - }; - - searchPrevious = searchTerm => { - this.searchAddon.findPrevious(searchTerm); + searchPrevious = (searchTerm: string) => { + this.searchAddon.findPrevious(searchTerm, { + ...this.state.searchOptions, + decorations: this.searchDecorations + }); }; closeSearchBox = () => { - this.props.toggleSearch(); + this.props.onCloseSearch(); + this.searchAddon.clearDecorations(); + this.searchAddon.clearActiveDecoration(); + this.setState((state) => ({ + ...state, + searchResults: undefined + })); + this.term.focus(); }; - resize(cols, rows) { + resize(cols: number, rows: number) { this.term.resize(cols, rows); } @@ -294,54 +416,52 @@ export default class Term extends React.PureComponent { this.fitAddon.fit(); } - keyboardHandler(e) { + keyboardHandler(e: any) { // Has Mousetrap flagged this event as a command? return !e.catched; } - componentDidUpdate(prevProps) { + setBellSound(bell: 'SOUND' | false, sound: string | null) { + if (bell && bell.toUpperCase() === 'SOUND') { + this.bellSound = sound ? new Audio(sound) : this.defaultBellSound; + } else { + this.bellSound = null; + } + } + + ringBell() { + void this.bellSound?.play(); + } + + componentDidUpdate(prevProps: TermProps) { if (!prevProps.cleared && this.props.cleared) { this.clear(); } const nextTermOptions = getTermOptions(this.props); - // Use bellSound in nextProps if it exists - // otherwise use the default sound found in xterm. - nextTermOptions.bellSound = this.props.bellSound || this.termDefaultBellSound; + if (prevProps.bell !== this.props.bell || prevProps.bellSound !== this.props.bellSound) { + this.setBellSound(this.props.bell, this.props.bellSound); + } - if (!prevProps.search && this.props.search) { - this.search(); + if (prevProps.search && !this.props.search) { + this.closeSearchBox(); } // Update only options that have changed. - Object.keys(nextTermOptions) - .filter(option => option !== 'theme' && nextTermOptions[option] !== this.termOptions[option]) - .forEach(option => { - try { - this.term.setOption(option, nextTermOptions[option]); - } catch (e) { - if (/The webgl renderer only works with the webgl char atlas/i.test(e.message)) { - // Ignore this because the char atlas will also be changed - } else { - throw e; - } - } - }); - - // Do we need to update theme? - const shouldUpdateTheme = - !this.termOptions.theme || - nextTermOptions.rendererType !== this.termOptions.rendererType || - Object.keys(nextTermOptions.theme).some( - option => nextTermOptions.theme[option] !== this.termOptions.theme[option] - ); - if (shouldUpdateTheme) { - this.term.setOption('theme', nextTermOptions.theme); - } + this.term.options = pickBy( + nextTermOptions, + (value, key) => !isEqual(this.termOptions[key as keyof ITerminalOptions], value) + ); this.termOptions = nextTermOptions; + try { + this.term.element!.style.padding = this.props.padding; + } catch (error) { + console.log(error); + } + if ( this.props.fontSize !== prevProps.fontSize || this.props.fontFamily !== prevProps.fontFamily || @@ -353,70 +473,11 @@ export default class Term extends React.PureComponent { } if (prevProps.rows !== this.props.rows || prevProps.cols !== this.props.cols) { - this.resize(this.props.cols, this.props.rows); + this.resize(this.props.cols!, this.props.rows!); } } - //TODO: Remove usage of legacy and soon deprecated lifecycle methods - UNSAFE_componentWillReceiveProps(nextProps) { - if (!this.props.cleared && nextProps.cleared) { - this.clear(); - } - - const nextTermOptions = getTermOptions(nextProps); - - // Use bellSound in nextProps if it exists - // otherwise use the default sound found in xterm. - nextTermOptions.bellSound = nextProps.bellSound || this.termDefaultBellSound; - - if (!this.props.search && nextProps.search) { - this.search(); - } - - // Update only options that have changed. - Object.keys(nextTermOptions) - .filter(option => option !== 'theme' && nextTermOptions[option] !== this.termOptions[option]) - .forEach(option => { - try { - this.term.setOption(option, nextTermOptions[option]); - } catch (e) { - if (/The webgl renderer only works with the webgl char atlas/i.test(e.message)) { - // Ignore this because the char atlas will also be changed - } else { - throw e; - } - } - }); - - // Do we need to update theme? - const shouldUpdateTheme = - !this.termOptions.theme || - nextTermOptions.rendererType !== this.termOptions.rendererType || - Object.keys(nextTermOptions.theme).some( - option => nextTermOptions.theme[option] !== this.termOptions.theme[option] - ); - if (shouldUpdateTheme) { - this.term.setOption('theme', nextTermOptions.theme); - } - - this.termOptions = nextTermOptions; - - if ( - this.props.fontSize !== nextProps.fontSize || - this.props.fontFamily !== nextProps.fontFamily || - this.props.lineHeight !== nextProps.lineHeight || - this.props.letterSpacing !== nextProps.letterSpacing - ) { - // resize to fit the container - this.fitResize(); - } - - if (nextProps.rows !== this.props.rows || nextProps.cols !== this.props.cols) { - this.resize(nextProps.cols, nextProps.rows); - } - } - - onTermWrapperRef = component => { + onTermWrapperRef = (component: HTMLElement | null) => { this.termWrapperRef = component; if (component) { @@ -434,14 +495,14 @@ export default class Term extends React.PureComponent { componentWillUnmount() { terms[this.props.uid] = null; - this.termWrapperRef.removeChild(this.termRef); + this.termWrapperRef?.removeChild(this.termRef!); this.props.ref_(this.props.uid, null); // to clean up the terminal, we remove the listeners // instead of invoking `destroy`, since it will make the // term insta un-attachable in the future (which we need // to do in case of splitting, see `componentDidMount` - this.disposableListeners.forEach(handler => handler.dispose()); + this.disposableListeners.forEach((handler) => handler.dispose()); this.disposableListeners = []; window.removeEventListener('paste', this.onWindowPaste, { @@ -451,24 +512,44 @@ export default class Term extends React.PureComponent { render() { return ( -
    +
    {this.props.customChildrenBefore}
    {this.props.customChildren} {this.props.search ? ( + this.setState({ + ...this.state, + searchOptions: {...this.state.searchOptions, caseSensitive: !this.state.searchOptions.caseSensitive} + }) + } + toggleWholeWord={() => + this.setState({ + ...this.state, + searchOptions: {...this.state.searchOptions, wholeWord: !this.state.searchOptions.wholeWord} + }) + } + toggleRegex={() => + this.setState({ + ...this.state, + searchOptions: {...this.state.searchOptions, regex: !this.state.searchOptions.regex} + }) + } + selectionColor={this.props.selectionColor} + backgroundColor={this.props.backgroundColor} + foregroundColor={this.props.foregroundColor} + borderColor={this.props.borderColor} + font={this.props.uiFontFamily} /> - ) : ( - '' - )} + ) : null} - - {/* - Add custom CSS to Hyper. - We add a scope to the customCSS so that it can get around the weighting applied by styled-jsx - */} - + + {/* + Add custom CSS to Hyper. + We add a scope to the customCSS so that it can get around the weighting applied by styled-jsx + */} +