Compare commits

..

No commits in common. "canary" and "3.0.1-canary.2" have entirely different histories.

231 changed files with 79980 additions and 246299 deletions

97
.circleci/config.yml Normal file
View file

@ -0,0 +1,97 @@
version: 2
jobs:
install:
macos:
xcode: "9.2.0"
working_directory: ~/repo
steps:
- checkout
- restore_cache:
key: cache-{{ checksum "yarn.lock" }}
- run:
name: Installing Dependencies
command: yarn --ignore-engines
- save_cache:
key: cache-{{ checksum "yarn.lock" }}
paths:
- node_modules
- run:
name: Getting build icon
command: if [[ $CIRCLE_BRANCH == canary ]]; then cp build/canary.icns build/icon.icns; fi
- persist_to_workspace:
root: .
paths:
- node_modules
test:
macos:
xcode: "9.2.0"
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Testing
command: yarn test
build:
macos:
xcode: "9.2.0"
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Building
command: yarn dist --publish 'never'
- store_artifacts:
path: dist
- persist_to_workspace:
root: .
paths:
- dist
release:
macos:
xcode: "9.2.0"
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Deploying to GitHub
command: yarn dist
workflows:
version: 2
build:
jobs:
- install:
filters:
tags:
only: /.*/
- test:
requires:
- install
filters:
tags:
only: /.*/
- build:
requires:
- test
filters:
branches:
only:
- master
- canary
tags:
ignore: /.*/
- release:
requires:
- test
filters:
tags:
only: /.*/
branches:
ignore: /.*/

View file

@ -4,11 +4,7 @@ app/static
app/bin
app/dist
app/node_modules
app/typings
assets
website
bin
dist
target
cache
schema.json
dist

View file

@ -1,161 +0,0 @@
{
"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"
}
}
]
}

3
.gitattributes vendored
View file

@ -1,5 +1,2 @@
* text=auto
*.js text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
bin/* linguist-vendored

View file

@ -1,20 +0,0 @@
---
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.

View file

@ -1,137 +0,0 @@
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
#

View file

@ -1,172 +0,0 @@
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
#

View file

@ -1,39 +0,0 @@
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

View file

@ -1,23 +1,14 @@
---
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.
If not, please try and fulfil 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
- [ ] I am on the [latest](https://github.com/zeit/hyper/releases/latest) Hyper.app version
- [ ] I have searched the [issues](https://github.com/zeit/hyper/issues) of this repo and believe that this is not a duplicate
<!--
Once those are done, if you're able to fill in the following list with your information,
@ -26,7 +17,7 @@ assignees: ''
- **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 -->
- **Link of a [Gist](https://gist.github.com/) with the contents of your .hyper.js**: <!-- 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.) -->

View file

@ -1 +1,8 @@
<!-- Please check `Allow edits from maintainers`. Thanks! -->
<!-- Hi there! Thanks for submitting a PR! We're excited to see what you've got for us.
- To help whoever reviews your PR, it'd be extremely helpful for you to list whether your PR is ready to be merged,
If there's anything left to do and if there are any related PRs
- It'd also be extremely helpful to enable us to update your PR incase we need to rebase or what-not by checking `Allow edits from maintainers`
- If your PR changes some API, please make a PR for hyper website too: https://github.com/zeit/hyper-site.
Thanks, again! -->

View file

@ -1,154 +0,0 @@
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

View file

@ -1,67 +0,0 @@
# 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

View file

@ -1,63 +0,0 @@
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 }}

View file

@ -1,194 +0,0 @@
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

10
.gitignore vendored
View file

@ -1,9 +1,7 @@
# build output
dist
app/renderer
target
bin/cli.*
cache
# dependencies
node_modules
@ -13,11 +11,7 @@ npm-debug.log
yarn-error.log
# optional dev config file and plugins directory
hyper.json
schema.json
plugins
.hyper.js
.hyper_plugins
.DS_Store
.vscode/*
!.vscode/launch.json
.idea

1
.husky/.gitignore vendored
View file

@ -1 +0,0 @@
_

View file

@ -1 +0,0 @@
yarn test

View file

@ -1 +0,0 @@
20.11.0

1
.nvmrc
View file

@ -1 +0,0 @@
20.11.0

41
.travis.yml Normal file
View file

@ -0,0 +1,41 @@
sudo: required
dist: trusty
language: node_js
matrix:
include:
- os: linux
node_js: 10.2.0
env: CC=clang CXX=clang++ npm_config_clang=1
compiler: clang
addons:
apt:
packages:
- gcc-multilib
- g++-multilib
- libgnome-keyring-dev
- icnsutils
- graphicsmagick
- xz-utils
- rpm
- bsdtar
- snapd
before_install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo snap install snapcraft --classic; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start; sleep 3; fi
cache: yarn
install:
- yarn
after_success:
- (git branch --contains $TRAVIS_COMMIT | grep canary > /dev/null || [[ "$TRAVIS_BRANCH" == "canary" ]] ) && (cd build; cp canary.icns icon.icns; cp canary.ico icon.ico)
- yarn run dist
branches:
except:
- "/^v\\d+\\.\\d+\\.\\d+$/"

2
.vscode/launch.json vendored
View file

@ -6,7 +6,7 @@
"request": "launch",
"name": "Launch Hyper",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"program": "${workspaceRoot}/target/index.js",
"program": "${workspaceRoot}/app/index.js",
"protocol": "inspector"
},
{

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
registry "https://registry.npmjs.org/"
save-exact true

View file

@ -1 +0,0 @@
yarnPath: .yarn/releases/yarn-classic.cjs

View file

@ -1,6 +1,6 @@
# MIT License
Copyright (c) 2018 Vercel, Inc.
Copyright (c) 2018 ZEIT, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -3,19 +3,19 @@
## Workflow
### Run Hyper in dev mode
Hyper can be run in dev mode by cloning this repository and following the ["Contributing" section of our README](https://github.com/vercel/hyper#contribute).
Hyper can be run in dev mode by cloning this repository and following the ["Contributing" section of our README](https://github.com/zeit/hyper#contribute).
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).
Prerequisites and steps are described in the ["Contributing" section of our README](https://github.com/zeit/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.
Copy your config file `.hyper.js` to the root of your cloned repository. Hyper, in dev mode, will use this copied config file. That means that you can continue to use your main installation of Hyper with your day-to-day configuration.
After the first run, Hyper, in dev mode, will have created a new `.hyper_plugins` directory in your repository directory.
### 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.
Go to your recently created `<repository_root>/.hyper_plugins/local` directory and create/clone your plugin repo. An even better method on macOS/Linux is to add a symlink to your plugin directory.
Edit your dev config file, and add your plugin name (directory name in your `local` directory) in the `localPlugins` array.
```js
@ -30,7 +30,7 @@ module.exports = {
```
### Running your plugin
To load, your plugin should expose at least one API method. All possible methods are listed [here](https://github.com/vercel/hyper/blob/canary/app/plugins/extensions.ts).
To load, your plugin should expose at least one API method. All possible methods are listed [here](https://github.com/zeit/hyper/blob/canary/app/plugins/extensions.js).
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.
@ -41,7 +41,7 @@ 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.
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 hierachy.
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
@ -70,7 +70,7 @@ exports.decorateTerms = (Terms, {React}) => {
// <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
@ -190,17 +190,6 @@ exports.decorateTerm = (Term, { React, notify }) => {
}
```
### 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`.

View file

@ -1,44 +1,23 @@
<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>
<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>
[![Node CI](https://github.com/quine-global/hyper/actions/workflows/ci.yml/badge.svg?branch=canary)](https://github.com/quine-global/hyper/actions/workflows/ci.yml)
![](https://assets.zeit.co/image/upload/v1549723846/repositories/hyper/hyper-3-repo-banner.png)
[![macOS CI Status](https://circleci.com/gh/zeit/hyper.svg?style=shield)](https://circleci.com/gh/zeit/hyper)
[![Windows CI status](https://ci.appveyor.com/api/projects/status/kqvb4oa772an58sc?svg=true)](https://ci.appveyor.com/project/zeit/hyper)
[![Linux CI status](https://travis-ci.org/zeit/hyper.svg?branch=master)](https://travis-ci.org/zeit/hyper)
[![Changelog #213](https://img.shields.io/badge/changelog-%23213-lightgrey.svg)](https://changelog.com/213)
[![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/zeit/hyper)
For more details, head to: https://hyper.is
## Project goals
The goal of the project is to create a beautiful and 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
[Download the latest release!](https://hyper.is/#installation)
### Linux
#### Arch and derivatives
Hyper is available in the [AUR](https://aur.archlinux.org/packages/hyper/). Use an AUR [package manager](https://wiki.archlinux.org/index.php/AUR_helpers) e.g. [paru](https://github.com/Morganamilo/paru)
Hyper is available in the [AUR](https://aur.archlinux.org/packages/hyper/). Use an AUR package manager like [aurman](https://github.com/polygamma/aurman)
```sh
paru -S hyper
```
#### NixOS
Hyper is available as [Nix package](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/hyper/default.nix), to install the app run this command:
```sh
nix-env -i hyper
aurman -S hyper
```
### macOS
@ -47,7 +26,7 @@ Use [Homebrew Cask](https://brew.sh) to download the app by running these comman
```bash
brew update
brew install --cask hyper
brew cask install hyper
```
### Windows
@ -104,10 +83,6 @@ make sure its build process is working correctly by running `yarn run rebuild-no
If you are on macOS, this typically is related to Xcode issues (like not having agreed
to the Terms of Service by running `sudo xcodebuild` after a fresh Xcode installation).
##### Error with `C++` on macOS when running `yarn`
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
@ -115,7 +90,8 @@ If you have issues in the `codesign` step when running `yarn run dist` on macOS,
## Related Repositories
- [Website](https://github.com/vercel/hyper-site)
- [Sample Extension](https://github.com/vercel/hyperpower)
- [Sample Theme](https://github.com/vercel/hyperyellow)
- [Art](https://github.com/zeit/art/tree/master/hyper)
- [Website](https://github.com/zeit/hyper-site)
- [Sample Extension](https://github.com/zeit/hyperpower)
- [Sample Theme](https://github.com/zeit/hyperyellow)
- [Awesome Hyper](https://github.com/bnb/awesome-hyper)

View file

@ -1 +0,0 @@
registry "https://registry.npmjs.org/"

View file

@ -1,9 +1,9 @@
import {EventEmitter} from 'events';
'use strict';
import fetch from 'electron-fetch';
const fetch = require('electron-fetch').default;
const {EventEmitter} = require('events');
class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
updateURL!: string;
class AutoUpdater extends EventEmitter {
quitAndInstall() {
this.emitError('QuitAndInstall unimplemented');
}
@ -11,8 +11,8 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
return this.updateURL;
}
setFeedURL(options: Electron.FeedURLOptions) {
this.updateURL = options.url;
setFeedURL(updateURL) {
this.updateURL = updateURL;
}
checkForUpdates() {
@ -22,31 +22,29 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
this.emit('checking-for-update');
fetch(this.updateURL)
.then((res) => {
.then(res => {
if (res.status === 204) {
this.emit('update-not-available');
return;
return this.emit('update-not-available');
}
return res.json().then(({name, notes, pub_date}: {name: string; notes: string; pub_date: string}) => {
return res.json().then(({name, notes, pub_date}) => {
// 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();
// If `null` is passed to Date constructor, current time will be used. This doesn't work with `undefined`
const date = new Date(pub_date || null);
this.emit('update-available', {}, notes, name, date);
});
})
.catch(this.emitError.bind(this));
}
emitError(error: string | Error) {
emitError(error) {
if (typeof error === 'string') {
error = new Error(error);
}
this.emit('error', error);
this.emit('error', error, error.message);
}
}
const autoUpdaterLinux = new AutoUpdater();
export default autoUpdaterLinux;
module.exports = new AutoUpdater();

127
app/commands.js Normal file
View file

@ -0,0 +1,127 @@
const {app, Menu} = require('electron');
const {openConfig, getConfig} = require('./config');
const {updatePlugins} = require('./plugins');
const {installCLI} = require('./utils/cli-install');
const commands = {
'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:splitVertical': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('split request vertical');
},
'pane:splitHorizontal': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('split request horizontal');
},
'pane:close': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('termgroup close req');
},
'window:preferences': () => {
openConfig();
},
'editor:clearBuffer': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session clear req');
},
'editor:selectAll': focusedWindow => {
focusedWindow.rpc.emit('term selectAll');
},
'plugins:update': () => {
updatePlugins();
},
'window:reload': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('reload');
},
'window:reloadFull': focusedWindow => {
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 && focusedWindow.rpc.emit('reset fontSize req');
},
'zoom:in': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('increase fontSize req');
},
'zoom:out': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('decrease fontSize req');
},
'tab:prev': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('move left req');
},
'tab:next': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('move right req');
},
'pane:prev': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('prev pane req');
},
'pane:next': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('next pane req');
},
'editor:movePreviousWord': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session move word left req');
},
'editor:moveNextWord': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session move word right req');
},
'editor:moveBeginningLine': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session move line beginning req');
},
'editor:moveEndLine': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session move line end req');
},
'editor:deletePreviousWord': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session del word left req');
},
'editor:deleteNextWord': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session del word right req');
},
'editor:deleteBeginningLine': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session del line beginning req');
},
'editor:deleteEndLine': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session del line end req');
},
'editor:break': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session break req');
},
'cli:install': () => {
installCLI(true);
},
'window:hamburgerMenu': () => {
if (getConfig().showHamburgerMenu) {
Menu.getApplicationMenu().popup({x: 15, y: 15});
}
}
};
//Special numeric command
[1, 2, 3, 4, 5, 6, 7, 8, 'last'].forEach(cmdIndex => {
const index = cmdIndex === 'last' ? cmdIndex : cmdIndex - 1;
commands[`tab:jump:${cmdIndex}`] = focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('move jump req', index);
};
});
exports.execCommand = (command, focusedWindow) => {
const fn = commands[command];
if (fn) {
fn(focusedWindow);
}
};

View file

@ -1,170 +0,0 @@
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);
}
};

154
app/config.js Normal file
View file

@ -0,0 +1,154 @@
const fs = require('fs');
const notify = require('./notify');
const {_import, getDefaultConfig} = require('./config/import');
const _openConfig = require('./config/open');
const win = require('./config/windows');
const {cfgPath, cfgDir} = require('./config/paths');
const {getColorMap} = require('./utils/colors');
const watchers = [];
let cfg = {};
let _watcher;
const _watch = function() {
if (_watcher) {
return _watcher;
}
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);
};
// Windows
if (process.platform === 'win32') {
// watch for changes on config every 2s on Windows
// https://github.com/zeit/hyper/pull/1772
_watcher = fs.watchFile(cfgPath, {interval: 2000}, (curr, prev) => {
if (curr.mtime === 0) {
//eslint-disable-next-line no-console
console.error('error watching config');
} else if (curr.mtime !== prev.mtime) {
onChange();
}
});
return;
}
// macOS/Linux
setWatcher();
function setWatcher() {
try {
_watcher = fs.watch(cfgPath, eventType => {
if (eventType === 'rename') {
_watcher.close();
// Ensure that new file has been written
setTimeout(() => setWatcher(), 500);
}
});
} catch (e) {
//eslint-disable-next-line no-console
console.error('Failed to watch config file:', cfgPath, e);
return;
}
_watcher.on('change', onChange);
_watcher.on('error', error => {
//eslint-disable-next-line no-console
console.error('error watching config', error);
});
}
};
exports.subscribe = fn => {
watchers.push(fn);
return () => {
watchers.splice(watchers.indexOf(fn), 1);
};
};
exports.getConfigDir = () => {
// expose config directory to load plugin from the right place
return cfgDir;
};
exports.getConfig = () => {
return cfg.config;
};
exports.openConfig = () => {
return _openConfig();
};
exports.getPlugins = () => {
return {
plugins: cfg.plugins,
localPlugins: cfg.localPlugins
};
};
exports.getKeymaps = () => {
return cfg.keymaps;
};
exports.setup = () => {
cfg = _import();
_watch();
checkDeprecatedConfig();
};
exports.getWin = win.get;
exports.winRecord = win.recordState;
exports.windowDefaults = win.defaults;
const getDeprecatedCSS = function(config) {
const deprecated = [];
const deprecatedCSS = ['x-screen', 'x-row', 'cursor-node', '::selection'];
deprecatedCSS.forEach(css => {
if ((config.css && config.css.indexOf(css) !== -1) || (config.termCSS && config.termCSS.indexOf(css) !== -1)) {
deprecated.push(css);
}
});
return deprecated;
};
exports.getDeprecatedCSS = getDeprecatedCSS;
const checkDeprecatedConfig = function() {
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})`);
};
exports.fixConfigDefaults = decoratedConfig => {
const defaultConfig = getDefaultConfig().config;
decoratedConfig.colors = getColorMap(decoratedConfig.colors) || {};
// We must have default colors for xterm css.
decoratedConfig.colors = Object.assign({}, defaultConfig.colors, decoratedConfig.colors);
return decoratedConfig;
};
exports.htermConfigTranslate = config => {
const cssReplacements = {
'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 && config.css.replace(searchvalue, newvalue);
config.termCSS = config.termCSS && config.termCSS.replace(searchvalue, newvalue);
});
return config;
};

View file

@ -1,156 +0,0 @@
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;
};

View file

@ -0,0 +1,160 @@
// Future versions of Hyper may add additional config options,
// which will not automatically be merged into this file.
// See https://hyper.is#cfg for all currently supported options.
module.exports = {
config: {
// choose either `'stable'` for receiving highly polished,
// or `'canary'` for less polished but more frequent updates
updateChannel: 'stable',
// default font size in pixels for all tabs
fontSize: 12,
// font family with optional fallbacks
fontFamily: 'Menlo, "DejaVu Sans Mono", Consolas, "Lucida Console", monospace',
// default font weight: 'normal' or 'bold'
fontWeight: 'normal',
// font weight for bold characters: 'normal' or 'bold'
fontWeightBold: 'bold',
// line height as a relative unit
lineHeight: 1,
// letter spacing as a relative unit
letterSpacing: 0,
// terminal cursor background color and opacity (hex, rgb, hsl, hsv, hwb or cmyk)
cursorColor: 'rgba(248,28,229,0.8)',
// terminal text color under BLOCK cursor
cursorAccentColor: '#000',
// `'BEAM'` for |, `'UNDERLINE'` for _, `'BLOCK'` for █
cursorShape: 'BLOCK',
// set to `true` (without backticks and without quotes) for blinking cursor
cursorBlink: false,
// color of the text
foregroundColor: '#fff',
// terminal background color
// opacity is only supported on macOS
backgroundColor: '#000',
// terminal selection color
selectionColor: 'rgba(248,28,229,0.3)',
// border color (window, tabs)
borderColor: '#333',
// custom CSS to embed in the main window
css: '',
// custom CSS to embed in the terminal window
termCSS: '',
// if you're using a Linux setup which show native menus, set to false
// default: `true` on Linux, `true` on Windows, ignored on macOS
showHamburgerMenu: '',
// set to `false` (without backticks and without quotes) if you want to hide the minimize, maximize and close buttons
// additionally, set to `'left'` if you want them on the left, like in Ubuntu
// default: `true` (without backticks and without quotes) on Windows and Linux, ignored on macOS
showWindowControls: '',
// custom padding (CSS format, i.e.: `top right bottom left`)
padding: '12px 14px',
// the full list. if you're going to provide the full color palette,
// including the 6 x 6 color cubes and the grayscale map, just provide
// an array here instead of a color map object
colors: {
black: '#000000',
red: '#C51E14',
green: '#1DC121',
yellow: '#C7C329',
blue: '#0A2FC4',
magenta: '#C839C5',
cyan: '#20C5C6',
white: '#C7C7C7',
lightBlack: '#686868',
lightRed: '#FD6F6B',
lightGreen: '#67F86F',
lightYellow: '#FFFA72',
lightBlue: '#6A76FB',
lightMagenta: '#FD7CFC',
lightCyan: '#68FDFE',
lightWhite: '#FFFFFF',
},
// the shell to run when spawning a new session (i.e. /usr/local/bin/fish)
// if left empty, your system's login shell will be used by default
//
// Windows
// - Make sure to use a full path if the binary name doesn't work
// - Remove `--login` in shellArgs
//
// Bash on Windows
// - Example: `C:\\Windows\\System32\\bash.exe`
//
// PowerShell on Windows
// - Example: `C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`
shell: '',
// for setting shell arguments (i.e. for using interactive shellArgs: `['-i']`)
// by default `['--login']` will be used
shellArgs: ['--login'],
// for environment variables
env: {},
// set to `false` for no bell
bell: 'SOUND',
// if `true` (without backticks and without quotes), selected text will automatically be copied to the clipboard
copyOnSelect: false,
// if `true` (without backticks and without quotes), hyper will be set as the default protocol client for SSH
defaultSSHApp: true,
// if `true` (without backticks and without quotes), on right click selected text will be copied or pasted if no
// selection is present (`true` by default on Windows and disables the context menu feature)
quickEdit: false,
// choose either `'vertical'`, if you want the column mode when Option key is hold during selection (Default)
// or `'force'`, if you want to force selection regardless of whether the terminal is in mouse events mode
// (inside tmux or vim with mouse mode enabled for example).
macOptionSelectionMode: 'vertical',
// URL to custom bell
// bellSoundURL: 'http://example.com/bell.mp3',
// Whether to use the WebGL renderer. Set it to false to use canvas-based
// rendering (slower, but supports transparent backgrounds)
webGLRenderer: true,
// for advanced config flags please refer to https://hyper.is/#cfg
},
// a list of plugins to fetch and install from npm
// format: [@org/]project[#version]
// examples:
// `hyperpower`
// `@company/project`
// `project#1.0.1`
plugins: [],
// in development, you can create a directory under
// `~/.hyper_plugins/local/` and include it here
// to load it and avoid it being `npm install`ed
localPlugins: [],
keymaps: {
// Example
// 'window:devtools': 'cmd+alt+o',
},
};

View file

@ -1,77 +0,0 @@
{
"$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": {}
}

114
app/config/import.js Normal file
View file

@ -0,0 +1,114 @@
const {moveSync, copySync, existsSync, writeFileSync, readFileSync} = require('fs-extra');
const {sync: mkdirpSync} = require('mkdirp');
const {defaultCfg, cfgPath, legacyCfgPath, plugs, defaultPlatformKeyPath} = require('./paths');
const {_init, _extractDefault} = require('./init');
const notify = require('../notify');
let defaultConfig;
const _write = function(path, data) {
// 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 = function(str) {
return str.replace(/\r?\n/g, '\r\n');
};
const format = process.platform === 'win32' ? crlfify(data.toString()) : data;
writeFileSync(path, format, 'utf8');
};
// Saves a file as backup by appending '.backup' or '.backup2', '.backup3', etc.
// so as to not override any existing files
const saveAsBackup = src => {
let attempt = 1;
while (attempt < 100) {
try {
const backupPath = src + '.backup' + (attempt === 1 ? '' : attempt);
moveSync(src, backupPath);
return backupPath;
} catch (e) {
if (e.code === 'EEXIST') {
attempt++;
} else {
throw e;
}
}
}
throw new Error('Failed to create backup for config file. Too many backups');
};
const migrate = (old, _new, oldBackupPath) => {
if (old === _new) {
return;
}
if (existsSync(old)) {
//eslint-disable-next-line no-console
console.log('Found legacy config. Migrating ', old, '->', _new);
if (existsSync(_new)) {
saveAsBackup(_new);
}
copySync(old, _new);
saveAsBackup(oldBackupPath || old);
return true;
}
return false;
};
const _importConf = function() {
// init plugin directories if not present
mkdirpSync(plugs.base);
// Migrate Hyper2 config to Hyper3
const migratedConfig = migrate(legacyCfgPath, cfgPath);
const migratedPlugins = migrate(plugs.legacyLocal, plugs.local, plugs.legacyBase);
if (migratedConfig || migratedPlugins) {
notify(
'Hyper 3',
`Settings location has changed to ${cfgPath}.\nWe've automatically migrated your existing config!\nPlease restart hyper`
);
}
// Run this after the migration so that we don't generate a ".backup" file for
// an empty local/ directory
mkdirpSync(plugs.local);
try {
const defaultCfgRaw = readFileSync(defaultCfg, 'utf8');
const _defaultCfg = _extractDefault(defaultCfgRaw);
// Importing platform specific keymap
try {
const content = readFileSync(defaultPlatformKeyPath(), 'utf8');
const mapping = JSON.parse(content);
_defaultCfg.keymaps = mapping;
} catch (err) {
//eslint-disable-next-line no-console
console.error(err);
}
// Import user config
try {
const userCfg = readFileSync(cfgPath, 'utf8');
return {userCfg, defaultCfg: _defaultCfg};
} catch (err) {
_write(cfgPath, defaultCfgRaw);
return {userCfg: defaultCfgRaw, defaultCfg: _defaultCfg};
}
} catch (err) {
//eslint-disable-next-line no-console
console.log(err);
}
};
exports._import = () => {
const imported = _importConf();
defaultConfig = imported.defaultCfg;
const result = _init(imported);
return result;
};
exports.getDefaultConfig = () => {
if (!defaultConfig) {
defaultConfig = _extractDefault(_importConf().defaultCfg);
}
return defaultConfig;
};

View file

@ -1,65 +0,0 @@
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;
};

48
app/config/init.js Normal file
View file

@ -0,0 +1,48 @@
const vm = require('vm');
const notify = require('../notify');
const mapKeys = require('../utils/map-keys');
const _extract = function(script) {
const module = {};
script.runInNewContext({module});
if (!module.exports) {
throw new Error('Error reading configuration: `module.exports` not set');
}
return module.exports;
};
const _syntaxValidation = function(cfg) {
try {
return new vm.Script(cfg, {filename: '.hyper.js', displayErrors: true});
} catch (err) {
notify('Error loading config:', `${err.name}, see DevTools for more info`, {error: err});
}
};
const _extractDefault = function(cfg) {
return _extract(_syntaxValidation(cfg));
};
// init config
const _init = function(cfg) {
const script = _syntaxValidation(cfg.userCfg);
if (script) {
const _cfg = _extract(script);
if (!_cfg.config) {
notify('Error reading configuration: `config` key is missing');
return cfg.defaultCfg;
}
// Merging platform specific keymaps with user defined keymaps
_cfg.keymaps = mapKeys(Object.assign({}, cfg.defaultCfg.keymaps, _cfg.keymaps));
// Ignore undefined values in plugin and localPlugins array Issue #1862
_cfg.plugins = (_cfg.plugins && _cfg.plugins.filter(Boolean)) || [];
_cfg.localPlugins = (_cfg.localPlugins && _cfg.localPlugins.filter(Boolean)) || [];
return _cfg;
}
return cfg.defaultCfg;
};
module.exports = {
_init,
_extractDefault
};

View file

@ -1,63 +0,0 @@
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};

View file

@ -1,190 +0,0 @@
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}`);
};

77
app/config/open.js Normal file
View file

@ -0,0 +1,77 @@
const {shell} = require('electron');
const {cfgPath} = require('./paths');
module.exports = () => Promise.resolve(shell.openItem(cfgPath));
// Windows opens .js files with WScript.exe by default
// If the user hasn't set up an editor for .js files, we fallback to notepad.
if (process.platform === 'win32') {
const Registry = require('winreg');
const {exec} = require('child_process');
const getUserChoiceKey = async () => {
// Load FileExts keys for .js files
const keys = await new Promise((resolve, reject) => {
new Registry({
hive: Registry.HKCU,
key: '\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.js'
}).keys((error, items) => {
if (error) {
reject(error);
} else {
resolve(items || []);
}
});
});
// Find UserChoice key
const userChoice = keys.find(k => k.key.endsWith('UserChoice'));
return userChoice;
};
const hasDefaultSet = async () => {
let userChoice = await getUserChoiceKey();
if (!userChoice) return false;
// Load key values
let values = await new Promise((resolve, reject) => {
userChoice.values((error, items) => {
if (error) {
reject(error);
}
resolve(items.map(item => item.value || '') || []);
});
});
// Look for default program
const hasDefaultProgramConfigured = values.every(
value => value && typeof value === 'string' && !value.includes('WScript.exe') && !value.includes('JSFile')
);
return hasDefaultProgramConfigured;
};
// This mimics shell.openItem, true if it worked, false if not.
const openNotepad = file =>
new Promise(resolve => {
exec(`start notepad.exe ${file}`, error => {
resolve(!error);
});
});
module.exports = () =>
hasDefaultSet()
.then(yes => {
if (yes) {
return shell.openItem(cfgPath);
}
//eslint-disable-next-line no-console
console.warn('No default app set for .js files, using notepad.exe fallback');
return openNotepad(cfgPath);
})
.catch(err => {
//eslint-disable-next-line no-console
console.error('Open config with default app error:', err);
return openNotepad(cfgPath);
});
}

View file

@ -1,80 +0,0 @@
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;

View file

@ -1,36 +1,24 @@
// This module exports paths, names, and other metadata that is referenced
import {statSync} from 'fs';
import {homedir} from 'os';
import {resolve, join} from 'path';
const {homedir} = require('os');
const {app} = require('electron');
const {statSync} = require('fs');
const {resolve, join} = require('path');
const isDev = require('electron-is-dev');
import {app} from 'electron';
import isDev from 'electron-is-dev';
const cfgFile = 'hyper.json';
const defaultCfgFile = 'config-default.json';
const schemaFile = 'schema.json';
const cfgFile = '.hyper.js';
const defaultCfgFile = 'config-default.js';
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(
const applicationDirectory =
process.env.XDG_CONFIG_HOME !== undefined
? join(process.env.XDG_CONFIG_HOME, 'hyper')
: process.platform == 'win32'
? app.getPath('userData')
: homedir(),
'.hyper.js'
);
: process.platform == 'win32' ? app.getPath('userData') : homedir();
let cfgPath = join(cfgDir, cfgFile);
const schemaPath = resolve(__dirname, schemaFile);
let cfgDir = applicationDirectory;
let cfgPath = join(applicationDirectory, cfgFile);
let legacyCfgPath = join(homeDirectory, cfgFile); // Hyper 2 config location
const devDir = resolve(__dirname, '../..');
const devCfg = join(devDir, cfgFile);
@ -42,14 +30,17 @@ if (isDev) {
statSync(devCfg);
cfgPath = devCfg;
cfgDir = devDir;
//eslint-disable-next-line no-console
console.log('using config file:', cfgPath);
} catch (err) {
// ignore
}
}
const plugins = resolve(cfgDir, 'plugins');
const plugins = resolve(cfgDir, '.hyper_plugins');
const plugs = {
legacyBase: resolve(homeDirectory, '.hyper_plugins'),
legacyLocal: resolve(homeDirectory, '.hyper_plugins', 'local'),
base: plugins,
local: resolve(plugins, 'local'),
cache: resolve(plugins, 'cache')
@ -78,7 +69,7 @@ const defaultPlatformKeyPath = () => {
}
};
export {
module.exports = {
cfgDir,
cfgPath,
legacyCfgPath,
@ -90,7 +81,5 @@ export {
yarn,
cliScriptPath,
cliLinkPath,
homeDirectory,
schemaFile,
schemaPath
homeDirectory
};

View file

@ -1,756 +0,0 @@
{
"$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"
}

22
app/config/windows.js Normal file
View file

@ -0,0 +1,22 @@
const Config = require('electron-config');
const defaults = {
windowPosition: [50, 50],
windowSize: [540, 380]
};
// local storage
const cfg = new Config({defaults});
module.exports = {
defaults,
get() {
const position = cfg.get('windowPosition');
const size = cfg.get('windowSize');
return {position, size};
},
recordState(win) {
cfg.set('windowPosition', win.getPosition());
cfg.set('windowSize', win.getSize());
}
};

View file

@ -1,21 +0,0 @@
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());
}

View file

@ -1,43 +1,72 @@
// eslint-disable-next-line import/order
import {cfgPath} from './config/paths';
// Print diagnostic information for a few arguments instead of running Hyper.
if (['--help', '-v', '--version'].includes(process.argv[1])) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {version} = require('./package');
const configLocation = process.platform === 'win32' ? process.env.userprofile + '\\.hyper.js' : '~/.hyper.js';
//eslint-disable-next-line no-console
console.log(`Hyper version ${version}`);
//eslint-disable-next-line no-console
console.log('Hyper does not accept any command line arguments. Please modify the config file instead.');
console.log(`Hyper configuration file located at: ${cfgPath}`);
//eslint-disable-next-line no-console
console.log(`Hyper configuration file located at: ${configLocation}`);
// eslint-disable-next-line unicorn/no-process-exit
process.exit();
}
// Enable remote module
// eslint-disable-next-line import/order
import {initialize as remoteInitialize} from '@electron/remote/main';
remoteInitialize();
const checkSquirrel = () => {
let squirrel;
// set up config
// eslint-disable-next-line import/order
import * as config from './config';
config.setup();
try {
squirrel = require('electron-squirrel-startup');
//eslint-disable-next-line no-empty
} catch (err) {}
if (squirrel) {
// eslint-disable-next-line unicorn/no-process-exit
process.exit();
}
};
// handle startup squirrel events
if (process.platform === 'win32') {
// eslint-disable-next-line import/order
const systemContextMenu = require('./system-context-menu');
switch (process.argv[1]) {
case '--squirrel-install':
case '--squirrel-updated':
systemContextMenu.add(() => {
checkSquirrel();
});
break;
case '--squirrel-uninstall':
systemContextMenu.remove(() => {
checkSquirrel();
});
break;
default:
checkSquirrel();
}
}
// Native
import {resolve} from 'path';
const {resolve} = require('path');
// Packages
import {app, BrowserWindow, Menu, screen} from 'electron';
const {app, BrowserWindow, Menu} = require('electron');
const {gitDescribe} = require('git-describe');
const isDev = require('electron-is-dev');
import isDev from 'electron-is-dev';
import {gitDescribe} from 'git-describe';
import parseUrl from 'parse-url';
const config = require('./config');
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';
// set up config
config.setup();
const windowSet = new Set<BrowserWindow>([]);
const plugins = require('./plugins');
const {installCLI} = require('./utils/cli-install');
const AppMenu = require('./menus/menu');
const Window = require('./ui/window');
const windowUtils = require('./utils/window-utils');
const windowSet = new Set([]);
// expose to plugins
app.config = config;
@ -55,54 +84,40 @@ app.getLastFocusedWindow = () => {
});
};
//eslint-disable-next-line no-console
console.log('Disabling Chromium GPU blacklist');
app.commandLine.appendSwitch('ignore-gpu-blacklist');
if (isDev) {
//eslint-disable-next-line no-console
console.log('running in dev mode');
// Override default appVersion which is set from package.json
gitDescribe({customArguments: ['--tags']}, (error: any, gitInfo: {raw: string}) => {
gitDescribe({customArguments: ['--tags']}, (error, gitInfo) => {
if (!error) {
app.setVersion(gitInfo.raw);
}
});
} else {
//eslint-disable-next-line no-console
console.log('running in prod mode');
}
const url = `file://${resolve(isDev ? __dirname : app.getAppPath(), 'index.html')}`;
const url = 'file://' + resolve(isDev ? __dirname : app.getAppPath(), 'index.html');
//eslint-disable-next-line no-console
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);
function createWindow(fn, options = {}) {
const cfg = plugins.getDecoratedConfig();
const winSet = config.getWin();
let [startX, startY] = winSet.position;
const [width, height] = options.size ? options.size : cfg.windowSize || winSet.size;
const {screen} = require('electron');
const winPos = options.position;
@ -139,13 +154,9 @@ app.on('ready', () =>
[startX, startY] = config.windowDefaults.windowPosition;
}
const hwin = newWindow({width, height, x: startX, y: startY}, cfg, fn, profileName);
const hwin = new Window({width, height, x: startX, y: startY}, cfg, fn);
windowSet.add(hwin);
void hwin.loadURL(url);
hwin.once('ready-to-show', () => {
hwin.show();
});
hwin.loadURL(url);
// the window can be closed by the browser process itself
hwin.on('close', () => {
@ -153,6 +164,12 @@ app.on('ready', () =>
windowSet.delete(hwin);
});
hwin.on('closed', () => {
if (process.platform !== 'darwin' && windowSet.size === 0) {
app.quit();
}
});
return hwin;
}
@ -171,12 +188,6 @@ app.on('ready', () =>
}
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
const makeMenu = () => {
const menu = plugins.decorateMenu(AppMenu.createMenu(createWindow, plugins.getLoadedPluginVersions));
@ -190,7 +201,7 @@ app.on('ready', () =>
}
}
]);
app.dock?.setMenu(dockMenu);
app.dock.setMenu(dockMenu);
}
Menu.setApplicationMenu(AppMenu.buildMenu(menu));
@ -203,26 +214,26 @@ app.on('ready', () =>
if (!isDev) {
// check if should be set/removed as default ssh protocol client
if (config.getConfig().defaultSSHApp && !app.isDefaultProtocolClient('ssh')) {
//eslint-disable-next-line no-console
console.log('Setting Hyper as default client for ssh:// protocol');
app.setAsDefaultProtocolClient('ssh');
} else if (!config.getConfig().defaultSSHApp && app.isDefaultProtocolClient('ssh')) {
//eslint-disable-next-line no-console
console.log('Removing Hyper from default client for ssh:// protocol');
app.removeAsDefaultProtocolClient('ssh');
}
void installCLI(false);
installCLI(false);
}
})
.catch((err) => {
.catch(err => {
//eslint-disable-next-line no-console
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) {
app.on('open-file', (event, path) => {
const lastWindow = app.getLastFocusedWindow();
const callback = win => win.rpc.emit('open file', {path});
if (lastWindow) {
callback(lastWindow);
} else if (!lastWindow && {}.hasOwnProperty.call(app, 'createWindow')) {
@ -232,16 +243,31 @@ function GetWindow(callback: (win: BrowserWindow) => void) {
// sets his callback to an app.windowCallback property.
app.windowCallback = callback;
}
});
app.on('open-url', (event, sshUrl) => {
const lastWindow = app.getLastFocusedWindow();
const callback = win => win.rpc.emit('open ssh', sshUrl);
if (lastWindow) {
callback(lastWindow);
} else if (!lastWindow && {}.hasOwnProperty.call(app, 'createWindow')) {
app.createWindow(callback);
} else {
// If createWindow doesn't exist yet ('ready' event was not fired),
// sets his callback to an app.windowCallback property.
app.windowCallback = callback;
}
});
function installDevExtensions(isDev_) {
if (!isDev_) {
return Promise.resolve();
}
// eslint-disable-next-line import/no-extraneous-dependencies
const installer = require('electron-devtools-installer');
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
const forceDownload = Boolean(process.env.UPGRADE_EXTENSIONS);
return Promise.all(extensions.map(name => installer.default(installer[name], forceDownload)));
}
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));
});
});

View file

@ -30,8 +30,8 @@
"tab:jump:prefix": "command",
"pane:next": "command+]",
"pane:prev": "command+[",
"pane:splitRight": "command+d",
"pane:splitDown": "command+shift+d",
"pane:splitVertical": "command+d",
"pane:splitHorizontal": "command+shift+d",
"pane:close": "command+w",
"editor:undo": "command+z",
"editor:redo": "command+y",
@ -39,8 +39,6 @@
"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",

View file

@ -3,7 +3,6 @@
"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+-",
@ -28,8 +27,8 @@
"tab:jump:prefix": "ctrl",
"pane:next": "ctrl+pageup",
"pane:prev": "ctrl+pagedown",
"pane:splitRight": "ctrl+shift+d",
"pane:splitDown": "ctrl+shift+e",
"pane:splitVertical": "ctrl+shift+d",
"pane:splitHorizontal": "ctrl+shift+e",
"pane:close": "ctrl+shift+w",
"editor:undo": "ctrl+shift+z",
"editor:redo": "ctrl+shift+y",
@ -37,8 +36,6 @@
"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",

View file

@ -3,7 +3,7 @@
"window:reload": "ctrl+shift+r",
"window:reloadFull": "ctrl+shift+f5",
"window:preferences": "ctrl+,",
"window:hamburgerMenu": "alt+f",
"window:hamburgerMenu": "alt",
"zoom:reset": "ctrl+0",
"zoom:in": "ctrl+=",
"zoom:out": "ctrl+-",
@ -17,16 +17,22 @@
],
"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:splitVertical": "ctrl+shift+d",
"pane:splitHorizontal": "ctrl+shift+e",
"pane:close": "ctrl+shift+w",
"editor:undo": "ctrl+shift+z",
"editor:redo": "ctrl+shift+y",
@ -34,10 +40,8 @@
"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:movePreviousWord": "ctrl+left",
"editor:moveNextWord": "ctrl+right",
"editor:moveBeginningLine": "Home",
"editor:moveEndLine": "End",
"editor:deletePreviousWord": "ctrl+backspace",

75
app/menus/menu.js Normal file
View file

@ -0,0 +1,75 @@
// Packages
const {app, dialog, Menu} = require('electron');
// Utilities
const {getConfig} = require('../config');
const {icon} = require('../config/paths');
const viewMenu = require('./menus/view');
const shellMenu = require('./menus/shell');
const editMenu = require('./menus/edit');
const pluginsMenu = require('./menus/plugins');
const windowMenu = require('./menus/window');
const helpMenu = require('./menus/help');
const darwinMenu = require('./menus/darwin');
const {getDecoratedKeymaps} = require('../plugins');
const {execCommand} = require('../commands');
const {getRendererTypes} = require('../utils/renderer-utils');
const appName = app.getName();
const appVersion = app.getVersion();
let menu_ = [];
exports.createMenu = (createWindow, getLoadedPluginVersions) => {
const config = getConfig();
// We take only first shortcut in array for each command
const allCommandKeys = getDecoratedKeymaps();
const commandKeys = Object.keys(allCommandKeys).reduce((result, command) => {
result[command] = allCommandKeys[command][0];
return result;
}, {});
let updateChannel = 'stable';
if (config && 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, type) => {
acc[type] = acc[type] ? acc[type] + 1 : 1;
return acc;
}, {});
const renderers = Object.entries(rendererCounts)
.map(([type, count]) => type + (count > 1 ? ` (${count})` : ''))
.join(', ');
dialog.showMessageBox({
title: `About ${appName}`,
message: `${appName} ${appVersion} (${updateChannel})`,
detail: `Renderers: ${renderers}\nPlugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright © 2019 ZEIT, Inc.`,
buttons: [],
icon
});
};
const menu = [
...(process.platform === 'darwin' ? [darwinMenu(commandKeys, execCommand, showAbout)] : []),
shellMenu(commandKeys, execCommand),
editMenu(commandKeys, execCommand),
viewMenu(commandKeys, execCommand),
pluginsMenu(commandKeys, execCommand),
windowMenu(commandKeys, execCommand),
helpMenu(commandKeys, showAbout)
];
return menu;
};
exports.buildMenu = template => {
menu_ = Menu.buildFromTemplate(template);
return menu_;
};

View file

@ -1,96 +0,0 @@
// 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_;
};

View file

@ -1,15 +1,10 @@
// 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 {app} = require('electron');
const darwinMenu = (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void,
showAbout: () => void
): MenuItemConstructorOptions => {
module.exports = (commandKeys, execCommand, showAbout) => {
return {
label: `${app.name}`,
label: `${app.getName()}`,
submenu: [
{
label: 'About Hyper',
@ -41,7 +36,7 @@ const darwinMenu = (
role: 'hide'
},
{
role: 'hideOthers'
role: 'hideothers'
},
{
role: 'unhide'
@ -55,5 +50,3 @@ const darwinMenu = (
]
};
};
export default darwinMenu;

View file

@ -1,10 +1,5 @@
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron';
const editMenu = (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
) => {
const submenu: MenuItemConstructorOptions[] = [
module.exports = (commandKeys, execCommand) => {
const submenu = [
{
label: 'Undo',
accelerator: commandKeys['editor:undo'],
@ -28,17 +23,16 @@ const editMenu = (
command: 'editor:copy',
accelerator: commandKeys['editor:copy'],
registerAccelerator: true
} as any,
},
{
role: 'paste',
accelerator: commandKeys['editor:paste'],
registerAccelerator: true
accelerator: commandKeys['editor:paste']
},
{
label: 'Select All',
accelerator: commandKeys['editor:selectAll'],
click(item, focusedWindow) {
execCommand('editor:selectAll', focusedWindow as BrowserWindow | undefined);
execCommand('editor:selectAll', focusedWindow);
}
},
{
@ -51,28 +45,28 @@ const editMenu = (
label: 'Previous word',
accelerator: commandKeys['editor:movePreviousWord'],
click(item, focusedWindow) {
execCommand('editor:movePreviousWord', focusedWindow as BrowserWindow | undefined);
execCommand('editor:movePreviousWord', focusedWindow);
}
},
{
label: 'Next word',
accelerator: commandKeys['editor:moveNextWord'],
click(item, focusedWindow) {
execCommand('editor:moveNextWord', focusedWindow as BrowserWindow | undefined);
execCommand('editor:moveNextWord', focusedWindow);
}
},
{
label: 'Line beginning',
accelerator: commandKeys['editor:moveBeginningLine'],
click(item, focusedWindow) {
execCommand('editor:moveBeginningLine', focusedWindow as BrowserWindow | undefined);
execCommand('editor:moveBeginningLine', focusedWindow);
}
},
{
label: 'Line end',
accelerator: commandKeys['editor:moveEndLine'],
click(item, focusedWindow) {
execCommand('editor:moveEndLine', focusedWindow as BrowserWindow | undefined);
execCommand('editor:moveEndLine', focusedWindow);
}
}
]
@ -84,28 +78,28 @@ const editMenu = (
label: 'Previous word',
accelerator: commandKeys['editor:deletePreviousWord'],
click(item, focusedWindow) {
execCommand('editor:deletePreviousWord', focusedWindow as BrowserWindow | undefined);
execCommand('editor:deletePreviousWord', focusedWindow);
}
},
{
label: 'Next word',
accelerator: commandKeys['editor:deleteNextWord'],
click(item, focusedWindow) {
execCommand('editor:deleteNextWord', focusedWindow as BrowserWindow | undefined);
execCommand('editor:deleteNextWord', focusedWindow);
}
},
{
label: 'Line beginning',
accelerator: commandKeys['editor:deleteBeginningLine'],
click(item, focusedWindow) {
execCommand('editor:deleteBeginningLine', focusedWindow as BrowserWindow | undefined);
execCommand('editor:deleteBeginningLine', focusedWindow);
}
},
{
label: 'Line end',
accelerator: commandKeys['editor:deleteEndLine'],
click(item, focusedWindow) {
execCommand('editor:deleteEndLine', focusedWindow as BrowserWindow | undefined);
execCommand('editor:deleteEndLine', focusedWindow);
}
}
]
@ -117,14 +111,7 @@ const editMenu = (
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);
execCommand('editor:clearBuffer', focusedWindow);
}
}
];
@ -147,5 +134,3 @@ const editMenu = (
submenu
};
};
export default editMenu;

83
app/menus/menus/help.js Normal file
View file

@ -0,0 +1,83 @@
const {release} = require('os');
const {app, shell} = require('electron');
const {getConfig, getPlugins} = require('../../config');
const {arch, env, platform, versions} = process;
const {version} = require('../../package.json');
module.exports = (commands, showAbout) => {
const submenu = [
{
label: `${app.getName()} Website`,
click() {
shell.openExternal('https://hyper.is');
}
},
{
label: 'Report Issue',
click() {
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 your using the [latest](https://github.com/zeit/hyper/releases/latest) Hyper.app version
- [ ] I have searched the [issues](https://github.com/zeit/hyper/issues) of this repo and believe that this is not a duplicate
---
- **Any relevant information from devtools?** _(CMD+ALT+I on macOS, CTRL+SHIFT+I elsewhere)_:
<!-- 👉 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.js config -->
- **${app.getName()} 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.js contents</strong></summary>
<pre>
<code>
${JSON.stringify(getConfig(), null, 2)}
${JSON.stringify(getPlugins(), null, 2)}
</code>
</pre>
</details>`;
shell.openExternal(`https://github.com/zeit/hyper/issues/new?body=${encodeURIComponent(body)}`);
}
}
];
if (process.platform !== 'darwin') {
submenu.push(
{type: 'separator'},
{
role: 'about',
click() {
showAbout();
}
}
);
}
return {
role: 'help',
submenu
};
};

View file

@ -1,114 +0,0 @@
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;

View file

@ -0,0 +1,23 @@
module.exports = (commands, execCommand) => {
return {
label: 'Plugins',
submenu: [
{
label: 'Update',
accelerator: commands['plugins:update'],
click() {
execCommand('plugins:update');
}
},
{
label: 'Install Hyper CLI command in PATH',
click() {
execCommand('cli:install');
}
},
{
type: 'separator'
}
]
};
};

55
app/menus/menus/shell.js Normal file
View file

@ -0,0 +1,55 @@
module.exports = (commandKeys, execCommand) => {
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 Horizontally',
accelerator: commandKeys['pane:splitHorizontal'],
click(item, focusedWindow) {
execCommand('pane:splitHorizontal', focusedWindow);
}
},
{
label: 'Split Vertically',
accelerator: commandKeys['pane:splitVertical'],
click(item, focusedWindow) {
execCommand('pane:splitVertical', 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']
}
]
};
};

View file

@ -1,104 +0,0 @@
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;

View file

@ -1,49 +0,0 @@
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;

View file

@ -1,9 +1,4 @@
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron';
const viewMenu = (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
module.exports = (commandKeys, execCommand) => {
return {
label: 'View',
submenu: [
@ -11,21 +6,21 @@ const viewMenu = (
label: 'Reload',
accelerator: commandKeys['window:reload'],
click(item, focusedWindow) {
execCommand('window:reload', focusedWindow as BrowserWindow);
execCommand('window:reload', focusedWindow);
}
},
{
label: 'Full Reload',
accelerator: commandKeys['window:reloadFull'],
click(item, focusedWindow) {
execCommand('window:reloadFull', focusedWindow as BrowserWindow);
execCommand('window:reloadFull', focusedWindow);
}
},
{
label: 'Developer Tools',
accelerator: commandKeys['window:devtools'],
click: (item, focusedWindow) => {
execCommand('window:devtools', focusedWindow as BrowserWindow);
execCommand('window:devtools', focusedWindow);
}
},
{
@ -35,25 +30,23 @@ const viewMenu = (
label: 'Reset Zoom Level',
accelerator: commandKeys['zoom:reset'],
click(item, focusedWindow) {
execCommand('zoom:reset', focusedWindow as BrowserWindow);
execCommand('zoom:reset', focusedWindow);
}
},
{
label: 'Zoom In',
accelerator: commandKeys['zoom:in'],
click(item, focusedWindow) {
execCommand('zoom:in', focusedWindow as BrowserWindow);
execCommand('zoom:in', focusedWindow);
}
},
{
label: 'Zoom Out',
accelerator: commandKeys['zoom:out'],
click(item, focusedWindow) {
execCommand('zoom:out', focusedWindow as BrowserWindow);
execCommand('zoom:out', focusedWindow);
}
}
]
};
};
export default viewMenu;

View file

@ -1,16 +1,11 @@
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron';
const windowMenu = (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
module.exports = (commandKeys, execCommand) => {
// Generating tab:jump array
const tabJump: MenuItemConstructorOptions[] = [];
const tabJump = [];
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,
label: label,
accelerator: commandKeys[`tab:jump:${label.toLowerCase()}`]
});
}
@ -37,14 +32,14 @@ const windowMenu = (
label: 'Previous',
accelerator: commandKeys['tab:prev'],
click: (item, focusedWindow) => {
execCommand('tab:prev', focusedWindow as BrowserWindow);
execCommand('tab:prev', focusedWindow);
}
},
{
label: 'Next',
accelerator: commandKeys['tab:next'],
click: (item, focusedWindow) => {
execCommand('tab:next', focusedWindow as BrowserWindow);
execCommand('tab:next', focusedWindow);
}
},
{
@ -63,14 +58,14 @@ const windowMenu = (
label: 'Previous',
accelerator: commandKeys['pane:prev'],
click: (item, focusedWindow) => {
execCommand('pane:prev', focusedWindow as BrowserWindow);
execCommand('pane:prev', focusedWindow);
}
},
{
label: 'Next',
accelerator: commandKeys['pane:next'],
click: (item, focusedWindow) => {
execCommand('pane:next', focusedWindow as BrowserWindow);
execCommand('pane:next', focusedWindow);
}
}
]
@ -81,12 +76,6 @@ const windowMenu = (
{
role: 'front'
},
{
label: 'Toggle Always on Top',
click: (item, focusedWindow) => {
execCommand('window:toggleKeepOnTop', focusedWindow as BrowserWindow);
}
},
{
role: 'togglefullscreen',
accelerator: commandKeys['window:toggleFullScreen']
@ -94,5 +83,3 @@ const windowMenu = (
]
};
};
export default windowMenu;

View file

@ -1,20 +1,20 @@
import type {BrowserWindow} from 'electron';
const ms = require('ms');
const fetch = require('electron-fetch').default;
import fetch from 'electron-fetch';
import ms from 'ms';
import {version} from './package.json';
const {version} = require('./package');
const NEWS_URL = 'https://hyper-news.now.sh';
export default function fetchNotifications(win: BrowserWindow) {
module.exports = function fetchNotifications(win) {
const {rpc} = win;
const retry = (err?: Error) => {
const retry = err => {
setTimeout(() => fetchNotifications(win), ms('30m'));
if (err) {
//eslint-disable-next-line no-console
console.error('Notification messages fetch error', err.stack);
}
};
//eslint-disable-next-line no-console
console.log('Checking for notification messages');
fetch(NEWS_URL, {
headers: {
@ -22,13 +22,14 @@ export default function fetchNotifications(win: BrowserWindow) {
'X-Hyper-Platform': process.platform
}
})
.then((res) => res.json())
.then((data) => {
const message: {text: string; url: string; dismissable: boolean} | '' = data.message || '';
.then(res => res.json())
.then(data => {
const {message} = data || {};
if (typeof message !== 'object' && message !== '') {
throw new Error('Bad response');
}
if (message === '') {
//eslint-disable-next-line no-console
console.log('No matching notification messages');
} else {
rpc.emit('add notification', message);
@ -37,4 +38,4 @@ export default function fetchNotifications(win: BrowserWindow) {
retry();
})
.catch(retry);
}
};

5
app/notify.html Normal file
View file

@ -0,0 +1,5 @@
<script>
require('electron').ipcRenderer.on('notification', (ev, { title, body }) => {
new Notification(title, { body });
});
</script>

44
app/notify.js Normal file
View file

@ -0,0 +1,44 @@
const {resolve} = require('path');
const {app, BrowserWindow} = require('electron');
const isDev = require('electron-is-dev');
let win;
// the hack of all hacks
// electron doesn't have a built in notification thing,
// so we launch a window on which we can use the
// HTML5 `Notification` API :'(
let buffer = [];
app.on('ready', () => {
const win_ = new BrowserWindow({
show: false
});
const url = 'file://' + resolve(isDev ? __dirname : app.getAppPath(), 'notify.html');
win_.loadURL(url);
win_.webContents.on('dom-ready', () => {
win = win_;
buffer.forEach(([title, body]) => {
notify(title, body);
});
buffer = null;
});
});
function notify(title, body, details = {}) {
//eslint-disable-next-line no-console
console.log(`[Notification] ${title}: ${body}`);
if (details.error) {
//eslint-disable-next-line no-console
console.error(details.error);
}
if (win) {
win.webContents.send('notification', {title, body});
} else {
buffer.push([title, body]);
}
}
module.exports = notify;

View file

@ -1,21 +0,0 @@
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();
};

View file

@ -2,50 +2,37 @@
"name": "hyper",
"productName": "Hyper",
"description": "A terminal built on web technologies",
"version": "4.0.0-q-canary.8",
"version": "3.0.1-canary.2",
"license": "MIT",
"author": {
"name": "ZEIT, Inc.",
"email": "team@zeit.co"
},
"repository": "quine-global/hyper",
"scripts": {
"postinstall": "npx patch-package"
},
"repository": "zeit/hyper",
"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",
"async-retry": "1.1.4",
"color": "2.0.1",
"convert-css-color-name-to-hex": "0.1.1",
"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"
"electron-config": "2.0.0",
"electron-fetch": "1.3.0",
"electron-is-dev": "1.0.1",
"electron-squirrel-startup": "1.0.0",
"file-uri-to-path": "1.0.0",
"fs-extra": "7.0.1",
"git-describe": "4.0.2",
"lodash": "4.17.5",
"mkdirp": "0.5.1",
"ms": "2.1.1",
"node-pty": "0.8.0",
"os-locale": "3.1.0",
"parse-url": "3.0.2",
"queue": "4.4.2",
"react": "16.2.0",
"react-dom": "16.2.1",
"semver": "5.5.0",
"shell-env": "0.3.0",
"uuid": "3.2.1",
"winreg": "1.2.4"
}
}

View file

@ -1,28 +1,18 @@
/* 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';
const {app, dialog} = require('electron');
const {resolve, basename} = require('path');
const {writeFileSync} = require('fs');
const Config = require('electron-config');
const ms = require('ms');
import {app, dialog, ipcMain as _ipcMain} from 'electron';
import type {BrowserWindow, App, MenuItemConstructorOptions} from 'electron';
import React from 'react';
const React = require('react');
const ReactDom = require('react-dom');
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';
const config = require('./config');
const notify = require('./notify');
const {availableExtensions} = require('./plugins/extensions');
const {install} = require('./plugins/install');
const {plugs} = require('./config/paths');
const mapKeys = require('./utils/map-keys');
// local storage
const cache = new Config();
@ -38,11 +28,11 @@ let paths = getPaths();
let id = getId(plugins);
let modules = requirePlugins();
function getId(plugins_: any) {
function getId(plugins_) {
return JSON.stringify(plugins_);
}
const watchers: Function[] = [];
const watchers = [];
// we listen on configuration updates to trigger
// plugin installation
@ -60,12 +50,11 @@ config.subscribe(() => {
// patching Module._load
// so plugins can `require` them without needing their own version
// https://github.com/vercel/hyper/issues/619
// https://github.com/zeit/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) {
Module._load = function _load(modulePath) {
// PLEASE NOTE: Code changes here, also need to be changed in
// lib/utils/plugins.js
switch (modulePath) {
@ -85,14 +74,13 @@ function patchModuleLoad() {
case 'hyper/decorate':
return Object;
default:
// eslint-disable-next-line prefer-rest-params
return originalLoad.apply(this, arguments);
}
};
}
function checkDeprecatedExtendKeymaps() {
modules.forEach((plugin) => {
modules.forEach(plugin => {
if (plugin.extendKeymaps) {
notify('Plugin warning!', `"${plugin._name}" use deprecated "extendKeymaps" handler`);
return;
@ -109,10 +97,11 @@ function updatePlugins({force = false} = {}) {
updating = true;
syncPackageJSON();
const id_ = id;
install((err) => {
install(err => {
updating = false;
if (err) {
//eslint-disable-next-line no-console
notify('Error updating plugins.', err, {error: err});
} else {
// flag successful plugin update
@ -134,9 +123,7 @@ function updatePlugins({force = false} = {}) {
cache.set('hyper.plugin-versions', pluginVersions);
// notify watchers
watchers.forEach((fn) => {
fn(err, {force});
});
watchers.forEach(fn => fn(err, {force}));
if (force || changed) {
if (changed) {
@ -152,10 +139,10 @@ function updatePlugins({force = false} = {}) {
function getPluginVersions() {
const paths_ = paths.plugins.concat(paths.localPlugins);
return paths_.map((path_) => {
let version: string | null = null;
return paths_.map(path_ => {
let version = null;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
//eslint-disable-next-line import/no-dynamic-require
version = require(resolve(path_, 'package.json')).version;
//eslint-disable-next-line no-empty
} catch (err) {}
@ -165,7 +152,7 @@ function getPluginVersions() {
function clearCache() {
// trigger unload hooks
modules.forEach((mod) => {
modules.forEach(mod => {
if (mod.onUnload) {
mod.onUnload(app);
}
@ -179,10 +166,10 @@ function clearCache() {
}
}
export {updatePlugins};
exports.updatePlugins = updatePlugins;
export const getLoadedPluginVersions = () => {
return modules.map((mod) => ({name: mod._name, version: mod._version}));
exports.getLoadedPluginVersions = () => {
return modules.map(mod => ({name: mod._name, version: mod._version}));
};
// we schedule the initial plugins update
@ -190,28 +177,24 @@ export const getLoadedPluginVersions = () => {
// to prevent slowness
if (cache.get('hyper.plugins') !== id || process.env.HYPER_FORCE_UPDATE) {
// install immediately if the user changed plugins
//eslint-disable-next-line no-console
console.log('plugins have changed / not init, scheduling plugins installation');
setTimeout(() => {
updatePlugins();
}, 1000);
}
(() => {
const baseConfig = config.getConfig();
if (baseConfig['autoUpdatePlugins']) {
// otherwise update plugins every 5 hours
setInterval(updatePlugins, ms(baseConfig['autoUpdatePlugins'] === true ? '5h' : baseConfig['autoUpdatePlugins']));
}
})();
// otherwise update plugins every 5 hours
setInterval(updatePlugins, ms('5h'));
function syncPackageJSON() {
const dependencies = toDependencies(plugins);
const pkg = {
name: 'hyper-plugins',
description: 'Auto-generated from `hyper.json`!',
description: 'Auto-generated from `~/.hyper.js`!',
private: true,
version: '0.0.1',
repository: 'quine-global/hyper',
repository: 'zeit/hyper',
license: 'MIT',
homepage: 'https://hyper.is',
dependencies
@ -225,22 +208,22 @@ function syncPackageJSON() {
}
}
function alert(message: string) {
void dialog.showMessageBox({
function alert(message) {
dialog.showMessageBox({
message,
buttons: ['Ok']
});
}
function toDependencies(plugins_: {plugins: string[]}) {
const obj: Record<string, string> = {};
plugins_.plugins.forEach((plugin) => {
function toDependencies(plugins_) {
const obj = {};
plugins_.plugins.forEach(plugin => {
const regex = /.(@|#)/;
const match = regex.exec(plugin);
if (match) {
const index = match.index + 1;
const pieces: string[] = [];
const pieces = [];
pieces[0] = plugin.substring(0, index);
pieces[1] = plugin.substring(index + 1, plugin.length);
@ -252,7 +235,7 @@ function toDependencies(plugins_: {plugins: string[]}) {
return obj;
}
export const subscribe = (fn: Function) => {
exports.subscribe = fn => {
watchers.push(fn);
return () => {
watchers.splice(watchers.indexOf(fn), 1);
@ -261,50 +244,53 @@ export const subscribe = (fn: Function) => {
function getPaths() {
return {
plugins: plugins.plugins.map((name) => {
return resolve(path, 'node_modules', name.split('#')[0]);
plugins: plugins.plugins.map(name => {
return resolve(path, 'node_modules', name.split('#')[0].split('@')[0]);
}),
localPlugins: plugins.localPlugins.map((name) => {
localPlugins: plugins.localPlugins.map(name => {
return resolve(localPath, name);
})
};
}
// expose to renderer
export {getPaths};
exports.getPaths = getPaths;
// get paths from renderer
export const getBasePaths = () => {
exports.getBasePaths = () => {
return {path, localPath};
};
function requirePlugins(): any[] {
function requirePlugins() {
const {plugins: plugins_, localPlugins} = paths;
const load = (path_: string) => {
let mod: Record<string, any>;
const load = path_ => {
let mod;
try {
// eslint-disable-next-line import/no-dynamic-require
mod = require(path_);
const exposed = mod && Object.keys(mod).some((key) => availableExtensions.has(key));
const exposed = mod && Object.keys(mod).some(key => availableExtensions.has(key));
if (!exposed) {
notify('Plugin error!', `${`Plugin "${basename(path_)}" does not expose any `}Hyper extension API methods`);
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
// eslint-disable-next-line import/no-dynamic-require
mod._version = require(resolve(path_, 'package.json')).version;
} catch (err) {
//eslint-disable-next-line no-console
console.warn(`No package.json found in ${path_}`);
}
//eslint-disable-next-line no-console
console.log(`Plugin ${mod._name} (${mod._version}) loaded.`);
return mod;
} catch (_err) {
const err = _err as {code: string; message: string};
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
//eslint-disable-next-line no-console
console.warn(`Plugin error while loading "${basename(path_)}" (${path_}): ${err.message}`);
} else {
notify('Plugin error!', `Plugin "${basename(path_)}" failed to load (${err.message})`, {error: err});
@ -312,17 +298,14 @@ function requirePlugins(): any[] {
}
};
return [
...localPlugins.filter((p) => basename(p) === 'migrated-hyper3-config'),
...plugins_,
...localPlugins.filter((p) => basename(p) !== 'migrated-hyper3-config')
]
return plugins_
.map(load)
.filter((v): v is Record<string, any> => Boolean(v));
.concat(localPlugins.map(load))
.filter(v => Boolean(v));
}
export const onApp = (app_: App) => {
modules.forEach((plugin) => {
exports.onApp = app_ => {
modules.forEach(plugin => {
if (plugin.onApp) {
try {
plugin.onApp(app_);
@ -335,8 +318,8 @@ export const onApp = (app_: App) => {
});
};
export const onWindowClass = (win: BrowserWindow) => {
modules.forEach((plugin) => {
exports.onWindowClass = win => {
modules.forEach(plugin => {
if (plugin.onWindowClass) {
try {
plugin.onWindowClass(win);
@ -349,8 +332,8 @@ export const onWindowClass = (win: BrowserWindow) => {
});
};
export const onWindow = (win: BrowserWindow) => {
modules.forEach((plugin) => {
exports.onWindow = win => {
modules.forEach(plugin => {
if (plugin.onWindow) {
try {
plugin.onWindow(win);
@ -365,9 +348,9 @@ export const onWindow = (win: BrowserWindow) => {
// decorates the base entity by calling plugin[key]
// for all the available plugins
function decorateEntity(base: any, key: string, type: 'object' | 'function') {
function decorateEntity(base, key, type) {
let decorated = base;
modules.forEach((plugin) => {
modules.forEach(plugin => {
if (plugin[key]) {
let res;
try {
@ -387,23 +370,23 @@ function decorateEntity(base: any, key: string, type: 'object' | 'function') {
return decorated;
}
function decorateObject<T>(base: T, key: string): T {
function decorateObject(base, key) {
return decorateEntity(base, key, 'object');
}
function decorateClass(base: any, key: string) {
function decorateClass(base, key) {
return decorateEntity(base, key, 'function');
}
export const getDeprecatedConfig = () => {
const deprecated: Record<string, {css: string[]}> = {};
exports.getDeprecatedConfig = () => {
const deprecated = {};
const baseConfig = config.getConfig();
modules.forEach((plugin) => {
modules.forEach(plugin => {
if (!plugin.decorateConfig) {
return;
}
// We need to clone config in case of plugin modifies config directly.
let configTmp: configOptions;
let configTmp;
try {
configTmp = plugin.decorateConfig(JSON.parse(JSON.stringify(baseConfig)));
} catch (e) {
@ -421,60 +404,43 @@ export const getDeprecatedConfig = () => {
return deprecated;
};
export const decorateMenu = (tpl: MenuItemConstructorOptions[]) => {
exports.decorateMenu = tpl => {
return decorateObject(tpl, 'decorateMenu');
};
export const getDecoratedEnv = (baseEnv: Record<string, string>) => {
exports.getDecoratedEnv = baseEnv => {
return decorateObject(baseEnv, 'decorateEnv');
};
export const getDecoratedConfig = (profile: string) => {
const baseConfig = config.getProfileConfig(profile);
exports.getDecoratedConfig = () => {
const baseConfig = config.getConfig();
const decoratedConfig = decorateObject(baseConfig, 'decorateConfig');
const fixedConfig = config.fixConfigDefaults(decoratedConfig);
const translatedConfig = config.htermConfigTranslate(fixedConfig);
return translatedConfig;
};
export const getDecoratedKeymaps = () => {
exports.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 => {
exports.getDecoratedBrowserOptions = defaults => {
return decorateObject(defaults, 'decorateBrowserOptions');
};
export const decorateWindowClass = <T>(defaults: T): T => {
exports.decorateWindowClass = defaults => {
return decorateObject(defaults, 'decorateWindowClass');
};
export const decorateSessionOptions = <T>(defaults: T): T => {
exports.decorateSessionOptions = defaults => {
return decorateObject(defaults, 'decorateSessionOptions');
};
export const decorateSessionClass = <T>(Session: T): T => {
exports.decorateSessionClass = Session => {
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());
exports._toDependencies = toDependencies;

44
app/plugins/extensions.js Normal file
View file

@ -0,0 +1,44 @@
module.exports = {
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'
])
};

View file

@ -1,42 +0,0 @@
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'
]);

50
app/plugins/install.js Normal file
View file

@ -0,0 +1,50 @@
const cp = require('child_process');
const queue = require('queue');
const ms = require('ms');
const {yarn, plugs} = require('../config/paths');
module.exports = {
install: fn => {
const spawnQueue = queue({concurrency: 1});
function yarnFn(args, cb) {
const env = {
NODE_ENV: 'production',
ELECTRON_RUN_AS_NODE: 'true'
};
spawnQueue.push(end => {
const cmd = [process.execPath, yarn].concat(args).join(' ');
//eslint-disable-next-line no-console
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);
});
}
};

View file

@ -1,49 +0,0 @@
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);
});
};

58
app/rpc.js Normal file
View file

@ -0,0 +1,58 @@
const {EventEmitter} = require('events');
const {ipcMain} = require('electron');
const uuid = require('uuid');
class Server extends EventEmitter {
constructor(win) {
super();
this.win = win;
this.ipcListener = this.ipcListener.bind(this);
if (this.destroyed) {
return;
}
const uid = uuid.v4();
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);
});
}
get wc() {
return this.win.webContents;
}
ipcListener(event, {ev, data}) {
super.emit(ev, data);
}
emit(ch, data) {
// 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});
}
}
destroy() {
this.removeAllListeners();
this.wc.removeAllListeners();
if (this.id) {
ipcMain.removeListener(this.id, this.ipcListener);
} else {
// mark for `genUid` in constructor
this.destroyed = true;
}
}
}
module.exports = win => {
return new Server(win);
};

View file

@ -1,83 +0,0 @@
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;

189
app/session.js Normal file
View file

@ -0,0 +1,189 @@
const {EventEmitter} = require('events');
const {StringDecoder} = require('string_decoder');
const defaultShell = require('default-shell');
const {getDecoratedEnv} = require('./plugins');
const {productName, version} = require('./package');
const config = require('./config');
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;
try {
spawn = require('node-pty').spawn;
} catch (err) {
throw createNodePtyError();
}
const envFromConfig = config.getConfig().env || {};
// 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 {
constructor(uid) {
super();
this.uid = uid;
this.decoder = new StringDecoder('utf8');
this.reset();
}
reset() {
this.data = this.uid;
this.timeout = null;
}
write(chunk) {
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 += 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);
}
}
module.exports = class Session extends EventEmitter {
constructor(options) {
super();
this.pty = null;
this.batcher = null;
this.shell = null;
this.ended = false;
this.init(options);
}
init({uid, rows, cols: columns, cwd, shell, shellArgs}) {
const osLocale = require('os-locale');
const baseEnv = Object.assign(
{},
process.env,
{
LANG: osLocale.sync() + '.UTF-8',
TERM: 'xterm-256color',
COLORTERM: 'truecolor',
TERM_PROGRAM: productName,
TERM_PROGRAM_VERSION: version
},
envFromConfig
);
// Electron has a default value for process.env.GOOGLE_API_KEY
// We don't want to leak this to the shell
// See https://github.com/zeit/hyper/issues/696
if (baseEnv.GOOGLE_API_KEY && process.env.GOOGLE_API_KEY === baseEnv.GOOGLE_API_KEY) {
delete baseEnv.GOOGLE_API_KEY;
}
const defaultShellArgs = ['--login'];
try {
this.pty = spawn(shell || defaultShell, shellArgs || defaultShellArgs, {
cols: columns,
rows,
cwd,
env: getDecoratedEnv(baseEnv)
});
} catch (err) {
if (/is not a function/.test(err.message)) {
throw createNodePtyError();
} else {
throw err;
}
}
this.batcher = new DataBatcher(uid);
this.pty.on('data', chunk => {
if (this.ended) {
return;
}
this.batcher.write(chunk);
});
this.batcher.on('flush', data => {
this.emit('data', data);
});
this.pty.on('exit', () => {
if (!this.ended) {
this.ended = true;
this.emit('exit');
}
});
this.shell = shell || defaultShell;
}
exit() {
this.destroy();
}
write(data) {
if (this.pty) {
this.pty.write(data);
} else {
//eslint-disable-next-line no-console
console.warn('Warning: Attempted to write to a session with no pty');
}
}
resize({cols, rows}) {
if (this.pty) {
try {
this.pty.resize(cols, rows);
} catch (err) {
//eslint-disable-next-line no-console
console.error(err.stack);
}
} else {
//eslint-disable-next-line no-console
console.warn('Warning: Attempted to resize a session with no pty');
}
}
destroy() {
if (this.pty) {
try {
this.pty.kill();
} catch (err) {
//eslint-disable-next-line no-console
console.error('exit error', err.stack);
}
} else {
//eslint-disable-next-line no-console
console.warn('Warning: Attempted to destroy a session with no pty');
}
this.emit('exit');
this.ended = true;
}
};

View file

@ -1,263 +0,0 @@
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;
}
}

View file

@ -0,0 +1,89 @@
const Registry = require('winreg');
const appPath = `"${process.execPath}"`;
const regKey = `\\Software\\Classes\\Directory\\background\\shell\\Hyper`;
const regParts = [
{key: 'command', name: '', value: `${appPath} "%V"`},
{name: '', value: 'Open Hyper here'},
{name: 'Icon', value: `${appPath}`}
];
function addValues(hyperKey, commandKey, callback) {
hyperKey.set(regParts[1].name, Registry.REG_SZ, regParts[1].value, error => {
if (error) {
//eslint-disable-next-line no-console
console.error(error.message);
}
hyperKey.set(regParts[2].name, Registry.REG_SZ, regParts[2].value, err => {
if (err) {
//eslint-disable-next-line no-console
console.error(err.message);
}
commandKey.set(regParts[0].name, Registry.REG_SZ, regParts[0].value, err_ => {
if (err_) {
//eslint-disable-next-line no-console
console.error(err_.message);
}
callback();
});
});
});
}
exports.add = callback => {
const hyperKey = new Registry({hive: 'HKCU', key: regKey});
const commandKey = new Registry({
hive: 'HKCU',
key: `${regKey}\\${regParts[0].key}`
});
hyperKey.keyExists((error, exists) => {
if (error) {
//eslint-disable-next-line no-console
console.error(error.message);
}
if (exists) {
commandKey.keyExists((err_, exists_) => {
if (err_) {
//eslint-disable-next-line no-console
console.error(err_.message);
}
if (exists_) {
addValues(hyperKey, commandKey, callback);
} else {
commandKey.create(err => {
if (err) {
//eslint-disable-next-line no-console
console.error(err.message);
}
addValues(hyperKey, commandKey, callback);
});
}
});
} else {
hyperKey.create(err => {
if (err) {
//eslint-disable-next-line no-console
console.error(err.message);
}
commandKey.create(err_ => {
if (err_) {
//eslint-disable-next-line no-console
console.error(err_.message);
}
addValues(hyperKey, commandKey, callback);
});
});
}
});
};
exports.remove = callback => {
new Registry({hive: 'HKCU', key: regKey}).destroy(err => {
if (err) {
//eslint-disable-next-line no-console
console.error(err.message);
}
callback();
});
};

View file

@ -1,20 +0,0 @@
{
"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/**/*"
]
}

27
app/ui/contextmenu.js Normal file
View file

@ -0,0 +1,27 @@
const editMenu = require('../menus/menus/edit');
const shellMenu = require('../menus/menus/shell');
const {execCommand} = require('../commands');
const {getDecoratedKeymaps} = require('../plugins');
const separator = {type: 'separator'};
const getCommandKeys = keymaps =>
Object.keys(keymaps).reduce((commandKeys, command) => {
return Object.assign(commandKeys, {
[command]: keymaps[command][0]
});
}, {});
// only display cut/copy when there's a cursor selection
const filterCutCopy = (selection, menuItem) => {
if (/^cut$|^copy$/.test(menuItem.role) && !selection) {
return;
}
return menuItem;
};
module.exports = (createWindow, selection) => {
const commandKeys = getCommandKeys(getDecoratedKeymaps());
const _shell = shellMenu(commandKeys, execCommand).submenu;
const _edit = editMenu(commandKeys, execCommand).submenu.filter(filterCutCopy.bind(null, selection));
return _edit.concat(separator, _shell).filter(menuItem => !menuItem.hasOwnProperty('enabled') || menuItem.enabled);
};

View file

@ -1,42 +0,0 @@
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;

325
app/ui/window.js Normal file
View file

@ -0,0 +1,325 @@
const {app, BrowserWindow, shell, Menu} = require('electron');
const {isAbsolute} = require('path');
const {parse: parseUrl} = require('url');
const uuid = require('uuid');
const fileUriToPath = require('file-uri-to-path');
const isDev = require('electron-is-dev');
const updater = require('../updater');
const toElectronBackgroundColor = require('../utils/to-electron-background-color');
const {icon, homeDirectory} = require('../config/paths');
const createRPC = require('../rpc');
const notify = require('../notify');
const fetchNotifications = require('../notifications');
const Session = require('../session');
const contextMenuTemplate = require('./contextmenu');
const {execCommand} = require('../commands');
const {setRendererType, unsetRendererType} = require('../utils/renderer-utils');
const {decorateSessionOptions, decorateSessionClass} = require('../plugins');
module.exports = class Window {
constructor(options_, cfg, fn) {
const classOpts = Object.assign({uid: uuid.v4()});
app.plugins.decorateWindowClass(classOpts);
this.uid = classOpts.uid;
app.plugins.onWindowClass(this);
const winOpts = Object.assign(
{
minWidth: 370,
minHeight: 190,
backgroundColor: toElectronBackgroundColor(cfg.backgroundColor || '#000'),
titleBarStyle: 'hiddenInset',
title: 'Hyper.app',
// we want to go frameless on Windows and Linux
frame: process.platform === 'darwin',
transparent: process.platform === 'darwin',
icon,
show: process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev,
acceptFirstMouse: true
},
options_
);
const window = new BrowserWindow(app.plugins.getDecoratedBrowserOptions(winOpts));
window.uid = classOpts.uid;
const rpc = createRPC(window);
const sessions = new Map();
const updateBackgroundColor = () => {
const cfg_ = app.plugins.getDecoratedConfig();
window.setBackgroundColor(toElectronBackgroundColor(cfg_.backgroundColor || '#000'));
};
// config changes
const cfgUnsubscribe = app.config.subscribe(() => {
const cfg_ = app.plugins.getDecoratedConfig();
// 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 => 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);
delete app.windowCallback;
fetchNotifications(window);
// auto updates
if (!isDev) {
updater(window);
} else {
//eslint-disable-next-line no-console
console.log('ignoring auto updates during dev');
}
});
function createSession(extraOptions = {}) {
const uid = uuid.v4();
const defaultOptions = Object.assign(
{
rows: 40,
cols: 100,
cwd: process.argv[1] && isAbsolute(process.argv[1]) ? process.argv[1] : homeDirectory,
splitDirection: undefined,
shell: cfg.shell,
shellArgs: cfg.shellArgs && Array.from(cfg.shellArgs)
},
extraOptions,
{uid}
);
const options = decorateSessionOptions(defaultOptions);
const DecoratedSession = decorateSessionClass(Session);
const session = new DecoratedSession(options);
sessions.set(uid, session);
return {session, options};
}
// Optimistically create the initial session so that when the window sends
// the first "new" IPC message, there's a session already warmed up.
function createInitialSession() {
let {session, options} = createSession();
const initialEvents = [];
const handleData = data => initialEvents.push(['session data', data]);
const handleExit = () => initialEvents.push(['session exit']);
session.on('data', handleData);
session.on('exit', handleExit);
function flushEvents() {
for (let args of initialEvents) {
rpc.emit(...args);
}
session.removeListener('data', handleData);
session.removeListener('exit', handleExit);
}
return {session, options, flushEvents};
}
let initialSession = createInitialSession();
rpc.on('new', extraOptions => {
const {session, options} = initialSession || 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.pid
});
// If this is the initial session, flush any events that might have
// occurred while the window was initializing
if (initialSession) {
initialSession.flushEvents();
initialSession = null;
}
session.on('data', data => {
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 = 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}) => {
shell.openExternal(url);
});
rpc.on('open context menu', selection => {
const {createWindow} = app;
const {buildFromTemplate} = 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
for (const ev of ['maximize', 'unmaximize', 'minimize', 'restore']) {
window.on(ev, () => rpc.emit('windowGeometry change'));
}
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);
});
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();
}
});
// If file is dropped onto the terminal window, navigate event is prevented
// and his path is added to active session.
window.webContents.on('will-navigate', (event, url) => {
const protocol = typeof url === 'string' && parseUrl(url).protocol;
if (protocol === 'file:') {
event.preventDefault();
const path = fileUriToPath(url);
rpc.emit('session data send', {data: path, escaped: true});
} else if (protocol === 'http:' || protocol === 'https:') {
event.preventDefault();
rpc.emit('session data send', {data: url});
}
});
// xterm makes link clickable
window.webContents.on('new-window', (event, url) => {
const protocol = typeof url === 'string' && parseUrl(url).protocol;
if (protocol === 'http:' || protocol === 'https:') {
event.preventDefault();
shell.openExternal(url);
}
});
// 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 => {
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;
}
};

View file

@ -1,376 +0,0 @@
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;
}

101
app/updater.js Normal file
View file

@ -0,0 +1,101 @@
// Packages
const electron = require('electron');
const {app} = electron;
const ms = require('ms');
const retry = require('async-retry');
// Utilities
// eslint-disable-next-line no-unused-vars
const {version} = require('./package');
const {getDecoratedConfig} = require('./plugins');
const {platform} = process;
const isLinux = platform === 'linux';
const autoUpdater = isLinux ? require('./auto-updater-linux') : electron.autoUpdater;
let isInit = false;
// Default to the "stable" update channel
let canaryUpdates = false;
const buildFeedUrl = (canary, currentVersion) => {
const updatePrefix = canary ? 'releases-canary' : 'releases';
return `https://${updatePrefix}.hyper.is/update/${isLinux ? 'deb' : platform}/${currentVersion}`;
};
const isCanary = updateChannel => updateChannel === 'canary';
async function init() {
autoUpdater.on('error', (err, msg) => {
//eslint-disable-next-line no-console
console.error('Error fetching updates', msg + ' (' + err.stack + ')');
});
const config = await retry(async () => {
const content = await getDecoratedConfig();
if (!content) {
throw new Error('No config content loaded');
}
return content;
});
// If defined in the config, switch to the "canary" channel
if (config.updateChannel && isCanary(config.updateChannel)) {
canaryUpdates = true;
}
const feedURL = buildFeedUrl(canaryUpdates, version);
autoUpdater.setFeedURL(feedURL);
setTimeout(() => {
autoUpdater.checkForUpdates();
}, ms('10s'));
setInterval(() => {
autoUpdater.checkForUpdates();
}, ms('30m'));
isInit = true;
}
module.exports = win => {
if (!isInit) {
init();
}
const {rpc} = win;
const onupdate = (ev, releaseNotes, releaseName, date, updateUrl, onQuitAndInstall) => {
const releaseUrl = updateUrl || `https://github.com/zeit/hyper/releases/tag/${releaseName}`;
rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !!onQuitAndInstall});
};
const eventName = isLinux ? 'update-available' : 'update-downloaded';
autoUpdater.on(eventName, onupdate);
rpc.once('quit and install', () => {
autoUpdater.quitAndInstall();
});
app.config.subscribe(() => {
const {updateChannel} = app.plugins.getDecoratedConfig();
const newUpdateIsCanary = isCanary(updateChannel);
if (newUpdateIsCanary !== canaryUpdates) {
const feedURL = buildFeedUrl(newUpdateIsCanary, version);
autoUpdater.setFeedURL(feedURL);
autoUpdater.checkForUpdates();
canaryUpdates = newUpdateIsCanary;
}
});
win.on('close', () => {
autoUpdater.removeListener(eventName, onupdate);
});
};

View file

@ -1,134 +0,0 @@
// 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;

122
app/utils/cli-install.js Normal file
View file

@ -0,0 +1,122 @@
const pify = require('pify');
const fs = require('fs');
const path = require('path');
const Registry = require('winreg');
const notify = require('../notify');
const {cliScriptPath, cliLinkPath} = require('../config/paths');
const readlink = pify(fs.readlink);
const symlink = pify(fs.symlink);
const checkInstall = () => {
return readlink(cliLinkPath)
.then(link => link === cliScriptPath)
.catch(err => {
if (err.code === 'ENOENT') {
return false;
}
throw err;
});
};
const addSymlink = () => {
return checkInstall().then(isInstalled => {
if (isInstalled) {
//eslint-disable-next-line no-console
console.log('Hyper CLI already in PATH');
return Promise.resolve();
}
//eslint-disable-next-line no-console
console.log('Linking HyperCLI');
return symlink(cliScriptPath, cliLinkPath);
});
};
const addBinToUserPath = () => {
// Can't use pify because of param order of Registry.values callback
return new Promise((resolve, reject) => {
const envKey = new Registry({hive: 'HKCU', key: '\\Environment'});
envKey.values((err, items) => {
if (err) {
reject(err);
return;
}
// C:\Users\<user>\AppData\Local\hyper\app-<version>\resources\bin
const binPath = path.dirname(cliScriptPath);
// C:\Users\<user>\AppData\Local\hyper
const basePath = path.resolve(binPath, '../../..');
const pathItem = items.find(item => item.name.toUpperCase() === 'PATH');
let newPathValue = binPath;
const pathItemName = pathItem ? pathItem.name : 'PATH';
if (pathItem) {
const pathParts = pathItem.value.split(';');
const existingPath = pathParts.find(pathPart => pathPart === binPath);
if (existingPath) {
//eslint-disable-next-line no-console
console.log('Hyper CLI already in PATH');
resolve();
return;
}
// Because version is in path we need to remove old path if present and add current path
newPathValue = pathParts
.filter(pathPart => !pathPart.startsWith(basePath))
.concat([binPath])
.join(';');
}
//eslint-disable-next-line no-console
console.log('Adding HyperCLI path (registry)');
envKey.set(pathItemName, Registry.REG_SZ, newPathValue, error => {
if (error) {
reject(error);
return;
}
resolve();
});
});
});
};
const logNotify = (withNotification, ...args) => {
//eslint-disable-next-line no-console
console.log(...args);
withNotification && notify(...args);
};
exports.installCLI = withNotification => {
if (process.platform === 'win32') {
addBinToUserPath()
.then(() =>
logNotify(
withNotification,
'Hyper CLI installed',
'You may need to restart your computer to complete this installation process.'
)
)
.catch(err =>
logNotify(withNotification, 'Hyper CLI installation failed', `Failed to add Hyper CLI path to user PATH ${err}`)
);
} else if (process.platform === 'darwin') {
addSymlink()
.then(() => logNotify(withNotification, 'Hyper CLI installed', `Symlink created at ${cliLinkPath}`))
.catch(err => {
// 'EINVAL' is returned by readlink,
// 'EEXIST' is returned by symlink
const error =
err.code === 'EEXIST' || err.code === 'EINVAL'
? `File already exists: ${cliLinkPath}`
: `Symlink creation failed: ${err.code}`;
//eslint-disable-next-line no-console
console.error(err);
logNotify(withNotification, 'Hyper CLI installation failed', error);
});
} else {
withNotification &&
notify('Hyper CLI installation', 'Command is added in PATH only at package installation. Please reinstall.');
}
};

View file

@ -1,159 +0,0 @@
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}`);
}
};

View file

@ -19,18 +19,14 @@ const colorList = [
'grayscale'
];
export const getColorMap: {
<T>(colors: T): T extends (infer U)[] ? {[k: string]: U} : T;
} = (colors) => {
exports.getColorMap = 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;
}, {});
};

View file

@ -1,29 +1,29 @@
const generatePrefixedCommand = (command: string, shortcuts: string[]) => {
const result: Record<string, string[]> = {};
const generatePrefixedCommand = (command, shortcuts) => {
const result = {};
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}`);
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) => {
module.exports = config => {
return Object.keys(config).reduce((keymap, command) => {
if (!command) {
return keymap;
return;
}
// 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) => {
const shortcuts = Array.isArray(config[command]) ? config[command] : [config[command]];
const fixedShortcuts = [];
shortcuts.forEach(shortcut => {
let newShortcut = shortcut;
if (newShortcut.indexOf('cmd') !== -1) {
// Mousetrap use `command` and not `cmd`
//eslint-disable-next-line no-console
console.warn('Your config use deprecated `cmd` in key combination. Please use `command` instead.');
newShortcut = newShortcut.replace('cmd', 'command');
}
@ -39,5 +39,3 @@ const mapKeys = (config: Record<string, string[] | string>) => {
return keymap;
}, {});
};
export default mapKeys;

View file

@ -0,0 +1,19 @@
const rendererTypes = {};
function getRendererTypes() {
return rendererTypes;
}
function setRendererType(uid, type) {
rendererTypes[uid] = type;
}
function unsetRendererType(uid) {
delete rendererTypes[uid];
}
module.exports = {
getRendererTypes,
setRendererType,
unsetRendererType
};

View file

@ -1,15 +0,0 @@
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};

View file

@ -1,25 +0,0 @@
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;
};

View file

@ -1,60 +0,0 @@
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);
}
});
};

View file

@ -1,10 +1,10 @@
// Packages
import Color from 'color';
const Color = require('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) => {
module.exports = bgColor => {
const color = Color(bgColor);
if (color.alpha() === 1) {
@ -13,7 +13,12 @@ const toElectronBackgroundColor = (bgColor: string) => {
// http://stackoverflow.com/a/11019879/1202488
const alphaHex = Math.round(color.alpha() * 255).toString(16);
return `#${alphaHex}${color.hex().toString().slice(1)}`;
return (
'#' +
alphaHex +
color
.hex()
.toString()
.substr(1)
);
};
export default toElectronBackgroundColor;

View file

@ -1,6 +1,6 @@
import electron from 'electron';
const electron = require('electron');
export function positionIsValid(position: [number, number]) {
function positionIsValid(position) {
const displays = electron.screen.getAllDisplays();
const [x, y] = position;
@ -8,3 +8,7 @@ export function positionIsValid(position: [number, number]) {
return x >= workArea.x && x <= workArea.x + workArea.width && y >= workArea.y && y <= workArea.y + workArea.height;
});
}
module.exports = {
positionIsValid
};

File diff suppressed because it is too large Load diff

32
appveyor.yml Normal file
View file

@ -0,0 +1,32 @@
# https://github.com/sindresorhus/appveyor-node/blob/master/appveyor.yml
environment:
matrix:
- platform: x64
image: Visual Studio 2015
init:
- yarn config set msvs_version 2015 # we need this to build `pty.js`
install:
- ps: Install-Product node 10.2.0 x64
- set CI=true
- yarn
build: off
matrix:
fast_finish: true
shallow_clone: true
test_script:
- node --version
- yarn --version
- yarn run test
on_success:
- IF %APPVEYOR_REPO_BRANCH%==canary cp build\canary.ico build\icon.ico
- yarn run dist
- ps: Get-ChildItem .\dist\squirrel-windows\*.exe | % { Push-AppveyorArtifact $_.FullName }

View file

@ -1,9 +0,0 @@
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
};

View file

@ -1,8 +0,0 @@
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
};

View file

@ -1,18 +0,0 @@
{
"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"
]
}

View file

@ -1,88 +0,0 @@
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')
}

Some files were not shown because too many files have changed in this diff Show more