mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-13 04:28:41 -09:00
Compare commits
No commits in common. "1.0.6" and "canary" have entirely different histories.
183 changed files with 324400 additions and 2235 deletions
12
.editorconfig
Normal file
12
.editorconfig
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
14
.eslintignore
Normal file
14
.eslintignore
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
build
|
||||||
|
app/renderer
|
||||||
|
app/static
|
||||||
|
app/bin
|
||||||
|
app/dist
|
||||||
|
app/node_modules
|
||||||
|
app/typings
|
||||||
|
assets
|
||||||
|
website
|
||||||
|
bin
|
||||||
|
dist
|
||||||
|
target
|
||||||
|
cache
|
||||||
|
schema.json
|
||||||
161
.eslintrc.json
Normal file
161
.eslintrc.json
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"react",
|
||||||
|
"prettier",
|
||||||
|
"@typescript-eslint",
|
||||||
|
"eslint-comments",
|
||||||
|
"lodash",
|
||||||
|
"import"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"plugin:eslint-comments/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true,
|
||||||
|
"impliedStrict": true,
|
||||||
|
"experimentalObjectRestSpread": true
|
||||||
|
},
|
||||||
|
"allowImportExportEverywhere": true,
|
||||||
|
"project": [
|
||||||
|
"./tsconfig.eslint.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"es6": true,
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
},
|
||||||
|
"import/resolver": {
|
||||||
|
"typescript": {}
|
||||||
|
},
|
||||||
|
"import/internal-regex": "^(electron|react)$"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"func-names": [
|
||||||
|
"error",
|
||||||
|
"as-needed"
|
||||||
|
],
|
||||||
|
"no-shadow": "error",
|
||||||
|
"no-extra-semi": 0,
|
||||||
|
"react/prop-types": 0,
|
||||||
|
"react/react-in-jsx-scope": 0,
|
||||||
|
"react/no-unescaped-entities": 0,
|
||||||
|
"react/jsx-no-target-blank": 0,
|
||||||
|
"react/no-string-refs": 0,
|
||||||
|
"prettier/prettier": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"bracketSpacing": false,
|
||||||
|
"semi": true,
|
||||||
|
"useTabs": false,
|
||||||
|
"bracketSameLine": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"eslint-comments/no-unused-disable": "error",
|
||||||
|
"react/no-unknown-property":[
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"ignore": [
|
||||||
|
"jsx",
|
||||||
|
"global"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**.ts",
|
||||||
|
"**.tsx"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"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/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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
5
.gitattributes
vendored
Normal file
5
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
* text=auto
|
||||||
|
*.js text eol=lf
|
||||||
|
*.ts text eol=lf
|
||||||
|
*.tsx text eol=lf
|
||||||
|
bin/* linguist-vendored
|
||||||
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help Hyper improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Hi there! Thank you for discovering and submitting an issue.
|
||||||
|
|
||||||
|
Before you submit this; let's make sure of a few things.
|
||||||
|
Please make sure the following boxes are ticked if they are correct.
|
||||||
|
If not, please try and fulfill these first.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Checked checkbox should look like this: [x] -->
|
||||||
|
- [ ] 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
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Once those are done, if you're able to fill in the following list with your information,
|
||||||
|
it'd be very helpful to whoever handles the issue.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- **OS version and name**: <!-- Replace with version + name -->
|
||||||
|
- **Hyper.app version**: <!-- Replace with version -->
|
||||||
|
- **Link of a [Gist](https://gist.github.com/) with the contents of your hyper.json**: <!-- Gist Link Here -->
|
||||||
|
- **Relevant information from devtools** _(CMD+ALT+I on macOS, CTRL+SHIFT+I elsewhere)_: <!-- Replace with info if applicable, or N/A -->
|
||||||
|
- **The issue is reproducible in vanilla Hyper.app**: <!-- Replace with info if applicable, or `Is Vanilla`. (Vanilla means Hyper.app without any add-ons or extras. Straight out of the box.) -->
|
||||||
|
|
||||||
|
## Issue
|
||||||
|
<!-- Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -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.
|
||||||
137
.github/actions/build-linux-arm/action.yml
vendored
Normal file
137
.github/actions/build-linux-arm/action.yml
vendored
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
name: 'Build Linux ARM'
|
||||||
|
description: 'Cross-compiles Hyper app for ARMv7l and ARM64 using arm-runner'
|
||||||
|
inputs:
|
||||||
|
node-version:
|
||||||
|
description: 'Node.js version to use'
|
||||||
|
required: true
|
||||||
|
matrix-name:
|
||||||
|
description: 'Matrix name (arch)'
|
||||||
|
required: true
|
||||||
|
matrix-cpu:
|
||||||
|
description: 'CPU architecture'
|
||||||
|
required: true
|
||||||
|
matrix-image:
|
||||||
|
description: 'Base OS image for ARM emulation'
|
||||||
|
required: true
|
||||||
|
upload-artifact:
|
||||||
|
description: 'Whether to upload artifacts'
|
||||||
|
required: false
|
||||||
|
default: 'true'
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ inputs.node-version }}
|
||||||
|
|
||||||
|
- name: Set up Python 3.10
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
|
||||||
|
- name: Fix node-gyp and Python
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
python3 -m pip install packaging setuptools
|
||||||
|
|
||||||
|
- name: Get yarn cache directory path
|
||||||
|
shell: bash
|
||||||
|
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: Delete old electron headers
|
||||||
|
# These can sometimes be present on a freshly-provisioned github runner, so remove them
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
rm -rf ~/.electron-gyp || true
|
||||||
|
|
||||||
|
- name: Install dependencies and tools
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
yarn install
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install libarchive-tools
|
||||||
|
|
||||||
|
- name: Rebuild node-pty and all native modules for ARM
|
||||||
|
uses: pguyot/arm-runner-action@v2.6.5
|
||||||
|
with:
|
||||||
|
image_additional_mb: 2000
|
||||||
|
base_image: ${{ inputs.matrix-image }}
|
||||||
|
cpu: ${{ inputs.matrix-cpu }}
|
||||||
|
shell: bash
|
||||||
|
copy_artifact_path: target
|
||||||
|
copy_artifact_dest: target
|
||||||
|
commands: |
|
||||||
|
# Install Python distutils in the ARM container
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y python3-pip python3-setuptools python3-dev
|
||||||
|
pip3 install setuptools --break-system-packages || pip3 install setuptools
|
||||||
|
|
||||||
|
cd target
|
||||||
|
# TODO upgrade node to 20.11.0 to match NODE_VERSION
|
||||||
|
wget https://nodejs.org/dist/v18.16.0/node-v18.16.0-linux-${{ inputs.matrix-name }}.tar.xz
|
||||||
|
tar -xJf node-v18.16.0-linux-${{ inputs.matrix-name }}.tar.xz
|
||||||
|
sudo cp node-v18.16.0-linux-${{ inputs.matrix-name }}/* /usr/local/ -R
|
||||||
|
rm node-v18.16.0-linux-${{ inputs.matrix-name }}.tar.xz
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
npm run rebuild-node-pty
|
||||||
|
|
||||||
|
cd target
|
||||||
|
npx electron-rebuild
|
||||||
|
|
||||||
|
- name: Compile
|
||||||
|
shell: bash
|
||||||
|
run: yarn run build
|
||||||
|
|
||||||
|
- name: Chown rebuilt node_modules
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
sudo chown -R $USER:$USER target/node_modules
|
||||||
|
|
||||||
|
- name: Prepare v8 snapshot (only armv7l)
|
||||||
|
if: ${{ inputs.matrix-name == 'armv7l' }}
|
||||||
|
shell: bash
|
||||||
|
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 final Electron App for ARM
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
yarn run electron-builder -p never -l deb rpm AppImage pacman --${{ inputs.matrix-name }} -c electron-builder-linux-ci.json
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ env.GH_TOKEN }}
|
||||||
|
|
||||||
|
- name: Archive Build Artifacts
|
||||||
|
if: ${{ inputs.upload-artifact == 'true' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: hyper-linux-${{ inputs.matrix-name }}
|
||||||
|
path: |
|
||||||
|
dist/*.snap
|
||||||
|
dist/*.AppImage
|
||||||
|
dist/*.deb
|
||||||
|
dist/*.rpm
|
||||||
|
dist/*.pacman
|
||||||
|
|
||||||
|
# - name: Run E2E Tests on Linux
|
||||||
|
# if: runner.os == 'Linux'
|
||||||
|
# uses: GabrielBB/xvfb-action@v1.7
|
||||||
|
# with:
|
||||||
|
# run: |
|
||||||
|
# yarn run test
|
||||||
|
#
|
||||||
172
.github/actions/build/action.yml
vendored
Normal file
172
.github/actions/build/action.yml
vendored
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
name: 'Build'
|
||||||
|
description: 'Builds, tests, and packages the app for release'
|
||||||
|
inputs:
|
||||||
|
node-version:
|
||||||
|
description: 'Node.js version to use'
|
||||||
|
required: true
|
||||||
|
matrix-name:
|
||||||
|
description: 'Matrix Name'
|
||||||
|
required: true
|
||||||
|
matrix-os:
|
||||||
|
description: 'Matrix OS'
|
||||||
|
required: true
|
||||||
|
upload-artifact:
|
||||||
|
description: 'Whether to upload artifacts'
|
||||||
|
required: false
|
||||||
|
default: 'true'
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
|
- name: Set up Python 3.10
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
|
||||||
|
- name: Fix node-gyp and Python
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [[ "$RUNNER_OS" == "macOS" ]]; then
|
||||||
|
brew install python-setuptools python-packaging
|
||||||
|
else
|
||||||
|
python3 -m pip install $EXTRA_ARGS packaging setuptools
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Get yarn cache directory path
|
||||||
|
shell: bash
|
||||||
|
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: Delete old electron headers
|
||||||
|
# These can sometimes be present on a freshly-provisioned github runner, so remove them
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
rm -rf ~/.electron-gyp || true
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
shell: bash
|
||||||
|
run: yarn install
|
||||||
|
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
|
||||||
|
shell: bash
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install libarchive-tools
|
||||||
|
|
||||||
|
- name: Rebuild native modules for Electron
|
||||||
|
shell: bash
|
||||||
|
run: npx electron-rebuild
|
||||||
|
|
||||||
|
- name: Lint and Run Unit Tests
|
||||||
|
shell: bash
|
||||||
|
run: yarn run test
|
||||||
|
|
||||||
|
- name: Getting Build Icon
|
||||||
|
shell: bash
|
||||||
|
if: github.ref == 'refs/heads/canary' || github.base_ref == 'canary'
|
||||||
|
run: |
|
||||||
|
cp build/canary.ico build/icon.ico
|
||||||
|
cp build/canary.icns build/icon.icns
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ -z "$CSC_LINK" ] ; then unset CSC_LINK ; fi
|
||||||
|
if [ -z "$CSC_KEY_PASSWORD" ] ; then unset CSC_KEY_PASSWORD ; fi
|
||||||
|
if [ -z "$WIN_CSC_LINK" ] ; then unset WIN_CSC_LINK ; fi
|
||||||
|
if [ -z "$WIN_CSC_KEY_PASSWORD" ] ; then unset WIN_CSC_KEY_PASSWORD ; fi
|
||||||
|
if [ -z "$APPLE_ID" ] ; then unset APPLE_ID ; fi
|
||||||
|
if [ -z "$APPLE_APP_SPECIFIC_PASSWORD" ] ; then unset APPLE_APP_SPECIFIC_PASSWORD ; fi
|
||||||
|
|
||||||
|
yarn run dist
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ env.GH_TOKEN }}
|
||||||
|
CSC_LINK: ${{ env.MAC_CERT_P12_BASE64 }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ env.MAC_CERT_P12_PASSWORD }}
|
||||||
|
WIN_CSC_LINK: ${{ env.WIN_CERT_P12_BASE64 }}
|
||||||
|
WIN_CSC_KEY_PASSWORD: ${{ env.WIN_CERT_P12_PASSWORD }}
|
||||||
|
APPLE_ID: ${{ env.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ env.APPLE_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Archive Build Artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: hyper-${{ runner.os }}-${{ inputs.matrix-name }}
|
||||||
|
path: |
|
||||||
|
dist/*.dmg
|
||||||
|
dist/*.snap
|
||||||
|
dist/*.AppImage
|
||||||
|
dist/*.deb
|
||||||
|
dist/*.pacman
|
||||||
|
dist/*.exe
|
||||||
|
|
||||||
|
- name: Run E2E Tests
|
||||||
|
shell: bash
|
||||||
|
if: runner.os != 'Linux'
|
||||||
|
run: yarn run test:e2e --verbose
|
||||||
|
|
||||||
|
- name: Run E2E Tests on Linux
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
uses: GabrielBB/xvfb-action@v1.7
|
||||||
|
with:
|
||||||
|
run: |
|
||||||
|
yarn run test:e2e
|
||||||
|
env:
|
||||||
|
SHELL: /bin/bash
|
||||||
|
DEBUG: "pw:browser*"
|
||||||
|
|
||||||
|
- name: Archive E2E test screenshot
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: e2e-${{ inputs.matrix-os }}-${{ strategy.job-index }}
|
||||||
|
path: dist/tmp/*.png
|
||||||
|
|
||||||
|
- name: Save the pr number in an artifact
|
||||||
|
shell: bash
|
||||||
|
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@v4
|
||||||
|
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') }}
|
||||||
|
|
||||||
|
# - name: Run E2E Tests (non-Linux)
|
||||||
|
# if: runner.os != 'Linux'
|
||||||
|
# shell: bash
|
||||||
|
# run: yarn run test:e2e --verbose
|
||||||
|
#
|
||||||
|
# - name: Run E2E Tests on Linux
|
||||||
|
# if: runner.os == 'Linux'
|
||||||
|
# uses: GabrielBB/xvfb-action@v1.7
|
||||||
|
# with:
|
||||||
|
# run: |
|
||||||
|
# yarn run test
|
||||||
|
#
|
||||||
39
.github/dependabot.yml
vendored
Normal file
39
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
time: '11:00'
|
||||||
|
target-branch: canary
|
||||||
|
versioning-strategy: increase
|
||||||
|
commit-message:
|
||||||
|
prefix: "chore(deps-dev):"
|
||||||
|
groups:
|
||||||
|
minorAndPatch:
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
open-pull-requests-limit: 100
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: "/app"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
time: '11:00'
|
||||||
|
target-branch: canary
|
||||||
|
versioning-strategy: increase
|
||||||
|
commit-message:
|
||||||
|
prefix: "chore(deps):"
|
||||||
|
groups:
|
||||||
|
minorAndPatch:
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
open-pull-requests-limit: 100
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
time: '11:00'
|
||||||
|
open-pull-requests-limit: 30
|
||||||
|
target-branch: canary
|
||||||
1
.github/pull_request_template.md
vendored
Normal file
1
.github/pull_request_template.md
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<!-- Please check `Allow edits from maintainers`. Thanks! -->
|
||||||
154
.github/workflows/ci.yml
vendored
Normal file
154
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
name: Node CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
workflow_dispatch:
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 20.11.0
|
||||||
|
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 }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-ubuntu:
|
||||||
|
name: Build Ubuntu
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: build-ubuntu-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
uses: ./.github/actions/build
|
||||||
|
env:
|
||||||
|
NODE_VERSION: ${{ env.NODE_VERSION }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
matrix-os: ubuntu-latest
|
||||||
|
matrix-name: ubuntu
|
||||||
|
upload-artifact: false
|
||||||
|
|
||||||
|
build-macos:
|
||||||
|
name: Build macOS
|
||||||
|
runs-on: macos-latest
|
||||||
|
concurrency:
|
||||||
|
group: build-macos-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
uses: ./.github/actions/build
|
||||||
|
env:
|
||||||
|
NODE_VERSION: ${{ env.NODE_VERSION }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
matrix-os: macos-latest
|
||||||
|
matrix-name: macos
|
||||||
|
upload-artifact: false
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
name: Build Windows
|
||||||
|
runs-on: windows-latest
|
||||||
|
concurrency:
|
||||||
|
group: build-win-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
uses: ./.github/actions/build
|
||||||
|
env:
|
||||||
|
NODE_VERSION: ${{ env.NODE_VERSION }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
matrix-os: windows-latest
|
||||||
|
matrix-name: win
|
||||||
|
upload-artifact: false
|
||||||
|
|
||||||
|
# ARM Linux:
|
||||||
|
|
||||||
|
build-linux-armv7l:
|
||||||
|
name: Build Linux ARMv7l
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: build-linux-armv7l-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU and binfmt
|
||||||
|
run: |
|
||||||
|
# Install qemu-user-static
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y qemu-user-static binfmt-support python3-setuptools
|
||||||
|
|
||||||
|
# Install distutils for node-gyp
|
||||||
|
sudo apt-get install -y python3-distutils || sudo apt-get install -y python3-dev python-is-python3
|
||||||
|
|
||||||
|
|
||||||
|
# Download and install binfmt configurations
|
||||||
|
wget https://github.com/qemu/qemu/raw/master/scripts/qemu-binfmt-conf.sh
|
||||||
|
chmod +x qemu-binfmt-conf.sh
|
||||||
|
sudo ./qemu-binfmt-conf.sh --qemu-path /usr/bin --qemu-suffix -static --debian
|
||||||
|
|
||||||
|
# Import the binfmt configurations
|
||||||
|
sudo update-binfmts --import qemu-arm
|
||||||
|
sudo update-binfmts --import qemu-aarch64
|
||||||
|
|
||||||
|
# Verify they exist
|
||||||
|
ls -la /proc/sys/fs/binfmt_misc/qemu-arm* || true
|
||||||
|
|
||||||
|
- name: Build ARMv7l
|
||||||
|
uses: ./.github/actions/build-linux-arm
|
||||||
|
env:
|
||||||
|
NODE_VERSION: ${{ env.NODE_VERSION }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
matrix-name: armv7l
|
||||||
|
matrix-cpu: cortex-a8
|
||||||
|
matrix-image: raspios_lite:latest
|
||||||
|
upload-artifact: false
|
||||||
|
|
||||||
|
build-linux-arm64:
|
||||||
|
name: Build Linux ARM64
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: build-linux-arm64-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build ARM64
|
||||||
|
uses: ./.github/actions/build-linux-arm
|
||||||
|
env:
|
||||||
|
NODE_VERSION: ${{ env.NODE_VERSION }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
matrix-name: arm64
|
||||||
|
matrix-cpu: cortex-a53
|
||||||
|
matrix-image: raspios_lite_arm64:latest
|
||||||
|
upload-artifact: false
|
||||||
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
|
@ -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
|
||||||
63
.github/workflows/e2e_comment.yml
vendored
Normal file
63
.github/workflows/e2e_comment.yml
vendored
Normal file
|
|
@ -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@v6
|
||||||
|
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@v6
|
||||||
|
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 }}
|
||||||
42
.github/workflows/macos-build.yml
vendored
42
.github/workflows/macos-build.yml
vendored
|
|
@ -1,42 +0,0 @@
|
||||||
name: electron-drag-click
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: macos-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Use Node.js 20.x
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20.x
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install Xcode Command Line Tools
|
|
||||||
run: |
|
|
||||||
xcode-select --install || true
|
|
||||||
|
|
||||||
- name: Install setuptools
|
|
||||||
run: brew install python-setuptools
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Verify native module build
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: native-module-20.x
|
|
||||||
path: |
|
|
||||||
build/
|
|
||||||
*.node
|
|
||||||
retention-days: 7
|
|
||||||
194
.github/workflows/release.yml
vendored
Normal file
194
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
name: Create Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: release-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 20.11.0
|
||||||
|
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 }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-ubuntu:
|
||||||
|
name: Build Ubuntu
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
uses: ./.github/actions/build
|
||||||
|
env:
|
||||||
|
NODE_VERSION: ${{ env.NODE_VERSION }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
matrix-os: ubuntu-latest
|
||||||
|
matrix-name: ubuntu
|
||||||
|
upload-artifact: true
|
||||||
|
|
||||||
|
build-macos:
|
||||||
|
name: Build macOS
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
uses: ./.github/actions/build
|
||||||
|
env:
|
||||||
|
NODE_VERSION: ${{ env.NODE_VERSION }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
matrix-os: macos-latest
|
||||||
|
matrix-name: macos
|
||||||
|
upload-artifact: true
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
name: Build Windows
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
uses: ./.github/actions/build
|
||||||
|
env:
|
||||||
|
NODE_VERSION: ${{ env.NODE_VERSION }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
matrix-os: windows-latest
|
||||||
|
matrix-name: win
|
||||||
|
upload-artifact: true
|
||||||
|
|
||||||
|
# ARM Linux:
|
||||||
|
|
||||||
|
build-linux-armv7l:
|
||||||
|
name: Build Linux ARMv7l
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU and binfmt
|
||||||
|
run: |
|
||||||
|
# Install qemu-user-static
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y qemu-user-static binfmt-support python3-setuptools
|
||||||
|
|
||||||
|
# Install distutils for node-gyp
|
||||||
|
sudo apt-get install -y python3-distutils || sudo apt-get install -y python3-dev python-is-python3
|
||||||
|
|
||||||
|
# Download and install binfmt configurations
|
||||||
|
wget https://github.com/qemu/qemu/raw/master/scripts/qemu-binfmt-conf.sh
|
||||||
|
chmod +x qemu-binfmt-conf.sh
|
||||||
|
sudo ./qemu-binfmt-conf.sh --qemu-path /usr/bin --qemu-suffix -static --debian
|
||||||
|
|
||||||
|
# Import the binfmt configurations
|
||||||
|
sudo update-binfmts --import qemu-arm
|
||||||
|
sudo update-binfmts --import qemu-aarch64
|
||||||
|
|
||||||
|
# Verify they exist
|
||||||
|
ls -la /proc/sys/fs/binfmt_misc/qemu-arm* || true
|
||||||
|
|
||||||
|
- name: Build ARMv7l
|
||||||
|
uses: ./.github/actions/build-linux-arm
|
||||||
|
env:
|
||||||
|
NODE_VERSION: ${{ env.NODE_VERSION }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
matrix-name: armv7l
|
||||||
|
matrix-cpu: cortex-a8
|
||||||
|
matrix-image: raspios_lite:latest
|
||||||
|
upload-artifact: true
|
||||||
|
|
||||||
|
build-linux-arm64:
|
||||||
|
name: Build Linux ARM64
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build ARM64
|
||||||
|
uses: ./.github/actions/build-linux-arm
|
||||||
|
env:
|
||||||
|
NODE_VERSION: ${{ env.NODE_VERSION }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
matrix-name: arm64
|
||||||
|
matrix-cpu: cortex-a53
|
||||||
|
matrix-image: raspios_lite_arm64:latest
|
||||||
|
upload-artifact: true
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
upload-release:
|
||||||
|
name: Upload and Create Release
|
||||||
|
needs:
|
||||||
|
- build-ubuntu
|
||||||
|
- build-macos
|
||||||
|
- build-windows
|
||||||
|
- build-linux-armv7l
|
||||||
|
- build-linux-arm64
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: ./artifacts
|
||||||
|
|
||||||
|
- name: List downloaded artifacts
|
||||||
|
run: ls -R ./artifacts
|
||||||
|
|
||||||
|
- name: Ensure non-empty artifacts
|
||||||
|
run: |
|
||||||
|
echo "Checking for zero-byte files in artifacts/..."
|
||||||
|
find artifacts/ -type f -empty -print -delete
|
||||||
|
echo "Cleaned up any zero-byte files."
|
||||||
|
echo "Remaining files:"
|
||||||
|
ls -R artifacts/
|
||||||
|
|
||||||
|
- name: Dump release assets for debugging
|
||||||
|
if: always()
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GitHub CLI needs this
|
||||||
|
run: |
|
||||||
|
echo "Fetching release assets for tag: ${{ github.ref_name }}"
|
||||||
|
gh release view "${{ github.ref_name }}" --repo "${{ github.repository }}" --json assets --jq '.assets[] | "\(.name) - \(.url)"' || true
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
# Pinned to work around https://github.com/softprops/action-gh-release/issues/445
|
||||||
|
uses: softprops/action-gh-release@v2.0.5
|
||||||
|
env:
|
||||||
|
NODE_VERSION: ${{ env.NODE_VERSION }}
|
||||||
|
ACTIONS_STEP_DEBUG: true
|
||||||
|
with:
|
||||||
|
preserve_order: true
|
||||||
|
overwrite: true
|
||||||
|
name: Release ${{ github.ref_name }}
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
draft: true
|
||||||
|
files: |
|
||||||
|
artifacts/**/*.dmg
|
||||||
|
artifacts/**/*.snap
|
||||||
|
artifacts/**/*.AppImage
|
||||||
|
artifacts/**/*.deb
|
||||||
|
artifacts/**/*.pacman
|
||||||
|
artifacts/**/*.exe
|
||||||
23
.gitignore
vendored
23
.gitignore
vendored
|
|
@ -1,4 +1,23 @@
|
||||||
|
# build output
|
||||||
|
dist
|
||||||
|
app/renderer
|
||||||
|
target
|
||||||
|
bin/cli.*
|
||||||
|
cache
|
||||||
|
|
||||||
|
# dependencies
|
||||||
node_modules
|
node_modules
|
||||||
*.log
|
|
||||||
build
|
# logs
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# optional dev config file and plugins directory
|
||||||
|
hyper.json
|
||||||
|
schema.json
|
||||||
|
plugins
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/launch.json
|
||||||
|
.idea
|
||||||
|
|
|
||||||
1
.husky/.gitignore
vendored
Normal file
1
.husky/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
_
|
||||||
1
.husky/pre-push
Normal file
1
.husky/pre-push
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
yarn test
|
||||||
1
.node-version
Normal file
1
.node-version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
20.11.0
|
||||||
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
20.11.0
|
||||||
22
.vscode/launch.json
vendored
Normal file
22
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Hyper",
|
||||||
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
||||||
|
"program": "${workspaceRoot}/target/index.js",
|
||||||
|
"protocol": "inspector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "cli",
|
||||||
|
"runtimeExecutable": "node",
|
||||||
|
"program": "${workspaceRoot}/bin/cli.js",
|
||||||
|
"args": ["--help"],
|
||||||
|
"protocol": "inspector"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
148049
.yarn/releases/yarn-classic.cjs
vendored
Executable file
148049
.yarn/releases/yarn-classic.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
1
.yarnrc
Normal file
1
.yarnrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
registry "https://registry.npmjs.org/"
|
||||||
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
yarnPath: .yarn/releases/yarn-classic.cjs
|
||||||
4
LICENSE
4
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
# MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 Gellert Hegyi
|
Copyright (c) 2018 Vercel, Inc.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
||||||
210
PLUGINS.md
Normal file
210
PLUGINS.md
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
# Plugin development
|
||||||
|
|
||||||
|
## 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/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/vercel/hyper#contribute).
|
||||||
|
Be sure to use the `canary` branch.
|
||||||
|
|
||||||
|
### Create a dev config file
|
||||||
|
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 `<repository_root>/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
|
||||||
|
module.exports = {
|
||||||
|
config: {
|
||||||
|
...
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
localPlugins: ['hyper-awesome-plugin'],
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running your plugin
|
||||||
|
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.
|
||||||
|
|
||||||
|
When you put a `console.log()` in your plugin code, it will be displayed in the Electron dev-tools, but only if it is located in a renderer method, like component decorators. If it is located in the Electron main process method, like the `onApp` handler, it will be displayed in your terminal where you ran `yarn run app` or in your VSCode console.
|
||||||
|
|
||||||
|
## Recipes
|
||||||
|
Almost all available API methods can be found on https://hyper.is.
|
||||||
|
If there's any missing, let us know or submit a PR to document it!
|
||||||
|
|
||||||
|
### Components
|
||||||
|
You can decorate almost all Hyper components with a Higher-Order Component (HOC). To understand their architecture, the easiest way is to use React dev-tools to dig in to their hierarchy.
|
||||||
|
|
||||||
|
Multiple plugins can decorate the same Hyper component. Thus, `Component` passed as first argument to your decorator function could possibly not be an original Hyper component but a HOC of a previous plugin. If you need to retrieve a reference to a real Hyper component, you can pass down a `onDecorated` handler.
|
||||||
|
```js
|
||||||
|
exports.decorateTerms = (Terms, {React}) => {
|
||||||
|
return class extends React.Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.terms = null;
|
||||||
|
this.onDecorated = this.onDecorated.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDecorated(terms) {
|
||||||
|
this.terms = terms;
|
||||||
|
// Don't forget to propagate it to HOC chain
|
||||||
|
if (this.props.onDecorated) this.props.onDecorated(terms);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return React.createElement(
|
||||||
|
Terms,
|
||||||
|
Object.assign({}, this.props, {
|
||||||
|
onDecorated: this.onDecorated
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// Or if you use JSX:
|
||||||
|
// <Terms onDecorated={this.onDecorated} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
: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
|
||||||
|
If you want to add some keymaps, you need to do 2 things:
|
||||||
|
|
||||||
|
#### Declare your key bindings
|
||||||
|
Use the `decorateKeymaps` API handler to modify existing keymaps and add yours with the following format `command: hotkeys`.
|
||||||
|
```js
|
||||||
|
// Adding Keymaps
|
||||||
|
exports.decorateKeymaps = keymaps => {
|
||||||
|
const newKeymaps = {
|
||||||
|
'pane:maximize': 'ctrl+shift+m',
|
||||||
|
'pane:invert': 'ctrl+shift+i'
|
||||||
|
}
|
||||||
|
return Object.assign({}, keymaps, newKeymaps);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The command name can be whatever you want, but the following is better to respect the default naming convention: `<context>:<action>`.
|
||||||
|
Hotkeys are composed by [Mousetrap supported keys](https://craig.is/killing/mice#keys).
|
||||||
|
|
||||||
|
**Bonus feature**: if your command ends with `:prefix`, it would mean that you want to use this command with an additional digit to the command. Then Hyper will create all your commands under the hood. For example, this keymap `'pane:hide:prefix': 'ctrl+shift'` will automatically generate the following:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
'pane:hide:1': 'ctrl+shift+1',
|
||||||
|
'pane:hide:2': 'ctrl+shift+2',
|
||||||
|
...
|
||||||
|
'pane:hide:8': 'ctrl+shift+8',
|
||||||
|
'pane:hide:last': 'ctrl+shift+9'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Notice that `9` has been replaced by `last` because most of the time this is handy if you have more than 9 items.
|
||||||
|
|
||||||
|
|
||||||
|
#### Register a handler for your commands
|
||||||
|
##### Renderer/Window
|
||||||
|
Most of time, you'll want to execute some sort of handler in context of the renderer, like dispatching a Redux action.
|
||||||
|
To trigger these handlers, you'll have to register them with the `registerCommands` Terms method.
|
||||||
|
```js
|
||||||
|
this.terms.registerCommands({
|
||||||
|
'pane:maximize': e => {
|
||||||
|
this.props.onMaximizePane();
|
||||||
|
// e parameter is React key event
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Main process
|
||||||
|
If there is no handler in the renderer for an existing command, an `rpc` message is emitted.
|
||||||
|
If you want to execute a handler in main process you have to subscribe to a message, for example:
|
||||||
|
```js
|
||||||
|
rpc.on('command pane:snapshot', () => {
|
||||||
|
/* Awesome snapshot feature */
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Menu
|
||||||
|
Your plugin can expose a `decorateMenu` function to modify the Hyper menu template.
|
||||||
|
Check the [Electron documentation](https://electronjs.org/docs/api/menu-item) for more details about the different menu item types/options available.
|
||||||
|
|
||||||
|
Be careful, a click handler will be executed on the main process. If you need to trigger a handler in the render process you need to use an `rpc` message like this:
|
||||||
|
```js
|
||||||
|
exports.decorateMenu = (menu) => {
|
||||||
|
debug('decorateMenu');
|
||||||
|
const isMac = process.platform === 'darwin';
|
||||||
|
// menu label is different on mac
|
||||||
|
const menuLabel = isMac ? 'Shell' : 'File';
|
||||||
|
|
||||||
|
return menu.map(menuCategory => {
|
||||||
|
if (menuCategory.label !== menuLabel) {
|
||||||
|
return menuItem;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
...menuCategory,
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Clear all panes in all tabs',
|
||||||
|
accelerator: 'ctrl+shift+y',
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
// on macOS, menu item can clicked without or minized window
|
||||||
|
if (focusedWindow) {
|
||||||
|
focusedWindow.rpc.emit('clear allPanes');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/* Plugin needs to register a rpc handler on renderer side for example in a Terms HOC*/
|
||||||
|
exports.decorateTerms = (Terms, { React }) => {
|
||||||
|
return class extends React.Component {
|
||||||
|
componentDidMount() {
|
||||||
|
window.rpc.on('clear allPanes',() => {
|
||||||
|
/* Awesome plugin feature */
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cursor
|
||||||
|
If your plugin needs to know cursor position/size, it can decorate the Term component and pass a handler. This handler will be called with each cursor move while passing back all information about the cursor.
|
||||||
|
```js
|
||||||
|
exports.decorateTerm = (Term, { React, notify }) => {
|
||||||
|
// Define and return our higher order component.
|
||||||
|
return class extends React.Component {
|
||||||
|
onCursorMove (cursorFrame) {
|
||||||
|
// Don't forget to propagate it to HOC chain
|
||||||
|
if (this.props.onCursorMove) this.props.onCursorMove(cursorFrame);
|
||||||
|
|
||||||
|
const { x, y, width, height, col, row } = cursorFrame;
|
||||||
|
/* Awesome cursor feature */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Require Electron
|
||||||
|
Hyper doesn't provide a reference to electron. However plugins can directly require electron.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const electron = require('electron')
|
||||||
|
// or
|
||||||
|
const { dialog, Menu } = require('electron')
|
||||||
|
```
|
||||||
|
|
||||||
|
This is needed in order to allow show/hide to have proper return of focus.
|
||||||
|
|
||||||
|
## Hyper v2 breaking changes
|
||||||
|
Hyper v2 uses `xterm.js` instead of `hterm`. It means that PTY output renders now in a canvas element, not with a hackable DOM structure.
|
||||||
|
For example, plugins can't use TermCSS in order to modify text or link styles anymore. It is now required to use available configuration params that are passed down to `xterm.js`.
|
||||||
|
|
||||||
|
If your plugin was deeply linked with the `hterm` API (even public methods), it certainly doesn't work anymore.
|
||||||
|
|
||||||
|
If your plugin needs some unavailable API to tweak `xterm.js`, please open an issue. We'll be happy to expose some existing `xterm.js` API or implement new ones.
|
||||||
137
README.md
Executable file → Normal file
137
README.md
Executable file → Normal file
|
|
@ -1,38 +1,121 @@
|
||||||
# electron-drag-click
|
<p align="center">
|
||||||
|
<img alt="hyper - modern web-based terminal" height=150 src="https://github.com/user-attachments/assets/3096f20a-8116-45ce-8c5e-0f1106107484">
|
||||||
|
</p>
|
||||||
|
|
||||||
## Description
|
<p>
|
||||||
|
<a aria-label="Vercel logo" href="https://vercel.com"><img
|
||||||
|
src="https://img.shields.io/badge/MADE%20BY%20Vercel-000000.svg?style=for-the-badge&logo=vercel&labelColor=000000&logoWidth=20"
|
||||||
|
/></a> <a aria-label="Quine logo" href="https://quineglobal.com"><img
|
||||||
|
width="143" height="28" alt="forked-by-quine" src="https://github.com/user-attachments/assets/57decaa2-7d8c-4d13-ada7-ff6b964346f7"
|
||||||
|
/></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
```js
|
[](https://github.com/quine-global/hyper/actions/workflows/ci.yml)
|
||||||
$ npm i electron-drag-click
|
|
||||||
```
|
|
||||||
|
|
||||||
This Native Node Module allows you to change the behavior of how frameless
|
[](https://changelog.com/213)
|
||||||
Electron browser windows handle pointer events on macOS. Chromium's built-in
|
|
||||||
mechanism ignores pointer events in draggable regions in frameless windows.
|
|
||||||
This module changes the built-in hit testing so in frameless windows pointer
|
|
||||||
events are propagated even in draggable regions.
|
|
||||||
|
|
||||||
It is based on my earlier PR in the Electron repository (https://github.com/electron/electron/pull/38208), which after some discussion with maintainers was decided not to be
|
For more details, head to: https://hyper.is
|
||||||
merged in, and rather be handled in a separate Native Module.
|
|
||||||
|
|
||||||
The code is using ObjectiveC's runtime method swizzling capability, which allows
|
## Project goals
|
||||||
you to alter the implementation of an existing selector. Shoutout to [@tzahola](https://github.com/tzahola), who helped me dealing with these APIs.
|
|
||||||
|
The goal of the project is to create a beautiful and customizable experience for command-line interface users, built on open web standards. We have picked up where Vercel left off, and intend to first offer stability, followed by a more tailored experience. We will still support customizability, but stability of theming APIs is not a goal.
|
||||||
|
|
||||||
|
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
|
## Usage
|
||||||
|
|
||||||
``` typescript
|
[Download the latest release!](https://hyper.is/#installation)
|
||||||
const { app, BrowserWindow } = require('electron');
|
|
||||||
const electronDragClick = require('electron-drag-click');
|
|
||||||
|
|
||||||
electronDragClick();
|
### Linux
|
||||||
|
#### Arch and derivatives
|
||||||
|
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)
|
||||||
|
|
||||||
app.on('ready', () => {
|
```sh
|
||||||
const win = new BrowserWindow({
|
paru -S hyper
|
||||||
width: 800,
|
|
||||||
height: 600,
|
|
||||||
frame: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
win.loadFile('./index.html');
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
|
Use [Homebrew Cask](https://brew.sh) to download the app by running these commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew update
|
||||||
|
brew install --cask hyper
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
Use [chocolatey](https://chocolatey.org/) to install the app by running the following command (package information can be found [here](https://chocolatey.org/packages/hyper/)):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
choco install hyper
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** The version available on [Homebrew Cask](https://brew.sh), [Chocolatey](https://chocolatey.org), [Snapcraft](https://snapcraft.io/store) or the [AUR](https://aur.archlinux.org) may not be the latest. Please consider downloading it from [here](https://hyper.is/#installation) if that's the case.
|
||||||
|
|
||||||
|
## Contribute
|
||||||
|
|
||||||
|
Regardless of the platform you are working on, you will need to have Yarn installed. If you have never installed Yarn before, you can find out how at: https://yarnpkg.com/en/docs/install.
|
||||||
|
|
||||||
|
1. Install necessary packages:
|
||||||
|
* Windows
|
||||||
|
- Be sure to run `yarn global add windows-build-tools` from an elevated prompt (as an administrator) to install `windows-build-tools`.
|
||||||
|
* macOS
|
||||||
|
- Once you have installed Yarn, you can skip this section!
|
||||||
|
* Linux (You can see [here](https://en.wikipedia.org/wiki/List_of_Linux_distributions) what your Linux is based on.)
|
||||||
|
- RPM-based
|
||||||
|
+ `GraphicsMagick`
|
||||||
|
+ `libicns-utils`
|
||||||
|
+ `xz` (Installed by default on some distributions.)
|
||||||
|
- Debian-based
|
||||||
|
+ `graphicsmagick`
|
||||||
|
+ `icnsutils`
|
||||||
|
+ `xz-utils`
|
||||||
|
2. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device
|
||||||
|
3. Install the dependencies: `yarn`
|
||||||
|
4. Build the code and watch for changes: `yarn run dev`
|
||||||
|
5. To run `hyper`
|
||||||
|
* `yarn run app` from another terminal tab/window/pane
|
||||||
|
* If you are using **Visual Studio Code**, select `Launch Hyper` in debugger configuration to launch a new Hyper instance with debugger attached.
|
||||||
|
* If you interrupt `yarn run dev`, you'll need to relaunch it each time you want to test something. Webpack will watch changes and will rebuild renderer code when needed (and only what have changed). You'll just have to relaunch electron by using yarn run app or VSCode launch task.
|
||||||
|
|
||||||
|
To make sure that your code works in the finished application, you can generate the binaries like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn run dist
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, you will see the binary in the `./dist` folder!
|
||||||
|
|
||||||
|
#### Known issues that can happen during development
|
||||||
|
|
||||||
|
##### Error building `node-pty`
|
||||||
|
|
||||||
|
If after building during development you get an alert dialog related to `node-pty` issues,
|
||||||
|
make sure its build process is working correctly by running `yarn run rebuild-node-pty`.
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
If you are getting compiler errors when running `yarn` add the environment variable `export CXX=clang++`
|
||||||
|
|
||||||
|
##### Error with `codesign` on macOS when running `yarn run dist`
|
||||||
|
|
||||||
|
If you have issues in the `codesign` step when running `yarn run dist` on macOS, you can temporarily disable code signing locally by setting
|
||||||
|
`export CSC_IDENTITY_AUTO_DISCOVERY=false` for the current terminal session.
|
||||||
|
|
||||||
|
## Related Repositories
|
||||||
|
|
||||||
|
- [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)
|
||||||
|
|
|
||||||
1
app/.yarnrc
Normal file
1
app/.yarnrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
registry "https://registry.npmjs.org/"
|
||||||
52
app/auto-updater-linux.ts
Normal file
52
app/auto-updater-linux.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import {EventEmitter} from 'events';
|
||||||
|
|
||||||
|
import fetch from 'electron-fetch';
|
||||||
|
|
||||||
|
class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
|
||||||
|
updateURL!: string;
|
||||||
|
quitAndInstall() {
|
||||||
|
this.emitError('QuitAndInstall unimplemented');
|
||||||
|
}
|
||||||
|
getFeedURL() {
|
||||||
|
return this.updateURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFeedURL(options: Electron.FeedURLOptions) {
|
||||||
|
this.updateURL = options.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForUpdates() {
|
||||||
|
if (!this.updateURL) {
|
||||||
|
return this.emitError('Update URL is not set');
|
||||||
|
}
|
||||||
|
this.emit('checking-for-update');
|
||||||
|
|
||||||
|
fetch(this.updateURL)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === 204) {
|
||||||
|
this.emit('update-not-available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
const date = pub_date ? new Date(pub_date) : new Date();
|
||||||
|
this.emit('update-available', {}, notes, name, date);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(this.emitError.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
emitError(error: string | Error) {
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
error = new Error(error);
|
||||||
|
}
|
||||||
|
this.emit('error', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoUpdaterLinux = new AutoUpdater();
|
||||||
|
|
||||||
|
export default autoUpdaterLinux;
|
||||||
170
app/commands.ts
Normal file
170
app/commands.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
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<string, (focusedWindow?: BrowserWindow) => void> = {
|
||||||
|
'window:new': () => {
|
||||||
|
// If window is created on the same tick, it will consume event too
|
||||||
|
setTimeout(app.createWindow, 0);
|
||||||
|
},
|
||||||
|
'tab:new': (focusedWindow) => {
|
||||||
|
if (focusedWindow) {
|
||||||
|
focusedWindow.rpc.emit('termgroup add req', {});
|
||||||
|
} else {
|
||||||
|
setTimeout(app.createWindow, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'pane:splitRight': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('split request vertical', {});
|
||||||
|
},
|
||||||
|
'pane:splitDown': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('split request horizontal', {});
|
||||||
|
},
|
||||||
|
'pane:close': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('termgroup close req');
|
||||||
|
},
|
||||||
|
'window:preferences': () => {
|
||||||
|
void openConfig();
|
||||||
|
},
|
||||||
|
'editor:clearBuffer': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('session clear req');
|
||||||
|
},
|
||||||
|
'editor:selectAll': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('term selectAll');
|
||||||
|
},
|
||||||
|
'plugins:update': () => {
|
||||||
|
updatePlugins();
|
||||||
|
},
|
||||||
|
'window:reload': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('reload');
|
||||||
|
},
|
||||||
|
'window:reloadFull': (focusedWindow) => {
|
||||||
|
focusedWindow?.reload();
|
||||||
|
},
|
||||||
|
'window:devtools': (focusedWindow) => {
|
||||||
|
if (!focusedWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const webContents = focusedWindow.webContents;
|
||||||
|
if (webContents.isDevToolsOpened()) {
|
||||||
|
webContents.closeDevTools();
|
||||||
|
} else {
|
||||||
|
webContents.openDevTools({mode: 'detach'});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'zoom:reset': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('reset fontSize req');
|
||||||
|
},
|
||||||
|
'zoom:in': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('increase fontSize req');
|
||||||
|
},
|
||||||
|
'zoom:out': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('decrease fontSize req');
|
||||||
|
},
|
||||||
|
'tab:prev': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('move left req');
|
||||||
|
},
|
||||||
|
'tab:next': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('move right req');
|
||||||
|
},
|
||||||
|
'pane:prev': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('prev pane req');
|
||||||
|
},
|
||||||
|
'pane:next': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('next pane req');
|
||||||
|
},
|
||||||
|
'editor:movePreviousWord': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('session move word left req');
|
||||||
|
},
|
||||||
|
'editor:moveNextWord': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('session move word right req');
|
||||||
|
},
|
||||||
|
'editor:moveBeginningLine': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('session move line beginning req');
|
||||||
|
},
|
||||||
|
'editor:moveEndLine': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('session move line end req');
|
||||||
|
},
|
||||||
|
'editor:deletePreviousWord': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('session del word left req');
|
||||||
|
},
|
||||||
|
'editor:deleteNextWord': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('session del word right req');
|
||||||
|
},
|
||||||
|
'editor:deleteBeginningLine': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('session del line beginning req');
|
||||||
|
},
|
||||||
|
'editor:deleteEndLine': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('session del line end req');
|
||||||
|
},
|
||||||
|
'editor:break': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('session break req');
|
||||||
|
},
|
||||||
|
'editor:stop': (focusedWindow) => {
|
||||||
|
focusedWindow?.rpc.emit('session stop req');
|
||||||
|
},
|
||||||
|
'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': () => {
|
||||||
|
void installCLI(true);
|
||||||
|
},
|
||||||
|
'window:hamburgerMenu': () => {
|
||||||
|
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) => {
|
||||||
|
const index = cmdIndex === 'last' ? cmdIndex : cmdIndex - 1;
|
||||||
|
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});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const execCommand = (command: string, focusedWindow?: BrowserWindow) => {
|
||||||
|
const fn = commands[command];
|
||||||
|
if (fn) {
|
||||||
|
fn(focusedWindow);
|
||||||
|
}
|
||||||
|
};
|
||||||
156
app/config.ts
Normal file
156
app/config.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
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 {cfgPath, cfgDir} from './config/paths';
|
||||||
|
import notify from './notify';
|
||||||
|
import {getColorMap} from './utils/colors';
|
||||||
|
|
||||||
|
const watchers: Function[] = [];
|
||||||
|
let cfg: parsedConfig = {} as any;
|
||||||
|
let _watcher: chokidar.FSWatcher;
|
||||||
|
|
||||||
|
export const getDeprecatedCSS = (config: configOptions) => {
|
||||||
|
const deprecated: string[] = [];
|
||||||
|
const deprecatedCSS = ['x-screen', 'x-row', 'cursor-node', '::selection'];
|
||||||
|
deprecatedCSS.forEach((css) => {
|
||||||
|
if (config.css?.includes(css) || config.termCSS?.includes(css)) {
|
||||||
|
deprecated.push(css);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return deprecated;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkDeprecatedConfig = () => {
|
||||||
|
if (!cfg.config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const deprecated = getDeprecatedCSS(cfg.config);
|
||||||
|
if (deprecated.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const deprecatedStr = deprecated.join(', ');
|
||||||
|
notify('Configuration warning', `Your configuration uses some deprecated CSS classes (${deprecatedStr})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _watch = () => {
|
||||||
|
if (_watcher) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = () => {
|
||||||
|
// Need to wait 100ms to ensure that write is complete
|
||||||
|
setTimeout(() => {
|
||||||
|
cfg = _import();
|
||||||
|
notify('Configuration updated', 'Hyper configuration reloaded!');
|
||||||
|
watchers.forEach((fn) => {
|
||||||
|
fn();
|
||||||
|
});
|
||||||
|
checkDeprecatedConfig();
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
_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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subscribe = (fn: Function) => {
|
||||||
|
watchers.push(fn);
|
||||||
|
return () => {
|
||||||
|
watchers.splice(watchers.indexOf(fn), 1);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getConfigDir = () => {
|
||||||
|
// expose config directory to load plugin from the right place
|
||||||
|
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 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 = () => {
|
||||||
|
return _openConfig();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPlugins = (): {plugins: string[]; localPlugins: string[]} => {
|
||||||
|
return {
|
||||||
|
plugins: cfg.plugins,
|
||||||
|
localPlugins: cfg.localPlugins
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getKeymaps = () => {
|
||||||
|
return cfg.keymaps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setup = () => {
|
||||||
|
cfg = _import();
|
||||||
|
_watch();
|
||||||
|
checkDeprecatedConfig();
|
||||||
|
};
|
||||||
|
|
||||||
|
export {get as getWin, recordState as winRecord, defaults as windowDefaults} from './config/windows';
|
||||||
|
|
||||||
|
export const fixConfigDefaults = (decoratedConfig: configOptions) => {
|
||||||
|
const defaultConfig = getDefaultConfig().config!;
|
||||||
|
decoratedConfig.colors = getColorMap(decoratedConfig.colors) || {};
|
||||||
|
// We must have default colors for xterm css.
|
||||||
|
decoratedConfig.colors = {...defaultConfig.colors, ...decoratedConfig.colors};
|
||||||
|
return decoratedConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const htermConfigTranslate = (config: configOptions) => {
|
||||||
|
const cssReplacements: Record<string, string> = {
|
||||||
|
'x-screen x-row([ {.[])': '.xterm-rows > div$1',
|
||||||
|
'.cursor-node([ {.[])': '.terminal-cursor$1',
|
||||||
|
'::selection([ {.[])': '.terminal .xterm-selection div$1',
|
||||||
|
'x-screen a([ {.[])': '.terminal a$1',
|
||||||
|
'x-row a([ {.[])': '.terminal a$1'
|
||||||
|
};
|
||||||
|
Object.keys(cssReplacements).forEach((pattern) => {
|
||||||
|
const searchvalue = new RegExp(pattern, 'g');
|
||||||
|
const newvalue = cssReplacements[pattern];
|
||||||
|
config.css = config.css?.replace(searchvalue, newvalue);
|
||||||
|
config.termCSS = config.termCSS?.replace(searchvalue, newvalue);
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
};
|
||||||
77
app/config/config-default.json
Normal file
77
app/config/config-default.json
Normal file
|
|
@ -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": {}
|
||||||
|
}
|
||||||
65
app/config/import.ts
Normal file
65
app/config/import.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import {readFileSync, mkdirpSync} from 'fs-extra';
|
||||||
|
|
||||||
|
import type {rawConfig} from '../../typings/config';
|
||||||
|
import notify from '../notify';
|
||||||
|
|
||||||
|
import {_init} from './init';
|
||||||
|
import {migrateHyper3Config} from './migrate';
|
||||||
|
import {defaultCfg, cfgPath, plugs, defaultPlatformKeyPath} from './paths';
|
||||||
|
|
||||||
|
let defaultConfig: rawConfig;
|
||||||
|
|
||||||
|
const _importConf = () => {
|
||||||
|
// init plugin directories if not present
|
||||||
|
mkdirpSync(plugs.base);
|
||||||
|
mkdirpSync(plugs.local);
|
||||||
|
|
||||||
|
try {
|
||||||
|
migrateHyper3Config();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultCfgRaw = '{}';
|
||||||
|
try {
|
||||||
|
defaultCfgRaw = readFileSync(defaultCfg, 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
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<string, string | string[]>;
|
||||||
|
_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.userCfg, imported.defaultCfg);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDefaultConfig = () => {
|
||||||
|
if (!defaultConfig) {
|
||||||
|
defaultConfig = _importConf().defaultCfg;
|
||||||
|
}
|
||||||
|
return defaultConfig;
|
||||||
|
};
|
||||||
63
app/config/init.ts
Normal file
63
app/config/init.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
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<string, any> => {
|
||||||
|
const module: Record<string, any> = {};
|
||||||
|
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'});
|
||||||
|
} catch (_err) {
|
||||||
|
const err = _err as {name: string};
|
||||||
|
notify(`Error loading config: ${err.name}`, JSON.stringify(err), {error: err});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _extractDefault = (cfg: string) => {
|
||||||
|
return _extract(_syntaxValidation(cfg));
|
||||||
|
};
|
||||||
|
|
||||||
|
// init config
|
||||||
|
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
|
||||||
|
keymaps: mapKeys({...defaultCfg.keymaps, ...userCfg?.keymaps}),
|
||||||
|
// Ignore undefined values in plugin and localPlugins array Issue #1862
|
||||||
|
plugins: userCfg?.plugins?.filter(Boolean) || [],
|
||||||
|
localPlugins: userCfg?.localPlugins?.filter(Boolean) || []
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {_init, _extractDefault};
|
||||||
190
app/config/migrate.ts
Normal file
190
app/config/migrate.ts
Normal file
|
|
@ -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}`);
|
||||||
|
};
|
||||||
80
app/config/open.ts
Normal file
80
app/config/open.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import {exec} from 'child_process';
|
||||||
|
|
||||||
|
import {shell} from 'electron';
|
||||||
|
|
||||||
|
import * as Registry from 'native-reg';
|
||||||
|
|
||||||
|
import {cfgPath} from './paths';
|
||||||
|
|
||||||
|
const getUserChoiceKey = () => {
|
||||||
|
try {
|
||||||
|
// Load FileExts keys for .js files
|
||||||
|
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.endsWith('UserChoice'));
|
||||||
|
return userChoice
|
||||||
|
? `Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.js\\${userChoice}`
|
||||||
|
: userChoice;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasDefaultSet = () => {
|
||||||
|
const userChoice = getUserChoiceKey();
|
||||||
|
if (!userChoice) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load key values
|
||||||
|
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')
|
||||||
|
);
|
||||||
|
|
||||||
|
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<boolean>((resolve) => {
|
||||||
|
exec(`start notepad.exe ${file}`, (error) => {
|
||||||
|
resolve(!error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
96
app/config/paths.ts
Normal file
96
app/config/paths.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
// This module exports paths, names, and other metadata that is referenced
|
||||||
|
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.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
|
||||||
|
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(),
|
||||||
|
'.hyper.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
let cfgPath = join(cfgDir, cfgFile);
|
||||||
|
const schemaPath = resolve(__dirname, schemaFile);
|
||||||
|
|
||||||
|
const devDir = resolve(__dirname, '../..');
|
||||||
|
const devCfg = join(devDir, cfgFile);
|
||||||
|
const defaultCfg = resolve(__dirname, defaultCfgFile);
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
// if a local config file exists, use it
|
||||||
|
try {
|
||||||
|
statSync(devCfg);
|
||||||
|
cfgPath = devCfg;
|
||||||
|
cfgDir = devDir;
|
||||||
|
console.log('using config file:', cfgPath);
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugins = resolve(cfgDir, 'plugins');
|
||||||
|
const plugs = {
|
||||||
|
base: plugins,
|
||||||
|
local: resolve(plugins, 'local'),
|
||||||
|
cache: resolve(plugins, 'cache')
|
||||||
|
};
|
||||||
|
const yarn = resolve(__dirname, '../../bin/yarn-standalone.js');
|
||||||
|
const cliScriptPath = resolve(__dirname, '../../bin/hyper');
|
||||||
|
const cliLinkPath = '/usr/local/bin/hyper';
|
||||||
|
|
||||||
|
const icon = resolve(__dirname, '../static/icon96x96.png');
|
||||||
|
|
||||||
|
const keymapPath = resolve(__dirname, '../keymaps');
|
||||||
|
const darwinKeys = join(keymapPath, 'darwin.json');
|
||||||
|
const win32Keys = join(keymapPath, 'win32.json');
|
||||||
|
const linuxKeys = join(keymapPath, 'linux.json');
|
||||||
|
|
||||||
|
const defaultPlatformKeyPath = () => {
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'darwin':
|
||||||
|
return darwinKeys;
|
||||||
|
case 'win32':
|
||||||
|
return win32Keys;
|
||||||
|
case 'linux':
|
||||||
|
return linuxKeys;
|
||||||
|
default:
|
||||||
|
return darwinKeys;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
cfgDir,
|
||||||
|
cfgPath,
|
||||||
|
legacyCfgPath,
|
||||||
|
cfgFile,
|
||||||
|
defaultCfg,
|
||||||
|
icon,
|
||||||
|
defaultPlatformKeyPath,
|
||||||
|
plugs,
|
||||||
|
yarn,
|
||||||
|
cliScriptPath,
|
||||||
|
cliLinkPath,
|
||||||
|
homeDirectory,
|
||||||
|
schemaFile,
|
||||||
|
schemaPath
|
||||||
|
};
|
||||||
756
app/config/schema.json
Normal file
756
app/config/schema.json
Normal file
|
|
@ -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<profileConfigOptions>": {
|
||||||
|
"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<profileConfigOptions>",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
|
||||||
21
app/config/windows.ts
Normal file
21
app/config/windows.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type {BrowserWindow} from 'electron';
|
||||||
|
|
||||||
|
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 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());
|
||||||
|
}
|
||||||
38
app/index.html
Normal file
38
app/index.html
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Hyper</title>
|
||||||
|
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="initial-scale=1.0">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
color: #fff;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-resolution: 2dppx) {
|
||||||
|
* {
|
||||||
|
text-rendering: geometricPrecision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="mount"></div>
|
||||||
|
|
||||||
|
<script>start = performance.now();</script>
|
||||||
|
<script src="renderer/bundle.js"></script>
|
||||||
|
<script>console.log('total init time', performance.now() - start);</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
247
app/index.ts
Normal file
247
app/index.ts
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
// 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');
|
||||||
|
console.log(`Hyper version ${version}`);
|
||||||
|
console.log('Hyper does not accept any command line arguments. Please modify the config file instead.');
|
||||||
|
console.log(`Hyper configuration file located at: ${cfgPath}`);
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable remote module
|
||||||
|
// eslint-disable-next-line import/order
|
||||||
|
import {initialize as remoteInitialize} from '@electron/remote/main';
|
||||||
|
remoteInitialize();
|
||||||
|
|
||||||
|
// set up config
|
||||||
|
// eslint-disable-next-line import/order
|
||||||
|
import * as config from './config';
|
||||||
|
config.setup();
|
||||||
|
|
||||||
|
// Native
|
||||||
|
import {resolve} from 'path';
|
||||||
|
|
||||||
|
// Packages
|
||||||
|
import {app, BrowserWindow, Menu, screen} from 'electron';
|
||||||
|
|
||||||
|
import isDev from 'electron-is-dev';
|
||||||
|
import {gitDescribe} from 'git-describe';
|
||||||
|
import parseUrl from 'parse-url';
|
||||||
|
|
||||||
|
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<BrowserWindow>([]);
|
||||||
|
|
||||||
|
// expose to plugins
|
||||||
|
app.config = config;
|
||||||
|
app.plugins = plugins;
|
||||||
|
app.getWindows = () => new Set([...windowSet]); // return a clone
|
||||||
|
|
||||||
|
// function to retrieve the last focused window in windowSet;
|
||||||
|
// added to app object in order to expose it to plugins.
|
||||||
|
app.getLastFocusedWindow = () => {
|
||||||
|
if (!windowSet.size) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Array.from(windowSet).reduce((lastWindow, win) => {
|
||||||
|
return win.focusTime > lastWindow.focusTime ? win : lastWindow;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Disabling Chromium GPU blacklist');
|
||||||
|
app.commandLine.appendSwitch('ignore-gpu-blacklist');
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
console.log('running in dev mode');
|
||||||
|
|
||||||
|
// Override default appVersion which is set from package.json
|
||||||
|
gitDescribe({customArguments: ['--tags']}, (error: any, gitInfo: {raw: string}) => {
|
||||||
|
if (!error) {
|
||||||
|
app.setVersion(gitInfo.raw);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('running in prod mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `file://${resolve(isDev ? __dirname : app.getAppPath(), 'index.html')}`;
|
||||||
|
console.log('electron will open', url);
|
||||||
|
|
||||||
|
async function installDevExtensions(isDev_: boolean) {
|
||||||
|
if (!isDev_) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const {default: installer, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS} = await import('electron-devtools-installer');
|
||||||
|
|
||||||
|
const extensions = [REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS];
|
||||||
|
const forceDownload = Boolean(process.env.UPGRADE_EXTENSIONS);
|
||||||
|
|
||||||
|
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: {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;
|
||||||
|
|
||||||
|
const winPos = options.position;
|
||||||
|
|
||||||
|
// Open the new window roughly the height of the header away from the
|
||||||
|
// previous window. This also ensures in multi monitor setups that the
|
||||||
|
// new terminal is on the correct screen.
|
||||||
|
const focusedWindow = BrowserWindow.getFocusedWindow() || app.getLastFocusedWindow();
|
||||||
|
// In case of options defaults position and size, we should ignore the focusedWindow.
|
||||||
|
if (winPos !== undefined) {
|
||||||
|
[startX, startY] = winPos;
|
||||||
|
} else if (focusedWindow) {
|
||||||
|
const points = focusedWindow.getPosition();
|
||||||
|
const currentScreen = screen.getDisplayNearestPoint({
|
||||||
|
x: points[0],
|
||||||
|
y: points[1]
|
||||||
|
});
|
||||||
|
|
||||||
|
const biggestX = points[0] + 100 + width - currentScreen.bounds.x;
|
||||||
|
const biggestY = points[1] + 100 + height - currentScreen.bounds.y;
|
||||||
|
|
||||||
|
if (biggestX > currentScreen.size.width) {
|
||||||
|
startX = 50;
|
||||||
|
} else {
|
||||||
|
startX = points[0] + 34;
|
||||||
|
}
|
||||||
|
if (biggestY > currentScreen.size.height) {
|
||||||
|
startY = 50;
|
||||||
|
} else {
|
||||||
|
startY = points[1] + 34;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!windowUtils.positionIsValid([startX, startY])) {
|
||||||
|
[startX, startY] = config.windowDefaults.windowPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hwin = newWindow({width, height, x: startX, y: startY}, cfg, fn, profileName);
|
||||||
|
windowSet.add(hwin);
|
||||||
|
void hwin.loadURL(url);
|
||||||
|
|
||||||
|
hwin.once('ready-to-show', () => {
|
||||||
|
hwin.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// the window can be closed by the browser process itself
|
||||||
|
hwin.on('close', () => {
|
||||||
|
hwin.clean();
|
||||||
|
windowSet.delete(hwin);
|
||||||
|
});
|
||||||
|
|
||||||
|
return hwin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when opening create a new window
|
||||||
|
createWindow();
|
||||||
|
|
||||||
|
// expose to plugins
|
||||||
|
app.createWindow = createWindow;
|
||||||
|
|
||||||
|
// mac only. when the dock icon is clicked
|
||||||
|
// and we don't have any active windows open,
|
||||||
|
// we open one
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (!windowSet.size) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeMenu = () => {
|
||||||
|
const menu = plugins.decorateMenu(AppMenu.createMenu(createWindow, plugins.getLoadedPluginVersions));
|
||||||
|
|
||||||
|
// If we're on Mac make a Dock Menu
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
const dockMenu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: 'New Window',
|
||||||
|
click() {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
app.dock?.setMenu(dockMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu.setApplicationMenu(AppMenu.buildMenu(menu));
|
||||||
|
};
|
||||||
|
|
||||||
|
plugins.onApp(app);
|
||||||
|
makeMenu();
|
||||||
|
plugins.subscribe(plugins.onApp.bind(undefined, app));
|
||||||
|
config.subscribe(makeMenu);
|
||||||
|
if (!isDev) {
|
||||||
|
// check if should be set/removed as default ssh protocol client
|
||||||
|
if (config.getConfig().defaultSSHApp && !app.isDefaultProtocolClient('ssh')) {
|
||||||
|
console.log('Setting Hyper as default client for ssh:// protocol');
|
||||||
|
app.setAsDefaultProtocolClient('ssh');
|
||||||
|
} else if (!config.getConfig().defaultSSHApp && app.isDefaultProtocolClient('ssh')) {
|
||||||
|
console.log('Removing Hyper from default client for ssh:// protocol');
|
||||||
|
app.removeAsDefaultProtocolClient('ssh');
|
||||||
|
}
|
||||||
|
void installCLI(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Error while loading devtools extensions', err);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
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-file', (_event, path) => {
|
||||||
|
GetWindow((win: BrowserWindow) => {
|
||||||
|
win.rpc.emit('open file', {path});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('open-url', (_event, sshUrl) => {
|
||||||
|
GetWindow((win: BrowserWindow) => {
|
||||||
|
win.rpc.emit('open ssh', parseUrl(sshUrl));
|
||||||
|
});
|
||||||
|
});
|
||||||
55
app/keymaps/darwin.json
Normal file
55
app/keymaps/darwin.json
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"window:devtools": "command+alt+i",
|
||||||
|
"window:reload": "command+shift+r",
|
||||||
|
"window:reloadFull": "command+shift+f5",
|
||||||
|
"window:preferences": "command+,",
|
||||||
|
"zoom:reset": "command+0",
|
||||||
|
"zoom:in": [
|
||||||
|
"command+plus",
|
||||||
|
"command+="
|
||||||
|
],
|
||||||
|
"zoom:out": "command+-",
|
||||||
|
"window:new": "command+n",
|
||||||
|
"window:minimize": "command+m",
|
||||||
|
"window:zoom": "ctrl+alt+command+m",
|
||||||
|
"window:toggleFullScreen": "command+ctrl+f",
|
||||||
|
"window:close": "command+shift+w",
|
||||||
|
"tab:new": "command+t",
|
||||||
|
"tab:next": [
|
||||||
|
"command+shift+]",
|
||||||
|
"command+shift+right",
|
||||||
|
"command+alt+right",
|
||||||
|
"ctrl+tab"
|
||||||
|
],
|
||||||
|
"tab:prev": [
|
||||||
|
"command+shift+[",
|
||||||
|
"command+shift+left",
|
||||||
|
"command+alt+left",
|
||||||
|
"ctrl+shift+tab"
|
||||||
|
],
|
||||||
|
"tab:jump:prefix": "command",
|
||||||
|
"pane:next": "command+]",
|
||||||
|
"pane:prev": "command+[",
|
||||||
|
"pane:splitRight": "command+d",
|
||||||
|
"pane:splitDown": "command+shift+d",
|
||||||
|
"pane:close": "command+w",
|
||||||
|
"editor:undo": "command+z",
|
||||||
|
"editor:redo": "command+y",
|
||||||
|
"editor:cut": "command+x",
|
||||||
|
"editor:copy": "command+c",
|
||||||
|
"editor:paste": "command+v",
|
||||||
|
"editor:selectAll": "command+a",
|
||||||
|
"editor:search": "command+f",
|
||||||
|
"editor:search-close": "esc",
|
||||||
|
"editor:movePreviousWord": "alt+left",
|
||||||
|
"editor:moveNextWord": "alt+right",
|
||||||
|
"editor:moveBeginningLine": "command+left",
|
||||||
|
"editor:moveEndLine": "command+right",
|
||||||
|
"editor:deletePreviousWord": "alt+backspace",
|
||||||
|
"editor:deleteNextWord": "alt+delete",
|
||||||
|
"editor:deleteBeginningLine": "command+backspace",
|
||||||
|
"editor:deleteEndLine": "command+delete",
|
||||||
|
"editor:clearBuffer": "command+k",
|
||||||
|
"editor:break": "ctrl+c",
|
||||||
|
"plugins:update": "command+shift+u"
|
||||||
|
}
|
||||||
53
app/keymaps/linux.json
Normal file
53
app/keymaps/linux.json
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
"window:devtools": "ctrl+shift+i",
|
||||||
|
"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+-",
|
||||||
|
"window:new": "ctrl+shift+n",
|
||||||
|
"window:minimize": "ctrl+shift+m",
|
||||||
|
"window:zoom": "ctrl+shift+alt+m",
|
||||||
|
"window:toggleFullScreen": "f11",
|
||||||
|
"window:close": "ctrl+shift+q",
|
||||||
|
"tab:new": "ctrl+shift+t",
|
||||||
|
"tab:next": [
|
||||||
|
"ctrl+shift+]",
|
||||||
|
"ctrl+shift+right",
|
||||||
|
"ctrl+alt+right",
|
||||||
|
"ctrl+tab"
|
||||||
|
],
|
||||||
|
"tab:prev": [
|
||||||
|
"ctrl+shift+[",
|
||||||
|
"ctrl+shift+left",
|
||||||
|
"ctrl+alt+left",
|
||||||
|
"ctrl+shift+tab"
|
||||||
|
],
|
||||||
|
"tab:jump:prefix": "ctrl",
|
||||||
|
"pane:next": "ctrl+pageup",
|
||||||
|
"pane:prev": "ctrl+pagedown",
|
||||||
|
"pane:splitRight": "ctrl+shift+d",
|
||||||
|
"pane:splitDown": "ctrl+shift+e",
|
||||||
|
"pane:close": "ctrl+shift+w",
|
||||||
|
"editor:undo": "ctrl+shift+z",
|
||||||
|
"editor:redo": "ctrl+shift+y",
|
||||||
|
"editor:cut": "ctrl+shift+x",
|
||||||
|
"editor:copy": "ctrl+shift+c",
|
||||||
|
"editor:paste": "ctrl+shift+v",
|
||||||
|
"editor:selectAll": "ctrl+shift+a",
|
||||||
|
"editor:search": "ctrl+shift+f",
|
||||||
|
"editor:search-close": "esc",
|
||||||
|
"editor:movePreviousWord": "ctrl+left",
|
||||||
|
"editor:moveNextWord": "ctrl+right",
|
||||||
|
"editor:moveBeginningLine": "home",
|
||||||
|
"editor:moveEndLine": "end",
|
||||||
|
"editor:deletePreviousWord": "ctrl+backspace",
|
||||||
|
"editor:deleteNextWord": "ctrl+del",
|
||||||
|
"editor:deleteBeginningLine": "ctrl+home",
|
||||||
|
"editor:deleteEndLine": "ctrl+end",
|
||||||
|
"editor:clearBuffer": "ctrl+shift+k",
|
||||||
|
"editor:break": "ctrl+c",
|
||||||
|
"plugins:update": "ctrl+shift+u"
|
||||||
|
}
|
||||||
50
app/keymaps/win32.json
Normal file
50
app/keymaps/win32.json
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"window:devtools": "ctrl+shift+i",
|
||||||
|
"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+-",
|
||||||
|
"window:new": "ctrl+shift+n",
|
||||||
|
"window:minimize": "ctrl+shift+m",
|
||||||
|
"window:zoom": "ctrl+shift+alt+m",
|
||||||
|
"window:toggleFullScreen": "f11",
|
||||||
|
"window:close": [
|
||||||
|
"ctrl+shift+q",
|
||||||
|
"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",
|
||||||
|
"pane:splitRight": "ctrl+shift+d",
|
||||||
|
"pane:splitDown": "ctrl+shift+e",
|
||||||
|
"pane:close": "ctrl+shift+w",
|
||||||
|
"editor:undo": "ctrl+shift+z",
|
||||||
|
"editor:redo": "ctrl+shift+y",
|
||||||
|
"editor:cut": "ctrl+shift+x",
|
||||||
|
"editor:copy": "ctrl+shift+c",
|
||||||
|
"editor:paste": "ctrl+shift+v",
|
||||||
|
"editor:selectAll": "ctrl+shift+a",
|
||||||
|
"editor:search": "ctrl+shift+f",
|
||||||
|
"editor:search-close": "esc",
|
||||||
|
"editor:movePreviousWord": "",
|
||||||
|
"editor:moveNextWord": "",
|
||||||
|
"editor:moveBeginningLine": "Home",
|
||||||
|
"editor:moveEndLine": "End",
|
||||||
|
"editor:deletePreviousWord": "ctrl+backspace",
|
||||||
|
"editor:deleteNextWord": "ctrl+del",
|
||||||
|
"editor:deleteBeginningLine": "ctrl+home",
|
||||||
|
"editor:deleteEndLine": "ctrl+end",
|
||||||
|
"editor:clearBuffer": "ctrl+shift+k",
|
||||||
|
"editor:break": "ctrl+c",
|
||||||
|
"plugins:update": "ctrl+shift+u"
|
||||||
|
}
|
||||||
96
app/menus/menu.ts
Normal file
96
app/menus/menu.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
// Packages
|
||||||
|
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 {getDecoratedKeymaps} from '../plugins';
|
||||||
|
import {getRendererTypes} from '../utils/renderer-utils';
|
||||||
|
|
||||||
|
import darwinMenu from './menus/darwin';
|
||||||
|
import editMenu from './menus/edit';
|
||||||
|
import helpMenu from './menus/help';
|
||||||
|
import shellMenu from './menus/shell';
|
||||||
|
import toolsMenu from './menus/tools';
|
||||||
|
import viewMenu from './menus/view';
|
||||||
|
import windowMenu from './menus/window';
|
||||||
|
|
||||||
|
const appName = app.name;
|
||||||
|
const appVersion = app.getVersion();
|
||||||
|
|
||||||
|
let menu_: Menu;
|
||||||
|
|
||||||
|
export const createMenu = (
|
||||||
|
createWindow: (fn?: (win: BrowserWindow) => void, options?: Record<string, any>) => BrowserWindow,
|
||||||
|
getLoadedPluginVersions: () => {name: string; version: string}[]
|
||||||
|
) => {
|
||||||
|
const config = getConfig();
|
||||||
|
// We take only first shortcut in array for each command
|
||||||
|
const allCommandKeys = getDecoratedKeymaps();
|
||||||
|
const commandKeys = Object.keys(allCommandKeys).reduce((result: Record<string, string>, command) => {
|
||||||
|
result[command] = allCommandKeys[command][0];
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
let updateChannel = 'stable';
|
||||||
|
|
||||||
|
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})`);
|
||||||
|
|
||||||
|
const rendererCounts = Object.values(getRendererTypes()).reduce((acc: Record<string, number>, type) => {
|
||||||
|
acc[type] = acc[type] ? acc[type] + 1 : 1;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
const renderers = Object.entries(rendererCounts)
|
||||||
|
.map(([type, count]) => type + (count > 1 ? ` (${count})` : ''))
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
void dialog.showMessageBox({
|
||||||
|
title: `About ${appName}`,
|
||||||
|
message: `${appName} ${appVersion} (${updateChannel})`,
|
||||||
|
detail: `
|
||||||
|
Renderers: ${renderers}
|
||||||
|
Plugins: ${pluginList}
|
||||||
|
|
||||||
|
Maintained by QUINE Global
|
||||||
|
Copyright © 2025
|
||||||
|
|
||||||
|
Created by Guillermo Rauch
|
||||||
|
Copyright © 2022 Vercel, Inc.
|
||||||
|
`
|
||||||
|
.split('\n')
|
||||||
|
.map((z) => z.trim())
|
||||||
|
.join('\n'),
|
||||||
|
buttons: [],
|
||||||
|
icon: icon as any
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const menu = [
|
||||||
|
...(process.platform === 'darwin' ? [darwinMenu(commandKeys, execCommand, showAbout)] : []),
|
||||||
|
shellMenu(
|
||||||
|
commandKeys,
|
||||||
|
(command, focusedWindow) => execCommand(command, focusedWindow as BrowserWindow | undefined),
|
||||||
|
getConfig().profiles.map((p) => p.name)
|
||||||
|
),
|
||||||
|
editMenu(commandKeys, execCommand),
|
||||||
|
viewMenu(commandKeys, execCommand),
|
||||||
|
toolsMenu(commandKeys, execCommand),
|
||||||
|
windowMenu(commandKeys, execCommand),
|
||||||
|
helpMenu(commandKeys, showAbout)
|
||||||
|
];
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildMenu = (template: Electron.MenuItemConstructorOptions[]): Electron.Menu => {
|
||||||
|
menu_ = Menu.buildFromTemplate(template);
|
||||||
|
return menu_;
|
||||||
|
};
|
||||||
59
app/menus/menus/darwin.ts
Normal file
59
app/menus/menus/darwin.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
// 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} from 'electron';
|
||||||
|
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron';
|
||||||
|
|
||||||
|
const darwinMenu = (
|
||||||
|
commandKeys: Record<string, string>,
|
||||||
|
execCommand: (command: string, focusedWindow?: BrowserWindow) => void,
|
||||||
|
showAbout: () => void
|
||||||
|
): MenuItemConstructorOptions => {
|
||||||
|
return {
|
||||||
|
label: `${app.name}`,
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'About Hyper',
|
||||||
|
click() {
|
||||||
|
showAbout();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Preferences...',
|
||||||
|
accelerator: commandKeys['window:preferences'],
|
||||||
|
click() {
|
||||||
|
execCommand('window:preferences');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'services',
|
||||||
|
submenu: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'hide'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'hideOthers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'unhide'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'quit'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default darwinMenu;
|
||||||
151
app/menus/menus/edit.ts
Normal file
151
app/menus/menus/edit.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron';
|
||||||
|
|
||||||
|
const editMenu = (
|
||||||
|
commandKeys: Record<string, string>,
|
||||||
|
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
|
||||||
|
) => {
|
||||||
|
const submenu: MenuItemConstructorOptions[] = [
|
||||||
|
{
|
||||||
|
label: 'Undo',
|
||||||
|
accelerator: commandKeys['editor:undo'],
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Redo',
|
||||||
|
accelerator: commandKeys['editor:redo'],
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cut',
|
||||||
|
accelerator: commandKeys['editor:cut'],
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'copy',
|
||||||
|
command: 'editor:copy',
|
||||||
|
accelerator: commandKeys['editor:copy'],
|
||||||
|
registerAccelerator: true
|
||||||
|
} as any,
|
||||||
|
{
|
||||||
|
role: 'paste',
|
||||||
|
accelerator: commandKeys['editor:paste'],
|
||||||
|
registerAccelerator: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Select All',
|
||||||
|
accelerator: commandKeys['editor:selectAll'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('editor:selectAll', focusedWindow as BrowserWindow | undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Move to...',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Previous word',
|
||||||
|
accelerator: commandKeys['editor:movePreviousWord'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('editor:movePreviousWord', focusedWindow as BrowserWindow | undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Next word',
|
||||||
|
accelerator: commandKeys['editor:moveNextWord'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('editor:moveNextWord', focusedWindow as BrowserWindow | undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Line beginning',
|
||||||
|
accelerator: commandKeys['editor:moveBeginningLine'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('editor:moveBeginningLine', focusedWindow as BrowserWindow | undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Line end',
|
||||||
|
accelerator: commandKeys['editor:moveEndLine'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('editor:moveEndLine', focusedWindow as BrowserWindow | undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delete...',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Previous word',
|
||||||
|
accelerator: commandKeys['editor:deletePreviousWord'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('editor:deletePreviousWord', focusedWindow as BrowserWindow | undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Next word',
|
||||||
|
accelerator: commandKeys['editor:deleteNextWord'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('editor:deleteNextWord', focusedWindow as BrowserWindow | undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Line beginning',
|
||||||
|
accelerator: commandKeys['editor:deleteBeginningLine'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('editor:deleteBeginningLine', focusedWindow as BrowserWindow | undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Line end',
|
||||||
|
accelerator: commandKeys['editor:deleteEndLine'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('editor:deleteEndLine', focusedWindow as BrowserWindow | undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Clear Buffer',
|
||||||
|
accelerator: commandKeys['editor:clearBuffer'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('editor:clearBuffer', focusedWindow as BrowserWindow | undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Search',
|
||||||
|
accelerator: commandKeys['editor:search'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('editor:search', focusedWindow as BrowserWindow | undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
submenu.push(
|
||||||
|
{type: 'separator'},
|
||||||
|
{
|
||||||
|
label: 'Preferences...',
|
||||||
|
accelerator: commandKeys['window:preferences'],
|
||||||
|
click() {
|
||||||
|
execCommand('window:preferences');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: 'Edit',
|
||||||
|
submenu
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default editMenu;
|
||||||
114
app/menus/menus/help.ts
Normal file
114
app/menus/menus/help.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
import {release} from 'os';
|
||||||
|
|
||||||
|
import {app, shell, dialog, clipboard} from 'electron';
|
||||||
|
import type {MenuItemConstructorOptions} from 'electron';
|
||||||
|
|
||||||
|
import {getConfig, getPlugins} from '../../config';
|
||||||
|
import {version} from '../../package.json';
|
||||||
|
|
||||||
|
const {arch, env, platform, versions} = process;
|
||||||
|
|
||||||
|
const helpMenu = (commands: Record<string, string>, showAbout: () => void): MenuItemConstructorOptions => {
|
||||||
|
const submenu: MenuItemConstructorOptions[] = [
|
||||||
|
{
|
||||||
|
label: `${app.name} Website`,
|
||||||
|
click() {
|
||||||
|
void shell.openExternal('https://hyper.is');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Report Issue',
|
||||||
|
click(menuItem, focusedWindow) {
|
||||||
|
const body = `<!--
|
||||||
|
Hi there! Thank you for discovering and submitting an issue.
|
||||||
|
Before you submit this; let's make sure of a few things.
|
||||||
|
Please make sure the following boxes are ✅ if they are correct.
|
||||||
|
If not, please try and fulfil these first.
|
||||||
|
-->
|
||||||
|
<!-- 👉 Checked checkbox should look like this: [x] -->
|
||||||
|
- [ ] 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)_:
|
||||||
|
<!-- 👉 Replace with info if applicable, or N/A -->
|
||||||
|
|
||||||
|
- **Is the issue reproducible in vanilla Hyper.app?**
|
||||||
|
<!-- 👉 Replace with info if applicable, or Is Vanilla. (Vanilla means Hyper.app without any add-ons or extras. Straight out of the box.) -->
|
||||||
|
|
||||||
|
## Issue
|
||||||
|
<!-- 👉 Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
<!-- hyper.json config -->
|
||||||
|
- **${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}
|
||||||
|
<details><summary><strong>hyper.json contents</strong></summary>
|
||||||
|
|
||||||
|
\`\`\`json
|
||||||
|
${JSON.stringify(getConfig(), null, 2)}
|
||||||
|
\`\`\`
|
||||||
|
</details>
|
||||||
|
<details><summary><strong>plugins</strong></summary>
|
||||||
|
|
||||||
|
\`\`\`json
|
||||||
|
${JSON.stringify(getPlugins(), null, 2)}
|
||||||
|
\`\`\`
|
||||||
|
</details>`;
|
||||||
|
|
||||||
|
const issueURL = `https://github.com/quine-global/hyper/issues/new?body=${encodeURIComponent(body)}`;
|
||||||
|
const copyAndSend = () => {
|
||||||
|
clipboard.writeText(body);
|
||||||
|
void shell.openExternal(
|
||||||
|
`https://github.com/quine-global/hyper/issues/new?body=${encodeURIComponent(
|
||||||
|
'<!-- We have written the needed data into your clipboard because it was too large to send. ' +
|
||||||
|
'Please paste. -->\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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
submenu.push(
|
||||||
|
{type: 'separator'},
|
||||||
|
{
|
||||||
|
label: 'About Hyper',
|
||||||
|
click() {
|
||||||
|
showAbout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
role: 'help',
|
||||||
|
submenu
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default helpMenu;
|
||||||
104
app/menus/menus/shell.ts
Normal file
104
app/menus/menus/shell.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import type {BaseWindow, MenuItemConstructorOptions} from 'electron';
|
||||||
|
|
||||||
|
const shellMenu = (
|
||||||
|
commandKeys: Record<string, string>,
|
||||||
|
execCommand: (command: string, focusedWindow?: BaseWindow) => void,
|
||||||
|
profiles: string[]
|
||||||
|
): MenuItemConstructorOptions => {
|
||||||
|
const isMac = process.platform === 'darwin';
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: isMac ? 'Shell' : 'File',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'New Tab',
|
||||||
|
accelerator: commandKeys['tab:new'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('tab:new', focusedWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'New Window',
|
||||||
|
accelerator: commandKeys['window:new'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('window:new', focusedWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Split Down',
|
||||||
|
accelerator: commandKeys['pane:splitDown'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('pane:splitDown', focusedWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Split Right',
|
||||||
|
accelerator: commandKeys['pane:splitRight'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('pane:splitRight', focusedWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('pane:close', focusedWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: isMac ? 'Close Window' : 'Quit',
|
||||||
|
role: 'close',
|
||||||
|
accelerator: commandKeys['window:close']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default shellMenu;
|
||||||
49
app/menus/menus/tools.ts
Normal file
49
app/menus/menus/tools.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron';
|
||||||
|
|
||||||
|
const toolsMenu = (
|
||||||
|
commands: Record<string, string>,
|
||||||
|
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'
|
||||||
|
? <MenuItemConstructorOptions[]>[
|
||||||
|
{
|
||||||
|
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;
|
||||||
59
app/menus/menus/view.ts
Normal file
59
app/menus/menus/view.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron';
|
||||||
|
|
||||||
|
const viewMenu = (
|
||||||
|
commandKeys: Record<string, string>,
|
||||||
|
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
|
||||||
|
): MenuItemConstructorOptions => {
|
||||||
|
return {
|
||||||
|
label: 'View',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Reload',
|
||||||
|
accelerator: commandKeys['window:reload'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('window:reload', focusedWindow as BrowserWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Full Reload',
|
||||||
|
accelerator: commandKeys['window:reloadFull'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('window:reloadFull', focusedWindow as BrowserWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Developer Tools',
|
||||||
|
accelerator: commandKeys['window:devtools'],
|
||||||
|
click: (item, focusedWindow) => {
|
||||||
|
execCommand('window:devtools', focusedWindow as BrowserWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Reset Zoom Level',
|
||||||
|
accelerator: commandKeys['zoom:reset'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('zoom:reset', focusedWindow as BrowserWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Zoom In',
|
||||||
|
accelerator: commandKeys['zoom:in'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('zoom:in', focusedWindow as BrowserWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Zoom Out',
|
||||||
|
accelerator: commandKeys['zoom:out'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('zoom:out', focusedWindow as BrowserWindow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default viewMenu;
|
||||||
98
app/menus/menus/window.ts
Normal file
98
app/menus/menus/window.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron';
|
||||||
|
|
||||||
|
const windowMenu = (
|
||||||
|
commandKeys: Record<string, string>,
|
||||||
|
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
|
||||||
|
): MenuItemConstructorOptions => {
|
||||||
|
// Generating tab:jump array
|
||||||
|
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}`;
|
||||||
|
tabJump.push({
|
||||||
|
label,
|
||||||
|
accelerator: commandKeys[`tab:jump:${label.toLowerCase()}`]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: 'window',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
role: 'minimize',
|
||||||
|
accelerator: commandKeys['window:minimize']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// It's the same thing as clicking the green traffc-light on macOS
|
||||||
|
role: 'zoom',
|
||||||
|
accelerator: commandKeys['window:zoom']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Select Tab',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Previous',
|
||||||
|
accelerator: commandKeys['tab:prev'],
|
||||||
|
click: (item, focusedWindow) => {
|
||||||
|
execCommand('tab:prev', focusedWindow as BrowserWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Next',
|
||||||
|
accelerator: commandKeys['tab:next'],
|
||||||
|
click: (item, focusedWindow) => {
|
||||||
|
execCommand('tab:next', focusedWindow as BrowserWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
...tabJump
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Select Pane',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Previous',
|
||||||
|
accelerator: commandKeys['pane:prev'],
|
||||||
|
click: (item, focusedWindow) => {
|
||||||
|
execCommand('pane:prev', focusedWindow as BrowserWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Next',
|
||||||
|
accelerator: commandKeys['pane:next'],
|
||||||
|
click: (item, focusedWindow) => {
|
||||||
|
execCommand('pane:next', focusedWindow as BrowserWindow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'front'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Toggle Always on Top',
|
||||||
|
click: (item, focusedWindow) => {
|
||||||
|
execCommand('window:toggleKeepOnTop', focusedWindow as BrowserWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'togglefullscreen',
|
||||||
|
accelerator: commandKeys['window:toggleFullScreen']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default windowMenu;
|
||||||
40
app/notifications.ts
Normal file
40
app/notifications.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import type {BrowserWindow} from 'electron';
|
||||||
|
|
||||||
|
import fetch from 'electron-fetch';
|
||||||
|
import ms from 'ms';
|
||||||
|
|
||||||
|
import {version} from './package.json';
|
||||||
|
|
||||||
|
const NEWS_URL = 'https://hyper-news.now.sh';
|
||||||
|
|
||||||
|
export default function fetchNotifications(win: BrowserWindow) {
|
||||||
|
const {rpc} = win;
|
||||||
|
const retry = (err?: Error) => {
|
||||||
|
setTimeout(() => fetchNotifications(win), ms('30m'));
|
||||||
|
if (err) {
|
||||||
|
console.error('Notification messages fetch error', err.stack);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
console.log('Checking for notification messages');
|
||||||
|
fetch(NEWS_URL, {
|
||||||
|
headers: {
|
||||||
|
'X-Hyper-Version': version,
|
||||||
|
'X-Hyper-Platform': process.platform
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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 === '') {
|
||||||
|
console.log('No matching notification messages');
|
||||||
|
} else {
|
||||||
|
rpc.emit('add notification', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
retry();
|
||||||
|
})
|
||||||
|
.catch(retry);
|
||||||
|
}
|
||||||
21
app/notify.ts
Normal file
21
app/notify.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import {app, Notification} from 'electron';
|
||||||
|
|
||||||
|
import {icon} from './config/paths';
|
||||||
|
|
||||||
|
export default function notify(title: string, body = '', details: {error?: any} = {}) {
|
||||||
|
console.log(`[Notification] ${title}: ${body}`);
|
||||||
|
if (details.error) {
|
||||||
|
console.error(details.error);
|
||||||
|
}
|
||||||
|
if (app.isReady()) {
|
||||||
|
_createNotification(title, body);
|
||||||
|
} else {
|
||||||
|
app.on('ready', () => {
|
||||||
|
_createNotification(title, body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _createNotification = (title: string, body: string) => {
|
||||||
|
new Notification({title, body, ...(process.platform === 'linux' && {icon})}).show();
|
||||||
|
};
|
||||||
51
app/package.json
Normal file
51
app/package.json
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"name": "hyper",
|
||||||
|
"productName": "Hyper",
|
||||||
|
"description": "A terminal built on web technologies",
|
||||||
|
"version": "4.0.0-q-canary.8",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": {
|
||||||
|
"name": "ZEIT, Inc.",
|
||||||
|
"email": "team@zeit.co"
|
||||||
|
},
|
||||||
|
"repository": "quine-global/hyper",
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "npx patch-package"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "7.27.0",
|
||||||
|
"@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-devtools-installer": "3.2.1",
|
||||||
|
"electron-fetch": "1.9.1",
|
||||||
|
"electron-is-dev": "2.0.0",
|
||||||
|
"electron-store": "8.2.0",
|
||||||
|
"fs-extra": "11.3.0",
|
||||||
|
"git-describe": "4.1.1",
|
||||||
|
"lodash": "4.17.21",
|
||||||
|
"ms": "2.1.3",
|
||||||
|
"native-process-working-directory": "^1.0.2",
|
||||||
|
"node-pty": "1.1.0-beta33",
|
||||||
|
"os-locale": "5.0.0",
|
||||||
|
"parse-url": "9.2.0",
|
||||||
|
"queue": "6.0.2",
|
||||||
|
"quine-electron-drag-click": "2.0.0",
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"recast": "0.23.11",
|
||||||
|
"semver": "7.7.1",
|
||||||
|
"shell-env": "3.0.1",
|
||||||
|
"sudo-prompt": "^9.2.1",
|
||||||
|
"uuid": "10.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"native-reg": "1.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"node-gyp": "^10.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
480
app/plugins.ts
Normal file
480
app/plugins.ts
Normal file
|
|
@ -0,0 +1,480 @@
|
||||||
|
/* 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 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 mapKeys from './utils/map-keys';
|
||||||
|
|
||||||
|
// local storage
|
||||||
|
const cache = new Config();
|
||||||
|
|
||||||
|
const path = plugs.base;
|
||||||
|
const localPath = plugs.local;
|
||||||
|
|
||||||
|
patchModuleLoad();
|
||||||
|
|
||||||
|
// caches
|
||||||
|
let plugins = config.getPlugins();
|
||||||
|
let paths = getPaths();
|
||||||
|
let id = getId(plugins);
|
||||||
|
let modules = requirePlugins();
|
||||||
|
|
||||||
|
function getId(plugins_: any) {
|
||||||
|
return JSON.stringify(plugins_);
|
||||||
|
}
|
||||||
|
|
||||||
|
const watchers: Function[] = [];
|
||||||
|
|
||||||
|
// we listen on configuration updates to trigger
|
||||||
|
// plugin installation
|
||||||
|
config.subscribe(() => {
|
||||||
|
const plugins_ = config.getPlugins();
|
||||||
|
if (plugins !== plugins_) {
|
||||||
|
const id_ = getId(plugins_);
|
||||||
|
if (id !== id_) {
|
||||||
|
id = id_;
|
||||||
|
plugins = plugins_;
|
||||||
|
updatePlugins();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// patching Module._load
|
||||||
|
// so plugins can `require` them without needing their own version
|
||||||
|
// https://github.com/vercel/hyper/issues/619
|
||||||
|
function patchModuleLoad() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const Module = require('module');
|
||||||
|
const originalLoad = Module._load;
|
||||||
|
Module._load = function _load(modulePath: string) {
|
||||||
|
// PLEASE NOTE: Code changes here, also need to be changed in
|
||||||
|
// lib/utils/plugins.js
|
||||||
|
switch (modulePath) {
|
||||||
|
case 'react':
|
||||||
|
// DEPRECATED
|
||||||
|
return React;
|
||||||
|
case 'react-dom':
|
||||||
|
// DEPRECATED
|
||||||
|
return ReactDom;
|
||||||
|
case 'hyper/component':
|
||||||
|
// DEPRECATED
|
||||||
|
return React.PureComponent;
|
||||||
|
// These return Object, since they work differently on the backend, than on the frontend.
|
||||||
|
// Still needs to be here, to prevent errors, while loading plugins.
|
||||||
|
case 'hyper/Notification':
|
||||||
|
case 'hyper/notify':
|
||||||
|
case 'hyper/decorate':
|
||||||
|
return Object;
|
||||||
|
default:
|
||||||
|
// eslint-disable-next-line prefer-rest-params
|
||||||
|
return originalLoad.apply(this, arguments);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkDeprecatedExtendKeymaps() {
|
||||||
|
modules.forEach((plugin) => {
|
||||||
|
if (plugin.extendKeymaps) {
|
||||||
|
notify('Plugin warning!', `"${plugin._name}" use deprecated "extendKeymaps" handler`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let updating = false;
|
||||||
|
|
||||||
|
function updatePlugins({force = false} = {}) {
|
||||||
|
if (updating) {
|
||||||
|
return notify('Plugin update in progress');
|
||||||
|
}
|
||||||
|
updating = true;
|
||||||
|
syncPackageJSON();
|
||||||
|
const id_ = id;
|
||||||
|
install((err) => {
|
||||||
|
updating = false;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
notify('Error updating plugins.', err, {error: err});
|
||||||
|
} else {
|
||||||
|
// flag successful plugin update
|
||||||
|
cache.set('hyper.plugins', id_);
|
||||||
|
|
||||||
|
// cache paths
|
||||||
|
paths = getPaths();
|
||||||
|
|
||||||
|
// clear require cache
|
||||||
|
clearCache();
|
||||||
|
|
||||||
|
// cache modules
|
||||||
|
modules = requirePlugins();
|
||||||
|
|
||||||
|
const loaded = modules.length;
|
||||||
|
const total = paths.plugins.length + paths.localPlugins.length;
|
||||||
|
const pluginVersions = JSON.stringify(getPluginVersions());
|
||||||
|
const changed = cache.get('hyper.plugin-versions') !== pluginVersions && loaded === total;
|
||||||
|
cache.set('hyper.plugin-versions', pluginVersions);
|
||||||
|
|
||||||
|
// notify watchers
|
||||||
|
watchers.forEach((fn) => {
|
||||||
|
fn(err, {force});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (force || changed) {
|
||||||
|
if (changed) {
|
||||||
|
notify('Plugins Updated', 'Restart the app or hot-reload with "View" > "Reload" to enjoy the updates!');
|
||||||
|
} else {
|
||||||
|
notify('Plugins Updated', 'No changes!');
|
||||||
|
}
|
||||||
|
checkDeprecatedExtendKeymaps();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPluginVersions() {
|
||||||
|
const paths_ = paths.plugins.concat(paths.localPlugins);
|
||||||
|
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) {}
|
||||||
|
return [basename(path_), version];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCache() {
|
||||||
|
// trigger unload hooks
|
||||||
|
modules.forEach((mod) => {
|
||||||
|
if (mod.onUnload) {
|
||||||
|
mod.onUnload(app);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// clear require cache
|
||||||
|
for (const entry in require.cache) {
|
||||||
|
if (entry.indexOf(path) === 0 || entry.indexOf(localPath) === 0) {
|
||||||
|
delete require.cache[entry];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {updatePlugins};
|
||||||
|
|
||||||
|
export const getLoadedPluginVersions = () => {
|
||||||
|
return modules.map((mod) => ({name: mod._name, version: mod._version}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// we schedule the initial plugins update
|
||||||
|
// a bit after the user launches the terminal
|
||||||
|
// to prevent slowness
|
||||||
|
if (cache.get('hyper.plugins') !== id || process.env.HYPER_FORCE_UPDATE) {
|
||||||
|
// install immediately if the user changed plugins
|
||||||
|
console.log('plugins have changed / not init, scheduling plugins installation');
|
||||||
|
setTimeout(() => {
|
||||||
|
updatePlugins();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const baseConfig = config.getConfig();
|
||||||
|
if (baseConfig['autoUpdatePlugins']) {
|
||||||
|
// otherwise update plugins every 5 hours
|
||||||
|
setInterval(updatePlugins, ms(baseConfig['autoUpdatePlugins'] === true ? '5h' : baseConfig['autoUpdatePlugins']));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
function syncPackageJSON() {
|
||||||
|
const dependencies = toDependencies(plugins);
|
||||||
|
const pkg = {
|
||||||
|
name: 'hyper-plugins',
|
||||||
|
description: 'Auto-generated from `hyper.json`!',
|
||||||
|
private: true,
|
||||||
|
version: '0.0.1',
|
||||||
|
repository: 'quine-global/hyper',
|
||||||
|
license: 'MIT',
|
||||||
|
homepage: 'https://hyper.is',
|
||||||
|
dependencies
|
||||||
|
};
|
||||||
|
|
||||||
|
const file = resolve(path, 'package.json');
|
||||||
|
try {
|
||||||
|
writeFileSync(file, JSON.stringify(pkg, null, 2));
|
||||||
|
} catch (err) {
|
||||||
|
alert(`An error occurred writing to ${file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function alert(message: string) {
|
||||||
|
void dialog.showMessageBox({
|
||||||
|
message,
|
||||||
|
buttons: ['Ok']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDependencies(plugins_: {plugins: string[]}) {
|
||||||
|
const obj: Record<string, string> = {};
|
||||||
|
plugins_.plugins.forEach((plugin) => {
|
||||||
|
const regex = /.(@|#)/;
|
||||||
|
const match = regex.exec(plugin);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const index = match.index + 1;
|
||||||
|
const pieces: string[] = [];
|
||||||
|
|
||||||
|
pieces[0] = plugin.substring(0, index);
|
||||||
|
pieces[1] = plugin.substring(index + 1, plugin.length);
|
||||||
|
obj[pieces[0]] = pieces[1];
|
||||||
|
} else {
|
||||||
|
obj[plugin] = 'latest';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const subscribe = (fn: Function) => {
|
||||||
|
watchers.push(fn);
|
||||||
|
return () => {
|
||||||
|
watchers.splice(watchers.indexOf(fn), 1);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function getPaths() {
|
||||||
|
return {
|
||||||
|
plugins: plugins.plugins.map((name) => {
|
||||||
|
return resolve(path, 'node_modules', name.split('#')[0]);
|
||||||
|
}),
|
||||||
|
localPlugins: plugins.localPlugins.map((name) => {
|
||||||
|
return resolve(localPath, name);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// expose to renderer
|
||||||
|
export {getPaths};
|
||||||
|
|
||||||
|
// get paths from renderer
|
||||||
|
export const getBasePaths = () => {
|
||||||
|
return {path, localPath};
|
||||||
|
};
|
||||||
|
|
||||||
|
function requirePlugins(): any[] {
|
||||||
|
const {plugins: plugins_, localPlugins} = paths;
|
||||||
|
|
||||||
|
const load = (path_: string) => {
|
||||||
|
let mod: Record<string, any>;
|
||||||
|
try {
|
||||||
|
mod = require(path_);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
console.warn(`No package.json found in ${path_}`);
|
||||||
|
}
|
||||||
|
console.log(`Plugin ${mod._name} (${mod._version}) loaded.`);
|
||||||
|
|
||||||
|
return mod;
|
||||||
|
} catch (_err) {
|
||||||
|
const err = _err as {code: string; message: string};
|
||||||
|
if (err.code === 'MODULE_NOT_FOUND') {
|
||||||
|
console.warn(`Plugin error while loading "${basename(path_)}" (${path_}): ${err.message}`);
|
||||||
|
} else {
|
||||||
|
notify('Plugin error!', `Plugin "${basename(path_)}" failed to load (${err.message})`, {error: err});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
...localPlugins.filter((p) => basename(p) === 'migrated-hyper3-config'),
|
||||||
|
...plugins_,
|
||||||
|
...localPlugins.filter((p) => basename(p) !== 'migrated-hyper3-config')
|
||||||
|
]
|
||||||
|
.map(load)
|
||||||
|
.filter((v): v is Record<string, any> => Boolean(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onApp = (app_: App) => {
|
||||||
|
modules.forEach((plugin) => {
|
||||||
|
if (plugin.onApp) {
|
||||||
|
try {
|
||||||
|
plugin.onApp(app_);
|
||||||
|
} catch (e) {
|
||||||
|
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`, {
|
||||||
|
error: e
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onWindowClass = (win: BrowserWindow) => {
|
||||||
|
modules.forEach((plugin) => {
|
||||||
|
if (plugin.onWindowClass) {
|
||||||
|
try {
|
||||||
|
plugin.onWindowClass(win);
|
||||||
|
} catch (e) {
|
||||||
|
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`, {
|
||||||
|
error: e
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onWindow = (win: BrowserWindow) => {
|
||||||
|
modules.forEach((plugin) => {
|
||||||
|
if (plugin.onWindow) {
|
||||||
|
try {
|
||||||
|
plugin.onWindow(win);
|
||||||
|
} catch (e) {
|
||||||
|
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`, {
|
||||||
|
error: e
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// decorates the base entity by calling plugin[key]
|
||||||
|
// for all the available plugins
|
||||||
|
function decorateEntity(base: any, key: string, type: 'object' | 'function') {
|
||||||
|
let decorated = base;
|
||||||
|
modules.forEach((plugin) => {
|
||||||
|
if (plugin[key]) {
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = plugin[key](decorated);
|
||||||
|
} catch (e) {
|
||||||
|
notify('Plugin error!', `"${plugin._name}" when decorating ${key}`, {error: e});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res && (!type || typeof res === type)) {
|
||||||
|
decorated = res;
|
||||||
|
} else {
|
||||||
|
notify('Plugin error!', `"${plugin._name}": invalid return type for \`${key}\``);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return decorated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decorateObject<T>(base: T, key: string): T {
|
||||||
|
return decorateEntity(base, key, 'object');
|
||||||
|
}
|
||||||
|
|
||||||
|
function decorateClass(base: any, key: string) {
|
||||||
|
return decorateEntity(base, key, 'function');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDeprecatedConfig = () => {
|
||||||
|
const deprecated: Record<string, {css: string[]}> = {};
|
||||||
|
const baseConfig = config.getConfig();
|
||||||
|
modules.forEach((plugin) => {
|
||||||
|
if (!plugin.decorateConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// We need to clone config in case of plugin modifies config directly.
|
||||||
|
let configTmp: configOptions;
|
||||||
|
try {
|
||||||
|
configTmp = plugin.decorateConfig(JSON.parse(JSON.stringify(baseConfig)));
|
||||||
|
} catch (e) {
|
||||||
|
notify('Plugin error!', `"${plugin._name}" has encountered an error. Check Developer Tools for details.`, {
|
||||||
|
error: e
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pluginCSSDeprecated = config.getDeprecatedCSS(configTmp);
|
||||||
|
if (pluginCSSDeprecated.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deprecated[plugin._name] = {css: pluginCSSDeprecated};
|
||||||
|
});
|
||||||
|
return deprecated;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decorateMenu = (tpl: MenuItemConstructorOptions[]) => {
|
||||||
|
return decorateObject(tpl, 'decorateMenu');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDecoratedEnv = (baseEnv: Record<string, string>) => {
|
||||||
|
return decorateObject(baseEnv, 'decorateEnv');
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
return translatedConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDecoratedKeymaps = () => {
|
||||||
|
const baseKeymaps = config.getKeymaps();
|
||||||
|
// Ensure that all keys are in an array and don't use deprecated key combination`
|
||||||
|
const decoratedKeymaps = mapKeys(decorateObject(baseKeymaps, 'decorateKeymaps'));
|
||||||
|
return decoratedKeymaps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDecoratedBrowserOptions = <T>(defaults: T): T => {
|
||||||
|
return decorateObject(defaults, 'decorateBrowserOptions');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decorateWindowClass = <T>(defaults: T): T => {
|
||||||
|
return decorateObject(defaults, 'decorateWindowClass');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decorateSessionOptions = <T>(defaults: T): T => {
|
||||||
|
return decorateObject(defaults, 'decorateSessionOptions');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decorateSessionClass = <T>(Session: T): T => {
|
||||||
|
return decorateClass(Session, 'decorateSessionClass');
|
||||||
|
};
|
||||||
|
|
||||||
|
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());
|
||||||
42
app/plugins/extensions.ts
Normal file
42
app/plugins/extensions.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
export const availableExtensions = new Set([
|
||||||
|
'onApp',
|
||||||
|
'onWindowClass',
|
||||||
|
'decorateWindowClass',
|
||||||
|
'onWindow',
|
||||||
|
'onRendererWindow',
|
||||||
|
'onUnload',
|
||||||
|
'decorateSessionClass',
|
||||||
|
'decorateSessionOptions',
|
||||||
|
'middleware',
|
||||||
|
'reduceUI',
|
||||||
|
'reduceSessions',
|
||||||
|
'reduceTermGroups',
|
||||||
|
'decorateBrowserOptions',
|
||||||
|
'decorateMenu',
|
||||||
|
'decorateTerm',
|
||||||
|
'decorateHyper',
|
||||||
|
'decorateHyperTerm', // for backwards compatibility with hyperterm
|
||||||
|
'decorateHeader',
|
||||||
|
'decorateTerms',
|
||||||
|
'decorateTab',
|
||||||
|
'decorateNotification',
|
||||||
|
'decorateNotifications',
|
||||||
|
'decorateTabs',
|
||||||
|
'decorateConfig',
|
||||||
|
'decorateKeymaps',
|
||||||
|
'decorateEnv',
|
||||||
|
'decorateTermGroup',
|
||||||
|
'decorateSplitPane',
|
||||||
|
'getTermProps',
|
||||||
|
'getTabProps',
|
||||||
|
'getTabsProps',
|
||||||
|
'getTermGroupProps',
|
||||||
|
'mapHyperTermState',
|
||||||
|
'mapTermsState',
|
||||||
|
'mapHeaderState',
|
||||||
|
'mapNotificationsState',
|
||||||
|
'mapHyperTermDispatch',
|
||||||
|
'mapTermsDispatch',
|
||||||
|
'mapHeaderDispatch',
|
||||||
|
'mapNotificationsDispatch'
|
||||||
|
]);
|
||||||
49
app/plugins/install.ts
Normal file
49
app/plugins/install.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import cp from 'child_process';
|
||||||
|
|
||||||
|
import ms from 'ms';
|
||||||
|
import queue from 'queue';
|
||||||
|
|
||||||
|
import {yarn, plugs} from '../config/paths';
|
||||||
|
|
||||||
|
export const install = (fn: (err: string | null) => void) => {
|
||||||
|
const spawnQueue = queue({concurrency: 1});
|
||||||
|
function yarnFn(args: string[], cb: (err: string | null) => void) {
|
||||||
|
const env = {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
ELECTRON_RUN_AS_NODE: 'true'
|
||||||
|
};
|
||||||
|
spawnQueue.push((end) => {
|
||||||
|
const cmd = [process.execPath, yarn].concat(args).join(' ');
|
||||||
|
console.log('Launching yarn:', cmd);
|
||||||
|
|
||||||
|
cp.execFile(
|
||||||
|
process.execPath,
|
||||||
|
[yarn].concat(args),
|
||||||
|
{
|
||||||
|
cwd: plugs.base,
|
||||||
|
env,
|
||||||
|
timeout: ms('5m'),
|
||||||
|
maxBuffer: 1024 * 1024
|
||||||
|
},
|
||||||
|
(err, stdout, stderr) => {
|
||||||
|
if (err) {
|
||||||
|
cb(stderr);
|
||||||
|
} else {
|
||||||
|
cb(null);
|
||||||
|
}
|
||||||
|
end?.();
|
||||||
|
spawnQueue.start();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
spawnQueue.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
yarnFn(['install', '--no-emoji', '--no-lockfile', '--cache-folder', plugs.cache], (err) => {
|
||||||
|
if (err) {
|
||||||
|
return fn(err);
|
||||||
|
}
|
||||||
|
fn(null);
|
||||||
|
});
|
||||||
|
};
|
||||||
83
app/rpc.ts
Normal file
83
app/rpc.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import {EventEmitter} from 'events';
|
||||||
|
|
||||||
|
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<MainEvents>;
|
||||||
|
destroyed = false;
|
||||||
|
win: BrowserWindow;
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
constructor(win: BrowserWindow) {
|
||||||
|
this.emitter = new EventEmitter();
|
||||||
|
this.win = win;
|
||||||
|
this.emit = this.emit.bind(this);
|
||||||
|
|
||||||
|
if (this.destroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uid = uuidv4();
|
||||||
|
this.id = uid;
|
||||||
|
|
||||||
|
ipcMain.on(uid, this.ipcListener);
|
||||||
|
|
||||||
|
// we intentionally subscribe to `on` instead of `once`
|
||||||
|
// to support reloading the window and re-initializing
|
||||||
|
// the channel
|
||||||
|
this.wc.on('did-finish-load', () => {
|
||||||
|
this.wc.send('init', uid, win.profileName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get wc() {
|
||||||
|
return this.win.webContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcListener = <U extends keyof MainEvents>(event: IpcMainEvent, {ev, data}: {ev: U; data: MainEvents[U]}) =>
|
||||||
|
this.emitter.emit(ev, data);
|
||||||
|
|
||||||
|
on = <U extends keyof MainEvents>(ev: U, fn: (arg0: MainEvents[U]) => void) => {
|
||||||
|
this.emitter.on(ev, fn);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
once = <U extends keyof MainEvents>(ev: U, fn: (arg0: MainEvents[U]) => void) => {
|
||||||
|
this.emitter.once(ev, fn);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
emit<U extends Exclude<keyof RendererEvents, FilterNever<RendererEvents>>>(ch: U): boolean;
|
||||||
|
emit<U extends FilterNever<RendererEvents>>(ch: U, data: RendererEvents[U]): boolean;
|
||||||
|
emit<U extends keyof RendererEvents>(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.emitter.removeAllListeners();
|
||||||
|
this.wc.removeAllListeners();
|
||||||
|
if (this.id) {
|
||||||
|
ipcMain.removeListener(this.id, this.ipcListener);
|
||||||
|
} else {
|
||||||
|
// mark for `genUid` in constructor
|
||||||
|
this.destroyed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createRPC = (win: BrowserWindow) => {
|
||||||
|
return new Server(win);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createRPC;
|
||||||
263
app/session.ts
Normal file
263
app/session.ts
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
import {EventEmitter} from 'events';
|
||||||
|
import {dirname} from 'path';
|
||||||
|
import {StringDecoder} from 'string_decoder';
|
||||||
|
|
||||||
|
import defaultShell from 'default-shell';
|
||||||
|
import type {IPty, IWindowsPtyForkOptions, spawn as npSpawn} from 'node-pty';
|
||||||
|
import osLocale from 'os-locale';
|
||||||
|
import shellEnv from 'shell-env';
|
||||||
|
|
||||||
|
import * as config from './config';
|
||||||
|
import {cliScriptPath} from './config/paths';
|
||||||
|
import {productName, version} from './package.json';
|
||||||
|
import {getDecoratedEnv} from './plugins';
|
||||||
|
import {getFallBackShellConfig} from './utils/shell-fallback';
|
||||||
|
|
||||||
|
const createNodePtyError = () =>
|
||||||
|
new Error(
|
||||||
|
'`node-pty` failed to load. Typically this means that it was built incorrectly. Please check the `readme.md` to more info.'
|
||||||
|
);
|
||||||
|
|
||||||
|
let spawn: typeof npSpawn;
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
spawn = require('node-pty').spawn;
|
||||||
|
} catch (err) {
|
||||||
|
throw createNodePtyError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const useConpty = config.getConfig().useConpty;
|
||||||
|
|
||||||
|
// Max duration to batch session data before sending it to the renderer process.
|
||||||
|
const BATCH_DURATION_MS = 16;
|
||||||
|
|
||||||
|
// Max size of a session data batch. Note that this value can be exceeded by ~4k
|
||||||
|
// (chunk sizes seem to be 4k at the most)
|
||||||
|
const BATCH_MAX_SIZE = 200 * 1024;
|
||||||
|
|
||||||
|
// Data coming from the pty is sent to the renderer process for further
|
||||||
|
// vt parsing and rendering. This class batches data to minimize the number of
|
||||||
|
// IPC calls. It also reduces GC pressure and CPU cost: each chunk is prefixed
|
||||||
|
// with the window ID which is then stripped on the renderer process and this
|
||||||
|
// overhead is reduced with batching.
|
||||||
|
class DataBatcher extends EventEmitter {
|
||||||
|
uid: string;
|
||||||
|
decoder: StringDecoder;
|
||||||
|
data!: string;
|
||||||
|
timeout!: NodeJS.Timeout | null;
|
||||||
|
constructor(uid: string) {
|
||||||
|
super();
|
||||||
|
this.uid = uid;
|
||||||
|
this.decoder = new StringDecoder('utf8');
|
||||||
|
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.data = this.uid;
|
||||||
|
this.timeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
this.timeout = null;
|
||||||
|
}
|
||||||
|
this.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data += typeof chunk === 'string' ? chunk : this.decoder.write(chunk);
|
||||||
|
|
||||||
|
if (!this.timeout) {
|
||||||
|
this.timeout = setTimeout(() => this.flush(), BATCH_DURATION_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flush() {
|
||||||
|
// Reset before emitting to allow for potential reentrancy
|
||||||
|
const data = this.data;
|
||||||
|
this.reset();
|
||||||
|
|
||||||
|
this.emit('flush', data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionOptions {
|
||||||
|
uid: 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, 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<string, string> = {
|
||||||
|
...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/vercel/hyper/issues/696
|
||||||
|
if (baseEnv.GOOGLE_API_KEY && process.env.GOOGLE_API_KEY === baseEnv.GOOGLE_API_KEY) {
|
||||||
|
delete baseEnv.GOOGLE_API_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: IWindowsPtyForkOptions = {
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
cwd,
|
||||||
|
env: getDecoratedEnv(baseEnv)
|
||||||
|
};
|
||||||
|
|
||||||
|
// if config do not set the useConpty, it will be judged by the node-pty
|
||||||
|
if (typeof useConpty === 'boolean') {
|
||||||
|
options.useConpty = useConpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
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 {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.batcher = new DataBatcher(uid);
|
||||||
|
this.pty.onData((chunk) => {
|
||||||
|
if (this.ended) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.batcher?.write(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.batcher.on('flush', (data: string) => {
|
||||||
|
this.emit('data', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pty.onExit((e) => {
|
||||||
|
if (!this.ended) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
exit() {
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
write(data: string) {
|
||||||
|
if (this.pty) {
|
||||||
|
this.pty.write(data);
|
||||||
|
} else {
|
||||||
|
console.warn('Warning: Attempted to write to a session with no pty');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resize({cols, rows}: {cols: number; rows: number}) {
|
||||||
|
if (this.pty) {
|
||||||
|
try {
|
||||||
|
this.pty.resize(cols, rows);
|
||||||
|
} catch (_err) {
|
||||||
|
const err = _err as {stack: any};
|
||||||
|
console.error(err.stack);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Warning: Attempted to resize a session with no pty');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.pty) {
|
||||||
|
try {
|
||||||
|
this.pty.kill();
|
||||||
|
} catch (_err) {
|
||||||
|
const err = _err as {stack: any};
|
||||||
|
console.error('exit error', err.stack);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Warning: Attempted to destroy a session with no pty');
|
||||||
|
}
|
||||||
|
this.emit('exit');
|
||||||
|
this.ended = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/static/icon.png
Normal file
BIN
app/static/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
BIN
app/static/icon96x96.png
Normal file
BIN
app/static/icon96x96.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
20
app/tsconfig.json
Normal file
20
app/tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"declarationDir": "../dist/tmp/appdts/",
|
||||||
|
"outDir": "../target/",
|
||||||
|
"composite": true,
|
||||||
|
"noImplicitAny": false
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*",
|
||||||
|
"./package.json",
|
||||||
|
"../typings/extend-electron.d.ts",
|
||||||
|
"../typings/ext-modules.d.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"../dist/**/*",
|
||||||
|
"../target/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
42
app/ui/contextmenu.ts
Normal file
42
app/ui/contextmenu.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
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 {getDecoratedKeymaps} from '../plugins';
|
||||||
|
|
||||||
|
const separator: MenuItemConstructorOptions = {type: 'separator'};
|
||||||
|
|
||||||
|
const getCommandKeys = (keymaps: Record<string, string[]>): Record<string, string> =>
|
||||||
|
Object.keys(keymaps).reduce((commandKeys: Record<string, string>, command) => {
|
||||||
|
return Object.assign(commandKeys, {
|
||||||
|
[command]: keymaps[command][0]
|
||||||
|
});
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// only display cut/copy when there's a cursor selection
|
||||||
|
const filterCutCopy = (selection: string, menuItem: MenuItemConstructorOptions) => {
|
||||||
|
if (/^cut$|^copy$/.test(menuItem.role!) && !selection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return menuItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextMenuTemplate = (
|
||||||
|
createWindow: (fn?: (win: BrowserWindow) => void, options?: Record<string, any>) => BrowserWindow,
|
||||||
|
selection: string
|
||||||
|
) => {
|
||||||
|
const commandKeys = getCommandKeys(getDecoratedKeymaps());
|
||||||
|
const _shell = shellMenu(
|
||||||
|
commandKeys,
|
||||||
|
(command, focusedWindow) => execCommand(command, focusedWindow as BrowserWindow | undefined),
|
||||||
|
getProfiles().map((p) => p.name)
|
||||||
|
).submenu as MenuItemConstructorOptions[];
|
||||||
|
const _edit = editMenu(commandKeys, execCommand).submenu.filter(filterCutCopy.bind(null, selection));
|
||||||
|
return _edit
|
||||||
|
.concat(separator, _shell)
|
||||||
|
.filter((menuItem) => !Object.prototype.hasOwnProperty.call(menuItem, 'enabled') || menuItem.enabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default contextMenuTemplate;
|
||||||
376
app/ui/window.ts
Normal file
376
app/ui/window.ts
Normal file
|
|
@ -0,0 +1,376 @@
|
||||||
|
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 {getWorkingDirectoryFromPID} from 'native-process-working-directory';
|
||||||
|
import electronDragClick from 'quine-electron-drag-click';
|
||||||
|
import {v4 as uuidv4} from 'uuid';
|
||||||
|
|
||||||
|
import type {sessionExtraOptions} from '../../typings/common';
|
||||||
|
import type {configOptions} from '../../typings/config';
|
||||||
|
import {execCommand} from '../commands';
|
||||||
|
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';
|
||||||
|
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
electronDragClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newWindow(
|
||||||
|
options_: BrowserWindowConstructorOptions,
|
||||||
|
cfg: configOptions,
|
||||||
|
fn?: (win: BrowserWindow) => void,
|
||||||
|
profileName: string = getDefaultProfile()
|
||||||
|
): BrowserWindow {
|
||||||
|
const classOpts = Object.assign({uid: uuidv4()});
|
||||||
|
app.plugins.decorateWindowClass(classOpts);
|
||||||
|
|
||||||
|
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_
|
||||||
|
};
|
||||||
|
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<string, Session>();
|
||||||
|
|
||||||
|
const updateBackgroundColor = () => {
|
||||||
|
const cfg_ = app.plugins.getDecoratedConfig(profileName);
|
||||||
|
window.setBackgroundColor(toElectronBackgroundColor(cfg_.backgroundColor || '#000'));
|
||||||
|
};
|
||||||
|
|
||||||
|
// config changes
|
||||||
|
const cfgUnsubscribe = app.config.subscribe(() => {
|
||||||
|
const cfg_ = app.plugins.getDecoratedConfig(profileName);
|
||||||
|
|
||||||
|
// notify renderer
|
||||||
|
window.webContents.send('config change');
|
||||||
|
|
||||||
|
// notify user that shell changes require new sessions
|
||||||
|
if (cfg_.shell !== cfg.shell || JSON.stringify(cfg_.shellArgs) !== JSON.stringify(cfg.shellArgs)) {
|
||||||
|
notify('Shell configuration changed!', 'Open a new tab or window to start using the new shell');
|
||||||
|
}
|
||||||
|
|
||||||
|
// update background color if necessary
|
||||||
|
updateBackgroundColor();
|
||||||
|
|
||||||
|
cfg = cfg_;
|
||||||
|
});
|
||||||
|
|
||||||
|
rpc.on('init', () => {
|
||||||
|
window.show();
|
||||||
|
updateBackgroundColor();
|
||||||
|
|
||||||
|
// 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', {});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// app.windowCallback is the createWindow callback
|
||||||
|
// that can be set before the 'ready' app event
|
||||||
|
// and createWindow definition. It's executed in place of
|
||||||
|
// the callback passed as parameter, and deleted right after.
|
||||||
|
(app.windowCallback || fn)(window);
|
||||||
|
app.windowCallback = undefined;
|
||||||
|
fetchNotifications(window);
|
||||||
|
// auto updates
|
||||||
|
if (!isDev) {
|
||||||
|
updater(window);
|
||||||
|
} else {
|
||||||
|
console.log('ignoring auto updates during dev');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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: cwd || workingDirectory,
|
||||||
|
splitDirection: undefined,
|
||||||
|
shell: profileCfg.shell,
|
||||||
|
shellArgs: profileCfg.shellArgs && Array.from(profileCfg.shellArgs)
|
||||||
|
},
|
||||||
|
extraOptionsFiltered,
|
||||||
|
{
|
||||||
|
profile: extraOptionsFiltered.profile || profileName,
|
||||||
|
uid
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const options = decorateSessionOptions(defaultOptions);
|
||||||
|
const DecoratedSession = decorateSessionClass(Session);
|
||||||
|
const session = new DecoratedSession(options);
|
||||||
|
sessions.set(uid, session);
|
||||||
|
return {session, options};
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc.on('new', (extraOptions) => {
|
||||||
|
const {session, options} = createSession(extraOptions);
|
||||||
|
|
||||||
|
sessions.set(options.uid, session);
|
||||||
|
rpc.emit('session add', {
|
||||||
|
rows: options.rows,
|
||||||
|
cols: options.cols,
|
||||||
|
uid: options.uid,
|
||||||
|
splitDirection: options.splitDirection,
|
||||||
|
shell: session.shell,
|
||||||
|
pid: session.pty ? session.pty.pid : null,
|
||||||
|
activeUid: options.activeUid ?? undefined,
|
||||||
|
profile: options.profile
|
||||||
|
});
|
||||||
|
|
||||||
|
session.on('data', (data: string) => {
|
||||||
|
rpc.emit('session data', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
session.on('exit', () => {
|
||||||
|
rpc.emit('session exit', {uid: options.uid});
|
||||||
|
unsetRendererType(options.uid);
|
||||||
|
sessions.delete(options.uid);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
rpc.on('exit', ({uid}) => {
|
||||||
|
const session = sessions.get(uid);
|
||||||
|
if (session) {
|
||||||
|
session.exit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rpc.on('unmaximize', () => {
|
||||||
|
window.unmaximize();
|
||||||
|
});
|
||||||
|
rpc.on('maximize', () => {
|
||||||
|
window.maximize();
|
||||||
|
});
|
||||||
|
rpc.on('minimize', () => {
|
||||||
|
window.minimize();
|
||||||
|
});
|
||||||
|
rpc.on('resize', ({uid, cols, rows}) => {
|
||||||
|
const session = sessions.get(uid);
|
||||||
|
if (session) {
|
||||||
|
session.resize({cols, rows});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rpc.on('data', ({uid, data, escaped}) => {
|
||||||
|
const session = uid && sessions.get(uid);
|
||||||
|
if (session) {
|
||||||
|
if (escaped) {
|
||||||
|
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
|
||||||
|
|
||||||
|
session.write(escapedData);
|
||||||
|
} else {
|
||||||
|
session.write(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rpc.on('info renderer', ({uid, type}) => {
|
||||||
|
// Used in the "About" dialog
|
||||||
|
setRendererType(uid, type);
|
||||||
|
});
|
||||||
|
rpc.on('open external', ({url}) => {
|
||||||
|
void shell.openExternal(url);
|
||||||
|
});
|
||||||
|
rpc.on('open context menu', (selection) => {
|
||||||
|
const {createWindow} = app;
|
||||||
|
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)});
|
||||||
|
});
|
||||||
|
// Same deal as above, grabbing the window titlebar when the window
|
||||||
|
// is maximized on Windows results in unmaximize, without hitting any
|
||||||
|
// app buttons
|
||||||
|
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]}});
|
||||||
|
});
|
||||||
|
rpc.on('close', () => {
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
|
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.win.on('leave-full-screen', () => {
|
||||||
|
rpc.emit('leave full screen');
|
||||||
|
});
|
||||||
|
const deleteSessions = () => {
|
||||||
|
sessions.forEach((session, key) => {
|
||||||
|
session.removeAllListeners();
|
||||||
|
session.destroy();
|
||||||
|
sessions.delete(key);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// we reset the rpc channel only upon
|
||||||
|
// subsequent refreshes (ie: F5)
|
||||||
|
let i = 0;
|
||||||
|
window.webContents.on('did-navigate', () => {
|
||||||
|
if (i++) {
|
||||||
|
deleteSessions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDroppedURL = (url: string) => {
|
||||||
|
const protocol = typeof url === 'string' && new URL(url).protocol;
|
||||||
|
if (protocol === 'file:') {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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
|
||||||
|
window.rpc = rpc;
|
||||||
|
window.sessions = sessions;
|
||||||
|
|
||||||
|
const load = () => {
|
||||||
|
app.plugins.onWindow(window);
|
||||||
|
};
|
||||||
|
|
||||||
|
// load plugins
|
||||||
|
load();
|
||||||
|
|
||||||
|
const pluginsUnsubscribe = app.plugins.subscribe((err: any) => {
|
||||||
|
if (!err) {
|
||||||
|
load();
|
||||||
|
window.webContents.send('plugins change');
|
||||||
|
updateBackgroundColor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep track of focus time of every window, to figure out
|
||||||
|
// which one of the existing window is the last focused.
|
||||||
|
// Works nicely even if a window is closed and removed.
|
||||||
|
const updateFocusTime = () => {
|
||||||
|
window.focusTime = process.uptime();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.on('focus', () => {
|
||||||
|
updateFocusTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
// the window can be closed by the browser process itself
|
||||||
|
window.clean = () => {
|
||||||
|
app.config.winRecord(window);
|
||||||
|
rpc.destroy();
|
||||||
|
deleteSessions();
|
||||||
|
cfgUnsubscribe();
|
||||||
|
pluginsUnsubscribe();
|
||||||
|
};
|
||||||
|
// Ensure focusTime is set on window open. The focus event doesn't
|
||||||
|
// fire from the dock (see bug #583)
|
||||||
|
updateFocusTime();
|
||||||
|
|
||||||
|
return window;
|
||||||
|
}
|
||||||
134
app/updater.ts
Normal file
134
app/updater.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
// Packages
|
||||||
|
import electron, {app} from 'electron';
|
||||||
|
import type {BrowserWindow, AutoUpdater as OriginalAutoUpdater} 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';
|
||||||
|
|
||||||
|
// Necessary due to typescript not handling overloads well
|
||||||
|
type AutoUpdaterEvent =
|
||||||
|
| 'error'
|
||||||
|
| 'checking-for-update'
|
||||||
|
| 'before-quit-for-update'
|
||||||
|
| 'update-downloaded'
|
||||||
|
| 'update-available'
|
||||||
|
| 'update-not-available';
|
||||||
|
|
||||||
|
interface AutoUpdater extends OriginalAutoUpdater {
|
||||||
|
on(event: AutoUpdaterEvent, listener: Function): this;
|
||||||
|
removeListener(event: AutoUpdaterEvent, listener: Function): this;
|
||||||
|
}
|
||||||
|
|
||||||
|
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/quine-global/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;
|
||||||
159
app/utils/cli-install.ts
Normal file
159
app/utils/cli-install.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
import {existsSync, readlink, symlink} from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
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';
|
||||||
|
|
||||||
|
const readLink = promisify(readlink);
|
||||||
|
const symLink = promisify(symlink);
|
||||||
|
const sudoExec = promisify(sudoPrompt.exec);
|
||||||
|
|
||||||
|
const checkInstall = () => {
|
||||||
|
return readLink(cliLinkPath)
|
||||||
|
.then((link) => link === cliScriptPath)
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.code === 'ENOENT') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSymlink = async (silent: boolean) => {
|
||||||
|
try {
|
||||||
|
const isInstalled = await checkInstall();
|
||||||
|
if (isInstalled) {
|
||||||
|
console.log('Hyper CLI already in PATH');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Linking HyperCLI');
|
||||||
|
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<void>((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const envKey = Registry.openKey(Registry.HKCU, 'Environment', Registry.Access.ALL_ACCESS)!;
|
||||||
|
|
||||||
|
// C:\Users\<user>\AppData\Local\Programs\hyper\resources\bin
|
||||||
|
const binPath = path.dirname(cliScriptPath);
|
||||||
|
// C:\Users\<user>\AppData\Local\hyper
|
||||||
|
const oldPath = path.resolve(process.env.LOCALAPPDATA!, 'hyper');
|
||||||
|
|
||||||
|
const items = Registry.enumValueNames(envKey);
|
||||||
|
const pathItem = items.find((item) => item.toUpperCase() === 'PATH');
|
||||||
|
const pathItemName = pathItem || 'PATH';
|
||||||
|
|
||||||
|
let newPathValue = binPath;
|
||||||
|
let type: ValueType = Registry.ValueType.SZ;
|
||||||
|
if (pathItem) {
|
||||||
|
type = Registry.queryValueRaw(envKey, pathItem)!.type;
|
||||||
|
if (type !== Registry.ValueType.SZ && type !== Registry.ValueType.EXPAND_SZ) {
|
||||||
|
reject(`Registry key type is ${type}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const value = Registry.queryValue(envKey, pathItem) as string;
|
||||||
|
let pathParts = value.split(';');
|
||||||
|
const existingPath = pathParts.includes(binPath);
|
||||||
|
const existingOldPath = pathParts.some((pathPart) => pathPart.startsWith(oldPath));
|
||||||
|
if (existingPath && !existingOldPath) {
|
||||||
|
console.log('Hyper CLI already in PATH');
|
||||||
|
Registry.closeKey(envKey);
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(';');
|
||||||
|
}
|
||||||
|
console.log('Adding HyperCLI path (registry)');
|
||||||
|
Registry.setValueRaw(envKey, pathItemName, type, Registry.formatString(newPathValue));
|
||||||
|
Registry.closeKey(envKey);
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const logNotify = (withNotification: boolean, title: string, body: string, details?: {error?: any}) => {
|
||||||
|
console.log(title, body, details);
|
||||||
|
withNotification && notify(title, body, details);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const installCLI = async (withNotification: boolean) => {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
try {
|
||||||
|
await addBinToUserPath();
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
} 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 {
|
||||||
|
logNotify(withNotification, 'Hyper CLI installation failed', `Unsupported platform ${process.platform}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
36
app/utils/colors.ts
Normal file
36
app/utils/colors.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
const colorList = [
|
||||||
|
'black',
|
||||||
|
'red',
|
||||||
|
'green',
|
||||||
|
'yellow',
|
||||||
|
'blue',
|
||||||
|
'magenta',
|
||||||
|
'cyan',
|
||||||
|
'white',
|
||||||
|
'lightBlack',
|
||||||
|
'lightRed',
|
||||||
|
'lightGreen',
|
||||||
|
'lightYellow',
|
||||||
|
'lightBlue',
|
||||||
|
'lightMagenta',
|
||||||
|
'lightCyan',
|
||||||
|
'lightWhite',
|
||||||
|
'colorCubes',
|
||||||
|
'grayscale'
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getColorMap: {
|
||||||
|
<T>(colors: T): T extends (infer U)[] ? {[k: string]: U} : T;
|
||||||
|
} = (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;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
43
app/utils/map-keys.ts
Normal file
43
app/utils/map-keys.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
const generatePrefixedCommand = (command: string, shortcuts: string[]) => {
|
||||||
|
const result: Record<string, string[]> = {};
|
||||||
|
const baseCmd = command.replace(/:prefix$/, '');
|
||||||
|
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}`);
|
||||||
|
result[`${baseCmd}:${index}`] = prefixedShortcuts;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapKeys = (config: Record<string, string[] | string>) => {
|
||||||
|
return Object.keys(config).reduce((keymap: Record<string, string[]>, command: string) => {
|
||||||
|
if (!command) {
|
||||||
|
return keymap;
|
||||||
|
}
|
||||||
|
// We can have different keys for a same command.
|
||||||
|
const _shortcuts = config[command];
|
||||||
|
const shortcuts = Array.isArray(_shortcuts) ? _shortcuts : [_shortcuts];
|
||||||
|
const fixedShortcuts: string[] = [];
|
||||||
|
shortcuts.forEach((shortcut) => {
|
||||||
|
let newShortcut = shortcut;
|
||||||
|
if (newShortcut.indexOf('cmd') !== -1) {
|
||||||
|
// Mousetrap use `command` and not `cmd`
|
||||||
|
console.warn('Your config use deprecated `cmd` in key combination. Please use `command` instead.');
|
||||||
|
newShortcut = newShortcut.replace('cmd', 'command');
|
||||||
|
}
|
||||||
|
fixedShortcuts.push(newShortcut);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (command.endsWith(':prefix')) {
|
||||||
|
return Object.assign(keymap, generatePrefixedCommand(command, fixedShortcuts));
|
||||||
|
}
|
||||||
|
|
||||||
|
keymap[command] = fixedShortcuts;
|
||||||
|
|
||||||
|
return keymap;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default mapKeys;
|
||||||
15
app/utils/renderer-utils.ts
Normal file
15
app/utils/renderer-utils.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
const rendererTypes: Record<string, string> = {};
|
||||||
|
|
||||||
|
function getRendererTypes() {
|
||||||
|
return rendererTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRendererType(uid: string, type: string) {
|
||||||
|
rendererTypes[uid] = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsetRendererType(uid: string) {
|
||||||
|
delete rendererTypes[uid];
|
||||||
|
}
|
||||||
|
|
||||||
|
export {getRendererTypes, setRendererType, unsetRendererType};
|
||||||
25
app/utils/shell-fallback.ts
Normal file
25
app/utils/shell-fallback.ts
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
60
app/utils/system-context-menu.ts
Normal file
60
app/utils/system-context-menu.ts
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
19
app/utils/to-electron-background-color.ts
Normal file
19
app/utils/to-electron-background-color.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Packages
|
||||||
|
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…)
|
||||||
|
const toElectronBackgroundColor = (bgColor: string) => {
|
||||||
|
const color = Color(bgColor);
|
||||||
|
|
||||||
|
if (color.alpha() === 1) {
|
||||||
|
return color.hex().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://stackoverflow.com/a/11019879/1202488
|
||||||
|
const alphaHex = Math.round(color.alpha() * 255).toString(16);
|
||||||
|
return `#${alphaHex}${color.hex().toString().slice(1)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default toElectronBackgroundColor;
|
||||||
10
app/utils/window-utils.ts
Normal file
10
app/utils/window-utils.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import electron from 'electron';
|
||||||
|
|
||||||
|
export function positionIsValid(position: [number, number]) {
|
||||||
|
const displays = electron.screen.getAllDisplays();
|
||||||
|
const [x, y] = position;
|
||||||
|
|
||||||
|
return displays.some(({workArea}) => {
|
||||||
|
return x >= workArea.x && x <= workArea.x + workArea.width && y >= workArea.y && y <= workArea.y + workArea.height;
|
||||||
|
});
|
||||||
|
}
|
||||||
1620
app/yarn.lock
Normal file
1620
app/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
61
assets/icons.svg
Normal file
61
assets/icons.svg
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<svg display="none" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<defs>
|
||||||
|
<symbol id="close-tab" viewBox="0 0 24 24">
|
||||||
|
<title>close tab</title>
|
||||||
|
<g><path d='M13.1919001,11.9997324 L23.7528721,22.5607045 C24.0822321,22.8904941 24.0822321,23.4241533 23.7528721,23.7526581 C23.4235121,24.0824473 22.8898521,24.0824473 22.5609201,23.7526581 L11.999948,13.1916857 L1.43897601,23.7526581 C1.109612,24.0824473 0.575952002,24.0824473 0.247020001,23.7526581 C-0.0823400003,23.4237253 -0.0823400003,22.8900657 0.247020001,22.5607045 L10.80842,11.9997324 L0.247020001,1.43961681 C-0.0823400003,1.110684 -0.0823400003,0.576168002 0.247020001,0.247663201 C0.576384002,-0.0825544003 1.11004,-0.0825544003 1.43897601,0.247663201 L11.999948,10.8082072 L22.5609201,0.247663201 C22.8902801,-0.0825544003 23.4239401,-0.0825544003 23.7528721,0.247663201 C24.0822321,0.576596002 24.0822321,1.111112 23.7528721,1.43961681 L13.1919001,11.9997324 L13.1919001,11.9997324 L13.1919001,11.9997324 Z'></path></g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="hamburger-menu" viewBox="0 0 10 10">
|
||||||
|
<title>hamburger menu</title>
|
||||||
|
<rect y="0.5" width="10" height="1" fill="currentColor"/>
|
||||||
|
<rect y="4.5" width="10" height="1" fill="currentColor"/>
|
||||||
|
<rect y="8.5" width="10" height="1" fill="currentColor"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="minimize-window" viewBox="0 0 10 10">
|
||||||
|
<title>minimize window</title>
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<polygon points="0 0 10 0 10 10 0 10"/>
|
||||||
|
<path stroke="currentColor" d="M9.5,5 L0.5,5" stroke-linecap="square"/>
|
||||||
|
</g>
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<polygon points="0 0 10 0 10 10 0 10"/>
|
||||||
|
<rect width="10" height="1" y="4.5" fill="currentColor"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="maximize-window" viewBox="0 0 10 10">
|
||||||
|
<title>maximize window</title>
|
||||||
|
<defs>
|
||||||
|
<polygon id="maximize-window-a" points="0 0 10 0 10 10 0 10"/>
|
||||||
|
<mask id="maximize-window-b" width="10" height="10" x="0" y="0">
|
||||||
|
<use xlink:href="#maximize-window-a"/>
|
||||||
|
</mask>
|
||||||
|
</defs>
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<polygon fill="none" points="0 0 10 0 10 10 0 10"/>
|
||||||
|
<use stroke="currentColor" stroke-width="2" mask="url(#maximize-window-b)" xlink:href="#maximize-window-a"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="restore-window" viewBox="0 0 10.2 10.2">
|
||||||
|
<title>restore window</title>
|
||||||
|
<defs>
|
||||||
|
<mask id="restore-window-b" width="10.2" height="10.2" x="0" y="0">
|
||||||
|
<use xlink:href="#restore-window-a"/>
|
||||||
|
</mask>
|
||||||
|
</defs>
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d='M2.1,0v2H0v8.1h8.2v-2h2V0H2.1z M7.2,9.2H1.1V3h6.1V9.2z M9.2,7.1h-1V2H3.1V1h6.1V7.1z' />
|
||||||
|
<use stroke="currentColor" xlink:href="#restore-window-a"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="close-window" viewBox="0 0 10 10">
|
||||||
|
<title>close window</title>
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<g stroke="currentColor" transform="translate(.25 .25)" stroke-linecap="square">
|
||||||
|
<path d="M0.5,0.5 L9,9"/>
|
||||||
|
<path d="M0.5,0.5 L9,9" transform="matrix(-1 0 0 1 9.5 0)"/>
|
||||||
|
</g>
|
||||||
|
<polygon points="0 0 10 0 10 10 0 10"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
9
ava-e2e.config.js
Normal file
9
ava-e2e.config.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports = {
|
||||||
|
files: ['test/*'],
|
||||||
|
extensions: ['ts'],
|
||||||
|
require: ['ts-node/register/transpile-only'],
|
||||||
|
timeout: '2m',
|
||||||
|
verbose: true,
|
||||||
|
// Due to permissions issues, Windows needs cache turned off
|
||||||
|
cache: false
|
||||||
|
};
|
||||||
8
ava.config.js
Normal file
8
ava.config.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
module.exports = {
|
||||||
|
files: ['test/unit/*'],
|
||||||
|
extensions: ['ts'],
|
||||||
|
require: ['ts-node/register/transpile-only'],
|
||||||
|
verbose: true,
|
||||||
|
// Due to permissions issues, Windows needs cache turned off
|
||||||
|
cache: false
|
||||||
|
};
|
||||||
18
babel.config.json
Normal file
18
babel.config.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"@babel/react",
|
||||||
|
"@babel/typescript"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"styled-jsx/babel",
|
||||||
|
{
|
||||||
|
"vendorPrefixes": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@babel/plugin-proposal-numeric-separator",
|
||||||
|
"@babel/proposal-class-properties",
|
||||||
|
"@babel/proposal-object-rest-spread",
|
||||||
|
"@babel/plugin-proposal-optional-chaining"
|
||||||
|
]
|
||||||
|
}
|
||||||
88
bin/cp-snapshot.js
vendored
Normal file
88
bin/cp-snapshot.js
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const fsPromises = require('fs/promises');
|
||||||
|
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.mkdirSync(pathToElectron, { recursive: true });
|
||||||
|
fs.copyFileSync(pathToBlob, path.join(pathToElectron, snapshotFileName));
|
||||||
|
fs.copyFileSync(pathToBlobV8, path.join(pathToElectron, v8ContextFileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPathToElectron() {
|
||||||
|
const electronPath = require.resolve('electron');
|
||||||
|
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'darwin':
|
||||||
|
return path.resolve(
|
||||||
|
electronPath,
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources'
|
||||||
|
);
|
||||||
|
case 'win32':
|
||||||
|
case 'linux':
|
||||||
|
return path.resolve(electronPath, '..', '..', '..', 'dist');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getV8ContextFileName(archToCopy) {
|
||||||
|
return `snapshot_blob.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);
|
||||||
|
useLoaderScriptFix(context);
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// copied and modified from https://github.com/gergof/electron-builder-sandbox-fix/blob/master/lib/index.js
|
||||||
|
// copied and modified from https://github.com/Adamant-im/adamant-im/blob/7b20272a717833ffb0b49b034ab9974118fc59ec/scripts/electron/sandboxFix.js
|
||||||
|
|
||||||
|
const useLoaderScriptFix = async (params) => {
|
||||||
|
if (params.electronPlatformName !== 'linux') {
|
||||||
|
// this fix is only required on linux
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const executable = path.join(params.appOutDir, params.packager.executableName)
|
||||||
|
|
||||||
|
const loaderScript = `#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
exec "$SCRIPT_DIR/${params.packager.executableName}.bin" "--no-sandbox" "$@"
|
||||||
|
`
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fsPromises.rename(executable, executable + '.bin')
|
||||||
|
await fsPromises.writeFile(executable, loaderScript)
|
||||||
|
await fsPromises.chmod(executable, 0o755)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('failed to create loader for sandbox fix: ' + e.message)
|
||||||
|
throw new Error('Failed to create loader for sandbox fix')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('sandbox fix successfully applied')
|
||||||
|
}
|
||||||
84
bin/mk-snapshot.js
vendored
Normal file
84
bin/mk-snapshot.js
vendored
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
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'];
|
||||||
|
|
||||||
|
const archMap = {
|
||||||
|
x64: 'x86_64',
|
||||||
|
arm64: 'arm64'
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const npmConfigArch = process.env.npm_config_arch;
|
||||||
|
if (!npmConfigArch) {
|
||||||
|
throw new Error('env var npm_config_arch is not specified')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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/${npmConfigArch}`;
|
||||||
|
await mkdirp(outputBlobPath);
|
||||||
|
|
||||||
|
let mksnapshotBinPath
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
mksnapshotBinPath =
|
||||||
|
require.resolve(
|
||||||
|
path.join("electron-mksnapshot", "bin", "mksnapshot.exe")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
mksnapshotBinPath =
|
||||||
|
require.resolve(
|
||||||
|
path.join("electron-mksnapshot", "bin", "mksnapshot")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mksnapshotBinPath = path.dirname(mksnapshotBinPath);
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startupBlobPath = path.join(outputBlobPath, 'snapshot_blob.bin');
|
||||||
|
|
||||||
|
console.log(`Generating startup blob in "${outputBlobPath}"`);
|
||||||
|
const res = childProcess.execFileSync(
|
||||||
|
require.resolve(`electron-mksnapshot/bin/mksnapshot${process.platform === 'win32' ? '.exe' : ''}`),
|
||||||
|
[
|
||||||
|
'--startup-src=' + snapshotScriptPath,
|
||||||
|
'--startup-blob=' + startupBlobPath,
|
||||||
|
`--target-arch=${archMap[process.env.npm_config_arch]}`,
|
||||||
|
//'--v8-context-snapshot=' + v8SnapshotPath
|
||||||
|
]
|
||||||
|
);
|
||||||
|
console.log('result:', res.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => console.error(err));
|
||||||
17
bin/notarize.js
vendored
Normal file
17
bin/notarize.js
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
exports.default = async function notarizing(context) {
|
||||||
|
|
||||||
|
const { electronPlatformName, appOutDir } = context;
|
||||||
|
if (electronPlatformName !== "darwin" || !process.env.APPLE_ID || !process.env.APPLE_PASSWORD) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { notarize } = await import('@electron/notarize');
|
||||||
|
|
||||||
|
const appName = context.packager.appInfo.productFilename;
|
||||||
|
return await notarize({
|
||||||
|
appBundleId: "com.quineglobal.hyper",
|
||||||
|
appPath: `${appOutDir}/${appName}.app`,
|
||||||
|
appleId: process.env.APPLE_ID,
|
||||||
|
appleIdPassword: process.env.APPLE_PASSWORD
|
||||||
|
});
|
||||||
|
};
|
||||||
3716
bin/rimraf-standalone.js
vendored
Normal file
3716
bin/rimraf-standalone.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
31
bin/snapshot-libs.js
vendored
Normal file
31
bin/snapshot-libs.js
vendored
Normal file
|
|
@ -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/xterm');
|
||||||
|
}
|
||||||
147315
bin/yarn-standalone.js
vendored
Normal file
147315
bin/yarn-standalone.js
vendored
Normal file
File diff suppressed because one or more lines are too long
24
binding.gyp
24
binding.gyp
|
|
@ -1,24 +0,0 @@
|
||||||
{
|
|
||||||
"targets": [{
|
|
||||||
"target_name": "electron_drag_click",
|
|
||||||
"sources": [ ],
|
|
||||||
"conditions": [
|
|
||||||
['OS=="mac"', {
|
|
||||||
"sources": [
|
|
||||||
"electron_drag_click.mm"
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
],
|
|
||||||
'include_dirs': [
|
|
||||||
"<!@(node -p \"require('node-addon-api').include\")"
|
|
||||||
],
|
|
||||||
'libraries': [],
|
|
||||||
'dependencies': [
|
|
||||||
"<!(node -p \"require('node-addon-api').gyp\")"
|
|
||||||
],
|
|
||||||
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
|
|
||||||
"xcode_settings": {
|
|
||||||
"OTHER_CPLUSPLUSFLAGS": ["-std=c++20", "-stdlib=libc++", "-mmacosx-version-min=10.12"],
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
BIN
build/canary.icns
Normal file
BIN
build/canary.icns
Normal file
Binary file not shown.
BIN
build/canary.ico
Normal file
BIN
build/canary.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
build/icon.fig
Normal file
BIN
build/icon.fig
Normal file
Binary file not shown.
BIN
build/icon.icns
Normal file
BIN
build/icon.icns
Normal file
Binary file not shown.
BIN
build/icon.ico
Normal file
BIN
build/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
6
build/linux/after-install.tpl
Normal file
6
build/linux/after-install.tpl
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
mkdir -p /usr/local/bin
|
||||||
|
|
||||||
|
# Link to the CLI bootstrap
|
||||||
|
ln -sf '/opt/${productFilename}/resources/bin/${executable}' '/usr/local/bin/${executable}'
|
||||||
34
build/linux/hyper
Executable file
34
build/linux/hyper
Executable file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/linux/bin/code.sh
|
||||||
|
|
||||||
|
# If root, ensure that --user-data-dir is specified
|
||||||
|
if [ "$(id -u)" = "0" ]; then
|
||||||
|
for i in $@
|
||||||
|
do
|
||||||
|
if [[ $i == --user-data-dir=* ]]; then
|
||||||
|
DATA_DIR_SET=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -z $DATA_DIR_SET ]; then
|
||||||
|
echo "It is recommended to start hyper as a normal user. To run as root, you must specify an alternate user data directory with the --user-data-dir argument." 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -L $0 ]; then
|
||||||
|
# if path is not a symlink, find relatively
|
||||||
|
HYPER_PATH="$(dirname $0)/../.."
|
||||||
|
else
|
||||||
|
if which readlink >/dev/null; then
|
||||||
|
# if readlink exists, follow the symlink and find relatively
|
||||||
|
HYPER_PATH="$(dirname $(readlink -f $0))/../.."
|
||||||
|
else
|
||||||
|
# else use the standard install location
|
||||||
|
HYPER_PATH="/opt/Hyper"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
ELECTRON="$HYPER_PATH/hyper"
|
||||||
|
CLI="$HYPER_PATH/resources/bin/cli.js"
|
||||||
|
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@"
|
||||||
|
exit $?
|
||||||
26
build/mac/entitlements.plist
Normal file
26
build/mac/entitlements.plist
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.automation.apple-events</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.device.audio-input</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.device.camera</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.personal-information.addressbook</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.personal-information.calendars</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.personal-information.location</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.personal-information.photos-library</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
28
build/mac/hyper
Executable file
28
build/mac/hyper
Executable file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.65.2/resources/darwin/bin/code.sh
|
||||||
|
|
||||||
|
# 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" "$@"
|
||||||
|
exit $?
|
||||||
25
build/win/hyper
Executable file
25
build/win/hyper
Executable file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/win/bin/code.sh
|
||||||
|
|
||||||
|
NAME="Hyper"
|
||||||
|
HYPER_PATH="$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")"
|
||||||
|
ELECTRON="$HYPER_PATH/$NAME.exe"
|
||||||
|
if grep -q Microsoft /proc/version; then
|
||||||
|
echo "Warning! Due to WSL limitations, you can't use CLI commands here. Please use Hyper CLI on cmd, PowerShell or GitBash/CygWin."
|
||||||
|
echo "Please see: https://github.com/Microsoft/WSL/issues/1494"
|
||||||
|
echo ""
|
||||||
|
# If running under WSL don't pass cli.js to Electron, as environment vars
|
||||||
|
# can't be transferred from WSL to Windows.
|
||||||
|
# See: https://github.com/Microsoft/BashOnWindows/issues/1363
|
||||||
|
# https://github.com/Microsoft/BashOnWindows/issues/1494
|
||||||
|
"$ELECTRON" "$@"
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
if [ "$(expr substr $(uname -s) 1 9)" == "CYGWIN_NT" ]; then
|
||||||
|
CLI=$(cygpath -m "$HYPER_PATH/resources/bin/cli.js")
|
||||||
|
else
|
||||||
|
CLI="$HYPER_PATH/resources/bin/cli.js"
|
||||||
|
fi
|
||||||
|
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@"
|
||||||
|
exit $?
|
||||||
|
|
||||||
5
build/win/hyper.cmd
Executable file
5
build/win/hyper.cmd
Executable file
|
|
@ -0,0 +1,5 @@
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
set ELECTRON_RUN_AS_NODE=1
|
||||||
|
call "%~dp0..\..\Hyper.exe" "%~dp0..\..\resources\bin\cli.js" %*
|
||||||
|
endlocal
|
||||||
28
build/win/installer.nsh
Normal file
28
build/win/installer.nsh
Normal file
|
|
@ -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
|
||||||
139
cli/api.ts
Normal file
139
cli/api.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
? path.join(process.env.XDG_CONFIG_HOME, 'Hyper')
|
||||||
|
: process.platform === 'win32'
|
||||||
|
? path.join(process.env.APPDATA!, 'Hyper')
|
||||||
|
: path.join(os.homedir(), '.config', 'Hyper');
|
||||||
|
|
||||||
|
const devConfigFileName = path.join(__dirname, `../hyper.json`);
|
||||||
|
|
||||||
|
const fileName =
|
||||||
|
process.env.NODE_ENV !== 'production' && fs.existsSync(devConfigFileName)
|
||||||
|
? devConfigFileName
|
||||||
|
: path.join(applicationDirectory, 'hyper.json');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to make sure the file reading and parsing is lazy so that failure to
|
||||||
|
* statically analyze the hyper configuration isn't fatal for all kinds of
|
||||||
|
* subcommands. We can use memoization to make reading and parsing lazy.
|
||||||
|
*/
|
||||||
|
function memoize<T extends (...args: any[]) => any>(fn: T): T {
|
||||||
|
let hasResult = false;
|
||||||
|
let result: any;
|
||||||
|
return ((...args: Parameters<T>) => {
|
||||||
|
if (!hasResult) {
|
||||||
|
result = fn(...args);
|
||||||
|
hasResult = true;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFileContents = memoize(() => {
|
||||||
|
return fs.readFileSync(fileName, 'utf8');
|
||||||
|
});
|
||||||
|
|
||||||
|
const getParsedFile = memoize(() => JSON.parse(getFileContents()));
|
||||||
|
|
||||||
|
const getPluginsByKey = (key: string): any[] => getParsedFile()[key] || [];
|
||||||
|
|
||||||
|
const getPlugins = memoize(() => {
|
||||||
|
return getPluginsByKey('plugins');
|
||||||
|
});
|
||||||
|
|
||||||
|
const getLocalPlugins = memoize(() => {
|
||||||
|
return getPluginsByKey('localPlugins');
|
||||||
|
});
|
||||||
|
|
||||||
|
function exists() {
|
||||||
|
return getFileContents() !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInstalled(plugin: string, locally?: boolean) {
|
||||||
|
const array = locally ? getLocalPlugins() : getPlugins();
|
||||||
|
if (array && Array.isArray(array)) {
|
||||||
|
return array.includes(plugin);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(config: any) {
|
||||||
|
return fs.writeFileSync(fileName, JSON.stringify(config, null, 2), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPackageName(plugin: string) {
|
||||||
|
const isScoped = plugin[0] === '@';
|
||||||
|
const nameWithoutVersion = plugin.split('#')[0];
|
||||||
|
|
||||||
|
if (isScoped) {
|
||||||
|
return `@${nameWithoutVersion.split('@')[1].replace('/', '%2f')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nameWithoutVersion.split('@')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function existsOnNpm(plugin: string) {
|
||||||
|
const name = getPackageName(plugin);
|
||||||
|
return got
|
||||||
|
.get<any>(registryUrl + name.toLowerCase(), {timeout: {request: 10000}, responseType: 'json'})
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.body.versions) {
|
||||||
|
return Promise.reject(res);
|
||||||
|
} else {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function install(plugin: string, locally?: boolean) {
|
||||||
|
const array = locally ? getLocalPlugins() : getPlugins();
|
||||||
|
return existsOnNpm(plugin)
|
||||||
|
.catch((err: any) => {
|
||||||
|
const {statusCode} = err;
|
||||||
|
if (statusCode && (statusCode === 404 || statusCode === 200)) {
|
||||||
|
return Promise.reject(`${plugin} not found on npm`);
|
||||||
|
}
|
||||||
|
return Promise.reject(`${err.message}\nPlugin check failed. Check your internet connection or retry later.`);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (isInstalled(plugin, locally)) {
|
||||||
|
return Promise.reject(`${plugin} is already installed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = getParsedFile();
|
||||||
|
config[locally ? 'localPlugins' : 'plugins'] = [...array, plugin];
|
||||||
|
save(config);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uninstall(plugin: string) {
|
||||||
|
if (!isInstalled(plugin)) {
|
||||||
|
return Promise.reject(`${plugin} is not installed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = getParsedFile();
|
||||||
|
config.plugins = getPlugins().filter((p) => p !== plugin);
|
||||||
|
save(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
function list() {
|
||||||
|
if (getPlugins().length > 0) {
|
||||||
|
return getPlugins().join('\n');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configPath = fileName;
|
||||||
|
export {exists, existsOnNpm, isInstalled, install, uninstall, list};
|
||||||
269
cli/index.ts
Normal file
269
cli/index.ts
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
// This is a CLI tool, using console is OK
|
||||||
|
/* eslint no-console: 0 */
|
||||||
|
import {spawn, exec} from 'child_process';
|
||||||
|
import type {SpawnOptions} from 'child_process';
|
||||||
|
import {existsSync} from 'fs';
|
||||||
|
import {isAbsolute, resolve} from 'path';
|
||||||
|
import {promisify} from 'util';
|
||||||
|
|
||||||
|
import args from 'args';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import _columnify from 'columnify';
|
||||||
|
import got from 'got';
|
||||||
|
import open from 'open';
|
||||||
|
import ora from 'ora';
|
||||||
|
|
||||||
|
import {version} from '../app/package.json';
|
||||||
|
|
||||||
|
import * as api from './api';
|
||||||
|
|
||||||
|
let commandPromise: Promise<void> | undefined;
|
||||||
|
|
||||||
|
const assertPluginName = (pluginName: string) => {
|
||||||
|
if (!pluginName) {
|
||||||
|
console.error(chalk.red('Plugin name is required'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkConfig = () => {
|
||||||
|
if (api.exists()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let msg = chalk.red(`Error! Config file not found: ${api.configPath}\n`);
|
||||||
|
msg += 'Please launch Hyper and retry.';
|
||||||
|
console.error(msg);
|
||||||
|
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',
|
||||||
|
(name, args_) => {
|
||||||
|
checkConfig();
|
||||||
|
const pluginName = args_[0];
|
||||||
|
assertPluginName(pluginName);
|
||||||
|
commandPromise = api
|
||||||
|
.install(pluginName)
|
||||||
|
.then(() => console.log(chalk.green(`${pluginName} installed successfully!`)))
|
||||||
|
.catch((err) => console.error(chalk.red(err)));
|
||||||
|
},
|
||||||
|
['i']
|
||||||
|
);
|
||||||
|
|
||||||
|
args.command(
|
||||||
|
'uninstall',
|
||||||
|
'Uninstall a plugin',
|
||||||
|
(name, args_) => {
|
||||||
|
checkConfig();
|
||||||
|
const pluginName = args_[0];
|
||||||
|
assertPluginName(pluginName);
|
||||||
|
commandPromise = api
|
||||||
|
.uninstall(pluginName)
|
||||||
|
.then(() => console.log(chalk.green(`${pluginName} uninstalled successfully!`)))
|
||||||
|
.catch((err) => console.error(chalk.red(err)));
|
||||||
|
},
|
||||||
|
['u', 'rm', 'remove']
|
||||||
|
);
|
||||||
|
|
||||||
|
args.command(
|
||||||
|
'list',
|
||||||
|
'List installed plugins',
|
||||||
|
() => {
|
||||||
|
checkConfig();
|
||||||
|
const plugins = api.list();
|
||||||
|
|
||||||
|
if (plugins) {
|
||||||
|
console.log(plugins);
|
||||||
|
} else {
|
||||||
|
console.log(chalk.red(`No plugins installed yet.`));
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
},
|
||||||
|
['ls']
|
||||||
|
);
|
||||||
|
|
||||||
|
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&size=250`;
|
||||||
|
type npmResult = {package: {name: string; description: string}};
|
||||||
|
return got(URL)
|
||||||
|
.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};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
args.command(
|
||||||
|
'search',
|
||||||
|
'Search for plugins on npm',
|
||||||
|
(name, args_) => {
|
||||||
|
const spinner = ora('Searching').start();
|
||||||
|
const query = args_[0] ? args_[0].toLowerCase() : '';
|
||||||
|
|
||||||
|
commandPromise = lsRemote(query)
|
||||||
|
.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 {
|
||||||
|
const msg = columnify(entries);
|
||||||
|
spinner.succeed();
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
spinner.fail();
|
||||||
|
console.error(chalk.red(err)); // TODO
|
||||||
|
});
|
||||||
|
},
|
||||||
|
['s']
|
||||||
|
);
|
||||||
|
|
||||||
|
args.command(
|
||||||
|
'list-remote',
|
||||||
|
'List plugins available on npm',
|
||||||
|
() => {
|
||||||
|
const spinner = ora('Searching').start();
|
||||||
|
|
||||||
|
commandPromise = lsRemote()
|
||||||
|
.then((entries) => {
|
||||||
|
const msg = columnify(entries);
|
||||||
|
spinner.succeed();
|
||||||
|
console.log(msg);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
spinner.fail();
|
||||||
|
console.error(chalk.red(err)); // TODO
|
||||||
|
});
|
||||||
|
},
|
||||||
|
['lsr', 'ls-remote']
|
||||||
|
);
|
||||||
|
|
||||||
|
args.command(
|
||||||
|
'docs',
|
||||||
|
'Open the npm page of a plugin',
|
||||||
|
(name, args_) => {
|
||||||
|
const pluginName = args_[0];
|
||||||
|
assertPluginName(pluginName);
|
||||||
|
void open(`http://ghub.io/${pluginName}`, {wait: false});
|
||||||
|
process.exit(0);
|
||||||
|
},
|
||||||
|
['d', 'h', 'home']
|
||||||
|
);
|
||||||
|
|
||||||
|
args.command(
|
||||||
|
'version',
|
||||||
|
'Show the version of hyper',
|
||||||
|
() => {
|
||||||
|
console.log(version);
|
||||||
|
process.exit(0);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
args.command('<default>', 'Launch Hyper');
|
||||||
|
|
||||||
|
args.option(['v', 'verbose'], 'Verbose mode', false);
|
||||||
|
|
||||||
|
const main = (argv: string[]) => {
|
||||||
|
const flags = args.parse(argv, {
|
||||||
|
name: 'hyper',
|
||||||
|
version: false,
|
||||||
|
mri: {
|
||||||
|
boolean: ['v', 'verbose']
|
||||||
|
},
|
||||||
|
mainColor: 'yellow',
|
||||||
|
subColor: 'dim'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (commandPromise) {
|
||||||
|
return commandPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = Object.assign({}, process.env, {
|
||||||
|
// this will signal Hyper that it was spawned from this module
|
||||||
|
HYPER_CLI: '1',
|
||||||
|
ELECTRON_NO_ATTACH_CONSOLE: '1'
|
||||||
|
});
|
||||||
|
|
||||||
|
delete env['ELECTRON_RUN_AS_NODE'];
|
||||||
|
|
||||||
|
if (flags.verbose) {
|
||||||
|
env['ELECTRON_ENABLE_LOGGING'] = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: SpawnOptions = {
|
||||||
|
detached: true,
|
||||||
|
env
|
||||||
|
};
|
||||||
|
|
||||||
|
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}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
return cwd;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!flags.verbose) {
|
||||||
|
options['stdio'] = 'ignore';
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
//Use `open` to prevent multiple Hyper process
|
||||||
|
const cmd = `open -b com.quineglobal.hyper ${args_}`;
|
||||||
|
const opts = {
|
||||||
|
env
|
||||||
|
};
|
||||||
|
return promisify(exec)(cmd, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const child = spawn(process.execPath, args_, options);
|
||||||
|
|
||||||
|
if (flags.verbose) {
|
||||||
|
// 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)));
|
||||||
|
}
|
||||||
|
child.unref();
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
function eventuallyExit(code: number) {
|
||||||
|
setTimeout(() => process.exit(code), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
main(process.argv)
|
||||||
|
.then(() => eventuallyExit(0))
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err.stack ? err.stack : err);
|
||||||
|
eventuallyExit(1);
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue