Compare commits

..

1 commit

Author SHA1 Message Date
dependabot-preview[bot]
c851fee97b
Create Dependabot config file 2020-06-16 08:23:37 +00:00
159 changed files with 10227 additions and 12885 deletions

98
.circleci/config.yml Normal file
View file

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

View file

@ -9,6 +9,4 @@ assets
website
bin
dist
target
cache
schema.json
target

View file

@ -3,9 +3,7 @@
"react",
"prettier",
"@typescript-eslint",
"eslint-comments",
"lodash",
"import"
"eslint-comments"
],
"extends": [
"eslint:recommended",
@ -15,16 +13,14 @@
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"impliedStrict": true,
"experimentalObjectRestSpread": true
},
"allowImportExportEverywhere": true,
"project": [
"./tsconfig.eslint.json"
]
"allowImportExportEverywhere": true
},
"env": {
"es6": true,
@ -34,11 +30,7 @@
"settings": {
"react": {
"version": "detect"
},
"import/resolver": {
"typescript": {}
},
"import/internal-regex": "^(electron|react)$"
}
},
"rules": {
"func-names": [
@ -62,21 +54,34 @@
"bracketSpacing": false,
"semi": true,
"useTabs": false,
"bracketSameLine": false
"jsxBracketSameLine": false
}
],
"eslint-comments/no-unused-disable": "error",
"react/no-unknown-property":[
"error",
{
"ignore": [
"jsx",
"global"
]
}
]
"eslint-comments/no-unused-disable": "error"
},
"overrides": [
{
"files": [
"app/config/config-default.js",
".hyper.js"
],
"rules": {
"prettier/prettier": [
"error",
{
"printWidth": 120,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": false,
"semi": true,
"useTabs": false,
"parser": "babel",
"jsxBracketSameLine": false
}
]
}
},
{
"files": [
"**.ts",
@ -84,77 +89,13 @@
],
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier"
"prettier/@typescript-eslint"
],
"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"
"@typescript-eslint/prefer-optional-chain": "error"
}
}
]

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

@ -5,21 +5,19 @@ updates:
schedule:
interval: weekly
time: '11:00'
open-pull-requests-limit: 30
open-pull-requests-limit: 10
target-branch: canary
versioning-strategy: increase
ignore:
- dependency-name: "@types/node"
versions:
- ">= 13.a, < 14"
- dependency-name: "@types/node"
versions:
- ">= 14.a, < 15"
- package-ecosystem: npm
directory: "/app"
schedule:
interval: weekly
time: '11:00'
open-pull-requests-limit: 30
target-branch: canary
versioning-strategy: increase
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
time: '11:00'
open-pull-requests-limit: 30
open-pull-requests-limit: 10
target-branch: canary

View file

@ -1,12 +1,3 @@
---
name: Bug report
about: Create a report to help Hyper improve
title: ''
labels: ''
assignees: ''
---
<!--
Hi there! Thank you for discovering and submitting an issue.
@ -16,8 +7,8 @@ assignees: ''
-->
<!-- 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

@ -3,6 +3,6 @@
- To help whoever reviews your PR, it'd be extremely helpful for you to list whether your PR is ready to be merged,
If there's anything left to do and if there are any related PRs
- It'd also be extremely helpful to enable us to update your PR incase we need to rebase or what-not by checking `Allow edits from maintainers`
- If your PR changes some API, please make a PR for hyper website too: https://github.com/vercel/hyper-site.
- 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,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@v3.1.4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: nodejs.yml
run_id: ${{ github.event.workflow_run.id }}
name: e2e
- name: Get PR number
uses: dawidd6/action-download-artifact@v3.1.4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: nodejs.yml
run_id: ${{ github.event.workflow_run.id }}
name: pr_num
- name: Read the pr_num file
id: pr_num_reader
uses: juliangruber/read-file-action@v1.1.7
with:
path: ./pr_num.txt
- name: List images
run: ls -al
- name: Upload images to imgur
id: upload_screenshots
uses: devicons/public-upload-to-imgur@v2.2.2
with:
path: ./*.png
client_id: ${{ secrets.IMGUR_CLIENT_ID }}
- name: Comment on the PR
uses: jungwinter/comment@v1
env:
IMG_MARKDOWN: ${{ join(fromJSON(steps.upload_screenshots.outputs.markdown_urls), '') }}
MESSAGE: |
Hi there,
Thank you for contributing to Hyper!
You can get the build artifacts from [here](https://nightly.link/{1}/actions/runs/{2}).
Here are screenshots of Hyper built from this pr.
{0}
with:
type: create
issue_number: ${{ steps.pr_num_reader.outputs.content }}
token: ${{ secrets.GITHUB_TOKEN }}
body: ${{ format(env.MESSAGE, env.IMG_MARKDOWN, github.repository, github.event.workflow_run.id) }}
- name: Hide older comments
uses: kanga333/comment-hider@v0.4.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
leave_visible: 1
issue_number: ${{ steps.pr_num_reader.outputs.content }}

View file

@ -5,49 +5,21 @@ on:
- master
- canary
pull_request:
defaults:
run:
shell: bash
env:
NODE_VERSION: 18.x
jobs:
build:
runs-on: ${{matrix.os}}
ci_macos:
runs-on: macos-latest
strategy:
matrix:
os:
- macos-latest
- ubuntu-latest
- windows-latest
fail-fast: false
node-version: [12.x]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ env.NODE_VERSION }}
- name: Fix node-gyp and Python
run: python3 -m pip install packaging setuptools
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache/restore@v4
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock', 'app/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
node-version: ${{ matrix.node-version }}
- name: Install
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
if: runner.os == 'Linux'
run: |
sudo apt update
sudo apt install libarchive-tools
- name: Lint and Run Unit Tests
- name: Test
run: yarn run test
- name: Getting Build Icon
if: github.ref == 'refs/heads/canary' || github.base_ref == 'canary'
@ -55,136 +27,112 @@ jobs:
cp build/canary.ico build/icon.ico
cp build/canary.icns build/icon.icns
- name: Build
run: yarn run dist --publish=never
env:
CI: true
- name: Get macOS Artifact Names
id: getmacosfilename
run: |
if [ -z "$CSC_LINK" ] ; then unset CSC_LINK ; fi
if [ -z "$CSC_KEY_PASSWORD" ] ; then unset CSC_KEY_PASSWORD ; fi
if [ -z "$WIN_CSC_LINK" ] ; then unset WIN_CSC_LINK ; fi
if [ -z "$WIN_CSC_KEY_PASSWORD" ] ; then unset WIN_CSC_KEY_PASSWORD ; fi
if [ -z "$APPLE_ID" ] ; then unset APPLE_ID ; fi
if [ -z "$APPLE_APP_SPECIFIC_PASSWORD" ] ; then unset APPLE_APP_SPECIFIC_PASSWORD ; fi
yarn run dist
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.MAC_CERT_P12_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_P12_PASSWORD }}
WIN_CSC_LINK: ${{ secrets.WIN_CERT_P12_BASE64 }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CERT_P12_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
- name: Archive Build Artifacts
uses: LabhanshAgrawal/upload-artifact@v3
echo "::set-output name=dmgName::$(ls dist/*.dmg | cut -d'/' -f2)"
echo "::set-output name=dmgPath::$(ls dist/*.dmg)"
- name: Archive macOS Build Artifacts
uses: actions/upload-artifact@v1
with:
path: |
dist/*.dmg
dist/*.snap
dist/*.AppImage
dist/*.deb
dist/*.rpm
dist/*.pacman
dist/*.exe
- name: Run E2E Tests
if: runner.os != 'Linux'
run: yarn run test:e2e
- name: Run E2E Tests on Linux
if: runner.os == 'Linux'
uses: GabrielBB/xvfb-action@v1.6
with:
run: yarn run test:e2e
env:
SHELL: /bin/bash
- name: Archive E2E test screenshot
uses: actions/upload-artifact@v3
with:
name: e2e
path: dist/tmp/*.png
- name: Save the pr number in an artifact
if: github.event_name == 'pull_request'
env:
PR_NUM: ${{ github.event.number }}
run: echo $PR_NUM > pr_num.txt
- name: Upload the pr num
uses: actions/upload-artifact@v3
if: github.event_name == 'pull_request'
with:
name: pr_num
path: ./pr_num.txt
- uses: actions/cache/save@v4
if: github.event_name == 'push'
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock', 'app/yarn.lock') }}
build-linux-arm:
name: ${{ steps.getmacosfilename.outputs.dmgName }}
path: ${{ steps.getmacosfilename.outputs.dmgPath }}
ci_linux:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- name: armv7l
cpu: cortex-a8
image: raspios_lite:latest
- name: arm64
cpu: cortex-a53
image: raspios_lite_arm64:latest
fail-fast: false
node-version: [12.x]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ env.NODE_VERSION }}
- name: Fix node-gyp and Python
run: python3 -m pip install packaging setuptools
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache/restore@v4
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock', 'app/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
node-version: ${{ matrix.node-version }}
- name: Install
run: yarn install
- name: Test
run: yarn run test
- name: Getting Build Icon
if: github.ref == 'refs/heads/canary' || github.base_ref == 'canary'
run: |
yarn install
sudo apt update
sudo apt install libarchive-tools
- name: Compile
run: yarn run build
- name: rebuild node-pty
uses: pguyot/arm-runner-action@v2.5.2
with:
image_additional_mb: 2000
base_image: ${{ matrix.image }}
cpu: ${{ matrix.cpu }}
shell: bash
copy_artifact_path: target/node_modules/node-pty
copy_artifact_dest: target/node_modules
commands: |
wget https://nodejs.org/dist/v18.16.0/node-v18.16.0-linux-${{ matrix.name }}.tar.xz
tar -xJf node-v18.16.0-linux-${{ matrix.name }}.tar.xz
sudo cp node-v18.16.0-linux-${{ matrix.name }}/* /usr/local/ -R
npm run rebuild-node-pty
- name: chown node-pty
run: |
sudo chown -R $USER:$USER target/node_modules/node-pty
- name: Prepare v8 snapshot
if: matrix.name == 'armv7l'
run: |
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install -y libglib2.0-0:i386 libexpat1:i386 libgcc-s1:i386
npm_config_arch=armv7l yarn run v8-snapshot:arch
cp build/canary.ico build/icon.ico
cp build/canary.icns build/icon.icns
- name: Build
run: yarn run electron-builder -l deb rpm AppImage pacman --${{ matrix.name }} -c electron-builder-linux-ci.json
run: yarn run dist --publish=never
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Archive Build Artifacts
uses: LabhanshAgrawal/upload-artifact@v3
CI: true
- name: Get Linux Artifact Names
id: getlinuxfilename
run: |
echo "::set-output name=snapName::$(ls dist/*.snap | cut -d'/' -f2)"
echo "::set-output name=snapPath::$(ls dist/*.snap)"
echo "::set-output name=AppImageName::$(ls dist/*.AppImage | cut -d'/' -f2)"
echo "::set-output name=AppImagePath::$(ls dist/*.AppImage)"
echo "::set-output name=debName::$(ls dist/*.deb | cut -d'/' -f2)"
echo "::set-output name=debPath::$(ls dist/*.deb)"
echo "::set-output name=rpmName::$(ls dist/*.rpm | cut -d'/' -f2)"
echo "::set-output name=rpmPath::$(ls dist/*.rpm)"
- name: Archive Linux Build Artifacts (Snap)
uses: actions/upload-artifact@v1
with:
path: |
dist/*.snap
dist/*.AppImage
dist/*.deb
dist/*.rpm
dist/*.pacman
name: ${{ steps.getlinuxfilename.outputs.snapName }}
path: ${{ steps.getlinuxfilename.outputs.snapPath }}
- name: Archive Linux Build Artifacts (AppImage)
uses: actions/upload-artifact@v1
with:
name: ${{ steps.getlinuxfilename.outputs.AppImageName }}
path: ${{ steps.getlinuxfilename.outputs.AppImagePath }}
- name: Archive Linux Build Artifacts (Deb)
uses: actions/upload-artifact@v1
with:
name: ${{ steps.getlinuxfilename.outputs.debName }}
path: ${{ steps.getlinuxfilename.outputs.debPath }}
- name: Archive Linux Build Artifacts (RPM)
uses: actions/upload-artifact@v1
with:
name: ${{ steps.getlinuxfilename.outputs.rpmName }}
path: ${{ steps.getlinuxfilename.outputs.rpmPath }}
ci_windows:
runs-on: windows-latest
strategy:
matrix:
node-version: [12.x]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install
run: yarn install
- name: Test
run: yarn run test
- name: Getting Build Icon
if: github.ref == 'refs/heads/canary' || github.base_ref == 'canary'
run: |
Copy-Item .\build\canary.ico .\build\icon.ico
Copy-Item .\build\canary.icns .\build\icon.icns
- name: Build
run: yarn run dist --publish=never
env:
CI: true
- name: Get Windows Artifact Names
id: getwindowsfilename
run: |
Write-Host "::set-output name=exeName::$(Get-ChildItem -Name .\dist\squirrel-windows\ | Select-String exe)"
Write-Host "::set-output name=exePath::dist/squirrel-windows/$(Get-ChildItem -Name .\dist\squirrel-windows\ | Select-String exe)"
Write-Host "::set-output name=nupkgName::$(Get-ChildItem -Name .\dist\squirrel-windows\ | Select-String nupkg)"
Write-Host "::set-output name=nupkgPath::dist/squirrel-windows/$(Get-ChildItem -Name .\dist\squirrel-windows\ | Select-String nupkg)"
- name: Archive Windows Build Artifacts (exe)
uses: actions/upload-artifact@v1
with:
name: ${{ steps.getwindowsfilename.outputs.exeName }}
path: ${{ steps.getwindowsfilename.outputs.exePath }}
- name: Archive Windows Build Artifacts (nupkg)
uses: actions/upload-artifact@v1
with:
name: ${{ steps.getwindowsfilename.outputs.nupkgName }}
path: ${{ steps.getwindowsfilename.outputs.nupkgPath }}

10
.gitignore vendored
View file

@ -3,7 +3,6 @@ dist
app/renderer
target
bin/cli.*
cache
# dependencies
node_modules
@ -13,11 +12,8 @@ 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
.vscode/settings.json

1
.husky/.gitignore vendored
View file

@ -1 +0,0 @@
_

View file

@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn test

6
.huskyrc.json Normal file
View file

@ -0,0 +1,6 @@
{
"$schema": "http://json.schemastore.org/huskyrc",
"hooks": {
"pre-push": "yarn test"
}
}

41
.travis.yml Normal file
View file

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

View file

@ -1 +1,6 @@
registry "https://registry.npmjs.org/"
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
child-concurrency "1"
lastUpdateCheck 1570388773781

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.ts).
After launching Hyper in dev mode, run `yarn run app`, it should log that your plugin has been correcty loaded: `Plugin hyper-awesome-plugin (0.1.0) loaded.`. Name and version printed are the ones in your plugins `package.json` file.
@ -70,7 +70,7 @@ exports.decorateTerms = (Terms, {React}) => {
// <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

View file

@ -1,32 +1,30 @@
![](https://assets.vercel.com/image/upload/v1549723846/repositories/hyper/hyper-3-repo-banner.png)
![](https://assets.zeit.co/image/upload/v1549723846/repositories/hyper/hyper-3-repo-banner.png)
<p align="center">
<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">
<img src="https://img.shields.io/badge/MADE%20BY%20Vercel-000000.svg?style=for-the-badge&logo=ZEIT&labelColor=000000&logoWidth=20">
</a>
</p>
[![Node CI](https://github.com/vercel/hyper/workflows/Node%20CI/badge.svg?event=push)](https://github.com/vercel/hyper/actions?query=workflow%3A%22Node+CI%22+branch%3Acanary+event%3Apush)
[![Node CI](https://github.com/zeit/hyper/workflows/Node%20CI/badge.svg?event=push)](https://github.com/zeit/hyper/actions?query=workflow%3A%22Node+CI%22+branch%3Acanary+event%3Apush)
[![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 extensible experience for command-line interface users, built on open web standards. In the beginning, our focus will be primarily around speed, stability and the development of the correct API for extension authors.
In the future, we anticipate the community will come up with innovative additions to enhance what could be the simplest, most powerful and well-tested interface for productivity.
## Usage
[Download the latest release!](https://hyper.is/#installation)
### Linux
#### Arch and derivatives
Hyper is available in the [AUR](https://aur.archlinux.org/packages/hyper/). Use an AUR [package manager](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
aurman -S hyper
```
#### NixOS
@ -42,7 +40,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
@ -99,7 +97,7 @@ make sure its build process is working correctly by running `yarn run rebuild-no
If you are on macOS, this typically is related to Xcode issues (like not having agreed
to the Terms of Service by running `sudo xcodebuild` after a fresh Xcode installation).
##### Error with `C++` on macOS when running `yarn`
##### Error with `c++` on macOS when running `yarn`
If you are getting compiler errors when running `yarn` add the environment variable `export CXX=clang++`
@ -110,7 +108,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,6 +1,5 @@
import {EventEmitter} from 'events';
import fetch from 'electron-fetch';
import {EventEmitter} from 'events';
class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
updateURL!: string;
@ -22,24 +21,26 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
this.emit('checking-for-update');
fetch(this.updateURL)
.then((res) => {
.then((res): any => {
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}) => {
// eslint-disable-next-line @typescript-eslint/camelcase
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`
// eslint-disable-next-line @typescript-eslint/camelcase
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: any) {
if (typeof error === 'string') {
error = new Error(error);
}
@ -47,6 +48,4 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
}
}
const autoUpdaterLinux = new AutoUpdater();
export default autoUpdaterLinux;
export default new AutoUpdater();

View file

@ -1,10 +1,7 @@
import {app, Menu} from 'electron';
import type {BrowserWindow} from 'electron';
import {app, Menu, 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': () => {
@ -28,7 +25,7 @@ const commands: Record<string, (focusedWindow?: BrowserWindow) => void> = {
focusedWindow?.rpc.emit('termgroup close req');
},
'window:preferences': () => {
void openConfig();
openConfig();
},
'editor:clearBuffer': (focusedWindow) => {
focusedWindow?.rpc.emit('session clear req');
@ -104,15 +101,6 @@ const commands: Record<string, (focusedWindow?: BrowserWindow) => void> = {
'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');
},
@ -120,21 +108,12 @@ const commands: Record<string, (focusedWindow?: BrowserWindow) => void> = {
focusedWindow?.rpc.emit('session search close');
},
'cli:install': () => {
void installCLI(true);
installCLI(true);
},
'window:hamburgerMenu': () => {
if (process.platform !== 'darwin' && ['', true].includes(getConfig().showHamburgerMenu)) {
Menu.getApplicationMenu()!.popup({x: 25, y: 22});
if (getConfig().showHamburgerMenu) {
Menu.getApplicationMenu()!.popup({x: 15, y: 15});
}
},
'systemContextMenu:add': () => {
systemContextMenu.add();
},
'systemContextMenu:remove': () => {
systemContextMenu.remove();
},
'window:toggleKeepOnTop': (focusedWindow) => {
focusedWindow?.setAlwaysOnTop(!focusedWindow.isAlwaysOnTop());
}
};
@ -146,22 +125,6 @@ const commands: Record<string, (focusedWindow?: BrowserWindow) => void> = {
};
});
//Profile specific commands
getConfig().profiles.forEach((profile) => {
commands[`window:new:${profile.name}`] = () => {
setTimeout(() => app.createWindow(undefined, undefined, profile.name), 0);
};
commands[`tab:new:${profile.name}`] = (focusedWindow) => {
focusedWindow?.rpc.emit('termgroup add req', {profile: profile.name});
};
commands[`pane:splitRight:${profile.name}`] = (focusedWindow) => {
focusedWindow?.rpc.emit('split request vertical', {profile: profile.name});
};
commands[`pane:splitDown:${profile.name}`] = (focusedWindow) => {
focusedWindow?.rpc.emit('split request horizontal', {profile: profile.name});
};
});
export const execCommand = (command: string, focusedWindow?: BrowserWindow) => {
const fn = commands[command];
if (fn) {

View file

@ -1,24 +1,21 @@
import {app} from 'electron';
import chokidar from 'chokidar';
import type {parsedConfig, configOptions} from '../typings/config';
import fs from 'fs';
import notify from './notify';
import {_import, getDefaultConfig} from './config/import';
import _openConfig from './config/open';
import win from './config/windows';
import {cfgPath, cfgDir} from './config/paths';
import notify from './notify';
import {getColorMap} from './utils/colors';
import {parsedConfig, configOptions} from '../lib/config';
const watchers: Function[] = [];
const watchers: any[] = [];
let cfg: parsedConfig = {} as any;
let _watcher: chokidar.FSWatcher;
let _watcher: fs.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)) {
if ((config.css && config.css.includes(css)) || (config.termCSS && config.termCSS.includes(css))) {
deprecated.push(css);
}
});
@ -39,7 +36,7 @@ const checkDeprecatedConfig = () => {
const _watch = () => {
if (_watcher) {
return;
return _watcher;
}
const onChange = () => {
@ -47,26 +44,44 @@ const _watch = () => {
setTimeout(() => {
cfg = _import();
notify('Configuration updated', 'Hyper configuration reloaded!');
watchers.forEach((fn) => {
fn();
});
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);
// Windows
if (process.platform === 'win32') {
// watch for changes on config every 2s on Windows
// https://github.com/zeit/hyper/pull/1772
_watcher = fs.watchFile(cfgPath, {interval: 2000}, (curr, prev) => {
if (!curr.mtime || curr.mtime.getTime() === 0) {
console.error('error watching config');
} else if (curr.mtime.getTime() !== prev.mtime.getTime()) {
onChange();
}
}) as any;
return;
}
// macOS/Linux
function setWatcher() {
try {
_watcher = fs.watch(cfgPath, (eventType) => {
if (eventType === 'rename') {
_watcher.close();
// Ensure that new file has been written
setTimeout(() => setWatcher(), 500);
}
});
} catch (e) {
console.error('Failed to watch config file:', cfgPath, e);
return;
}
});
_watcher.on('change', onChange);
_watcher.on('error', (error) => {
console.error('error watching config', error);
});
}
setWatcher();
};
export const subscribe = (fn: Function) => {
@ -81,30 +96,8 @@ export const getConfigDir = () => {
return cfgDir;
};
export const getDefaultProfile = () => {
return cfg.config.defaultProfile || cfg.config.profiles[0]?.name || 'default';
};
// get config for the default profile, keeping it for backward compatibility
export const getConfig = () => {
return 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};
return cfg.config;
};
export const openConfig = () => {
@ -128,7 +121,9 @@ export const setup = () => {
checkDeprecatedConfig();
};
export {get as getWin, recordState as winRecord, defaults as windowDefaults} from './config/windows';
export const getWin = win.get;
export const winRecord = win.recordState;
export const windowDefaults = win.defaults;
export const fixConfigDefaults = (decoratedConfig: configOptions) => {
const defaultConfig = getDefaultConfig().config!;
@ -149,8 +144,8 @@ export const htermConfigTranslate = (config: configOptions) => {
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);
config.css = config.css && config.css.replace(searchvalue, newvalue);
config.termCSS = config.termCSS && config.termCSS.replace(searchvalue, newvalue);
});
return config;
};

View file

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

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

View file

@ -1,32 +1,105 @@
import {readFileSync, mkdirpSync} from 'fs-extra';
import type {rawConfig} from '../../typings/config';
import {moveSync, copySync, existsSync, writeFileSync, readFileSync, lstatSync} from 'fs-extra';
import {sync as mkdirpSync} from 'mkdirp';
import {defaultCfg, cfgPath, legacyCfgPath, plugs, defaultPlatformKeyPath} from './paths';
import {_init, _extractDefault} from './init';
import notify from '../notify';
import {_init} from './init';
import {migrateHyper3Config} from './migrate';
import {defaultCfg, cfgPath, plugs, defaultPlatformKeyPath} from './paths';
import {rawConfig} from '../../lib/config';
let defaultConfig: rawConfig;
const _write = (path: string, data: any) => {
// This method will take text formatted as Unix line endings and transform it
// to text formatted with DOS line endings. We do this because the default
// text editor on Windows (notepad) doesn't Deal with LF files. Still. In 2017.
const crlfify = (str: string) => {
return str.replace(/\r?\n/g, '\r\n');
};
const format = process.platform === 'win32' ? crlfify(data.toString()) : data;
writeFileSync(path, format, 'utf8');
};
// Saves a file as backup by appending '.backup' or '.backup2', '.backup3', etc.
// so as to not override any existing files
const saveAsBackup = (src: string) => {
let attempt = 1;
while (attempt < 100) {
try {
const backupPath = `${src}.backup${attempt === 1 ? '' : attempt}`;
moveSync(src, backupPath);
return backupPath;
} catch (e) {
if (e.code === 'EEXIST') {
attempt++;
} else {
throw e;
}
}
}
throw new Error('Failed to create backup for config file. Too many backups');
};
// Migrate Hyper2 config to Hyper3 but only if the user hasn't manually
// touched the new config and if the old config is not a symlink
const migrateHyper2Config = () => {
if (cfgPath === legacyCfgPath) {
// No need to migrate
return;
}
if (!existsSync(legacyCfgPath)) {
// Already migrated or user never used Hyper 2
return;
}
const existsNew = existsSync(cfgPath);
if (lstatSync(legacyCfgPath).isSymbolicLink() || (existsNew && lstatSync(cfgPath).isSymbolicLink())) {
// One of the files is a symlink, there could be a number of complications
// in this case so let's avoid those and not do automatic migration
return;
}
if (existsNew) {
const cfg1 = readFileSync(defaultCfg, 'utf8').replace(/\r|\n/g, '');
const cfg2 = readFileSync(cfgPath, 'utf8').replace(/\r|\n/g, '');
const hasNewConfigBeenTouched = cfg1 !== cfg2;
if (hasNewConfigBeenTouched) {
// Assume the user has migrated manually but rename old config to .backup so
// we don't keep trying to migrate on every launch
const backupPath = saveAsBackup(legacyCfgPath);
notify(
'Hyper 3',
`Settings location has changed to ${cfgPath}.\nWe've backed up your old Hyper config to ${backupPath}`
);
return;
}
}
// Migrate
copySync(legacyCfgPath, cfgPath);
saveAsBackup(legacyCfgPath);
notify(
'Hyper 3',
`Settings location has changed to ${cfgPath}.\nWe've automatically migrated your existing config!\nPlease restart Hyper now`
);
};
const _importConf = () => {
// init plugin directories if not present
mkdirpSync(plugs.base);
mkdirpSync(plugs.local);
try {
migrateHyper3Config();
migrateHyper2Config();
} catch (err) {
console.error(err);
}
let defaultCfgRaw = '{}';
let defaultCfgRaw = '';
try {
defaultCfgRaw = readFileSync(defaultCfg, 'utf8');
} catch (err) {
console.log(err);
}
const _defaultCfg = JSON.parse(defaultCfgRaw) as rawConfig;
const _defaultCfg = _extractDefault(defaultCfgRaw) as rawConfig;
// Importing platform specific keymap
let content = '{}';
@ -39,12 +112,12 @@ const _importConf = () => {
_defaultCfg.keymaps = mapping;
// Import user config
let userCfg: rawConfig;
let userCfg: string;
try {
userCfg = JSON.parse(readFileSync(cfgPath, 'utf8'));
userCfg = readFileSync(cfgPath, 'utf8');
} catch (err) {
notify("Couldn't parse config file. Using default config instead.");
userCfg = JSON.parse(defaultCfgRaw);
_write(cfgPath, defaultCfgRaw);
userCfg = defaultCfgRaw;
}
return {userCfg, defaultCfg: _defaultCfg};
@ -53,7 +126,7 @@ const _importConf = () => {
export const _import = () => {
const imported = _importConf();
defaultConfig = imported.defaultCfg;
const result = _init(imported.userCfg, imported.defaultCfg);
const result = _init(imported!);
return result;
};

View file

@ -1,27 +1,22 @@
import vm from 'vm';
import merge from 'lodash/merge';
import type {parsedConfig, rawConfig, configOptions} from '../../typings/config';
import notify from '../notify';
import mapKeys from '../utils/map-keys';
import {parsedConfig, rawConfig, configOptions} from '../../lib/config';
const _extract = (script?: vm.Script): Record<string, any> => {
const module: Record<string, any> = {};
script?.runInNewContext({module}, {displayErrors: true});
script?.runInNewContext({module});
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});
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});
}
};
@ -30,33 +25,23 @@ const _extractDefault = (cfg: string) => {
};
// init config
const _init = (userCfg: rawConfig, defaultCfg: rawConfig): parsedConfig => {
const _init = (cfg: {userCfg: string; defaultCfg: rawConfig}): parsedConfig => {
const script = _syntaxValidation(cfg.userCfg);
const _cfg = script && (_extract(script) as rawConfig);
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);
if (_cfg?.config) {
return _cfg.config;
} else {
notify('Error reading configuration: `config` key is missing');
return defaultCfg.config || ({} as configOptions);
return cfg.defaultCfg.config || ({} as configOptions);
}
})(),
// Merging platform specific keymaps with user defined keymaps
keymaps: mapKeys({...defaultCfg.keymaps, ...userCfg?.keymaps}),
keymaps: mapKeys({...cfg.defaultCfg.keymaps, ..._cfg?.keymaps}),
// Ignore undefined values in plugin and localPlugins array Issue #1862
plugins: userCfg?.plugins?.filter(Boolean) || [],
localPlugins: userCfg?.localPlugins?.filter(Boolean) || []
plugins: (_cfg?.plugins && _cfg.plugins.filter(Boolean)) || [],
localPlugins: (_cfg?.localPlugins && _cfg.localPlugins.filter(Boolean)) || []
};
};

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}`);
};

View file

@ -1,80 +1,86 @@
import {exec} from 'child_process';
import {shell} from 'electron';
import * as Registry from 'native-reg';
import {cfgPath} from './paths';
import * as regTypes from '../typings/native-reg';
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 = () => {
export default () => {
// 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');
// eslint-disable-next-line no-var, @typescript-eslint/no-var-requires
var Registry: typeof regTypes = require('native-reg');
} catch (err) {
console.error('Open config with default app error:', err);
console.error(err);
}
return openNotepad(cfgPath);
}
return shell.openPath(cfgPath).then((error) => error === '');
};
const {exec} = require('child_process') as typeof import('child_process');
export default openConfig;
const getUserChoiceKey = async () => {
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 = async () => {
const userChoice = await 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);
});
});
return hasDefaultSet()
.then((yes) => {
if (yes) {
return shell.openPath(cfgPath).then((error) => error === '');
}
console.warn('No default app set for .js files, using notepad.exe fallback');
return openNotepad(cfgPath);
})
.catch((err) => {
console.error('Open config with default app error:', err);
return openNotepad(cfgPath);
});
} else {
return Promise.resolve(shell.openPath(cfgPath).then((error) => error === ''));
}
};

View file

@ -1,36 +1,26 @@
// This module exports paths, names, and other metadata that is referenced
import {statSync} from 'fs';
import {homedir} from 'os';
import {resolve, join} from 'path';
import {app} from 'electron';
import {statSync} from 'fs';
import {resolve, join} from 'path';
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'
);
? app.getPath('userData')
: homedir();
let cfgPath = join(cfgDir, cfgFile);
const schemaPath = resolve(__dirname, schemaFile);
let cfgDir = applicationDirectory;
let cfgPath = join(applicationDirectory, cfgFile);
const legacyCfgPath = join(homeDirectory, cfgFile); // Hyper 2 config location
const devDir = resolve(__dirname, '../..');
const devCfg = join(devDir, cfgFile);
@ -48,8 +38,10 @@ if (isDev) {
}
}
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')
@ -90,7 +82,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"
}

View file

@ -1,21 +1,23 @@
import type {BrowserWindow} from 'electron';
import Config from 'electron-store';
import {BrowserWindow} from 'electron';
export const defaults = {
windowPosition: [50, 50] as [number, number],
windowSize: [540, 380] as [number, number]
const defaults = {
windowPosition: [50, 50],
windowSize: [540, 380]
};
// 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());
}
export default {
defaults,
get() {
const position = cfg.get('windowPosition');
const size = cfg.get('windowSize');
return {position, size};
},
recordState(win: BrowserWindow) {
cfg.set('windowPosition', win.getPosition());
cfg.set('windowSize', win.getSize());
}
};

8
app/ext-modules.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
declare module 'git-describe' {
export function gitDescribe(...args: any[]): void;
}
declare module 'default-shell' {
const val: string;
export default val;
}

1
app/index.d.ts vendored Normal file
View file

@ -0,0 +1 @@
// Dummy file, required by tsc

View file

@ -1,40 +1,86 @@
// eslint-disable-next-line import/order
import {cfgPath} from './config/paths';
// Print diagnostic information for a few arguments instead of running Hyper.
if (['--help', '-v', '--version'].includes(process.argv[1])) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {version} = require('./package');
const configLocation = process.platform === 'win32' ? `${process.env.userprofile}\\.hyper.js` : '~/.hyper.js';
console.log(`Hyper version ${version}`);
console.log('Hyper does not accept any command line arguments. Please modify the config file instead.');
console.log(`Hyper configuration file located at: ${cfgPath}`);
console.log(`Hyper configuration file located at: ${configLocation}`);
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) {
process.exit();
}
};
// handle startup squirrel events
if (process.platform === 'win32') {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const systemContextMenu = require('./system-context-menu');
switch (process.argv[1]) {
case '--squirrel-install':
case '--squirrel-updated':
systemContextMenu.add(() => {
checkSquirrel();
});
break;
case '--squirrel-uninstall':
systemContextMenu.remove(() => {
checkSquirrel();
});
break;
default:
checkSquirrel();
}
}
// Native
import {resolve} from 'path';
// Packages
import {app, BrowserWindow, Menu, screen} from 'electron';
import isDev from 'electron-is-dev';
import {app, BrowserWindow, Menu} from 'electron';
import {gitDescribe} from 'git-describe';
import parseUrl from 'parse-url';
import isDev from 'electron-is-dev';
import * as config from './config';
// Hack - this declararion doesn't work when put into ./ext-modules.d.ts for some reason so it's in this file for the time being
declare module 'electron' {
interface App {
config: typeof import('./config');
plugins: typeof import('./plugins');
getWindows: () => Set<BrowserWindow>;
getLastFocusedWindow: () => BrowserWindow | null;
windowCallback: (win: BrowserWindow) => void;
createWindow: (fn?: (win: BrowserWindow) => void, options?: Record<string, any>) => BrowserWindow;
setVersion: (version: string) => void;
}
type Server = import('./rpc').Server;
interface BrowserWindow {
uid: string;
sessions: Map<any, any>;
focusTime: number;
clean: () => void;
rpc: Server;
}
}
// set up config
config.setup();
import * as AppMenu from './menus/menu';
import * as plugins from './plugins';
import {newWindow} from './ui/window';
import {installCLI} from './utils/cli-install';
import * as AppMenu from './menus/menu';
import {newWindow} from './ui/window';
import * as windowUtils from './utils/window-utils';
const windowSet = new Set<BrowserWindow>([]);
@ -62,7 +108,7 @@ if (isDev) {
console.log('running in dev mode');
// Override default appVersion which is set from package.json
gitDescribe({customArguments: ['--tags']}, (error: any, gitInfo: {raw: string}) => {
gitDescribe({customArguments: ['--tags']}, (error: any, gitInfo: any) => {
if (!error) {
app.setVersion(gitInfo.raw);
}
@ -74,35 +120,30 @@ if (isDev) {
const url = `file://${resolve(isDev ? __dirname : app.getAppPath(), 'index.html')}`;
console.log('electron will open', url);
async function installDevExtensions(isDev_: boolean) {
function installDevExtensions(isDev_: boolean) {
if (!isDev_) {
return [];
return Promise.resolve([]);
}
const {default: installer, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS} = await import('electron-devtools-installer');
const installer = require('electron-devtools-installer') as typeof import('electron-devtools-installer');
const extensions = [REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS];
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'] as const;
const forceDownload = Boolean(process.env.UPGRADE_EXTENSIONS);
return Promise.all(
extensions.map((extension) => installer(extension, {forceDownload, loadExtensionOptions: {allowFileAccess: true}}))
);
return Promise.all(extensions.map((name) => installer.default(installer[name], forceDownload)));
}
// 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?: (win: BrowserWindow) => void, options: Record<string, any> = {}) {
const cfg = plugins.getDecoratedConfig();
const winSet = config.getWin();
let [startX, startY] = winSet.position;
const [width, height] = options.size ? options.size : cfg.windowSize || winSet.size;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {screen} = require('electron');
const winPos = options.position;
@ -139,9 +180,9 @@ app.on('ready', () =>
[startX, startY] = config.windowDefaults.windowPosition;
}
const hwin = newWindow({width, height, x: startX, y: startY}, cfg, fn, profileName);
const hwin = newWindow({width, height, x: startX, y: startY}, cfg, fn);
windowSet.add(hwin);
void hwin.loadURL(url);
hwin.loadURL(url);
// the window can be closed by the browser process itself
hwin.on('close', () => {
@ -149,6 +190,12 @@ app.on('ready', () =>
windowSet.delete(hwin);
});
hwin.on('closed', () => {
if (process.platform !== 'darwin' && windowSet.size === 0) {
app.quit();
}
});
return hwin;
}
@ -167,12 +214,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));
@ -205,7 +246,7 @@ app.on('ready', () =>
console.log('Removing Hyper from default client for ssh:// protocol');
app.removeAsDefaultProtocolClient('ssh');
}
void installCLI(false);
installCLI(false);
}
})
.catch((err) => {
@ -213,12 +254,9 @@ app.on('ready', () =>
})
);
/**
* 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: BrowserWindow) => win.rpc.emit('open file', {path});
if (lastWindow) {
callback(lastWindow);
} else if (!lastWindow && {}.hasOwnProperty.call(app, 'createWindow')) {
@ -228,16 +266,18 @@ function GetWindow(callback: (win: BrowserWindow) => void) {
// sets his callback to an app.windowCallback property.
app.windowCallback = callback;
}
}
app.on('open-file', (_event, path) => {
GetWindow((win: BrowserWindow) => {
win.rpc.emit('open file', {path});
});
});
app.on('open-url', (_event, sshUrl) => {
GetWindow((win: BrowserWindow) => {
win.rpc.emit('open ssh', parseUrl(sshUrl));
});
app.on('open-url', (event, sshUrl) => {
const lastWindow = app.getLastFocusedWindow();
const callback = (win: BrowserWindow) => win.rpc.emit('open ssh', sshUrl);
if (lastWindow) {
callback(lastWindow);
} else if (!lastWindow && {}.hasOwnProperty.call(app, 'createWindow')) {
app.createWindow(callback);
} else {
// If createWindow doesn't exist yet ('ready' event was not fired),
// sets his callback to an app.windowCallback property.
app.windowCallback = callback;
}
});

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+-",

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+-",
@ -16,12 +16,6 @@
"alt+f4"
],
"tab:new": "ctrl+shift+t",
"tab:next": [
"ctrl+tab"
],
"tab:prev": [
"ctrl+shift+tab"
],
"tab:jump:prefix": "ctrl",
"pane:next": "ctrl+pageup",
"pane:prev": "ctrl+pagedown",

View file

@ -1,21 +1,19 @@
// Packages
import {app, dialog, Menu} from 'electron';
import type {BrowserWindow} from 'electron';
import {app, dialog, Menu, 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 shellMenu from './menus/shell';
import editMenu from './menus/edit';
import pluginsMenu from './menus/plugins';
import windowMenu from './menus/window';
import helpMenu from './menus/help';
import darwinMenu from './menus/darwin';
import {getDecoratedKeymaps} from '../plugins';
import {execCommand} from '../commands';
import {getRendererTypes} from '../utils/renderer-utils';
const appName = app.name;
const appVersion = app.getVersion();
@ -53,24 +51,20 @@ export const createMenu = (
.map(([type, count]) => type + (count > 1 ? ` (${count})` : ''))
.join(', ');
void dialog.showMessageBox({
dialog.showMessageBox({
title: `About ${appName}`,
message: `${appName} ${appVersion} (${updateChannel})`,
detail: `Renderers: ${renderers}\nPlugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright © 2022 Vercel, Inc.`,
detail: `Renderers: ${renderers}\nPlugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright © 2020 ZEIT, Inc.`,
buttons: [],
icon: icon as any
});
};
const menu = [
...(process.platform === 'darwin' ? [darwinMenu(commandKeys, execCommand, showAbout)] : []),
shellMenu(
commandKeys,
execCommand,
getConfig().profiles.map((p) => p.name)
),
shellMenu(commandKeys, execCommand),
editMenu(commandKeys, execCommand),
viewMenu(commandKeys, execCommand),
toolsMenu(commandKeys, execCommand),
pluginsMenu(commandKeys, execCommand),
windowMenu(commandKeys, execCommand),
helpMenu(commandKeys, showAbout)
];

View file

@ -1,9 +1,8 @@
// 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';
import {app, BrowserWindow, MenuItemConstructorOptions} from 'electron';
const darwinMenu = (
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void,
showAbout: () => void
@ -55,5 +54,3 @@ const darwinMenu = (
]
};
};
export default darwinMenu;

View file

@ -1,6 +1,6 @@
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron';
import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
const editMenu = (
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
) => {
@ -31,8 +31,7 @@ const editMenu = (
} as any,
{
role: 'paste',
accelerator: commandKeys['editor:paste'],
registerAccelerator: true
accelerator: commandKeys['editor:paste']
},
{
label: 'Select All',
@ -147,5 +146,3 @@ const editMenu = (
submenu
};
};
export default editMenu;

View file

@ -1,19 +1,15 @@
import {release} from 'os';
import {app, shell, dialog, clipboard} from 'electron';
import type {MenuItemConstructorOptions} from 'electron';
import {app, shell, MenuItemConstructorOptions, dialog, clipboard} from 'electron';
import {getConfig, getPlugins} from '../../config';
const {arch, env, platform, versions} = process;
import {version} from '../../package.json';
const {arch, env, platform, versions} = process;
const helpMenu = (commands: Record<string, string>, showAbout: () => void): MenuItemConstructorOptions => {
export default (commands: Record<string, string>, showAbout: () => void): MenuItemConstructorOptions => {
const submenu: MenuItemConstructorOptions[] = [
{
label: `${app.name} Website`,
click() {
void shell.openExternal('https://hyper.is');
shell.openExternal('https://hyper.is');
}
},
{
@ -26,8 +22,8 @@ const helpMenu = (commands: Record<string, string>, showAbout: () => void): Menu
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
- [ ] 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+OPTION+I on macOS, CTRL+SHIFT+I elsewhere)_:
<!-- 👉 Replace with info if applicable, or N/A -->
@ -43,12 +39,12 @@ const helpMenu = (commands: Record<string, string>, showAbout: () => void): Menu
---
<!-- hyper.json config -->
<!-- ~/.hyper.js 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>
<details><summary><strong>.hyper.js contents</strong></summary>
\`\`\`json
${JSON.stringify(getConfig(), null, 2)}
@ -61,20 +57,9 @@ ${JSON.stringify(getPlugins(), null, 2)}
\`\`\`
</details>`;
const issueURL = `https://github.com/vercel/hyper/issues/new?body=${encodeURIComponent(body)}`;
const copyAndSend = () => {
clipboard.writeText(body);
void shell.openExternal(
`https://github.com/vercel/hyper/issues/new?body=${encodeURIComponent(
'<!-- 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
const issueURL = `https://github.com/zeit/hyper/issues/new?body=${encodeURIComponent(body)}`;
if (issueURL.length > 6144) {
dialog
.showMessageBox(focusedWindow, {
message:
'There is too much data to send to GitHub directly. The data will be copied to the clipboard, ' +
@ -84,11 +69,17 @@ ${JSON.stringify(getPlugins(), null, 2)}
})
.then((result) => {
if (result.response === 0) {
copyAndSend();
clipboard.writeText(body);
shell.openExternal(
`https://github.com/zeit/hyper/issues/new?body=${encodeURIComponent(
'<!-- We have written the needed data into your clipboard because it was too large to send. ' +
'Please paste. -->\n'
)}`
);
}
});
} else {
void shell.openExternal(issueURL);
shell.openExternal(issueURL);
}
}
}
@ -98,7 +89,7 @@ ${JSON.stringify(getPlugins(), null, 2)}
submenu.push(
{type: 'separator'},
{
label: 'About Hyper',
role: 'about',
click() {
showAbout();
}
@ -110,5 +101,3 @@ ${JSON.stringify(getPlugins(), null, 2)}
submenu
};
};
export default helpMenu;

View file

@ -0,0 +1,28 @@
import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
export default (
commands: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
return {
label: 'Plugins',
submenu: [
{
label: 'Update',
accelerator: commands['plugins:update'],
click() {
execCommand('plugins:update');
}
},
{
label: 'Install Hyper CLI command in PATH',
click() {
execCommand('cli:install');
}
},
{
type: 'separator'
}
]
};
};

View file

@ -1,9 +1,8 @@
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron';
import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
const shellMenu = (
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void,
profiles: string[]
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
const isMac = process.platform === 'darwin';
@ -44,47 +43,6 @@ const shellMenu = (
{
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'],
@ -100,5 +58,3 @@ const shellMenu = (
]
};
};
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,6 +1,6 @@
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron';
import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
const viewMenu = (
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
@ -55,5 +55,3 @@ const viewMenu = (
]
};
};
export default viewMenu;

View file

@ -1,11 +1,11 @@
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron';
import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
const windowMenu = (
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
// 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}`;
@ -81,12 +81,6 @@ const windowMenu = (
{
role: 'front'
},
{
label: 'Toggle Always on Top',
click: (item, focusedWindow) => {
execCommand('window:toggleKeepOnTop', focusedWindow);
}
},
{
role: 'togglefullscreen',
accelerator: commandKeys['window:toggleFullScreen']
@ -94,5 +88,3 @@ const windowMenu = (
]
};
};
export default windowMenu;

View file

@ -1,15 +1,13 @@
import type {BrowserWindow} from 'electron';
import fetch from 'electron-fetch';
import ms from 'ms';
import fetch from 'electron-fetch';
import {version} from './package.json';
import {BrowserWindow} from 'electron';
const NEWS_URL = 'https://hyper-news.now.sh';
export default function fetchNotifications(win: BrowserWindow) {
const {rpc} = win;
const retry = (err?: Error) => {
const retry = (err?: any) => {
setTimeout(() => fetchNotifications(win), ms('30m'));
if (err) {
console.error('Notification messages fetch error', err.stack);
@ -24,7 +22,7 @@ export default function fetchNotifications(win: BrowserWindow) {
})
.then((res) => res.json())
.then((data) => {
const message: {text: string; url: string; dismissable: boolean} | '' = data.message || '';
const {message} = data || {};
if (typeof message !== 'object' && message !== '') {
throw new Error('Bad response');
}

View file

@ -1,21 +1,10 @@
import {app, Notification} from 'electron';
import {Notification} from 'electron';
import {icon} from './config/paths';
export default function notify(title: string, body = '', details: {error?: any} = {}) {
export default function notify(title: string, body = '', details: 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,46 +2,40 @@
"name": "hyper",
"productName": "Hyper",
"description": "A terminal built on web technologies",
"version": "4.0.0-canary.5",
"version": "3.1.0-canary.4",
"license": "MIT",
"author": {
"name": "ZEIT, Inc.",
"email": "team@zeit.co"
},
"repository": "zeit/hyper",
"scripts": {
"postinstall": "npx patch-package"
},
"dependencies": {
"@babel/parser": "7.24.4",
"@electron/remote": "2.1.2",
"ast-types": "^0.16.1",
"async-retry": "1.3.3",
"chokidar": "^3.6.0",
"color": "4.2.3",
"async-retry": "1.3.1",
"color": "3.1.2",
"convert-css-color-name-to-hex": "0.1.1",
"default-shell": "1.0.1",
"electron-devtools-installer": "3.2.0",
"electron-fetch": "1.9.1",
"electron-is-dev": "2.0.0",
"electron-store": "8.2.0",
"fs-extra": "11.2.0",
"git-describe": "4.1.1",
"lodash": "4.17.21",
"ms": "2.1.3",
"native-process-working-directory": "^1.0.2",
"node-pty": "1.0.0",
"electron-fetch": "1.4.0",
"electron-is-dev": "1.2.0",
"electron-squirrel-startup": "1.0.0",
"electron-store": "5.2.0",
"file-uri-to-path": "2.0.0",
"fs-extra": "9.0.1",
"git-describe": "4.0.4",
"lodash": "4.17.15",
"mkdirp": "1.0.4",
"ms": "2.1.2",
"node-pty": "0.9.0",
"os-locale": "5.0.0",
"parse-url": "8.1.0",
"queue": "6.0.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"recast": "0.23.6",
"semver": "7.6.0",
"shell-env": "3.0.1",
"sudo-prompt": "^9.2.1",
"uuid": "9.0.1"
"parse-url": "5.0.1",
"pify": "5.0.0",
"queue": "6.0.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"semver": "7.3.2",
"shell-env": "3.0.0",
"uuid": "8.1.0"
},
"optionalDependencies": {
"native-reg": "1.1.1"
"native-reg": "0.3.4"
}
}

View file

@ -1,15 +0,0 @@
diff --git a/node_modules/node-pty/src/win/conpty.cc b/node_modules/node-pty/src/win/conpty.cc
index 47af75c..884d542 100644
--- a/node_modules/node-pty/src/win/conpty.cc
+++ b/node_modules/node-pty/src/win/conpty.cc
@@ -472,10 +472,6 @@ static NAN_METHOD(PtyKill) {
}
}
- DisconnectNamedPipe(handle->hIn);
- DisconnectNamedPipe(handle->hOut);
- CloseHandle(handle->hIn);
- CloseHandle(handle->hOut);
CloseHandle(handle->hShell);
}

View file

@ -1,28 +1,19 @@
/* 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';
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable @typescript-eslint/no-use-before-define */
import {app, dialog, BrowserWindow, App} from 'electron';
import {resolve, basename} from 'path';
import {promisify} from 'util';
import {app, dialog, ipcMain as _ipcMain} from 'electron';
import type {BrowserWindow, App, MenuItemConstructorOptions} from 'electron';
import React from 'react';
import {writeFileSync} from 'fs';
import Config from 'electron-store';
import ms from 'ms';
import React from 'react';
import ReactDom from 'react-dom';
import type {IpcMainWithCommands} from '../typings/common';
import type {configOptions} from '../typings/config';
import * as config from './config';
import {plugs} from './config/paths';
import notify from './notify';
import {availableExtensions} from './plugins/extensions';
import {install} from './plugins/install';
import {plugs} from './config/paths';
import mapKeys from './utils/map-keys';
import {configOptions} from '../lib/config';
// local storage
const cache = new Config();
@ -60,7 +51,7 @@ 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');
@ -109,7 +100,7 @@ function updatePlugins({force = false} = {}) {
updating = true;
syncPackageJSON();
const id_ = id;
install((err) => {
install((err: any) => {
updating = false;
if (err) {
@ -134,9 +125,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) {
@ -153,9 +142,8 @@ function updatePlugins({force = false} = {}) {
function getPluginVersions() {
const paths_ = paths.plugins.concat(paths.localPlugins);
return paths_.map((path_) => {
let version: string | null = null;
let version = null;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
version = require(resolve(path_, 'package.json')).version;
//eslint-disable-next-line no-empty
} catch (err) {}
@ -208,10 +196,10 @@ 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: 'vercel/hyper',
repository: 'zeit/hyper',
license: 'MIT',
homepage: 'https://hyper.is',
dependencies
@ -226,7 +214,7 @@ function syncPackageJSON() {
}
function alert(message: string) {
void dialog.showMessageBox({
dialog.showMessageBox({
message,
buttons: ['Ok']
});
@ -240,7 +228,7 @@ function toDependencies(plugins_: {plugins: string[]}) {
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);
@ -282,7 +270,7 @@ function requirePlugins(): any[] {
const {plugins: plugins_, localPlugins} = paths;
const load = (path_: string) => {
let mod: Record<string, any>;
let mod: any;
try {
mod = require(path_);
const exposed = mod && Object.keys(mod).some((key) => availableExtensions.has(key));
@ -294,7 +282,6 @@ function requirePlugins(): any[] {
// populate the name for internal errors here
mod._name = basename(path_);
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
mod._version = require(resolve(path_, 'package.json')).version;
} catch (err) {
console.warn(`No package.json found in ${path_}`);
@ -302,8 +289,7 @@ function requirePlugins(): any[] {
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') {
console.warn(`Plugin error while loading "${basename(path_)}" (${path_}): ${err.message}`);
} else {
@ -312,13 +298,10 @@ 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) => {
@ -421,7 +404,7 @@ export const getDeprecatedConfig = () => {
return deprecated;
};
export const decorateMenu = (tpl: MenuItemConstructorOptions[]) => {
export const decorateMenu = (tpl: any) => {
return decorateObject(tpl, 'decorateMenu');
};
@ -429,8 +412,8 @@ export const getDecoratedEnv = (baseEnv: Record<string, string>) => {
return decorateObject(baseEnv, 'decorateEnv');
};
export const getDecoratedConfig = (profile: string) => {
const baseConfig = config.getProfileConfig(profile);
export const getDecoratedConfig = () => {
const baseConfig = config.getConfig();
const decoratedConfig = decorateObject(baseConfig, 'decorateConfig');
const fixedConfig = config.fixConfigDefaults(decoratedConfig);
const translatedConfig = config.htermConfigTranslate(fixedConfig);
@ -461,20 +444,3 @@ export const decorateSessionClass = <T>(Session: T): T => {
};
export {toDependencies as _toDependencies};
const ipcMain = _ipcMain as IpcMainWithCommands;
ipcMain.handle('child_process.exec', (event, command, options) => {
return promisify(exec)(command, options);
});
ipcMain.handle('child_process.execFile', (event, file, args, options) => {
return promisify(execFile)(file, args, options);
});
ipcMain.handle('getLoadedPluginVersions', () => getLoadedPluginVersions());
ipcMain.handle('getPaths', () => getPaths());
ipcMain.handle('getBasePaths', () => getBasePaths());
ipcMain.handle('getDeprecatedConfig', () => getDeprecatedConfig());
ipcMain.handle('getDecoratedConfig', (e, profile) => getDecoratedConfig(profile));
ipcMain.handle('getDecoratedKeymaps', () => getDecoratedKeymaps());

View file

@ -1,13 +1,11 @@
import cp from 'child_process';
import ms from 'ms';
import queue from 'queue';
import ms from 'ms';
import {yarn, plugs} from '../config/paths';
export const install = (fn: (err: string | null) => void) => {
export const install = (fn: Function) => {
const spawnQueue = queue({concurrency: 1});
function yarnFn(args: string[], cb: (err: string | null) => void) {
function yarnFn(args: string[], cb: Function) {
const env = {
NODE_ENV: 'production',
ELECTRON_RUN_AS_NODE: 'true'
@ -40,7 +38,7 @@ export const install = (fn: (err: string | null) => void) => {
spawnQueue.start();
}
yarnFn(['install', '--no-emoji', '--no-lockfile', '--cache-folder', plugs.cache], (err) => {
yarnFn(['install', '--no-emoji', '--no-lockfile', '--cache-folder', plugs.cache], (err: any) => {
if (err) {
return fn(err);
}

View file

@ -1,22 +1,15 @@
import {EventEmitter} from 'events';
import {ipcMain} from 'electron';
import type {BrowserWindow, IpcMainEvent} from 'electron';
import {ipcMain, BrowserWindow} from 'electron';
import {v4 as uuidv4} from 'uuid';
import type {TypedEmitter, MainEvents, RendererEvents, FilterNever} from '../typings/common';
export class Server {
emitter: TypedEmitter<MainEvents>;
export class Server extends EventEmitter {
destroyed = false;
win: BrowserWindow;
id!: string;
constructor(win: BrowserWindow) {
this.emitter = new EventEmitter();
super();
this.win = win;
this.emit = this.emit.bind(this);
this.ipcListener = this.ipcListener.bind(this);
if (this.destroyed) {
return;
@ -31,7 +24,7 @@ export class Server {
// to support reloading the window and re-initializing
// the channel
this.wc.on('did-finish-load', () => {
this.wc.send('init', uid, win.profileName);
this.wc.send('init', uid);
});
}
@ -39,33 +32,20 @@ export class Server {
return this.win.webContents;
}
ipcListener = <U extends keyof MainEvents>(event: IpcMainEvent, {ev, data}: {ev: U; data: MainEvents[U]}) =>
this.emitter.emit(ev, data);
ipcListener(event: any, {ev, data}: {ev: string; data: any}) {
super.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]) {
emit(ch: string, data: any = {}): any {
// 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.removeAllListeners();
this.wc.removeAllListeners();
if (this.id) {
ipcMain.removeListener(this.id, this.ipcListener);
@ -76,8 +56,6 @@ export class Server {
}
}
const createRPC = (win: BrowserWindow) => {
export default (win: BrowserWindow) => {
return new Server(win);
};
export default createRPC;

View file

@ -1,17 +1,10 @@
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';
import {productName, version} from './package.json';
import * as config from './config';
import {IPty, IWindowsPtyForkOptions, spawn as npSpawn} from 'node-pty';
const createNodePtyError = () =>
new Error(
@ -20,12 +13,12 @@ const createNodePtyError = () =>
let spawn: typeof npSpawn;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
spawn = require('node-pty').spawn;
} catch (err) {
throw createNodePtyError();
}
const envFromConfig = config.getConfig().env || {};
const useConpty = config.getConfig().useConpty;
// Max duration to batch session data before sending it to the renderer process.
@ -58,7 +51,7 @@ class DataBatcher extends EventEmitter {
this.timeout = null;
}
write(chunk: Buffer | string) {
write(chunk: Buffer) {
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) {
@ -68,7 +61,7 @@ class DataBatcher extends EventEmitter {
this.flush();
}
this.data += typeof chunk === 'string' ? chunk : this.decoder.write(chunk);
this.data += this.decoder.write(chunk);
if (!this.timeout) {
this.timeout = setTimeout(() => this.flush(), BATCH_DURATION_MS);
@ -86,66 +79,52 @@ class DataBatcher extends EventEmitter {
interface SessionOptions {
uid: string;
rows?: number;
cols?: number;
cwd?: string;
shell?: string;
shellArgs?: string[];
profile: string;
rows: number;
cols: number;
cwd: string;
shell: string;
shellArgs: 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(':');
}
init({uid, rows, cols: columns, cwd, shell, shellArgs}: SessionOptions) {
const osLocale = require('os-locale') as typeof import('os-locale');
const baseEnv = Object.assign(
{},
process.env,
{
LANG: `${osLocale.sync().replace(/-/, '_')}.UTF-8`,
TERM: 'xterm-256color',
COLORTERM: 'truecolor',
TERM_PROGRAM: productName,
TERM_PROGRAM_VERSION: version
},
envFromConfig
);
// 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
// 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'];
const options: IWindowsPtyForkOptions = {
cols,
cols: columns,
rows,
cwd,
env: getDecoratedEnv(baseEnv)
@ -157,9 +136,8 @@ export default class Session extends EventEmitter {
}
try {
this.pty = spawn(shell, shellArgs, options);
} catch (_err) {
const err = _err as {message: string};
this.pty = spawn(shell || defaultShell, shellArgs || defaultShellArgs, options);
} catch (err) {
if (/is not a function/.test(err.message)) {
throw createNodePtyError();
} else {
@ -172,53 +150,21 @@ export default class Session extends EventEmitter {
if (this.ended) {
return;
}
this.batcher?.write(chunk);
this.batcher?.write(chunk as any);
});
this.batcher.on('flush', (data: string) => {
this.batcher.on('flush', (data) => {
this.emit('data', data);
});
this.pty.onExit((e) => {
this.pty.onExit(() => {
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.ended = true;
this.emit('exit');
}
});
this.shell = shell;
this.shell = shell || defaultShell;
}
exit() {
@ -237,8 +183,7 @@ No fallback available, please check the shell config.
if (this.pty) {
try {
this.pty.resize(cols, rows);
} catch (_err) {
const err = _err as {stack: any};
} catch (err) {
console.error(err.stack);
}
} else {
@ -250,8 +195,7 @@ No fallback available, please check the shell config.
if (this.pty) {
try {
this.pty.kill();
} catch (_err) {
const err = _err as {stack: any};
} catch (err) {
console.error('exit error', err.stack);
}
} else {

View file

@ -0,0 +1,61 @@
import * as regTypes from './typings/native-reg';
if (process.platform === 'win32') {
try {
// eslint-disable-next-line no-var, @typescript-eslint/no-var-requires
var Registry: typeof regTypes = require('native-reg');
} catch (err) {
console.error(err);
}
}
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: regTypes.HKEY, commandKey: regTypes.HKEY, callback: Function) {
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_);
}
callback();
}
export const add = (callback: Function) => {
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, callback);
Registry.closeKey(hyperKey);
Registry.closeKey(commandKey);
} catch (error) {
console.error(error);
}
};
export const remove = (callback: Function) => {
try {
Registry.deleteTree(Registry.HKCU, regKey);
} catch (err) {
console.error(err);
}
callback();
};

View file

@ -2,13 +2,10 @@
"extends": "../tsconfig.base.json",
"compilerOptions": {
"declarationDir": "../dist/tmp/appdts/",
"outDir": "../target/",
"noImplicitAny": false
"outDir": "../target/"
},
"include": [
"./**/*",
"./package.json",
"../typings/extend-electron.d.ts",
"../typings/ext-modules.d.ts"
"./package.json"
]
}

105
app/typings/native-reg.d.ts vendored Normal file
View file

@ -0,0 +1,105 @@
/// <reference types="node" />
export declare enum HKEY {
CLASSES_ROOT = 2147483648,
CURRENT_USER = 2147483649,
LOCAL_MACHINE = 2147483650,
USERS = 2147483651,
PERFORMANCE_DATA = 2147483652,
PERFORMANCE_TEXT = 2147483728,
PERFORMANCE_NLSTEXT = 2147483744,
CURRENT_CONFIG = 2147483653,
DYN_DATA = 2147483654,
CURRENT_USER_LOCAL_SETTINGS = 2147483655
}
export declare enum CreateKeyOptions {
NON_VOLATILE = 0,
VOLATILE = 1,
CREATE_LINK = 2,
BACKUP_RESTORE = 4
}
export declare enum OpenKeyOptions {
OPEN_LINK = 8
}
export declare enum Access {
QUERY_VALUE = 1,
SET_VALUE = 2,
CREATE_SUB_KEY = 4,
ENUMERATE_SUB_KEYS = 8,
NOTIFY = 16,
CREATE_LINK = 32,
WOW64_64KEY = 256,
WOW64_32KEY = 512,
READ = 131097,
WRITE = 131078,
EXECUTE = 131097,
ALL_ACCESS = 983103
}
export declare enum ValueType {
NONE = 0,
SZ = 1,
EXPAND_SZ = 2,
BINARY = 3,
DWORD = 4,
DWORD_LITTLE_ENDIAN = 4,
DWORD_BIG_ENDIAN = 5,
LINK = 6,
MULTI_SZ = 7,
RESOURCE_LIST = 8,
FULL_RESOURCE_DESCRIPTOR = 9,
RESOURCE_REQUIREMENTS_LIST = 10,
QWORD = 11,
QWORD_LITTLE_ENDIAN = 11
}
export declare enum GetValueFlags {
RT_ANY = 65535,
RT_REG_NONE = 1,
RT_REG_SZ = 2,
RT_REG_EXPAND_SZ = 4,
RT_REG_BINARY = 8,
RT_REG_DWORD = 16,
RT_REG_MULTI_SZ = 32,
RT_REG_QWORD = 64,
RT_DWORD = 24,
RT_QWORD = 72,
NO_EXPAND = 268435456,
SUBKEY_WOW6464KEY = 65536,
SUBKEY_WOW6432KEY = 131072
}
export declare const HKCR = HKEY.CLASSES_ROOT;
export declare const HKCU = HKEY.CURRENT_USER;
export declare const HKLM = HKEY.LOCAL_MACHINE;
export declare const HKU = HKEY.USERS;
export declare type Value = Buffer & {
type: ValueType;
};
export declare function isHKEY(hkey: any): hkey is HKEY;
export declare function createKey(hkey: HKEY, subKey: string, access: Access, options?: CreateKeyOptions): HKEY;
export declare function openKey(hkey: HKEY, subKey: string, access: Access, options?: OpenKeyOptions): HKEY | null;
export declare function openCurrentUser(access?: Access): HKEY;
export declare function loadAppKey(file: string, access: Access): HKEY | null;
export declare function enumKeyNames(hkey: HKEY): string[];
export declare function enumValueNames(hkey: HKEY): string[];
export declare function queryValueRaw(hkey: HKEY, valueName: string): Value | null;
export declare function getValueRaw(hkey: HKEY, subKey: string, valueName: string, flags?: GetValueFlags): Value | null;
export declare function setValueRaw(hkey: HKEY, valueName: string, valueType: ValueType, data: Buffer): void;
export declare function deleteKey(hkey: HKEY, subKey: string): boolean;
export declare function deleteTree(hkey: HKEY, subKey: string): boolean;
export declare function deleteKeyValue(hkey: HKEY, subKey: string, valueName: string): boolean;
export declare function deleteValue(hkey: HKEY, valueName: string): boolean;
export declare function closeKey(hkey: HKEY | null | undefined): void;
export declare type ParsedValue = number | string | string[] | Buffer;
export declare function parseValue(value: Value | null): ParsedValue | null;
export declare function parseString(value: Buffer): string;
export declare function parseMultiString(value: Buffer): string[];
export declare function formatString(value: string): Buffer;
export declare function formatMultiString(values: string[]): Buffer;
export declare function formatDWORD(value: number): Buffer;
export declare function formatQWORD(value: number): Buffer;
export declare function setValueSZ(hkey: HKEY, valueName: string, value: string): void;
export declare function setValueEXPAND_SZ(hkey: HKEY, valueName: string, value: string): void;
export declare function setValueMULTI_SZ(hkey: HKEY, valueName: string, value: string[]): void;
export declare function setValueDWORD(hkey: HKEY, valueName: string, value: number): void;
export declare function setValueQWORD(hkey: HKEY, valueName: string, value: number): void;
export declare function getValue(hkey: HKEY, subKey: string, valueName: string, flags?: GetValueFlags): ParsedValue | null;
export declare function queryValue(hkey: HKEY, valueName: string): ParsedValue | null;
//# sourceMappingURL=index.d.ts.map

View file

@ -1,11 +1,8 @@
import type {MenuItemConstructorOptions, BrowserWindow} from 'electron';
import {execCommand} from '../commands';
import {getProfiles} from '../config';
import editMenu from '../menus/menus/edit';
import shellMenu from '../menus/menus/shell';
import {execCommand} from '../commands';
import {getDecoratedKeymaps} from '../plugins';
import {MenuItemConstructorOptions, BrowserWindow} from 'electron';
const separator: MenuItemConstructorOptions = {type: 'separator'};
const getCommandKeys = (keymaps: Record<string, string[]>): Record<string, string> =>
@ -23,20 +20,14 @@ const filterCutCopy = (selection: string, menuItem: MenuItemConstructorOptions)
return menuItem;
};
const contextMenuTemplate = (
export default (
createWindow: (fn?: (win: BrowserWindow) => void, options?: Record<string, any>) => BrowserWindow,
selection: string
) => {
const commandKeys = getCommandKeys(getDecoratedKeymaps());
const _shell = shellMenu(
commandKeys,
execCommand,
getProfiles().map((p) => p.name)
).submenu as MenuItemConstructorOptions[];
const _shell = shellMenu(commandKeys, execCommand).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;

View file

@ -1,82 +1,75 @@
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 {app, BrowserWindow, shell, Menu, BrowserWindowConstructorOptions} from 'electron';
import {isAbsolute} from 'path';
import {parse as parseUrl} from 'url';
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 fileUriToPath from 'file-uri-to-path';
import isDev from 'electron-is-dev';
import updater from '../updater';
import {setRendererType, unsetRendererType} from '../utils/renderer-utils';
import toElectronBackgroundColor from '../utils/to-electron-background-color';
import {icon, homeDirectory} from '../config/paths';
import createRPC from '../rpc';
import notify from '../notify';
import fetchNotifications from '../notifications';
import Session from '../session';
import contextMenuTemplate from './contextmenu';
import {execCommand} from '../commands';
import {setRendererType, unsetRendererType} from '../utils/renderer-utils';
import {decorateSessionOptions, decorateSessionClass} from '../plugins';
export function newWindow(
options_: BrowserWindowConstructorOptions,
cfg: configOptions,
fn?: (win: BrowserWindow) => void,
profileName: string = getDefaultProfile()
cfg: any,
fn?: (win: BrowserWindow) => void
): 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
const winOpts = Object.assign(
{
minWidth: 370,
minHeight: 190,
backgroundColor: toElectronBackgroundColor(cfg.backgroundColor || '#000'),
titleBarStyle: 'hiddenInset',
title: 'Hyper.app',
// we want to go frameless on Windows and Linux
frame: process.platform === 'darwin',
transparent: process.platform === 'darwin',
icon,
show: process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev,
acceptFirstMouse: true,
webPreferences: {
nodeIntegration: true,
navigateOnDragDrop: true
}
},
...options_
};
options_
);
const window = new BrowserWindow(app.plugins.getDecoratedBrowserOptions(winOpts));
window.profileName = profileName;
// Enable remote module on this window
remoteEnable(window.webContents);
window.uid = classOpts.uid;
app.plugins.onWindowClass(window);
window.uid = classOpts.uid;
const rpc = createRPC(window);
const sessions = new Map<string, Session>();
const sessions = new Map();
const updateBackgroundColor = () => {
const cfg_ = app.plugins.getDecoratedConfig(profileName);
const cfg_ = app.plugins.getDecoratedConfig();
window.setBackgroundColor(toElectronBackgroundColor(cfg_.backgroundColor || '#000'));
};
// set working directory
let workingDirectory = homeDirectory;
if (process.argv[1] && isAbsolute(process.argv[1])) {
workingDirectory = process.argv[1];
} else if (cfg.workingDirectory && isAbsolute(cfg.workingDirectory)) {
workingDirectory = cfg.workingDirectory;
}
// config changes
const cfgUnsubscribe = app.config.subscribe(() => {
const cfg_ = app.plugins.getDecoratedConfig(profileName);
const cfg_ = app.plugins.getDecoratedConfig();
// notify renderer
window.webContents.send('config change');
@ -99,9 +92,7 @@ export function newWindow(
// If no callback is passed to createWindow,
// a new session will be created by default.
if (!fn) {
fn = (win: BrowserWindow) => {
win.rpc.emit('termgroup add req', {});
};
fn = (win: BrowserWindow) => win.rpc.emit('termgroup add req', {});
}
// app.windowCallback is the createWindow callback
@ -109,7 +100,7 @@ export function newWindow(
// and createWindow definition. It's executed in place of
// the callback passed as parameter, and deleted right after.
(app.windowCallback || fn)(window);
app.windowCallback = undefined;
delete app.windowCallback;
fetchNotifications(window);
// auto updates
if (!isDev) {
@ -119,58 +110,23 @@ export function newWindow(
}
});
function createSession(extraOptions: sessionExtraOptions = {}) {
function createSession(extraOptions: any = {}) {
const uid = uuidv4();
const extraOptionsFiltered: sessionExtraOptions = {};
const extraOptionsFiltered: any = {};
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,
cwd: workingDirectory,
splitDirection: undefined,
shell: profileCfg.shell,
shellArgs: profileCfg.shellArgs && Array.from(profileCfg.shellArgs)
shell: cfg.shell,
shellArgs: cfg.shellArgs && Array.from(cfg.shellArgs)
},
extraOptionsFiltered,
{
profile: extraOptionsFiltered.profile || profileName,
uid
}
{uid}
);
const options = decorateSessionOptions(defaultOptions);
const DecoratedSession = decorateSessionClass(Session);
@ -190,11 +146,10 @@ export function newWindow(
splitDirection: options.splitDirection,
shell: session.shell,
pid: session.pty ? session.pty.pid : null,
activeUid: options.activeUid ?? undefined,
profile: options.profile
activeUid: options.activeUid
});
session.on('data', (data: string) => {
session.on('data', (data: any) => {
rpc.emit('session data', data);
});
@ -227,10 +182,10 @@ export function newWindow(
}
});
rpc.on('data', ({uid, data, escaped}) => {
const session = uid && sessions.get(uid);
const session = sessions.get(uid);
if (session) {
if (escaped) {
const escapedData = session.shell?.endsWith('cmd.exe')
const escapedData = session.shell.endsWith('cmd.exe')
? `"${data}"` // This is how cmd.exe does it
: `'${data.replace(/'/g, `'\\''`)}'`; // Inside a single-quoted string nothing is interpreted
@ -245,11 +200,12 @@ export function newWindow(
setRendererType(uid, type);
});
rpc.on('open external', ({url}) => {
void shell.openExternal(url);
shell.openExternal(url);
});
rpc.on('open context menu', (selection) => {
const {createWindow} = app;
Menu.buildFromTemplate(contextMenuTemplate(createWindow, selection)).popup({window});
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)});
@ -257,12 +213,9 @@ export function newWindow(
// Same deal as above, grabbing the window titlebar when the window
// is maximized on Windows results in unmaximize, without hitting any
// app buttons
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);
for (const ev of ['maximize', 'unmaximize', 'minimize', 'restore'] as any) {
window.on(ev, () => rpc.emit('windowGeometry change', {}));
}
window.on('move', () => {
const position = window.getPosition();
rpc.emit('move', {bounds: {x: position[0], y: position[1]}});
@ -276,10 +229,10 @@ export function newWindow(
});
// pass on the full screen events from the window to react
rpc.win.on('enter-full-screen', () => {
rpc.emit('enter full screen');
rpc.emit('enter full screen', {});
});
rpc.win.on('leave-full-screen', () => {
rpc.emit('leave full screen');
rpc.emit('leave full screen', {});
});
const deleteSessions = () => {
sessions.forEach((session, key) => {
@ -297,32 +250,29 @@ export function newWindow(
}
});
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.
// 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 data = handleDroppedURL(url);
if (data) {
const protocol = typeof url === 'string' && parseUrl(url).protocol;
if (protocol === 'file:') {
event.preventDefault();
rpc.emit('session data send', data);
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});
}
});
window.webContents.setWindowOpenHandler(({url}) => {
const data = handleDroppedURL(url);
if (data) {
rpc.emit('session data send', data);
return {action: 'deny'};
// 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);
}
return {action: 'allow'};
});
// expose internals to extension authors

View file

@ -1,46 +1,25 @@
// Packages
import electron, {app} from 'electron';
import type {BrowserWindow, AutoUpdater} from 'electron';
import retry from 'async-retry';
import electron, {app, BrowserWindow, AutoUpdater} from 'electron';
import ms from 'ms';
import retry from 'async-retry';
// Utilities
import autoUpdaterLinux from './auto-updater-linux';
import {getDefaultProfile} from './config';
import {version} from './package.json';
import {getDecoratedConfig} from './plugins';
import autoUpdaterLinux from './auto-updater-linux';
const {platform} = process;
const isLinux = platform === 'linux';
const autoUpdater: AutoUpdater = isLinux ? autoUpdaterLinux : electron.autoUpdater;
const getDecoratedConfigWithRetry = async () => {
return await retry(() => {
const content = getDecoratedConfig(getDefaultProfile());
if (!content) {
throw new Error('No config content loaded');
}
return content;
});
};
const checkForUpdates = async () => {
const config = await getDecoratedConfigWithRetry();
if (!config.disableAutoUpdates) {
autoUpdater.checkForUpdates();
}
};
let isInit = false;
// Default to the "stable" update channel
let canaryUpdates = false;
const buildFeedUrl = (canary: boolean, currentVersion: string) => {
const updatePrefix = canary ? 'releases-canary' : 'releases';
const archSuffix = process.arch === 'arm64' || app.runningUnderARM64Translation ? '_arm64' : '';
return `https://${updatePrefix}.hyper.is/update/${isLinux ? 'deb' : platform}${archSuffix}/${currentVersion}`;
return `https://${updatePrefix}.hyper.is/update/${isLinux ? 'deb' : platform}/${currentVersion}`;
};
const isCanary = (updateChannel: string) => updateChannel === 'canary';
@ -50,7 +29,15 @@ async function init() {
console.error('Error fetching updates', `${err.message} (${err.stack})`);
});
const config = await getDecoratedConfigWithRetry();
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)) {
@ -62,59 +49,58 @@ async function init() {
autoUpdater.setFeedURL({url: feedURL});
setTimeout(() => {
void checkForUpdates();
autoUpdater.checkForUpdates();
}, ms('10s'));
setInterval(() => {
void checkForUpdates();
autoUpdater.checkForUpdates();
}, ms('30m'));
isInit = true;
}
const updater = (win: BrowserWindow) => {
export default (win: BrowserWindow) => {
if (!isInit) {
void init();
init();
}
const {rpc} = win;
const onupdate = (ev: Event, releaseNotes: string, releaseName: string, date: Date, updateUrl: string) => {
const releaseUrl = updateUrl || `https://github.com/vercel/hyper/releases/tag/${releaseName}`;
rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !isLinux});
const onupdate = (
ev: Event,
releaseNotes: string,
releaseName: string,
date: Date,
updateUrl: string,
onQuitAndInstall: any
) => {
const releaseUrl = updateUrl || `https://github.com/zeit/hyper/releases/tag/${releaseName}`;
rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !!onQuitAndInstall});
};
if (isLinux) {
autoUpdater.on('update-available', onupdate);
} else {
autoUpdater.on('update-downloaded', onupdate);
}
const eventName: any = isLinux ? 'update-available' : 'update-downloaded';
autoUpdater.on(eventName, onupdate);
rpc.once('quit and install', () => {
autoUpdater.quitAndInstall();
});
app.config.subscribe(async () => {
const {updateChannel} = await getDecoratedConfigWithRetry();
app.config.subscribe(() => {
const {updateChannel} = app.plugins.getDecoratedConfig();
const newUpdateIsCanary = isCanary(updateChannel);
if (newUpdateIsCanary !== canaryUpdates) {
const feedURL = buildFeedUrl(newUpdateIsCanary, version);
autoUpdater.setFeedURL({url: feedURL});
void checkForUpdates();
autoUpdater.checkForUpdates();
canaryUpdates = newUpdateIsCanary;
}
});
win.on('close', () => {
if (isLinux) {
autoUpdater.removeListener('update-available', onupdate);
} else {
autoUpdater.removeListener('update-downloaded', onupdate);
}
autoUpdater.removeListener(eventName, onupdate);
});
};
export default updater;

View file

@ -1,23 +1,24 @@
import {existsSync, readlink, symlink} from 'fs';
import pify from 'pify';
import fs 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';
import {cliScriptPath, cliLinkPath} from '../config/paths';
const readLink = promisify(readlink);
const symLink = promisify(symlink);
const sudoExec = promisify(sudoPrompt.exec);
import * as regTypes from '../typings/native-reg';
if (process.platform === 'win32') {
try {
// eslint-disable-next-line no-var, @typescript-eslint/no-var-requires
var Registry: typeof regTypes = require('native-reg');
} catch (err) {
console.error(err);
}
}
const readlink = pify(fs.readlink);
const symlink = pify(fs.symlink);
const checkInstall = () => {
return readLink(cliLinkPath)
return readlink(cliLinkPath)
.then((link) => link === cliScriptPath)
.catch((err) => {
if (err.code === 'ENOENT') {
@ -27,70 +28,33 @@ const checkInstall = () => {
});
};
const addSymlink = async (silent: boolean) => {
try {
const isInstalled = await checkInstall();
const addSymlink = () => {
return checkInstall().then((isInstalled) => {
if (isInstalled) {
console.log('Hyper CLI already in PATH');
return;
return Promise.resolve();
}
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;
}
return symlink(cliScriptPath, cliLinkPath);
});
};
const addBinToUserPath = () => {
return new Promise<void>((resolve, reject) => {
return new Promise((resolve, reject) => {
try {
const envKey = Registry.openKey(Registry.HKCU, 'Environment', Registry.Access.ALL_ACCESS)!;
// C:\Users\<user>\AppData\Local\Programs\hyper\resources\bin
// C:\Users\<user>\AppData\Local\hyper\app-<version>\resources\bin
const binPath = path.dirname(cliScriptPath);
// C:\Users\<user>\AppData\Local\hyper
const oldPath = path.resolve(process.env.LOCALAPPDATA!, 'hyper');
const basePath = path.resolve(binPath, '../../..');
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;
let type: regTypes.ValueType = Registry.ValueType.SZ;
if (pathItem) {
type = Registry.queryValueRaw(envKey, pathItem)!.type;
if (type !== Registry.ValueType.SZ && type !== Registry.ValueType.EXPAND_SZ) {
@ -98,21 +62,19 @@ const addBinToUserPath = () => {
return;
}
const value = Registry.queryValue(envKey, pathItem) as string;
let pathParts = value.split(';');
const pathParts = value.split(';');
const existingPath = pathParts.includes(binPath);
const existingOldPath = pathParts.some((pathPart) => pathPart.startsWith(oldPath));
if (existingPath && !existingOldPath) {
if (existingPath) {
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(';');
// 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(';');
}
console.log('Adding HyperCLI path (registry)');
Registry.setValueRaw(envKey, pathItemName, type, Registry.formatString(newPathValue));
@ -124,36 +86,40 @@ const addBinToUserPath = () => {
});
};
const logNotify = (withNotification: boolean, title: string, body: string, details?: {error?: any}) => {
const logNotify = (withNotification: boolean, title: string, body: string, details?: any) => {
console.log(title, body, details);
withNotification && notify(title, body, details);
};
export const installCLI = async (withNotification: boolean) => {
export const installCLI = (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.'
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}`)
);
} 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 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}`;
console.error(err);
logNotify(withNotification, 'Hyper CLI installation failed', error);
});
} else {
logNotify(withNotification, 'Hyper CLI installation failed', `Unsupported platform ${process.platform}`);
withNotification &&
notify('Hyper CLI installation', 'Command is added in PATH only at package installation. Please reinstall.');
}
};

View file

@ -25,12 +25,10 @@ export const getColorMap: {
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

@ -11,7 +11,7 @@ const generatePrefixedCommand = (command: string, shortcuts: string[]) => {
return result;
};
const mapKeys = (config: Record<string, string[] | string>) => {
export default (config: Record<string, string[] | string>) => {
return Object.keys(config).reduce((keymap: Record<string, string[]>, command: string) => {
if (!command) {
return keymap;
@ -39,5 +39,3 @@ const mapKeys = (config: Record<string, string[] | string>) => {
return keymap;
}, {});
};
export default mapKeys;

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

@ -4,7 +4,7 @@ import Color from 'color';
// returns a background color that's in hex
// format including the alpha channel (e.g.: `#00000050`)
// input can be any css value (rgb, hsl, string…)
const toElectronBackgroundColor = (bgColor: string) => {
export default (bgColor: string) => {
const color = Color(bgColor);
if (color.alpha() === 1) {
@ -13,7 +13,5 @@ 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;

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
branches:
only:
- master
image: Visual Studio 2019
install:
- ps: Install-Product node 12 x64
- set CI=true
- yarn
build: off
matrix:
fast_finish: true
shallow_clone: true
test_script:
- node --version
- yarn --version
- yarn run test
on_success:
- IF %APPVEYOR_REPO_BRANCH%==canary cp build\canary.ico build\icon.ico
- yarn run dist
- ps: Get-ChildItem .\dist\squirrel-windows\*.exe | % { Push-AppveyorArtifact $_.FullName }

View file

@ -1,6 +0,0 @@
module.exports = {
files: ['test/*'],
extensions: ['ts'],
require: ['ts-node/register/transpile-only'],
timeout: '30s'
};

View file

@ -1,5 +1,9 @@
module.exports = {
export default {
files: ['test/unit/*'],
babel: {
compileEnhancements: false,
compileAsTests: ['**/testUtils/**/*']
},
extensions: ['ts'],
require: ['ts-node/register/transpile-only']
};

53
bin/cp-snapshot.js vendored
View file

@ -1,53 +0,0 @@
const path = require('path');
const fs = require('fs');
const {Arch} = require('electron-builder');
function copySnapshot(pathToElectron, archToCopy) {
const snapshotFileName = 'snapshot_blob.bin';
const v8ContextFileName = getV8ContextFileName(archToCopy);
const pathToBlob = path.resolve(__dirname, '..', 'cache', archToCopy, snapshotFileName);
const pathToBlobV8 = path.resolve(__dirname, '..', 'cache', archToCopy, v8ContextFileName);
console.log('Copying v8 snapshots from', pathToBlob, 'to', pathToElectron);
fs.copyFileSync(pathToBlob, path.join(pathToElectron, snapshotFileName));
fs.copyFileSync(pathToBlobV8, path.join(pathToElectron, v8ContextFileName));
}
function getPathToElectron() {
switch (process.platform) {
case 'darwin':
return path.resolve(
__dirname,
'..',
'node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources'
);
case 'win32':
case 'linux':
return path.resolve(__dirname, '..', 'node_modules', 'electron', 'dist');
}
}
function getV8ContextFileName(archToCopy) {
if (process.platform === 'darwin') {
return `v8_context_snapshot${archToCopy === 'arm64' ? '.arm64' : '.x86_64'}.bin`;
} else {
return `v8_context_snapshot.bin`;
}
}
exports.default = async (context) => {
const archToCopy = Arch[context.arch];
const pathToElectron =
process.platform === 'darwin'
? `${context.appOutDir}/Hyper.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources`
: context.appOutDir;
copySnapshot(pathToElectron, archToCopy);
};
if (require.main === module) {
const archToCopy = process.env.npm_config_arch;
const pathToElectron = getPathToElectron();
if ((process.arch.startsWith('arm') ? 'arm64' : 'x64') === archToCopy) {
copySnapshot(pathToElectron, archToCopy);
}
}

51
bin/mk-snapshot.js vendored
View file

@ -1,51 +0,0 @@
const childProcess = require('child_process');
const vm = require('vm');
const path = require('path');
const fs = require('fs');
const electronLink = require('electron-link');
const {mkdirp} = require('fs-extra');
const excludedModules = {};
const crossArchDirs = ['clang_x86_v8_arm', 'clang_x64_v8_arm64', 'win_clang_x64'];
async function main() {
const baseDirPath = path.resolve(__dirname, '..');
console.log('Creating a linked script..');
const result = await electronLink({
baseDirPath: baseDirPath,
mainPath: `${__dirname}/snapshot-libs.js`,
cachePath: `${baseDirPath}/cache`,
// eslint-disable-next-line no-prototype-builtins
shouldExcludeModule: (modulePath) => excludedModules.hasOwnProperty(modulePath)
});
const snapshotScriptPath = `${baseDirPath}/cache/snapshot-libs.js`;
fs.writeFileSync(snapshotScriptPath, result.snapshotScript);
// Verify if we will be able to use this in `mksnapshot`
vm.runInNewContext(result.snapshotScript, undefined, {filename: snapshotScriptPath, displayErrors: true});
const outputBlobPath = `${baseDirPath}/cache/${process.env.npm_config_arch}`;
await mkdirp(outputBlobPath);
if (process.platform !== 'darwin') {
const mksnapshotBinPath = `${baseDirPath}/node_modules/electron-mksnapshot/bin`;
const matchingDirs = crossArchDirs.map((dir) => `${mksnapshotBinPath}/${dir}`).filter((dir) => fs.existsSync(dir));
for (const dir of matchingDirs) {
if (fs.existsSync(`${mksnapshotBinPath}/gen/v8/embedded.S`)) {
await mkdirp(`${dir}/gen/v8`);
fs.copyFileSync(`${mksnapshotBinPath}/gen/v8/embedded.S`, `${dir}/gen/v8/embedded.S`);
}
}
}
console.log(`Generating startup blob in "${outputBlobPath}"`);
childProcess.execFileSync(
path.resolve(__dirname, '..', 'node_modules', '.bin', 'mksnapshot' + (process.platform === 'win32' ? '.cmd' : '')),
[snapshotScriptPath, '--output_dir', outputBlobPath]
);
}
main().catch((err) => console.error(err));

16
bin/notarize.js vendored
View file

@ -1,16 +0,0 @@
const { notarize } = require("@electron/notarize");
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context;
if (electronPlatformName !== "darwin" || !process.env.APPLE_ID || !process.env.APPLE_PASSWORD) {
return;
}
const appName = context.packager.appInfo.productFilename;
return await notarize({
appBundleId: "co.zeit.hyper",
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_PASSWORD
});
};

31
bin/snapshot-libs.js vendored
View file

@ -1,31 +0,0 @@
require('color-convert');
require('color-string');
require('columnify');
require('lodash');
require('ms');
require('normalize-url');
require('parse-url');
require('php-escape-shell');
require('plist');
require('redux-thunk');
require('redux');
require('reselect');
require('seamless-immutable');
require('stylis');
require('xterm-addon-unicode11');
// eslint-disable-next-line no-constant-condition
if (false) {
require('args');
require('mousetrap');
require('open');
require('react-dom');
require('react-redux');
require('react');
require('xterm-addon-fit');
require('xterm-addon-image');
require('xterm-addon-search');
require('xterm-addon-web-links');
require('xterm-addon-webgl');
require('xterm-addon-canvas');
require('xterm');
}

34
build/Info.plist Normal file
View file

@ -0,0 +1,34 @@
<plist version="1.0">
<dict>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Folders</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>public.folder</string>
<string>com.apple.bundle</string>
<string>com.apple.package</string>
<string>com.apple.resolvable</string>
</array>
<key>LSHandlerRank</key>
<string>Alternate</string>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>UnixExecutables</string>
<key>CFBundleTypeRole</key>
<string>Shell</string>
<key>LSItemContentTypes</key>
<array>
<string>public.unix-executable</string>
</array>
<key>LSHandlerRank</key>
<string>Alternate</string>
</dict>
</array>
</dict>
</plist>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.personal-information.addressbook</key>
<true/>
<key>com.apple.security.personal-information.calendars</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.security.personal-information.photos-library</key>
<true/>
</dict>
</plist>

View file

@ -1,27 +1,8 @@
#!/usr/bin/env bash
# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.65.2/resources/darwin/bin/code.sh
# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/darwin/bin/code.sh
# TODO: bash is deprecated on macOS and will be removed.
# Port this to /bin/sh or /bin/zsh
function app_realpath() {
SOURCE=$1
while [ -h "$SOURCE" ]; do
DIR=$(dirname "$SOURCE")
SOURCE=$(readlink "$SOURCE")
[[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE
done
SOURCE_DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )"
echo "${SOURCE_DIR%%"${SOURCE_DIR#*.app}"}"
}
APP_PATH="$(app_realpath "${BASH_SOURCE[0]}")"
if [ -z "$APP_PATH" ]; then
echo "Unable to determine app path from symlink : ${BASH_SOURCE[0]}"
exit 1
fi
CONTENTS="$APP_PATH/Contents"
function realpath() { /usr/bin/python -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$0"; }
CONTENTS="$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")"
ELECTRON="$CONTENTS/MacOS/Hyper"
CLI="$CONTENTS/Resources/bin/cli.js"
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@"

View file

@ -1,28 +0,0 @@
!macro customInstall
WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper" "" "Open &Hyper here"
WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper" "Icon" `"$appExe"`
WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper\command" "" `"$appExe" "%V"`
WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper" "" "Open &Hyper here"
WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper" "Icon" `"$appExe"`
WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper\command" "" `"$appExe" "%V"`
WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper" "" "Open &Hyper here"
WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper" "Icon" `"$appExe"`
WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper\command" "" `"$appExe" "%V"`
!macroend
!macro customUnInstall
DeleteRegKey HKCU "Software\Classes\Directory\Background\shell\Hyper"
DeleteRegKey HKCU "Software\Classes\Directory\shell\Hyper"
DeleteRegKey HKCU "Software\Classes\Drive\shell\Hyper"
!macroend
!macro customInstallMode
StrCpy $isForceCurrentInstall "1"
!macroend
!macro customInit
IfFileExists $LOCALAPPDATA\Hyper\Update.exe 0 +2
nsExec::Exec '"$LOCALAPPDATA\Hyper\Update.exe" --uninstall -s'
!macroend

View file

@ -1,28 +1,27 @@
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable @typescript-eslint/no-unsafe-return */
import fs from 'fs';
import os from 'os';
import path from 'path';
import got from 'got';
import registryUrlModule from 'registry-url';
const registryUrl = registryUrlModule();
import pify from 'pify';
import * as recast from 'recast';
import path from 'path';
// If the user defines XDG_CONFIG_HOME they definitely want their config there,
// otherwise use the home directory in linux/mac and userdata in windows
const applicationDirectory = process.env.XDG_CONFIG_HOME
? path.join(process.env.XDG_CONFIG_HOME, 'Hyper')
: process.platform === 'win32'
const applicationDirectory =
process.env.XDG_CONFIG_HOME !== undefined
? path.join(process.env.XDG_CONFIG_HOME, 'hyper')
: process.platform == 'win32'
? path.join(process.env.APPDATA!, 'Hyper')
: path.join(os.homedir(), '.config', 'Hyper');
: os.homedir();
const devConfigFileName = path.join(__dirname, `../hyper.json`);
const devConfigFileName = path.join(__dirname, `../.hyper.js`);
const fileName =
process.env.NODE_ENV !== 'production' && fs.existsSync(devConfigFileName)
? devConfigFileName
: path.join(applicationDirectory, 'hyper.json');
: path.join(applicationDirectory, '.hyper.js');
/**
* We need to make sure the file reading and parsing is lazy so that failure to
@ -32,7 +31,7 @@ const fileName =
function memoize<T extends (...args: any[]) => any>(fn: T): T {
let hasResult = false;
let result: any;
return ((...args: Parameters<T>) => {
return ((...args: any[]) => {
if (!hasResult) {
result = fn(...args);
hasResult = true;
@ -42,12 +41,33 @@ function memoize<T extends (...args: any[]) => any>(fn: T): T {
}
const getFileContents = memoize(() => {
return fs.readFileSync(fileName, 'utf8');
try {
return fs.readFileSync(fileName, 'utf8');
} catch (err) {
if (err.code !== 'ENOENT') {
// ENOENT === !exists()
throw err;
}
}
return null;
});
const getParsedFile = memoize(() => JSON.parse(getFileContents()));
const getParsedFile = memoize(() => recast.parse(getFileContents()!));
const getPluginsByKey = (key: string): any[] => getParsedFile()[key] || [];
const getProperties = memoize(() => ((getParsedFile()?.program?.body as any[]) || []).map((obj) => obj));
const getPluginsByKey = (key: string) => {
const properties = getProperties();
for (let i = 0; i < properties.length; i++) {
const rightProperties = Object.values<any>(properties[i]?.expression?.right?.properties || {});
for (let j = 0; j < rightProperties.length; j++) {
const plugin = rightProperties[j];
if (plugin?.key?.name === key) {
return (plugin?.value?.elements as any[]) || [];
}
}
}
};
const getPlugins = memoize(() => {
return getPluginsByKey('plugins');
@ -62,15 +82,15 @@ function exists() {
}
function isInstalled(plugin: string, locally?: boolean) {
const array = locally ? getLocalPlugins() : getPlugins();
const array = (locally ? getLocalPlugins() : getPlugins()) || [];
if (array && Array.isArray(array)) {
return array.includes(plugin);
return array.some((entry) => entry.value === plugin);
}
return false;
}
function save(config: any) {
return fs.writeFileSync(fileName, JSON.stringify(config, null, 2), 'utf8');
function save() {
return pify(fs.writeFile)(fileName, recast.print(getParsedFile()).code, 'utf8');
}
function getPackageName(plugin: string) {
@ -87,7 +107,7 @@ function getPackageName(plugin: string) {
function existsOnNpm(plugin: string) {
const name = getPackageName(plugin);
return got
.get<any>(registryUrl + name.toLowerCase(), {timeout: {request: 10000}, responseType: 'json'})
.get<any>(registryUrl + name.toLowerCase(), {timeout: 10000, responseType: 'json'})
.then((res) => {
if (!res.body.versions) {
return Promise.reject(res);
@ -98,7 +118,7 @@ function existsOnNpm(plugin: string) {
}
function install(plugin: string, locally?: boolean) {
const array = locally ? getLocalPlugins() : getPlugins();
const array = (locally ? getLocalPlugins() : getPlugins()) || [];
return existsOnNpm(plugin)
.catch((err: any) => {
const {statusCode} = err;
@ -112,25 +132,26 @@ function install(plugin: string, locally?: boolean) {
return Promise.reject(`${plugin} is already installed`);
}
const config = getParsedFile();
config[locally ? 'localPlugins' : 'plugins'] = [...array, plugin];
save(config);
array.push(recast.types.builders.literal(plugin));
return save();
});
}
async function uninstall(plugin: string) {
function uninstall(plugin: string) {
if (!isInstalled(plugin)) {
return Promise.reject(`${plugin} is not installed`);
}
const config = getParsedFile();
config.plugins = getPlugins().filter((p) => p !== plugin);
save(config);
const index = getPlugins()!.findIndex((entry) => entry.value === plugin);
getPlugins()!.splice(index, 1);
return save();
}
function list() {
if (getPlugins().length > 0) {
return getPlugins().join('\n');
if (Array.isArray(getPlugins())) {
return getPlugins()!
.map((plugin) => plugin.value)
.join('\n');
}
return false;
}

View file

@ -1,23 +1,19 @@
// This is a CLI tool, using console is OK
/* eslint no-console: 0 */
import {spawn, exec} from 'child_process';
import type {SpawnOptions} from 'child_process';
import {existsSync} from 'fs';
import {isAbsolute, resolve} from 'path';
import {promisify} from 'util';
import {existsSync} from 'fs';
import {version} from '../app/package.json';
import pify from 'pify';
import args from 'args';
import chalk from 'chalk';
import _columnify from 'columnify';
import got from 'got';
import open from 'open';
import columnify from 'columnify';
import got from 'got';
import ora from 'ora';
import {version} from '../app/package.json';
import * as api from './api';
let commandPromise: Promise<void> | undefined;
let commandPromise: Promise<void>;
const assertPluginName = (pluginName: string) => {
if (!pluginName) {
@ -36,22 +32,6 @@ const checkConfig = () => {
process.exit(1);
};
const columnify = (data: {name: string; description: string}[]) => {
const maxNameLength = Math.max(...data.map((entry) => entry.name.length), 0);
const descriptionWidth = process.stdout.columns - maxNameLength - 1;
return _columnify(data, {
showHeaders: false,
config: {
description: {
maxWidth: descriptionWidth
},
name: {
dataTransform: (nameValue) => chalk.green(nameValue)
}
}
}).replace(/\s+$/gm, ''); // remove padding from the end of all lines
};
args.command(
'install',
'Install a plugin',
@ -62,7 +42,7 @@ args.command(
commandPromise = api
.install(pluginName)
.then(() => console.log(chalk.green(`${pluginName} installed successfully!`)))
.catch((err) => console.error(chalk.red(err)));
.catch((err: any) => console.error(chalk.red(err)));
},
['i']
);
@ -77,7 +57,7 @@ args.command(
commandPromise = api
.uninstall(pluginName)
.then(() => console.log(chalk.green(`${pluginName} uninstalled successfully!`)))
.catch((err) => console.error(chalk.red(err)));
.catch((err) => console.log(chalk.red(err)));
},
['u', 'rm', 'remove']
);
@ -104,14 +84,19 @@ const lsRemote = (pattern?: string) => {
const URL = `https://api.npms.io/v2/search?q=${
(pattern && `${pattern}+`) || ''
}keywords:hyper-plugin,hyper-theme&size=250`;
type npmResult = {package: {name: string; description: string}};
return got(URL)
.then((response) => JSON.parse(response.body).results as npmResult[])
.then((response) => JSON.parse(response.body).results as any[])
.then((entries) => entries.map((entry) => entry.package))
.then((entries) =>
entries.map(({name, description}) => {
return {name, description};
})
)
.then((entries) =>
entries.map((entry) => {
entry.name = chalk.green(entry.name);
return entry;
})
);
};
@ -130,8 +115,9 @@ args.command(
console.error(`${chalk.red('Try')} ${chalk.green('hyper ls-remote')}`);
process.exit(1);
} else {
const msg = columnify(entries);
let msg = columnify(entries);
spinner.succeed();
msg = msg.substring(msg.indexOf('\n') + 1); // remove header
console.log(msg);
}
})
@ -151,8 +137,10 @@ args.command(
commandPromise = lsRemote()
.then((entries) => {
const msg = columnify(entries);
let msg = columnify(entries);
spinner.succeed();
msg = msg.substring(msg.indexOf('\n') + 1); // remove header
console.log(msg);
})
.catch((err) => {
@ -169,7 +157,7 @@ args.command(
(name, args_) => {
const pluginName = args_[0];
assertPluginName(pluginName);
void open(`http://ghub.io/${pluginName}`, {wait: false});
open(`http://ghub.io/${pluginName}`, {wait: false, url: true});
process.exit(0);
},
['d', 'h', 'home']
@ -195,10 +183,8 @@ const main = (argv: string[]) => {
version: false,
mri: {
boolean: ['v', 'verbose']
},
mainColor: 'yellow',
subColor: 'dim'
});
}
} as any);
if (commandPromise) {
return commandPromise;
@ -216,7 +202,7 @@ const main = (argv: string[]) => {
env['ELECTRON_ENABLE_LOGGING'] = '1';
}
const options: SpawnOptions = {
const options: any = {
detached: true,
env
};
@ -238,17 +224,15 @@ const main = (argv: string[]) => {
const opts = {
env
};
return promisify(exec)(cmd, opts);
return pify(exec)(cmd, opts);
}
}
const child = spawn(process.execPath, args_, options);
if (flags.verbose) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
child.stdout?.on('data', (data) => console.log(data.toString('utf8')));
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
child.stderr?.on('data', (data) => console.error(data.toString('utf8')));
child.stdout.on('data', (data) => console.log(data.toString('utf8')));
child.stderr.on('data', (data) => console.error(data.toString('utf8')));
}
if (flags.verbose) {
return new Promise((c) => child.once('exit', () => c(null)));
@ -263,7 +247,7 @@ function eventuallyExit(code: number) {
main(process.argv)
.then(() => eventuallyExit(0))
.catch((err) => {
.catch((err: any) => {
console.error(err.stack ? err.stack : err);
eventuallyExit(1);
});

View file

@ -1,6 +0,0 @@
{
"$schema": "http://json.schemastore.org/electron-builder",
"extends": "electron-builder.json",
"afterSign": null,
"npmRebuild": false
}

View file

@ -1,8 +1,6 @@
{
"$schema": "http://json.schemastore.org/electron-builder",
"appId": "co.zeit.hyper",
"afterSign": "./bin/notarize.js",
"afterPack": "./bin/cp-snapshot.js",
"directories": {
"app": "target"
},
@ -17,102 +15,54 @@
]
}
],
"artifactName": "${productName}-${version}-${arch}.${ext}",
"linux": {
"category": "TerminalEmulator",
"target": [
"deb",
"AppImage",
"rpm",
"snap",
"pacman"
{
"target": "deb",
"arch": [
"x64"
]
},
{
"target": "AppImage",
"arch": [
"x64"
]
},
{
"target": "rpm",
"arch": [
"x64"
]
},
{
"target": "snap",
"arch": [
"x64"
]
}
]
},
"win": {
"target": {
"target": "nsis",
"arch": [
"x64",
"arm64"
]
},
"target": [
"squirrel"
],
"rfc3161TimeStampServer": "http://timestamp.comodoca.com"
},
"nsis": {
"include": "build/win/installer.nsh",
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true
},
"mac": {
"target": {
"target": "default",
"arch": [
"x64",
"arm64"
]
},
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
"category": "public.app-category.developer-tools",
"entitlements": "./build/mac/entitlements.plist",
"entitlementsInherit": "./build/mac/entitlements.plist",
"extendInfo": {
"CFBundleDocumentTypes": [
{
"CFBundleTypeName": "Folders",
"CFBundleTypeRole": "Viewer",
"LSHandlerRank": "Alternate",
"LSItemContentTypes": [
"public.folder",
"com.apple.bundle",
"com.apple.package",
"com.apple.resolvable"
]
},
{
"CFBundleTypeName": "UnixExecutables",
"CFBundleTypeRole": "Shell",
"LSHandlerRank": "Alternate",
"LSItemContentTypes": [
"public.unix-executable"
]
}
],
"NSAppleEventsUsageDescription": "An application in Hyper wants to use AppleScript.",
"NSCalendarsUsageDescription": "An application in Hyper wants to access Calendar data.",
"NSCameraUsageDescription": "An application in Hyper wants to use the Camera.",
"NSContactsUsageDescription": "An application in Hyper wants to access your Contacts.",
"NSDesktopFolderUsageDescription": "An application in Hyper wants to access the Desktop folder.",
"NSDocumentsFolderUsageDescription": "An application in Hyper wants to access the Documents folder.",
"NSDownloadsFolderUsageDescription": "An application in Hyper wants to access the Downloads folder.",
"NSFileProviderDomainUsageDescription": "An application in Hyper wants to access files managed by a file provider.",
"NSFileProviderPresenceUsageDescription": "An application in Hyper wants to be informed when other apps access files that it manages.",
"NSLocationUsageDescription": "An application in Hyper wants to access your location information.",
"NSMicrophoneUsageDescription": "An application in Hyper wants to use your microphone.",
"NSMotionUsageDescription": "An application in Hyper wants to use the devices accelerometer.",
"NSNetworkVolumesUsageDescription": "An application in Hyper wants to access files on a network volume.",
"NSPhotoLibraryUsageDescription": "An application in Hyper wants to access the photo library.",
"NSRemindersUsageDescription": "An application in Hyper wants to access your reminders.",
"NSRemovableVolumesUsageDescription": "An application in Hyper wants to access files on a removable volume.",
"NSSpeechRecognitionUsageDescription": "An application in Hyper wants to send user data to Apples speech recognition servers.",
"NSSystemAdministrationUsageDescription": "The operation being performed by an application in Hyper requires elevated permission."
},
"extendInfo": "build/Info.plist",
"darkModeSupport": true
},
"deb": {
"compression": "bzip2",
"afterInstall": "./build/linux/after-install.tpl"
},
"rpm": {
"afterInstall": "./build/linux/after-install.tpl",
"fpm": [
"--rpm-rpmbuild-define",
"_build_id_links none"
]
"afterInstall": "./build/linux/after-install.tpl"
},
"snap": {
"confinement": "classic",
"publish": "github"
"confinement": "classic"
},
"protocols": {
"name": "ssh URL",

View file

@ -1,6 +1,6 @@
import type {configOptions} from '../../typings/config';
import {CONFIG_LOAD, CONFIG_RELOAD} from '../../typings/constants/config';
import type {HyperActions} from '../../typings/hyper';
import {CONFIG_LOAD, CONFIG_RELOAD} from '../constants/config';
import {HyperActions} from '../hyper';
import {configOptions} from '../config';
export function loadConfig(config: configOptions): HyperActions {
return {

View file

@ -1,15 +1,14 @@
import {CLOSE_TAB, CHANGE_TAB} from '../../typings/constants/tabs';
import {CLOSE_TAB, CHANGE_TAB} from '../constants/tabs';
import {
UI_WINDOW_MAXIMIZE,
UI_WINDOW_UNMAXIMIZE,
UI_OPEN_HAMBURGER_MENU,
UI_WINDOW_MINIMIZE,
UI_WINDOW_CLOSE
} from '../../typings/constants/ui';
import type {HyperDispatch} from '../../typings/hyper';
} from '../constants/ui';
import rpc from '../rpc';
import {userExitTermGroup, setActiveGroup} from './term-groups';
import {HyperDispatch} from '../hyper';
export function closeTab(uid: string) {
return (dispatch: HyperDispatch) => {
@ -40,7 +39,7 @@ export function maximize() {
dispatch({
type: UI_WINDOW_MAXIMIZE,
effect() {
rpc.emit('maximize');
rpc.emit('maximize', null);
}
});
};
@ -51,7 +50,7 @@ export function unmaximize() {
dispatch({
type: UI_WINDOW_UNMAXIMIZE,
effect() {
rpc.emit('unmaximize');
rpc.emit('unmaximize', null);
}
});
};
@ -73,7 +72,7 @@ export function minimize() {
dispatch({
type: UI_WINDOW_MINIMIZE,
effect() {
rpc.emit('minimize');
rpc.emit('minimize', null);
}
});
};
@ -84,7 +83,7 @@ export function close() {
dispatch({
type: UI_WINDOW_CLOSE,
effect() {
rpc.emit('close');
rpc.emit('close', null);
}
});
};

View file

@ -1,6 +1,6 @@
import {INIT} from '../../typings/constants';
import type {HyperDispatch} from '../../typings/hyper';
import rpc from '../rpc';
import {INIT} from '../constants';
import {HyperDispatch} from '../hyper';
export default function init() {
return (dispatch: HyperDispatch) => {

View file

@ -1,5 +1,5 @@
import {NOTIFICATION_MESSAGE, NOTIFICATION_DISMISS} from '../../typings/constants/notifications';
import type {HyperActions} from '../../typings/hyper';
import {NOTIFICATION_MESSAGE, NOTIFICATION_DISMISS} from '../constants/notifications';
import {HyperActions} from '../hyper';
export function dismissNotification(id: string): HyperActions {
return {

View file

@ -1,4 +1,6 @@
import type {Session} from '../../typings/common';
import rpc from '../rpc';
import {keys} from '../utils/object';
import findBySession from '../utils/term-groups';
import {
SESSION_ADD,
SESSION_RESIZE,
@ -11,14 +13,12 @@ import {
SESSION_CLEAR_ACTIVE,
SESSION_USER_DATA,
SESSION_SET_XTERM_TITLE,
SESSION_SEARCH
} from '../../typings/constants/sessions';
import type {HyperState, HyperDispatch, HyperActions} from '../../typings/hyper';
import rpc from '../rpc';
import {keys} from '../utils/object';
import findBySession from '../utils/term-groups';
SESSION_SEARCH,
SESSION_SEARCH_CLOSE
} from '../constants/sessions';
import {HyperState, session, HyperDispatch, HyperActions} from '../hyper';
export function addSession({uid, shell, pid, cols = null, rows = null, splitDirection, activeUid, profile}: Session) {
export function addSession({uid, shell, pid, cols, rows, splitDirection, activeUid}: session) {
return (dispatch: HyperDispatch, getState: () => HyperState) => {
const {sessions} = getState();
const now = Date.now();
@ -31,26 +31,26 @@ export function addSession({uid, shell, pid, cols = null, rows = null, splitDire
rows,
splitDirection,
activeUid: activeUid ? activeUid : sessions.activeUid,
now,
profile
now
});
};
}
export function requestSession(profile: string | undefined) {
export function requestSession() {
return (dispatch: HyperDispatch, getState: () => HyperState) => {
dispatch({
type: SESSION_REQUEST,
effect: () => {
const {ui} = getState();
const {cwd} = ui;
rpc.emit('new', {cwd, profile});
// the cols and rows from preview session maybe not accurate. so remove.
const {/*cols, rows,*/ cwd} = ui;
rpc.emit('new', {cwd});
}
});
};
}
export function addSessionData(uid: string, data: string) {
export function addSessionData(uid: string, data: any) {
return (dispatch: HyperDispatch) => {
dispatch({
type: SESSION_ADD_DATA,
@ -135,13 +135,12 @@ export function resizeSession(uid: string, cols: number, rows: number) {
};
}
export function openSearch(uid?: string) {
export function onSearch(uid?: string) {
return (dispatch: HyperDispatch, getState: () => HyperState) => {
const targetUid = uid || getState().sessions.activeUid!;
dispatch({
type: SESSION_SEARCH,
uid: targetUid,
value: true
uid: targetUid
});
};
}
@ -151,9 +150,8 @@ export function closeSearch(uid?: string, keyEvent?: any) {
const targetUid = uid || getState().sessions.activeUid!;
if (getState().sessions.sessions[targetUid]?.search) {
dispatch({
type: SESSION_SEARCH,
uid: targetUid,
value: false
type: SESSION_SEARCH_CLOSE,
uid: targetUid
});
} else {
if (keyEvent) {
@ -163,7 +161,7 @@ export function closeSearch(uid?: string, keyEvent?: any) {
};
}
export function sendSessionData(uid: string | null, data: string, escaped?: boolean) {
export function sendSessionData(uid: string | null, data: any, escaped?: any) {
return (dispatch: HyperDispatch, getState: () => HyperState) => {
dispatch({
type: SESSION_USER_DATA,

View file

@ -1,36 +1,32 @@
import {SESSION_REQUEST} from '../../typings/constants/sessions';
import rpc from '../rpc';
import {
DIRECTION,
TERM_GROUP_RESIZE,
TERM_GROUP_REQUEST,
TERM_GROUP_EXIT,
TERM_GROUP_EXIT_ACTIVE
} from '../../typings/constants/term-groups';
import type {ITermState, ITermGroup, HyperState, HyperDispatch, HyperActions} from '../../typings/hyper';
import rpc from '../rpc';
import {getRootGroups} from '../selectors';
} from '../constants/term-groups';
import {SESSION_REQUEST} from '../constants/sessions';
import findBySession from '../utils/term-groups';
import {getRootGroups} from '../selectors';
import {setActiveSession, ptyExitSession, userExitSession} from './sessions';
import {ITermState, ITermGroup, HyperState, HyperDispatch, HyperActions} from '../hyper';
import {Immutable} from 'seamless-immutable';
function requestSplit(direction: 'VERTICAL' | 'HORIZONTAL') {
return (_activeUid: string | undefined, _profile: string | undefined) =>
(dispatch: HyperDispatch, getState: () => HyperState): void => {
dispatch({
type: SESSION_REQUEST,
effect: () => {
const {ui, sessions} = getState();
const activeUid = _activeUid ? _activeUid : sessions.activeUid;
const profile = _profile ? _profile : activeUid ? sessions.sessions[activeUid].profile : window.profileName;
rpc.emit('new', {
splitDirection: direction,
cwd: ui.cwd,
activeUid,
profile
});
}
});
};
return (activeUid: string) => (dispatch: HyperDispatch, getState: () => HyperState): void => {
dispatch({
type: SESSION_REQUEST,
effect: () => {
const {ui, sessions} = getState();
rpc.emit('new', {
splitDirection: direction,
cwd: ui.cwd,
activeUid: activeUid ? activeUid : sessions.activeUid
});
}
});
};
}
export const requestVerticalSplit = requestSplit(DIRECTION.VERTICAL);
@ -44,20 +40,17 @@ export function resizeTermGroup(uid: string, sizes: number[]): HyperActions {
};
}
export function requestTermGroup(_activeUid: string | undefined, _profile: string | undefined) {
export function requestTermGroup(activeUid: string) {
return (dispatch: HyperDispatch, getState: () => HyperState) => {
dispatch({
type: TERM_GROUP_REQUEST,
effect: () => {
const {ui, sessions} = getState();
const {ui} = getState();
const {cwd} = ui;
const activeUid = _activeUid ? _activeUid : sessions.activeUid;
const profile = _profile ? _profile : activeUid ? sessions.sessions[activeUid].profile : window.profileName;
rpc.emit('new', {
isNewGroup: true,
cwd,
activeUid,
profile
activeUid
});
}
});
@ -74,7 +67,7 @@ export function setActiveGroup(uid: string) {
// When we've found the next group which we want to
// set as active (after closing something), we also need
// to find the first child group which has a sessionUid.
const findFirstSession = (state: ITermState, group: ITermGroup): string | undefined => {
const findFirstSession = (state: Immutable<ITermState>, group: Immutable<ITermGroup>): string | undefined => {
if (group.sessionUid) {
return group.sessionUid;
}
@ -97,7 +90,7 @@ const findPrevious = <T>(list: T[], old: T) => {
return index ? list[index - 1] : list[1];
};
const findNextSessionUid = (state: ITermState, group: ITermGroup) => {
const findNextSessionUid = (state: Immutable<ITermState>, group: Immutable<ITermGroup>) => {
// If we're closing a root group (i.e. a whole tab),
// the next group needs to be a root group as well:
if (state.activeRootGroup === group.uid) {
@ -108,7 +101,7 @@ const findNextSessionUid = (state: ITermState, group: ITermGroup) => {
const {children} = state.termGroups[group.parentUid!];
const nextUid = findPrevious(children.asMutable(), group.uid);
return findFirstSession(state, state.termGroups[nextUid]);
return findFirstSession(state, state.termGroups[nextUid!]);
};
export function ptyExitTermGroup(sessionUid: string) {
@ -175,7 +168,7 @@ export function exitActiveTermGroup() {
effect() {
const {sessions, termGroups} = getState();
const {uid} = findBySession(termGroups, sessions.activeUid!)!;
dispatch(userExitTermGroup(uid));
dispatch(userExitTermGroup(uid!));
}
});
};

View file

@ -1,9 +1,10 @@
import {stat} from 'fs';
import type {Stats} from 'fs';
import type parseUrl from 'parse-url';
import {php_escapeshellcmd as escapeShellCmd} from 'php-escape-shell';
import {isExecutable} from '../utils/file';
import {getRootGroups} from '../selectors';
import findBySession from '../utils/term-groups';
import notify from '../utils/notify';
import rpc from '../rpc';
import {requestSession, sendSessionData, setActiveSession} from './sessions';
import {
UI_FONT_SIZE_SET,
UI_FONT_SIZE_INCR,
@ -23,18 +24,16 @@ import {
UI_OPEN_SSH_URL,
UI_CONTEXTMENU_OPEN,
UI_COMMAND_EXEC
} from '../../typings/constants/ui';
import type {HyperState, HyperDispatch, HyperActions, ITermGroups} from '../../typings/hyper';
import rpc from '../rpc';
import {getRootGroups} from '../selectors';
import {isExecutable} from '../utils/file';
import notify from '../utils/notify';
import findBySession from '../utils/term-groups';
} from '../constants/ui';
import {requestSession, sendSessionData, setActiveSession} from './sessions';
import {setActiveGroup} from './term-groups';
import parseUrl from 'parse-url';
import {HyperState, HyperDispatch, HyperActions} from '../hyper';
import {Stats} from 'fs';
export function openContextMenu(uid: string, selection: string) {
const {stat} = window.require('fs');
export function openContextMenu(uid: string, selection: any) {
return (dispatch: HyperDispatch, getState: () => HyperState) => {
dispatch({
type: UI_CONTEXTMENU_OPEN,
@ -105,24 +104,24 @@ export function setFontSmoothing() {
};
}
export function windowGeometryUpdated({isMaximized}: {isMaximized: boolean}): HyperActions {
export function windowGeometryUpdated(): HyperActions {
return {
type: UI_WINDOW_GEOMETRY_CHANGED,
isMaximized
type: UI_WINDOW_GEOMETRY_CHANGED
};
}
// Find all sessions that are below the given
// termGroup uid in the hierarchy:
const findChildSessions = (termGroups: ITermGroups, uid: string): string[] => {
const findChildSessions = (termGroups: any, uid: string): string[] => {
const group = termGroups[uid];
if (group.sessionUid) {
return [uid];
}
return group.children
.asMutable()
.reduce((total: string[], childUid: string) => total.concat(findChildSessions(termGroups, childUid)), []);
return group.children.reduce(
(total: string[], childUid: string) => total.concat(findChildSessions(termGroups, childUid)),
[]
);
};
// Get the index of the next or previous group,
@ -146,7 +145,7 @@ function moveToNeighborPane(type: typeof UI_MOVE_NEXT_PANE | typeof UI_MOVE_PREV
if (childGroups.length === 1) {
console.log('ignoring move for single group');
} else {
const index = getNeighborIndex(childGroups, uid, type);
const index = getNeighborIndex(childGroups, uid!, type);
const {sessionUid} = termGroups.termGroups[childGroups[index]];
dispatch(setActiveSession(sessionUid!));
}
@ -272,11 +271,11 @@ export function openFile(path: string) {
}
rpc.once('session add', ({uid}) => {
rpc.once('session data', () => {
dispatch(sendSessionData(uid, command));
dispatch(sendSessionData(uid, command, null));
});
});
}
dispatch(requestSession(undefined));
dispatch(requestSession());
});
}
});
@ -295,30 +294,31 @@ export function leaveFullScreen(): HyperActions {
};
}
export function openSSH(parsedUrl: ReturnType<typeof parseUrl>) {
export function openSSH(url: string) {
return (dispatch: HyperDispatch) => {
dispatch({
type: UI_OPEN_SSH_URL,
effect() {
let command = `${parsedUrl.protocol} ${parsedUrl.user ? `${parsedUrl.user}@` : ''}${parsedUrl.resource}`;
const parsedUrl = parseUrl(url, true);
let command = parsedUrl.protocol + ' ' + (parsedUrl.user ? `${parsedUrl.user}@` : '') + parsedUrl.resource;
if (parsedUrl.port) command += ` -p ${parsedUrl.port}`;
if (parsedUrl.port) command += ' -p ' + parsedUrl.port;
command += '\n';
rpc.once('session add', ({uid}) => {
rpc.once('session data', () => {
dispatch(sendSessionData(uid, command));
dispatch(sendSessionData(uid, command, null));
});
});
dispatch(requestSession(undefined));
dispatch(requestSession());
}
});
};
}
export function execCommand(command: string, fn: (e: any, dispatch: HyperDispatch) => void, e: any) {
export function execCommand(command: string, fn: (...args: any[]) => void, e: any) {
return (dispatch: HyperDispatch) =>
dispatch({
type: UI_COMMAND_EXEC,

View file

@ -1,12 +1,12 @@
import {UPDATE_INSTALL, UPDATE_AVAILABLE} from '../../typings/constants/updater';
import type {HyperActions} from '../../typings/hyper';
import {UPDATE_INSTALL, UPDATE_AVAILABLE} from '../constants/updater';
import rpc from '../rpc';
import {HyperActions} from '../hyper';
export function installUpdate(): HyperActions {
return {
type: UPDATE_INSTALL,
effect: () => {
rpc.emit('quit and install');
rpc.emit('quit and install', null);
}
};
}

View file

@ -1,21 +1,22 @@
import type {HyperDispatch} from '../typings/hyper';
import {remote} from 'electron';
import {HyperDispatch} from './hyper';
import {closeSearch} from './actions/sessions';
import {ipcRenderer} from './utils/ipc';
// TODO: Should be updates to new async API https://medium.com/@nornagon/electrons-remote-module-considered-harmful-70d69500f31
const {getDecoratedKeymaps} = remote.require('./plugins');
let commands: Record<string, (event: any, dispatch: HyperDispatch) => void> = {
'editor:search-close': (e, dispatch) => {
dispatch(closeSearch(undefined, e));
window.focusActiveTerm();
}
};
export const getRegisteredKeys = async () => {
const keymaps = await ipcRenderer.invoke('getDecoratedKeymaps');
export const getRegisteredKeys = () => {
const keymaps = getDecoratedKeymaps();
return Object.keys(keymaps).reduce((result: Record<string, string>, actionName) => {
const commandKeys = keymaps[actionName];
commandKeys.forEach((shortcut) => {
commandKeys.forEach((shortcut: string) => {
result[shortcut] = actionName;
});
return result;

View file

@ -1,27 +1,27 @@
import React, {forwardRef, useState} from 'react';
import React from 'react';
import type {HeaderProps} from '../../typings/hyper';
import {decorate, getTabsProps} from '../utils/plugins';
import Tabs_ from './tabs';
import {HeaderProps} from '../hyper';
const Tabs = decorate(Tabs_, 'Tabs');
const Header = forwardRef<HTMLElement, HeaderProps>((props, ref) => {
const [headerMouseDownWindowX, setHeaderMouseDownWindowX] = useState<number>(0);
const [headerMouseDownWindowY, setHeaderMouseDownWindowY] = useState<number>(0);
export default class Header extends React.PureComponent<HeaderProps> {
headerMouseDownWindowX!: number;
headerMouseDownWindowY!: number;
const onChangeIntent = (active: string) => {
onChangeIntent = (active: string) => {
// we ignore clicks if they're a byproduct of a drag
// motion to move the window
if (window.screenX !== headerMouseDownWindowX || window.screenY !== headerMouseDownWindowY) {
if (window.screenX !== this.headerMouseDownWindowX || window.screenY !== this.headerMouseDownWindowY) {
return;
}
props.onChangeTab(active);
this.props.onChangeTab(active);
};
const handleHeaderMouseDown = () => {
handleHeaderMouseDown = () => {
// the hack of all hacks, this prevents the term
// iframe from losing focus, for example, when
// the user drags the nav around
@ -30,43 +30,43 @@ const Header = forwardRef<HTMLElement, HeaderProps>((props, ref) => {
// persist start positions of a potential drag motion
// to differentiate dragging from clicking
setHeaderMouseDownWindowX(window.screenX);
setHeaderMouseDownWindowY(window.screenY);
this.headerMouseDownWindowX = window.screenX;
this.headerMouseDownWindowY = window.screenY;
};
const handleHamburgerMenuClick = (event: React.MouseEvent) => {
handleHamburgerMenuClick = (event: React.MouseEvent) => {
let {right: x, bottom: y} = event.currentTarget.getBoundingClientRect();
x -= 15; // to compensate padding
y -= 12; // ^ same
props.openHamburgerMenu({x, y});
this.props.openHamburgerMenu({x, y});
};
const handleMaximizeClick = () => {
if (props.maximized) {
props.unmaximize();
handleMaximizeClick = () => {
if (this.props.maximized) {
this.props.unmaximize();
} else {
props.maximize();
this.props.maximize();
}
};
const handleMinimizeClick = () => {
props.minimize();
handleMinimizeClick = () => {
this.props.minimize();
};
const handleCloseClick = () => {
props.close();
handleCloseClick = () => {
this.props.close();
};
const getWindowHeaderConfig = () => {
const {showHamburgerMenu, showWindowControls} = props;
getWindowHeaderConfig() {
const {showHamburgerMenu, showWindowControls} = this.props;
const defaults = {
hambMenu: !props.isMac, // show by default on windows and linux
winCtrls: !props.isMac // show by default on Windows and Linux
hambMenu: !this.props.isMac, // show by default on windows and linux
winCtrls: !this.props.isMac // show by default on Windows and Linux
};
// don't allow the user to change defaults on macOS
if (props.isMac) {
if (this.props.isMac) {
return defaults;
}
@ -74,187 +74,182 @@ const Header = forwardRef<HTMLElement, HeaderProps>((props, ref) => {
hambMenu: showHamburgerMenu === '' ? defaults.hambMenu : showHamburgerMenu,
winCtrls: showWindowControls === '' ? defaults.winCtrls : showWindowControls
};
};
const {isMac} = props;
const {borderColor} = props;
let title = 'Hyper';
if (props.tabs.length === 1 && props.tabs[0].title) {
// if there's only one tab we use its title as the window title
title = props.tabs[0].title;
}
const {hambMenu, winCtrls} = getWindowHeaderConfig();
const left = winCtrls === 'left';
const maxButtonHref = props.maximized
? './renderer/assets/icons.svg#restore-window'
: './renderer/assets/icons.svg#maximize-window';
return (
<header
className={`header_header ${isMac && 'header_headerRounded'}`}
onMouseDown={handleHeaderMouseDown}
onMouseUp={() => window.focusActiveTerm()}
onDoubleClick={handleMaximizeClick}
ref={ref}
>
{!isMac && (
<div
className={`header_windowHeader ${props.tabs.length > 1 ? 'header_windowHeaderWithBorder' : ''}`}
style={{borderColor}}
>
{hambMenu && (
<svg
className={`header_shape ${left ? 'header_hamburgerMenuRight' : 'header_hamburgerMenuLeft'}`}
onClick={handleHamburgerMenuClick}
>
<use xlinkHref="./renderer/assets/icons.svg#hamburger-menu" />
</svg>
)}
<span className="header_appTitle">{title}</span>
{winCtrls && (
<div className={`header_windowControls ${left ? 'header_windowControlsLeft' : ''}`}>
<div className={`${left ? 'header_minimizeWindowLeft' : ''}`} onClick={handleMinimizeClick}>
<svg className="header_shape">
<use xlinkHref="./renderer/assets/icons.svg#minimize-window" />
</svg>
render() {
const {isMac} = this.props;
const props = getTabsProps(this.props, {
tabs: this.props.tabs,
borderColor: this.props.borderColor,
onClose: this.props.onCloseTab,
onChange: this.onChangeIntent,
fullScreen: this.props.fullScreen
});
const {borderColor} = props;
let title = 'Hyper';
if (props.tabs.length === 1 && props.tabs[0].title) {
// if there's only one tab we use its title as the window title
title = props.tabs[0].title;
}
const {hambMenu, winCtrls} = this.getWindowHeaderConfig();
const left = winCtrls === 'left';
const maxButtonHref = this.props.maximized
? './renderer/assets/icons.svg#restore-window'
: './renderer/assets/icons.svg#maximize-window';
return (
<header
className={`header_header ${isMac && 'header_headerRounded'}`}
onMouseDown={this.handleHeaderMouseDown}
onMouseUp={() => window.focusActiveTerm()}
onDoubleClick={this.handleMaximizeClick}
>
{!isMac && (
<div
className={`header_windowHeader ${props.tabs.length > 1 ? 'header_windowHeaderWithBorder' : ''}`}
style={{borderColor}}
>
{hambMenu && (
<svg
className={`header_shape ${left ? 'header_hamburgerMenuRight' : 'header_hamburgerMenuLeft'}`}
onClick={this.handleHamburgerMenuClick}
>
<use xlinkHref="./renderer/assets/icons.svg#hamburger-menu" />
</svg>
)}
<span className="header_appTitle">{title}</span>
{winCtrls && (
<div className={`header_windowControls ${left ? 'header_windowControlsLeft' : ''}`}>
<div className={`${left ? 'header_minimizeWindowLeft' : ''}`} onClick={this.handleMinimizeClick}>
<svg className="header_shape">
<use xlinkHref="./renderer/assets/icons.svg#minimize-window" />
</svg>
</div>
<div className={`${left ? 'header_maximizeWindowLeft' : ''}`} onClick={this.handleMaximizeClick}>
<svg className="header_shape">
<use xlinkHref={maxButtonHref} />
</svg>
</div>
<div
className={`header_closeWindow ${left ? 'header_closeWindowLeft' : ''}`}
onClick={this.handleCloseClick}
>
<svg className="header_shape">
<use xlinkHref="./renderer/assets/icons.svg#close-window" />
</svg>
</div>
</div>
<div className={`${left ? 'header_maximizeWindowLeft' : ''}`} onClick={handleMaximizeClick}>
<svg className="header_shape">
<use xlinkHref={maxButtonHref} />
</svg>
</div>
<div className={`header_closeWindow ${left ? 'header_closeWindowLeft' : ''}`} onClick={handleCloseClick}>
<svg className="header_shape">
<use xlinkHref="./renderer/assets/icons.svg#close-window" />
</svg>
</div>
</div>
)}
</div>
)}
{props.customChildrenBefore}
<Tabs
{...getTabsProps(props, {
tabs: props.tabs,
borderColor: props.borderColor,
backgroundColor: props.backgroundColor,
onClose: props.onCloseTab,
onChange: onChangeIntent,
fullScreen: props.fullScreen,
defaultProfile: props.defaultProfile,
profiles: props.profiles.asMutable({deep: true}),
openNewTab: props.openNewTab
})}
/>
{props.customChildren}
)}
</div>
)}
{this.props.customChildrenBefore}
<Tabs {...props} />
{this.props.customChildren}
<style jsx>{`
.header_header {
position: fixed;
top: 1px;
left: 1px;
right: 1px;
z-index: 100;
}
<style jsx>{`
.header_header {
position: fixed;
top: 1px;
left: 1px;
right: 1px;
z-index: 100;
}
.header_headerRounded {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.header_headerRounded {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.header_windowHeader {
height: 34px;
width: 100%;
position: fixed;
top: 1px;
left: 1px;
right: 1px;
-webkit-app-region: drag;
-webkit-user-select: none;
display: flex;
justify-content: center;
align-items: center;
}
.header_windowHeader {
height: 34px;
width: 100%;
position: fixed;
top: 1px;
left: 1px;
right: 1px;
-webkit-app-region: drag;
-webkit-user-select: none;
display: flex;
justify-content: center;
align-items: center;
}
.header_windowHeaderWithBorder {
border-color: #ccc;
border-bottom-style: solid;
border-bottom-width: 1px;
}
.header_windowHeaderWithBorder {
border-color: #ccc;
border-bottom-style: solid;
border-bottom-width: 1px;
}
.header_appTitle {
font-size: 12px;
}
.header_appTitle {
font-size: 12px;
}
.header_shape,
.header_shape > svg {
width: 40px;
height: 34px;
padding: 12px 15px 12px 15px;
-webkit-app-region: no-drag;
color: #fff;
opacity: 0.5;
shape-rendering: crispEdges;
}
.header_shape,
.header_shape > svg {
width: 40px;
height: 34px;
padding: 12px 15px 12px 15px;
-webkit-app-region: no-drag;
color: #fff;
opacity: 0.5;
shape-rendering: crispEdges;
}
.header_shape:hover {
opacity: 1;
}
.header_shape:hover {
opacity: 1;
}
.header_shape:active {
opacity: 0.3;
}
.header_shape:active {
opacity: 0.3;
}
.header_hamburgerMenuLeft {
position: fixed;
top: 0;
left: 0;
}
.header_hamburgerMenuLeft {
position: fixed;
top: 0;
left: 0;
}
.header_hamburgerMenuRight {
position: fixed;
top: 0;
right: 0;
}
.header_hamburgerMenuRight {
position: fixed;
top: 0;
right: 0;
}
.header_windowControls {
display: flex;
width: 120px;
height: 34px;
justify-content: space-between;
position: fixed;
top: 0;
right: 0;
}
.header_windowControls {
display: flex;
width: 120px;
height: 34px;
justify-content: space-between;
position: fixed;
top: 0;
right: 0;
}
.header_windowControlsLeft {
left: 0px;
}
.header_windowControlsLeft {
left: 0px;
}
.header_closeWindowLeft {
order: 1;
}
.header_closeWindowLeft {
order: 1;
}
.header_minimizeWindowLeft {
order: 2;
}
.header_minimizeWindowLeft {
order: 2;
}
.header_maximizeWindowLeft {
order: 3;
}
.header_maximizeWindowLeft {
order: 3;
}
.header_closeWindow:hover {
color: #fe354e;
}
.header_closeWindow:hover {
color: #fe354e;
}
.header_closeWindow:active {
color: #fe354e;
}
`}</style>
</header>
);
});
Header.displayName = 'Header';
export default Header;
.header_closeWindow:active {
color: #fe354e;
}
`}</style>
</header>
);
}
}

View file

@ -1,149 +0,0 @@
import React, {useRef, useState} from 'react';
import {VscChevronDown} from '@react-icons/all-files/vsc/VscChevronDown';
import useClickAway from 'react-use/lib/useClickAway';
import type {configOptions} from '../../typings/config';
interface Props {
defaultProfile: string;
profiles: configOptions['profiles'];
openNewTab: (name: string) => void;
backgroundColor: string;
borderColor: string;
tabsVisible: boolean;
}
const isMac = /Mac/.test(navigator.userAgent);
const DropdownButton = ({defaultProfile, profiles, openNewTab, backgroundColor, borderColor, tabsVisible}: Props) => {
const [dropdownOpen, setDropdownOpen] = useState(false);
const ref = useRef(null);
const toggleDropdown = () => {
setDropdownOpen(!dropdownOpen);
};
useClickAway(ref, () => {
setDropdownOpen(false);
});
return (
<div
ref={ref}
title="New Tab"
className={`new_tab ${tabsVisible ? 'tabs_visible' : 'tabs_hidden'}`}
onClick={toggleDropdown}
onDoubleClick={(e) => e.stopPropagation()}
onBlur={() => setDropdownOpen(false)}
>
<VscChevronDown style={{verticalAlign: 'middle'}} />
{dropdownOpen && (
<ul
key="dropdown"
className="profile_dropdown"
style={{
borderColor,
backgroundColor
}}
>
{profiles.map((profile) => (
<li
key={profile.name}
onClick={() => {
openNewTab(profile.name);
setDropdownOpen(false);
}}
className={`profile_dropdown_item ${
profile.name === defaultProfile && profiles.length > 1 ? 'profile_dropdown_item_default' : ''
}`}
>
{profile.name}
</li>
))}
</ul>
)}
<style jsx>{`
.profile_dropdown {
border-width: 1px;
border-style: solid;
border-bottom-width: 0px;
border-right-width: 0px;
position: absolute;
top: 33px;
right: 0px;
z-index: 1000;
padding: 0px;
margin: 0px;
list-style-type: none;
white-space: nowrap;
min-width: 120px;
}
.profile_dropdown_item {
padding: 0px 20px;
height: 34px;
line-height: 34px;
cursor: pointer;
font-size: 12px;
color: #fff;
background-color: transparent;
border-width: 0px;
border-style: solid;
border-color: transparent;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: ${borderColor};
text-align: start;
text-transform: capitalize;
}
.profile_dropdown_item:hover {
background-color: ${borderColor};
}
.profile_dropdown_item_default {
font-weight: bold;
}
.new_tab {
background: transparent;
color: #fff;
border-left: 1px;
border-bottom: 1px;
border-left-style: solid;
border-bottom-style: solid;
border-left-width: 1px;
border-bottom-width: 1px;
cursor: pointer;
font-size: 12px;
height: 34px;
line-height: 34px;
padding: 0 16px;
position: relative;
text-align: center;
-webkit-user-select: none;
${isMac ? '-webkit-app-region: drag;' : ''}
top: '0px';
}
.tabs_visible {
border-color: ${borderColor};
}
.tabs_hidden {
border-color: transparent;
position: absolute;
right: 0px;
}
.tabs_hidden:hover {
border-color: ${borderColor};
}
`}</style>
</div>
);
};
export default DropdownButton;

View file

@ -1,110 +1,115 @@
import React, {forwardRef, useEffect, useRef, useState} from 'react';
import React from 'react';
import {NotificationProps, NotificationState} from '../hyper';
import type {NotificationProps} from '../../typings/hyper';
export default class Notification extends React.PureComponent<NotificationProps, NotificationState> {
dismissTimer!: NodeJS.Timeout;
constructor(props: NotificationProps) {
super(props);
this.state = {
dismissing: false
};
}
const Notification = forwardRef<HTMLDivElement, React.PropsWithChildren<NotificationProps>>((props, ref) => {
const dismissTimer = useRef<NodeJS.Timeout | undefined>(undefined);
const [dismissing, setDismissing] = useState(false);
componentDidMount() {
if (this.props.dismissAfter) {
this.setDismissTimer();
}
}
useEffect(() => {
setDismissTimer();
}, []);
useEffect(() => {
componentDidUpdate(prevProps: NotificationProps, prevState: NotificationState) {
// if we have a timer going and the notification text
// changed we reset the timer
resetDismissTimer();
setDismissing(false);
}, [props.text]);
if (this.props.text !== prevProps.text) {
if (prevProps.dismissAfter) {
this.resetDismissTimer();
}
if (prevState.dismissing) {
this.setState({dismissing: false});
}
}
}
const handleDismiss = () => {
setDismissing(true);
handleDismiss = () => {
this.setState({dismissing: true});
};
const onElement = (el: HTMLDivElement | null) => {
onElement = (el: HTMLDivElement | null) => {
if (el) {
el.addEventListener('webkitTransitionEnd', () => {
if (dismissing) {
props.onDismiss();
if (this.state.dismissing) {
this.props.onDismiss();
}
});
const {backgroundColor} = props;
const {backgroundColor} = this.props;
if (backgroundColor) {
el.style.setProperty('background-color', backgroundColor, 'important');
}
if (ref) {
if (typeof ref === 'function') ref(el);
else ref.current = el;
}
}
};
const setDismissTimer = () => {
if (typeof props.dismissAfter === 'number') {
dismissTimer.current = setTimeout(() => {
handleDismiss();
}, props.dismissAfter);
}
};
setDismissTimer() {
this.dismissTimer = setTimeout(() => {
this.handleDismiss();
}, this.props.dismissAfter!);
}
const resetDismissTimer = () => {
clearTimeout(dismissTimer.current);
setDismissTimer();
};
resetDismissTimer() {
clearTimeout(this.dismissTimer);
this.setDismissTimer();
}
useEffect(() => {
return () => {
clearTimeout(dismissTimer.current);
};
}, []);
componentWillUnmount() {
clearTimeout(this.dismissTimer);
}
const {backgroundColor, color} = props;
const opacity = dismissing ? 0 : 1;
return (
<div ref={onElement} style={{opacity, backgroundColor, color}} className="notification_indicator">
{props.customChildrenBefore}
{props.children || props.text}
{props.userDismissable ? (
<a className="notification_dismissLink" onClick={handleDismiss} style={{color: props.userDismissColor}}>
[x]
</a>
) : null}
{props.customChildren}
render() {
const {backgroundColor, color} = this.props;
const opacity = this.state.dismissing ? 0 : 1;
return (
<div ref={this.onElement} style={{opacity, backgroundColor, color}} className="notification_indicator">
{this.props.customChildrenBefore}
{this.props.children || this.props.text}
{this.props.userDismissable ? (
<a
className="notification_dismissLink"
onClick={this.handleDismiss}
style={{color: this.props.userDismissColor}}
>
[x]
</a>
) : null}
{this.props.customChildren}
<style jsx>{`
.notification_indicator {
display: inline-block;
cursor: default;
-webkit-user-select: none;
background: rgba(255, 255, 255, 0.2);
padding: 8px 14px 9px;
margin-left: 10px;
transition: 150ms opacity ease;
color: #fff;
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
}
<style jsx>{`
.notification_indicator {
display: inline-block;
cursor: default;
-webkit-user-select: none;
background: rgba(255, 255, 255, 0.2);
padding: 8px 14px 9px;
margin-left: 10px;
transition: 150ms opacity ease;
color: #fff;
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
}
.notification_dismissLink {
position: relative;
left: 4px;
cursor: pointer;
font-weight: 600;
color: currentColor;
transition: font-weight 0.1s ease-in-out;
}
.notification_dismissLink {
position: relative;
left: 4px;
cursor: pointer;
opacity: 0.5;
color: currentColor;
transition: opacity 0.1s ease-in-out;
}
.notification_dismissLink:hover,
.notification_dismissLink:focus {
font-weight: 900;
}
`}</style>
</div>
);
});
Notification.displayName = 'Notification';
export default Notification;
.notification_dismissLink:hover,
.notification_dismissLink:focus {
opacity: 1;
}
`}</style>
</div>
);
}
}

View file

@ -1,132 +1,132 @@
import React, {forwardRef} from 'react';
import React from 'react';
import type {NotificationsProps} from '../../typings/hyper';
import {decorate} from '../utils/plugins';
import Notification_ from './notification';
import {NotificationsProps} from '../hyper';
const Notification = decorate(Notification_, 'Notification');
const Notifications = forwardRef<HTMLDivElement, NotificationsProps>((props, ref) => {
return (
<div className="notifications_view" ref={ref}>
{props.customChildrenBefore}
{props.fontShowing && (
<Notification
key="font"
backgroundColor="rgba(255, 255, 255, .2)"
text={`${props.fontSize}px`}
userDismissable={false}
onDismiss={props.onDismissFont}
dismissAfter={1000}
/>
)}
export default class Notifications extends React.PureComponent<NotificationsProps> {
render() {
return (
<div className="notifications_view">
{this.props.customChildrenBefore}
{this.props.fontShowing && (
<Notification
key="font"
backgroundColor="rgba(255, 255, 255, .2)"
text={`${this.props.fontSize}px`}
userDismissable={false}
onDismiss={this.props.onDismissFont}
dismissAfter={1000}
/>
)}
{props.resizeShowing && (
<Notification
key="resize"
backgroundColor="rgba(255, 255, 255, .2)"
text={`${props.cols}x${props.rows}`}
userDismissable={false}
onDismiss={props.onDismissResize}
dismissAfter={1000}
/>
)}
{this.props.resizeShowing && (
<Notification
key="resize"
backgroundColor="rgba(255, 255, 255, .2)"
text={`${this.props.cols}x${this.props.rows}`}
userDismissable={false}
onDismiss={this.props.onDismissResize}
dismissAfter={1000}
/>
)}
{props.messageShowing && (
<Notification
key="message"
backgroundColor="#FE354E"
color="#fff"
text={props.messageText}
onDismiss={props.onDismissMessage}
userDismissable={props.messageDismissable}
>
{props.messageURL ? (
<>
{props.messageText} (
<a
style={{color: '#fff'}}
onClick={(ev) => {
void window.require('electron').shell.openExternal(ev.currentTarget.href);
ev.preventDefault();
}}
href={props.messageURL}
>
more
</a>
)
</>
) : null}
</Notification>
)}
{props.updateShowing && (
<Notification
key="update"
backgroundColor="#18E179"
color="#000"
text={`Version ${props.updateVersion} ready`}
onDismiss={props.onDismissUpdate}
userDismissable
>
Version <b>{props.updateVersion}</b> ready.
{props.updateNote && ` ${props.updateNote.trim().replace(/\.$/, '')}`} (
<a
style={{color: '#000'}}
onClick={(ev) => {
void window.require('electron').shell.openExternal(ev.currentTarget.href);
ev.preventDefault();
}}
href={`https://github.com/vercel/hyper/releases/tag/${props.updateVersion}`}
{this.props.messageShowing && (
<Notification
key="message"
backgroundColor="#FE354E"
text={this.props.messageText}
onDismiss={this.props.onDismissMessage}
userDismissable={this.props.messageDismissable}
userDismissColor="#AA2D3C"
>
notes
</a>
).{' '}
{props.updateCanInstall ? (
{this.props.messageURL
? [
this.props.messageText,
' (',
<a
key="link"
style={{color: '#fff'}}
onClick={(ev) => {
window.require('electron').shell.openExternal(ev.currentTarget.href);
ev.preventDefault();
}}
href={this.props.messageURL}
>
more
</a>,
')'
]
: null}
</Notification>
)}
{this.props.updateShowing && (
<Notification
key="update"
backgroundColor="#18E179"
color="#000"
text={`Version ${this.props.updateVersion} ready`}
onDismiss={this.props.onDismissUpdate}
userDismissable
>
Version <b>{this.props.updateVersion}</b> ready.
{this.props.updateNote && ` ${this.props.updateNote.trim().replace(/\.$/, '')}`} (
<a
style={{
cursor: 'pointer',
textDecoration: 'underline',
fontWeight: 'bold'
}}
onClick={props.onUpdateInstall}
>
Restart
</a>
) : (
<a
style={{
color: '#000',
cursor: 'pointer',
textDecoration: 'underline',
fontWeight: 'bold'
}}
style={{color: '#000'}}
onClick={(ev) => {
void window.require('electron').shell.openExternal(ev.currentTarget.href);
window.require('electron').shell.openExternal(ev.currentTarget.href);
ev.preventDefault();
}}
href={props.updateReleaseUrl!}
href={`https://github.com/zeit/hyper/releases/tag/${this.props.updateVersion}`}
>
Download
notes
</a>
)}
.{' '}
</Notification>
)}
{props.customChildren}
).{' '}
{this.props.updateCanInstall ? (
<a
style={{
cursor: 'pointer',
textDecoration: 'underline',
fontWeight: 'bold'
}}
onClick={this.props.onUpdateInstall}
>
Restart
</a>
) : (
<a
style={{
color: '#000',
cursor: 'pointer',
textDecoration: 'underline',
fontWeight: 'bold'
}}
onClick={(ev) => {
window.require('electron').shell.openExternal(ev.currentTarget.href);
ev.preventDefault();
}}
href={this.props.updateReleaseUrl!}
>
Download
</a>
)}
.{' '}
</Notification>
)}
{this.props.customChildren}
<style jsx>{`
.notifications_view {
position: fixed;
bottom: 20px;
right: 20px;
}
`}</style>
</div>
);
});
Notifications.displayName = 'Notifications';
export default Notifications;
<style jsx>{`
.notifications_view {
position: fixed;
bottom: 20px;
right: 20px;
}
`}</style>
</div>
);
}
}

View file

@ -1,237 +1,79 @@
import React, {useCallback, useRef, useEffect, forwardRef} from 'react';
import React from 'react';
import {SearchBoxProps} from '../hyper';
import {VscArrowDown} from '@react-icons/all-files/vsc/VscArrowDown';
import {VscArrowUp} from '@react-icons/all-files/vsc/VscArrowUp';
import {VscCaseSensitive} from '@react-icons/all-files/vsc/VscCaseSensitive';
import {VscClose} from '@react-icons/all-files/vsc/VscClose';
import {VscRegex} from '@react-icons/all-files/vsc/VscRegex';
import {VscWholeWord} from '@react-icons/all-files/vsc/VscWholeWord';
import clsx from 'clsx';
import type {SearchBoxProps} from '../../typings/hyper';
type SearchButtonColors = {
foregroundColor: string;
selectionColor: string;
backgroundColor: string;
const searchBoxStyling: React.CSSProperties = {
float: 'right',
height: '28px',
backgroundColor: 'white',
position: 'absolute',
right: '10px',
top: '25px',
width: '224px',
zIndex: 9999
};
type SearchButtonProps = React.PropsWithChildren<
{
onClick: () => void;
active: boolean;
title: string;
} & SearchButtonColors
>;
const enterKey = 13;
const SearchButton = ({
onClick,
active,
title,
foregroundColor,
backgroundColor,
selectionColor,
children
}: SearchButtonProps) => {
const handleKeyUp = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
onClick();
}
},
[onClick]
);
export default class SearchBox extends React.PureComponent<SearchBoxProps> {
searchTerm: string;
constructor(props: SearchBoxProps) {
super(props);
this.searchTerm = '';
}
return (
<div
onClick={onClick}
className={clsx('search-button', {'search-button-active': active})}
tabIndex={0}
onKeyUp={handleKeyUp}
title={title}
>
{children}
<style jsx>
{`
.search-button {
cursor: pointer;
color: ${foregroundColor};
padding: 2px;
margin: 4px 0px;
height: 18px;
width: 18px;
border-radius: 2px;
}
.search-button:focus {
outline: ${selectionColor} solid 2px;
}
.search-button:hover {
background-color: ${backgroundColor};
}
.search-button-active {
background-color: ${selectionColor};
}
.search-button-active:hover {
background-color: ${selectionColor};
}
`}
</style>
</div>
);
};
const SearchBox = forwardRef<HTMLDivElement, SearchBoxProps>((props, ref) => {
const {
caseSensitive,
wholeWord,
regex,
results,
toggleCaseSensitive,
toggleWholeWord,
toggleRegex,
next,
prev,
close,
backgroundColor,
foregroundColor,
borderColor,
selectionColor,
font
} = props;
const searchTermRef = useRef<string>('');
const inputRef = useRef<HTMLInputElement | null>(null);
const handleChange = useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
searchTermRef.current = event.currentTarget.value;
if (event.shiftKey && event.key === 'Enter') {
prev(searchTermRef.current);
} else if (event.key === 'Enter') {
next(searchTermRef.current);
}
},
[prev, next]
);
useEffect(() => {
inputRef.current?.focus();
}, [inputRef.current]);
const searchButtonColors: SearchButtonColors = {
backgroundColor: borderColor,
selectionColor,
foregroundColor
handleChange = (event: React.KeyboardEvent<HTMLInputElement>) => {
this.searchTerm = event.currentTarget.value;
if (event.keyCode === enterKey) {
this.props.search(event.currentTarget.value);
}
};
return (
<div className="flex-row search-container" ref={ref}>
<div className="flex-row search-box">
<input className="search-input" type="text" onKeyDown={handleChange} ref={inputRef} placeholder="Search" />
render() {
return (
<div style={searchBoxStyling}>
<input type="text" className="search-box" onKeyUp={this.handleChange} ref={(input) => input?.focus()} />
<span className="search-button" onClick={() => this.props.prev(this.searchTerm)}>
{' '}
&#x2190;{' '}
</span>
<span className="search-button" onClick={() => this.props.next(this.searchTerm)}>
{' '}
&#x2192;{' '}
</span>
<span className="search-button" onClick={() => this.props.close()}>
{' '}
x{' '}
</span>
<style jsx>
{`
.search-box {
font-size: 18px;
padding: 6px;
width: 145px;
border: none;
}
<SearchButton onClick={toggleCaseSensitive} active={caseSensitive} title="Match Case" {...searchButtonColors}>
<VscCaseSensitive size="14px" />
</SearchButton>
.search-box:focus {
outline: none;
}
<SearchButton onClick={toggleWholeWord} active={wholeWord} title="Match Whole Word" {...searchButtonColors}>
<VscWholeWord size="14px" />
</SearchButton>
<SearchButton onClick={toggleRegex} active={regex} title="Use Regular Expression" {...searchButtonColors}>
<VscRegex size="14px" />
</SearchButton>
.search-button {
background-color: #ffffff;
color: black;
padding: 7px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
transition-duration: 0.4s;
cursor: pointer;
}
.search-button:hover {
background-color: #e7e7e7;
}
`}
</style>
</div>
<span style={{minWidth: '60px', marginLeft: '4px'}}>
{results === undefined
? ''
: results.resultCount === 0
? 'No results'
: `${results.resultIndex + 1} of ${results.resultCount}`}
</span>
<div className="flex-row">
<SearchButton
onClick={() => prev(searchTermRef.current)}
active={false}
title="Previous Match"
{...searchButtonColors}
>
<VscArrowUp size="14px" />
</SearchButton>
<SearchButton
onClick={() => next(searchTermRef.current)}
active={false}
title="Next Match"
{...searchButtonColors}
>
<VscArrowDown size="14px" />
</SearchButton>
<SearchButton onClick={close} active={false} title="Close" {...searchButtonColors}>
<VscClose size="14px" />
</SearchButton>
</div>
<style jsx>
{`
.search-container {
background-color: ${backgroundColor};
border: 1px solid ${borderColor};
border-radius: 2px;
position: absolute;
right: 13px;
top: 4px;
z-index: 10;
padding: 4px;
font-family: ${font};
font-size: 12px;
}
.search-input {
outline: none;
background-color: transparent;
border: none;
color: ${foregroundColor};
align-self: stretch;
width: 100px;
}
.flex-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 4px;
}
.search-box {
border: none;
border-radius: 2px;
outline: ${borderColor} solid 1px;
background-color: ${backgroundColor};
color: ${foregroundColor};
padding: 0px 4px;
}
.search-input::placeholder {
color: ${foregroundColor};
}
.search-box:focus-within {
outline: ${selectionColor} solid 2px;
}
`}
</style>
</div>
);
});
SearchBox.displayName = 'SearchBox';
export default SearchBox;
);
}
}

View file

@ -1,191 +1,217 @@
import React, {useState, useEffect, useRef, forwardRef} from 'react';
import React from 'react';
import _ from 'lodash';
import {SplitPaneProps} from '../hyper';
import sum from 'lodash/sum';
export default class SplitPane extends React.PureComponent<SplitPaneProps, {dragging: boolean}> {
dragPanePosition!: number;
dragTarget!: Element;
panes!: Element[];
paneIndex!: number;
d1!: 'height' | 'width';
d2!: 'top' | 'left';
d3!: 'clientX' | 'clientY';
panesSize!: number;
dragging!: boolean;
constructor(props: SplitPaneProps) {
super(props);
this.state = {dragging: false};
}
import type {SplitPaneProps} from '../../typings/hyper';
componentDidUpdate(prevProps: SplitPaneProps) {
if (this.state.dragging && prevProps.sizes !== this.props.sizes) {
// recompute positions for ongoing dragging
this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2];
}
}
const SplitPane = forwardRef<HTMLDivElement, SplitPaneProps>((props, ref) => {
const dragPanePosition = useRef<number>(0);
const dragTarget = useRef<HTMLDivElement | null>(null);
const paneIndex = useRef<number>(0);
const d1 = props.direction === 'horizontal' ? 'height' : 'width';
const d2 = props.direction === 'horizontal' ? 'top' : 'left';
const d3 = props.direction === 'horizontal' ? 'clientY' : 'clientX';
const panesSize = useRef<number | null>(null);
const [dragging, setDragging] = useState(false);
setupPanes(ev: any) {
this.panes = Array.from(ev.target.parentNode.childNodes);
this.paneIndex = this.panes.indexOf(ev.target);
this.paneIndex -= Math.ceil(this.paneIndex / 2);
}
const handleAutoResize = (ev: React.MouseEvent<HTMLDivElement>, index: number) => {
handleAutoResize = (ev: React.MouseEvent) => {
ev.preventDefault();
paneIndex.current = index;
this.setupPanes(ev);
const sizes_ = getSizes();
sizes_[paneIndex.current] = 0;
sizes_[paneIndex.current + 1] = 0;
const sizes_ = this.getSizes();
sizes_[this.paneIndex] = 0;
sizes_[this.paneIndex + 1] = 0;
const availableWidth = 1 - sum(sizes_);
sizes_[paneIndex.current] = availableWidth / 2;
sizes_[paneIndex.current + 1] = availableWidth / 2;
const availableWidth = 1 - _.sum(sizes_);
sizes_[this.paneIndex] = availableWidth / 2;
sizes_[this.paneIndex + 1] = availableWidth / 2;
props.onResize(sizes_);
this.props.onResize(sizes_);
};
const handleDragStart = (ev: React.MouseEvent<HTMLDivElement>, index: number) => {
handleDragStart = (ev: any) => {
ev.preventDefault();
setDragging(true);
window.addEventListener('mousemove', onDrag);
window.addEventListener('mouseup', onDragEnd);
this.setState({dragging: true});
window.addEventListener('mousemove', this.onDrag);
window.addEventListener('mouseup', this.onDragEnd);
const target = ev.target as HTMLDivElement;
dragTarget.current = target;
dragPanePosition.current = dragTarget.current.getBoundingClientRect()[d2];
panesSize.current = target.parentElement!.getBoundingClientRect()[d1];
paneIndex.current = index;
// dimensions to consider
if (this.props.direction === 'horizontal') {
this.d1 = 'height';
this.d2 = 'top';
this.d3 = 'clientY';
} else {
this.d1 = 'width';
this.d2 = 'left';
this.d3 = 'clientX';
}
this.dragTarget = ev.target;
this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2];
this.panesSize = ev.target.parentNode.getBoundingClientRect()[this.d1];
this.setupPanes(ev);
};
const getSizes = () => {
const {sizes} = props;
getSizes() {
const {sizes} = this.props;
let sizes_: number[];
if (sizes) {
sizes_ = [...sizes.asMutable()];
} else {
const total = props.children.length;
const total = (this.props.children as React.ReactNodeArray).length;
const count = new Array<number>(total).fill(1 / total);
sizes_ = count;
}
return sizes_;
};
}
const onDrag = (ev: MouseEvent) => {
const sizes_ = getSizes();
onDrag = (ev: MouseEvent) => {
const sizes_ = this.getSizes();
const i = paneIndex.current;
const pos = ev[d3];
const d = Math.abs(dragPanePosition.current - pos) / panesSize.current!;
if (pos > dragPanePosition.current) {
const i = this.paneIndex;
const pos = ev[this.d3];
const d = Math.abs(this.dragPanePosition - pos) / this.panesSize;
if (pos > this.dragPanePosition) {
sizes_[i] += d;
sizes_[i + 1] -= d;
} else {
sizes_[i] -= d;
sizes_[i + 1] += d;
}
props.onResize(sizes_);
this.props.onResize(sizes_);
};
const onDragEnd = () => {
window.removeEventListener('mousemove', onDrag);
window.removeEventListener('mouseup', onDragEnd);
setDragging(false);
onDragEnd = () => {
if (this.state.dragging) {
window.removeEventListener('mousemove', this.onDrag);
window.removeEventListener('mouseup', this.onDragEnd);
this.setState({dragging: false});
}
};
useEffect(() => {
return () => {
onDragEnd();
};
}, []);
const {children, direction, borderColor} = props;
const sizeProperty = direction === 'horizontal' ? 'height' : 'width';
// workaround for the fact that if we don't specify
// sizes, sometimes flex fails to calculate the
// right height for the horizontal panes
const sizes = props.sizes || new Array<number>(children.length).fill(1 / children.length);
return (
<div className={`splitpane_panes splitpane_panes_${direction}`} ref={ref}>
{children.map((child, i) => {
const style = {
// flexBasis doesn't work for the first horizontal pane, height need to be specified
[sizeProperty]: `${sizes[i] * 100}%`,
flexBasis: `${sizes[i] * 100}%`,
flexGrow: 0
};
return (
<React.Fragment key={i}>
<div className="splitpane_pane" style={style}>
render() {
const children = this.props.children as React.ReactNodeArray;
const {direction, borderColor} = this.props;
const sizeProperty = direction === 'horizontal' ? 'height' : 'width';
// workaround for the fact that if we don't specify
// sizes, sometimes flex fails to calculate the
// right height for the horizontal panes
const sizes = this.props.sizes || new Array<number>(children.length).fill(1 / children.length);
return (
<div className={`splitpane_panes splitpane_panes_${direction}`}>
{React.Children.map(children, (child, i) => {
const style = {
// flexBasis doesn't work for the first horizontal pane, height need to be specified
[sizeProperty]: `${sizes[i] * 100}%`,
flexBasis: `${sizes[i] * 100}%`,
flexGrow: 0
};
return [
<div key="pane" className="splitpane_pane" style={style}>
{child}
</div>
{i < children.length - 1 ? (
</div>,
i < children.length - 1 ? (
<div
onMouseDown={(e) => handleDragStart(e, i)}
onDoubleClick={(e) => handleAutoResize(e, i)}
key="divider"
onMouseDown={this.handleDragStart}
onDoubleClick={this.handleAutoResize}
style={{backgroundColor: borderColor}}
className={`splitpane_divider splitpane_divider_${direction}`}
/>
) : null}
</React.Fragment>
);
})}
<div style={{display: dragging ? 'block' : 'none'}} className="splitpane_shim" />
) : null
];
})}
<div style={{display: this.state.dragging ? 'block' : 'none'}} className="splitpane_shim" />
<style jsx>{`
.splitpane_panes {
display: flex;
flex: 1;
outline: none;
position: relative;
width: 100%;
height: 100%;
}
<style jsx>{`
.splitpane_panes {
display: flex;
flex: 1;
outline: none;
position: relative;
width: 100%;
height: 100%;
}
.splitpane_panes_vertical {
flex-direction: row;
}
.splitpane_panes_vertical {
flex-direction: row;
}
.splitpane_panes_horizontal {
flex-direction: column;
}
.splitpane_panes_horizontal {
flex-direction: column;
}
.splitpane_pane {
flex: 1;
outline: none;
position: relative;
}
.splitpane_pane {
flex: 1;
outline: none;
position: relative;
}
.splitpane_divider {
box-sizing: border-box;
z-index: 1;
background-clip: padding-box;
flex-shrink: 0;
}
.splitpane_divider {
box-sizing: border-box;
z-index: 1;
background-clip: padding-box;
flex-shrink: 0;
}
.splitpane_divider_vertical {
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
width: 11px;
margin: 0 -5px;
cursor: col-resize;
}
.splitpane_divider_vertical {
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
width: 11px;
margin: 0 -5px;
cursor: col-resize;
}
.splitpane_divider_horizontal {
height: 11px;
margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
.splitpane_divider_horizontal {
height: 11px;
margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
/*
this shim is used to make sure mousemove events
trigger in all the draggable area of the screen
this is not the case due to hterm's <iframe>
*/
.splitpane_shim {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
}
`}</style>
</div>
);
});
/*
this shim is used to make sure mousemove events
trigger in all the draggable area of the screen
this is not the case due to hterm's <iframe>
*/
.splitpane_shim {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
}
`}</style>
</div>
);
}
SplitPane.displayName = 'SplitPane';
export default SplitPane;
componentWillUnmount() {
// ensure drag end
if (this.dragging) {
this.onDragEnd();
}
}
}

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