Merge branch 'canary'

This commit is contained in:
Labhansh Agrawal 2021-07-16 00:06:48 +05:30
commit e1d57077c5
188 changed files with 91172 additions and 76893 deletions

View file

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

View file

@ -4,7 +4,9 @@ app/static
app/bin app/bin
app/dist app/dist
app/node_modules app/node_modules
app/typings
assets assets
website website
bin bin
dist dist
target

113
.eslintrc.json Normal file
View file

@ -0,0 +1,113 @@
{
"plugins": [
"react",
"prettier",
"@typescript-eslint",
"eslint-comments"
],
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:prettier/recommended",
"plugin:eslint-comments/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"impliedStrict": true,
"experimentalObjectRestSpread": true
},
"allowImportExportEverywhere": true,
"project": [
"./tsconfig.eslint.json"
]
},
"env": {
"es6": true,
"browser": true,
"node": true
},
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"func-names": [
"error",
"as-needed"
],
"no-shadow": "error",
"no-extra-semi": 0,
"react/prop-types": 0,
"react/react-in-jsx-scope": 0,
"react/no-unescaped-entities": 0,
"react/jsx-no-target-blank": 0,
"react/no-string-refs": 0,
"prettier/prettier": [
"error",
{
"printWidth": 120,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": false,
"semi": true,
"useTabs": false,
"jsxBracketSameLine": false
}
],
"eslint-comments/no-unused-disable": "error"
},
"overrides": [
{
"files": [
"app/config/config-default.js",
".hyper.js"
],
"rules": {
"prettier/prettier": [
"error",
{
"printWidth": 120,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": false,
"semi": true,
"useTabs": false,
"parser": "babel",
"jsxBracketSameLine": false
}
]
}
},
{
"files": [
"**.ts",
"**.tsx"
],
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier"
],
"rules": {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/prefer-optional-chain": "error",
"@typescript-eslint/ban-types": "off",
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/restrict-template-expressions": "off"
}
}
]
}

3
.gitattributes vendored
View file

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

View file

@ -1,14 +1,23 @@
---
name: Bug report
about: Create a report to help Hyper improve
title: ''
labels: ''
assignees: ''
---
<!-- <!--
Hi there! Thank you for discovering and submitting an issue. Hi there! Thank you for discovering and submitting an issue.
Before you submit this; let's make sure of a few things. Before you submit this; let's make sure of a few things.
Please make sure the following boxes are ticked if they are correct. Please make sure the following boxes are ticked if they are correct.
If not, please try and fulfil these first. If not, please try and fulfill these first.
--> -->
<!-- Checked checkbox should look like this: [x] --> <!-- Checked checkbox should look like this: [x] -->
- [ ] I am on the [latest](https://github.com/zeit/hyper/releases/latest) Hyper.app version - [ ] I am on the [latest](https://github.com/vercel/hyper/releases/latest) Hyper.app version
- [ ] I have searched the [issues](https://github.com/zeit/hyper/issues) of this repo and believe that this is not a duplicate - [ ] I have searched the [issues](https://github.com/vercel/hyper/issues) of this repo and believe that this is not a duplicate
<!-- <!--
Once those are done, if you're able to fill in the following list with your information, Once those are done, if you're able to fill in the following list with your information,

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea/feature for Hyper
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

16
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,16 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
time: '11:00'
open-pull-requests-limit: 30
target-branch: canary
- package-ecosystem: npm
directory: "/app"
schedule:
interval: weekly
time: '11:00'
open-pull-requests-limit: 30
target-branch: canary

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, - 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 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` - It'd also be extremely helpful to enable us to update your PR incase we need to rebase or what-not by checking `Allow edits from maintainers`
- If your PR changes some API, please make a PR for hyper website too: https://github.com/zeit/hyper-site. - If your PR changes some API, please make a PR for hyper website too: https://github.com/vercel/hyper-site.
Thanks, again! --> Thanks, again! -->

67
.github/workflows/codeql-analysis.yml vendored Normal file
View file

@ -0,0 +1,67 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ canary ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ canary ]
schedule:
- cron: '37 6 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
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@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ 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@v1

76
.github/workflows/nodejs.yml vendored Normal file
View file

@ -0,0 +1,76 @@
name: Node CI
on:
push:
branches:
- master
- canary
pull_request:
defaults:
run:
shell: bash
jobs:
build:
runs-on: ${{matrix.os}}
strategy:
matrix:
node-version: [14.x]
os: [macos-11.0, ubuntu-latest, windows-latest]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
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: |
cp build/canary.ico build/icon.ico
cp build/canary.icns build/icon.icns
- name: Build (pr)
if: github.event_name == 'pull_request'
run: yarn run dist --publish=never
- name: Build (push)
if: github.event_name == 'push'
run: yarn run dist
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.MAC_CERT_P12_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_P12_PASSWORD }}
WIN_CSC_LINK: ${{ secrets.WIN_CERT_P12_BASE64 }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CERT_P12_PASSWORD }}
- name: Test Spectron
if: runner.os != 'Linux'
run: yarn run test:spectron
- name: Archive Spectron test screenshot
if: runner.os != 'Linux'
uses: actions/upload-artifact@v2
with:
name: spectron
path: dist/tmp/*.png
- name: Archive Build Artifacts
uses: LabhanshAgrawal/upload-artifact@v3
with:
path: |
dist/*.dmg
dist/*.snap
dist/*.AppImage
dist/*.deb
dist/*.rpm
dist/*.exe
- name: Save the pr number in an artifact
if: github.event_name == 'pull_request'
env:
PR_NUM: ${{ github.event.number }}
run: echo $PR_NUM > pr_num.txt
- name: Upload the pr num
uses: actions/upload-artifact@v2
if: github.event_name == 'pull_request'
with:
name: pr_num
path: ./pr_num.txt

56
.github/workflows/spectron_comment.yml vendored Normal file
View file

@ -0,0 +1,56 @@
name: Comment spectron screenshots on PR
on:
workflow_run:
workflows: ['Node CI']
types:
- completed
jobs:
spectron_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@v2.11.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: nodejs.yml
run_id: ${{ github.event.workflow_run.id }}
name: spectron
- name: Get PR number
uses: dawidd6/action-download-artifact@v2.11.0
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.0.0
with:
path: ./pr_num.txt
- name: List images
run: ls -al
- name: Upload images to imgur
id: upload_screenshots
uses: devicons/public-upload-to-imgur@v2.2.1
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,
Here are screenshots of Hyper built from this pr.
{0}
Thank you for contributing to Hyper!
with:
type: create
issue_number: ${{ steps.pr_num_reader.outputs.content }}
token: ${{ secrets.GITHUB_TOKEN }}
body: ${{ format(env.MESSAGE, env.IMG_MARKDOWN) }}

4
.gitignore vendored
View file

@ -1,6 +1,7 @@
# build output # build output
dist dist
app/renderer app/renderer
target
bin/cli.* bin/cli.*
# dependencies # dependencies
@ -15,3 +16,6 @@ yarn-error.log
.hyper_plugins .hyper_plugins
.DS_Store .DS_Store
.vscode/*
!.vscode/launch.json
.idea

1
.husky/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
_

4
.husky/pre-push Executable file
View file

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

View file

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

2
.vscode/launch.json vendored
View file

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

View file

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

View file

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

View file

@ -3,11 +3,11 @@
## Workflow ## Workflow
### Run Hyper in dev mode ### Run Hyper in dev mode
Hyper can be run in dev mode by cloning this repository and following the ["Contributing" section of our README](https://github.com/zeit/hyper#contribute). Hyper can be run in dev mode by cloning this repository and following the ["Contributing" section of our README](https://github.com/vercel/hyper#contribute).
In dev mode you'll get more ouput and access to React/Redux dev-tools in Electron. In dev mode you'll get more ouput and access to React/Redux dev-tools in Electron.
Prerequisites and steps are described in the ["Contributing" section of our README](https://github.com/zeit/hyper#contribute). Prerequisites and steps are described in the ["Contributing" section of our README](https://github.com/vercel/hyper#contribute).
Be sure to use the `canary` branch. Be sure to use the `canary` branch.
### Create a dev config file ### Create a dev config file
@ -30,7 +30,7 @@ module.exports = {
``` ```
### Running your plugin ### Running your plugin
To load, your plugin should expose at least one API method. All possible methods are listed [here](https://github.com/zeit/hyper/blob/canary/app/plugins/extensions.js). To load, your plugin should expose at least one API method. All possible methods are listed [here](https://github.com/vercel/hyper/blob/canary/app/plugins/extensions.ts).
After launching Hyper in dev mode, run `yarn run app`, it should log that your plugin has been correcty loaded: `Plugin hyper-awesome-plugin (0.1.0) loaded.`. Name and version printed are the ones in your plugins `package.json` file. After launching Hyper in dev mode, run `yarn run app`, it should log that your plugin has been correcty loaded: `Plugin hyper-awesome-plugin (0.1.0) loaded.`. Name and version printed are the ones in your plugins `package.json` file.
@ -41,7 +41,7 @@ Almost all available API methods can be found on https://hyper.is.
If there's any missing, let us know or submit a PR to document it! If there's any missing, let us know or submit a PR to document it!
### Components ### Components
You can decorate almost all Hyper components with a Higher-Order Component (HOC). To understand their architecture, the easiest way is to use React dev-tools to dig in to their hierachy. You can decorate almost all Hyper components with a Higher-Order Component (HOC). To understand their architecture, the easiest way is to use React dev-tools to dig in to their hierarchy.
Multiple plugins can decorate the same Hyper component. Thus, `Component` passed as first argument to your decorator function could possibly not be an original Hyper component but a HOC of a previous plugin. If you need to retrieve a reference to a real Hyper component, you can pass down a `onDecorated` handler. Multiple plugins can decorate the same Hyper component. Thus, `Component` passed as first argument to your decorator function could possibly not be an original Hyper component but a HOC of a previous plugin. If you need to retrieve a reference to a real Hyper component, you can pass down a `onDecorated` handler.
```js ```js
@ -70,7 +70,7 @@ exports.decorateTerms = (Terms, {React}) => {
// <Terms onDecorated={this.onDecorated} /> // <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. :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 ### Keymaps
@ -190,6 +190,17 @@ exports.decorateTerm = (Term, { React, notify }) => {
} }
``` ```
### Require Electron
Hyper doesn't provide a reference to electron. However plugins can directly require electron.
```js
const electron = require('electron')
// or
const { dialog, Menu } = require('electron')
```
This is needed in order to allow show/hide to have proper return of focus.
## Hyper v2 breaking changes ## Hyper v2 breaking changes
Hyper v2 uses `xterm.js` instead of `hterm`. It means that PTY output renders now in a canvas element, not with a hackable DOM structure. Hyper v2 uses `xterm.js` instead of `hterm`. It means that PTY output renders now in a canvas element, not with a hackable DOM structure.
For example, plugins can't use TermCSS in order to modify text or link styles anymore. It is now required to use available configuration params that are passed down to `xterm.js`. For example, plugins can't use TermCSS in order to modify text or link styles anymore. It is now required to use available configuration params that are passed down to `xterm.js`.

View file

@ -1,23 +1,39 @@
![](https://assets.zeit.co/image/upload/v1549723846/repositories/hyper/hyper-3-repo-banner.png) ![](https://assets.vercel.com/image/upload/v1549723846/repositories/hyper/hyper-3-repo-banner.png)
[![macOS CI Status](https://circleci.com/gh/zeit/hyper.svg?style=shield)](https://circleci.com/gh/zeit/hyper) <p align="center">
[![Windows CI status](https://ci.appveyor.com/api/projects/status/kqvb4oa772an58sc?svg=true)](https://ci.appveyor.com/project/zeit/hyper) <a aria-label="Vercel logo" href="https://vercel.com">
[![Linux CI status](https://travis-ci.org/zeit/hyper.svg?branch=master)](https://travis-ci.org/zeit/hyper) <img src="https://img.shields.io/badge/MADE%20BY%20Vercel-000000.svg?style=for-the-badge&logo=vercel&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)
[![Changelog #213](https://img.shields.io/badge/changelog-%23213-lightgrey.svg)](https://changelog.com/213) [![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 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 ## Usage
[Download the latest release!](https://hyper.is/#installation) [Download the latest release!](https://hyper.is/#installation)
### Linux ### Linux
#### Arch and derivatives #### Arch and derivatives
Hyper is available in the [AUR](https://aur.archlinux.org/packages/hyper/). Use an AUR package manager like [aurman](https://github.com/polygamma/aurman) Hyper is available in the [AUR](https://aur.archlinux.org/packages/hyper/). Use an AUR [package manager](https://wiki.archlinux.org/index.php/AUR_helpers) e.g. [paru](https://github.com/Morganamilo/paru)
```sh ```sh
aurman -S hyper paru -S hyper
```
#### NixOS
Hyper is available as [Nix package](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/hyper/default.nix), to install the app run this command:
```sh
nix-env -i hyper
``` ```
### macOS ### macOS
@ -26,7 +42,7 @@ Use [Homebrew Cask](https://brew.sh) to download the app by running these comman
```bash ```bash
brew update brew update
brew cask install hyper brew install --cask hyper
``` ```
### Windows ### Windows
@ -83,6 +99,10 @@ 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 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). to the Terms of Service by running `sudo xcodebuild` after a fresh Xcode installation).
##### Error with `c++` on macOS when running `yarn`
If you are getting compiler errors when running `yarn` add the environment variable `export CXX=clang++`
##### Error with `codesign` on macOS when running `yarn run dist` ##### Error with `codesign` on macOS when running `yarn run dist`
If you have issues in the `codesign` step when running `yarn run dist` on macOS, you can temporarily disable code signing locally by setting If you have issues in the `codesign` step when running `yarn run dist` on macOS, you can temporarily disable code signing locally by setting
@ -90,8 +110,8 @@ If you have issues in the `codesign` step when running `yarn run dist` on macOS,
## Related Repositories ## Related Repositories
- [Art](https://github.com/zeit/art/tree/master/hyper) - [Art](https://github.com/vercel/art/tree/master/hyper)
- [Website](https://github.com/zeit/hyper-site) - [Website](https://github.com/vercel/hyper-site)
- [Sample Extension](https://github.com/zeit/hyperpower) - [Sample Extension](https://github.com/vercel/hyperpower)
- [Sample Theme](https://github.com/zeit/hyperyellow) - [Sample Theme](https://github.com/vercel/hyperyellow)
- [Awesome Hyper](https://github.com/bnb/awesome-hyper) - [Awesome Hyper](https://github.com/bnb/awesome-hyper)

1
app/.yarnrc Normal file
View file

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

View file

@ -1,9 +1,8 @@
'use strict'; import fetch from 'electron-fetch';
import {EventEmitter} from 'events';
const fetch = require('electron-fetch').default; class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
const {EventEmitter} = require('events'); updateURL!: string;
class AutoUpdater extends EventEmitter {
quitAndInstall() { quitAndInstall() {
this.emitError('QuitAndInstall unimplemented'); this.emitError('QuitAndInstall unimplemented');
} }
@ -11,8 +10,8 @@ class AutoUpdater extends EventEmitter {
return this.updateURL; return this.updateURL;
} }
setFeedURL(updateURL) { setFeedURL(options: Electron.FeedURLOptions) {
this.updateURL = updateURL; this.updateURL = options.url;
} }
checkForUpdates() { checkForUpdates() {
@ -22,9 +21,10 @@ class AutoUpdater extends EventEmitter {
this.emit('checking-for-update'); this.emit('checking-for-update');
fetch(this.updateURL) fetch(this.updateURL)
.then(res => { .then((res) => {
if (res.status === 204) { if (res.status === 204) {
return this.emit('update-not-available'); this.emit('update-not-available');
return;
} }
return res.json().then(({name, notes, pub_date}) => { return res.json().then(({name, notes, pub_date}) => {
// Only name is mandatory, needed to construct release URL. // Only name is mandatory, needed to construct release URL.
@ -39,12 +39,12 @@ class AutoUpdater extends EventEmitter {
.catch(this.emitError.bind(this)); .catch(this.emitError.bind(this));
} }
emitError(error) { emitError(error: string | Error) {
if (typeof error === 'string') { if (typeof error === 'string') {
error = new Error(error); error = new Error(error);
} }
this.emit('error', error, error.message); this.emit('error', error);
} }
} }
module.exports = new AutoUpdater(); export default new AutoUpdater();

View file

@ -1,127 +0,0 @@
const {app, Menu} = require('electron');
const {openConfig, getConfig} = require('./config');
const {updatePlugins} = require('./plugins');
const {installCLI} = require('./utils/cli-install');
const commands = {
'window:new': () => {
// If window is created on the same tick, it will consume event too
setTimeout(app.createWindow, 0);
},
'tab:new': focusedWindow => {
if (focusedWindow) {
focusedWindow.rpc.emit('termgroup add req');
} else {
setTimeout(app.createWindow, 0);
}
},
'pane:splitVertical': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('split request vertical');
},
'pane:splitHorizontal': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('split request horizontal');
},
'pane:close': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('termgroup close req');
},
'window:preferences': () => {
openConfig();
},
'editor:clearBuffer': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session clear req');
},
'editor:selectAll': focusedWindow => {
focusedWindow.rpc.emit('term selectAll');
},
'plugins:update': () => {
updatePlugins();
},
'window:reload': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('reload');
},
'window:reloadFull': focusedWindow => {
focusedWindow && focusedWindow.reload();
},
'window:devtools': focusedWindow => {
if (!focusedWindow) {
return;
}
const webContents = focusedWindow.webContents;
if (webContents.isDevToolsOpened()) {
webContents.closeDevTools();
} else {
webContents.openDevTools({mode: 'detach'});
}
},
'zoom:reset': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('reset fontSize req');
},
'zoom:in': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('increase fontSize req');
},
'zoom:out': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('decrease fontSize req');
},
'tab:prev': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('move left req');
},
'tab:next': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('move right req');
},
'pane:prev': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('prev pane req');
},
'pane:next': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('next pane req');
},
'editor:movePreviousWord': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session move word left req');
},
'editor:moveNextWord': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session move word right req');
},
'editor:moveBeginningLine': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session move line beginning req');
},
'editor:moveEndLine': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session move line end req');
},
'editor:deletePreviousWord': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session del word left req');
},
'editor:deleteNextWord': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session del word right req');
},
'editor:deleteBeginningLine': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session del line beginning req');
},
'editor:deleteEndLine': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session del line end req');
},
'editor:break': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session break req');
},
'cli:install': () => {
installCLI(true);
},
'window:hamburgerMenu': () => {
if (getConfig().showHamburgerMenu) {
Menu.getApplicationMenu().popup({x: 15, y: 15});
}
}
};
//Special numeric command
[1, 2, 3, 4, 5, 6, 7, 8, 'last'].forEach(cmdIndex => {
const index = cmdIndex === 'last' ? cmdIndex : cmdIndex - 1;
commands[`tab:jump:${cmdIndex}`] = focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('move jump req', index);
};
});
exports.execCommand = (command, focusedWindow) => {
const fn = commands[command];
if (fn) {
fn(focusedWindow);
}
};

152
app/commands.ts Normal file
View file

@ -0,0 +1,152 @@
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': () => {
// If window is created on the same tick, it will consume event too
setTimeout(app.createWindow, 0);
},
'tab:new': (focusedWindow) => {
if (focusedWindow) {
focusedWindow.rpc.emit('termgroup add req', {});
} else {
setTimeout(app.createWindow, 0);
}
},
'pane:splitRight': (focusedWindow) => {
focusedWindow?.rpc.emit('split request vertical', {});
},
'pane:splitDown': (focusedWindow) => {
focusedWindow?.rpc.emit('split request horizontal', {});
},
'pane:close': (focusedWindow) => {
focusedWindow?.rpc.emit('termgroup close req');
},
'window:preferences': () => {
void openConfig();
},
'editor:clearBuffer': (focusedWindow) => {
focusedWindow?.rpc.emit('session clear req');
},
'editor:selectAll': (focusedWindow) => {
focusedWindow?.rpc.emit('term selectAll');
},
'plugins:update': () => {
updatePlugins();
},
'window:reload': (focusedWindow) => {
focusedWindow?.rpc.emit('reload');
},
'window:reloadFull': (focusedWindow) => {
focusedWindow?.reload();
},
'window:devtools': (focusedWindow) => {
if (!focusedWindow) {
return;
}
const webContents = focusedWindow.webContents;
if (webContents.isDevToolsOpened()) {
webContents.closeDevTools();
} else {
webContents.openDevTools({mode: 'detach'});
}
},
'zoom:reset': (focusedWindow) => {
focusedWindow?.rpc.emit('reset fontSize req');
},
'zoom:in': (focusedWindow) => {
focusedWindow?.rpc.emit('increase fontSize req');
},
'zoom:out': (focusedWindow) => {
focusedWindow?.rpc.emit('decrease fontSize req');
},
'tab:prev': (focusedWindow) => {
focusedWindow?.rpc.emit('move left req');
},
'tab:next': (focusedWindow) => {
focusedWindow?.rpc.emit('move right req');
},
'pane:prev': (focusedWindow) => {
focusedWindow?.rpc.emit('prev pane req');
},
'pane:next': (focusedWindow) => {
focusedWindow?.rpc.emit('next pane req');
},
'editor:movePreviousWord': (focusedWindow) => {
focusedWindow?.rpc.emit('session move word left req');
},
'editor:moveNextWord': (focusedWindow) => {
focusedWindow?.rpc.emit('session move word right req');
},
'editor:moveBeginningLine': (focusedWindow) => {
focusedWindow?.rpc.emit('session move line beginning req');
},
'editor:moveEndLine': (focusedWindow) => {
focusedWindow?.rpc.emit('session move line end req');
},
'editor:deletePreviousWord': (focusedWindow) => {
focusedWindow?.rpc.emit('session del word left req');
},
'editor:deleteNextWord': (focusedWindow) => {
focusedWindow?.rpc.emit('session del word right req');
},
'editor:deleteBeginningLine': (focusedWindow) => {
focusedWindow?.rpc.emit('session del line beginning req');
},
'editor:deleteEndLine': (focusedWindow) => {
focusedWindow?.rpc.emit('session del line end req');
},
'editor:break': (focusedWindow) => {
focusedWindow?.rpc.emit('session break req');
},
'editor:stop': (focusedWindow) => {
focusedWindow?.rpc.emit('session stop req');
},
'editor:quit': (focusedWindow) => {
focusedWindow?.rpc.emit('session quit req');
},
'editor:tmux': (focusedWindow) => {
focusedWindow?.rpc.emit('session tmux req');
},
'editor:search': (focusedWindow) => {
focusedWindow?.rpc.emit('session search');
},
'editor:search-close': (focusedWindow) => {
focusedWindow?.rpc.emit('session search close');
},
'cli:install': () => {
void installCLI(true);
},
'window:hamburgerMenu': () => {
if (process.platform !== 'darwin' && ['', true].includes(getConfig().showHamburgerMenu)) {
Menu.getApplicationMenu()!.popup({x: 25, y: 22});
}
},
'systemContextMenu:add': () => {
systemContextMenu.add();
},
'systemContextMenu:remove': () => {
systemContextMenu.remove();
},
'window:toggleKeepOnTop': (focusedWindow) => {
focusedWindow?.setAlwaysOnTop(!focusedWindow.isAlwaysOnTop());
}
};
//Special numeric command
([1, 2, 3, 4, 5, 6, 7, 8, 'last'] as const).forEach((cmdIndex) => {
const index = cmdIndex === 'last' ? cmdIndex : cmdIndex - 1;
commands[`tab:jump:${cmdIndex}`] = (focusedWindow) => {
focusedWindow?.rpc.emit('move jump req', index);
};
});
export const execCommand = (command: string, focusedWindow?: BrowserWindow) => {
const fn = commands[command];
if (fn) {
fn(focusedWindow);
}
};

View file

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

137
app/config.ts Normal file
View file

@ -0,0 +1,137 @@
import chokidar from 'chokidar';
import notify from './notify';
import {_import, getDefaultConfig} from './config/import';
import _openConfig from './config/open';
import {cfgPath, cfgDir} from './config/paths';
import {getColorMap} from './utils/colors';
import {parsedConfig, configOptions} from '../lib/config';
import {app} from 'electron';
const watchers: Function[] = [];
let cfg: parsedConfig = {} as any;
let _watcher: chokidar.FSWatcher;
export const getDeprecatedCSS = (config: configOptions) => {
const deprecated: string[] = [];
const deprecatedCSS = ['x-screen', 'x-row', 'cursor-node', '::selection'];
deprecatedCSS.forEach((css) => {
if (config.css?.includes(css) || config.termCSS?.includes(css)) {
deprecated.push(css);
}
});
return deprecated;
};
const checkDeprecatedConfig = () => {
if (!cfg.config) {
return;
}
const deprecated = getDeprecatedCSS(cfg.config);
if (deprecated.length === 0) {
return;
}
const deprecatedStr = deprecated.join(', ');
notify('Configuration warning', `Your configuration uses some deprecated CSS classes (${deprecatedStr})`);
};
const _watch = () => {
if (_watcher) {
return;
}
const onChange = () => {
// Need to wait 100ms to ensure that write is complete
setTimeout(() => {
cfg = _import();
notify('Configuration updated', 'Hyper configuration reloaded!');
watchers.forEach((fn) => {
fn();
});
checkDeprecatedConfig();
}, 100);
};
_watcher = chokidar.watch(cfgPath);
_watcher.on('change', onChange);
_watcher.on('error', (error) => {
console.error('error watching config', error);
});
app.on('before-quit', (e) => {
if (Object.keys(_watcher.getWatched()).length > 0) {
e.preventDefault();
_watcher
.close()
.catch((err) => {
console.warn(err);
})
.finally(() => {
app.quit();
});
}
});
};
export const subscribe = (fn: Function) => {
watchers.push(fn);
return () => {
watchers.splice(watchers.indexOf(fn), 1);
};
};
export const getConfigDir = () => {
// expose config directory to load plugin from the right place
return cfgDir;
};
export const getConfig = () => {
return cfg.config;
};
export const openConfig = () => {
return _openConfig();
};
export const getPlugins = (): {plugins: string[]; localPlugins: string[]} => {
return {
plugins: cfg.plugins,
localPlugins: cfg.localPlugins
};
};
export const getKeymaps = () => {
return cfg.keymaps;
};
export const setup = () => {
cfg = _import();
_watch();
checkDeprecatedConfig();
};
export {get as getWin, recordState as winRecord, defaults as windowDefaults} from './config/windows';
export const fixConfigDefaults = (decoratedConfig: configOptions) => {
const defaultConfig = getDefaultConfig().config!;
decoratedConfig.colors = getColorMap(decoratedConfig.colors) || {};
// We must have default colors for xterm css.
decoratedConfig.colors = {...defaultConfig.colors, ...decoratedConfig.colors};
return decoratedConfig;
};
export const htermConfigTranslate = (config: configOptions) => {
const cssReplacements: Record<string, string> = {
'x-screen x-row([ {.[])': '.xterm-rows > div$1',
'.cursor-node([ {.[])': '.terminal-cursor$1',
'::selection([ {.[])': '.terminal .xterm-selection div$1',
'x-screen a([ {.[])': '.terminal a$1',
'x-row a([ {.[])': '.terminal a$1'
};
Object.keys(cssReplacements).forEach((pattern) => {
const searchvalue = new RegExp(pattern, 'g');
const newvalue = cssReplacements[pattern];
config.css = config.css?.replace(searchvalue, newvalue);
config.termCSS = config.termCSS?.replace(searchvalue, newvalue);
});
return config;
};

View file

@ -57,6 +57,9 @@ module.exports = {
// custom CSS to embed in the terminal window // custom CSS to embed in the terminal window
termCSS: '', 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 // if you're using a Linux setup which show native menus, set to false
// default: `true` on Linux, `true` on Windows, ignored on macOS // default: `true` on Linux, `true` on Windows, ignored on macOS
showHamburgerMenu: '', showHamburgerMenu: '',
@ -89,6 +92,8 @@ module.exports = {
lightMagenta: '#FD7CFC', lightMagenta: '#FD7CFC',
lightCyan: '#68FDFE', lightCyan: '#68FDFE',
lightWhite: '#FFFFFF', lightWhite: '#FFFFFF',
limeGreen: '#32CD32',
lightCoral: '#F08080',
}, },
// the shell to run when spawning a new session (i.e. /usr/local/bin/fish) // the shell to run when spawning a new session (i.e. /usr/local/bin/fish)
@ -98,11 +103,17 @@ module.exports = {
// - Make sure to use a full path if the binary name doesn't work // - Make sure to use a full path if the binary name doesn't work
// - Remove `--login` in shellArgs // - Remove `--login` in shellArgs
// //
// Bash on Windows // Windows Subsystem for Linux (WSL) - previously Bash on Windows
// - Example: `C:\\Windows\\System32\\bash.exe` // - Example: `C:\\Windows\\System32\\wsl.exe`
//
// Git-bash on Windows
// - Example: `C:\\Program Files\\Git\\bin\\bash.exe`
// //
// PowerShell on Windows // PowerShell on Windows
// - Example: `C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe` // - Example: `C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`
//
// Cygwin
// - Example: `C:\\cygwin64\\bin\\bash.exe`
shell: '', shell: '',
// for setting shell arguments (i.e. for using interactive shellArgs: `['-i']`) // for setting shell arguments (i.e. for using interactive shellArgs: `['-i']`)
@ -112,9 +123,14 @@ module.exports = {
// for environment variables // for environment variables
env: {}, env: {},
// set to `false` for no bell // Supported Options:
// 1. 'SOUND' -> Enables the bell as a sound
// 2. false: turns off the bell
bell: 'SOUND', 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 // if `true` (without backticks and without quotes), selected text will automatically be copied to the clipboard
copyOnSelect: false, copyOnSelect: false,
@ -130,12 +146,16 @@ module.exports = {
// (inside tmux or vim with mouse mode enabled for example). // (inside tmux or vim with mouse mode enabled for example).
macOptionSelectionMode: 'vertical', macOptionSelectionMode: 'vertical',
// URL to custom bell
// bellSoundURL: 'http://example.com/bell.mp3',
// Whether to use the WebGL renderer. Set it to false to use canvas-based // Whether to use the WebGL renderer. Set it to false to use canvas-based
// rendering (slower, but supports transparent backgrounds) // rendering (slower, but supports transparent backgrounds)
webGLRenderer: true, webGLRenderer: false,
// keypress required for weblink activation: [ctrl|alt|meta|shift]
// todo: does not pick up config changes automatically, need to restart terminal :/
webLinksActivationKey: '',
// if `false` (without backticks and without quotes), Hyper will use ligatures provided by some fonts
disableLigatures: true,
// for advanced config flags please refer to https://hyper.is/#cfg // for advanced config flags please refer to https://hyper.is/#cfg
}, },

View file

@ -1,16 +1,17 @@
const {moveSync, copySync, existsSync, writeFileSync, readFileSync, lstatSync} = require('fs-extra'); import {moveSync, copySync, existsSync, writeFileSync, readFileSync, lstatSync} from 'fs-extra';
const {sync: mkdirpSync} = require('mkdirp'); import {sync as mkdirpSync} from 'mkdirp';
const {defaultCfg, cfgPath, legacyCfgPath, plugs, defaultPlatformKeyPath} = require('./paths'); import {defaultCfg, cfgPath, legacyCfgPath, plugs, defaultPlatformKeyPath} from './paths';
const {_init, _extractDefault} = require('./init'); import {_init, _extractDefault} from './init';
const notify = require('../notify'); import notify from '../notify';
import {rawConfig} from '../../lib/config';
let defaultConfig; let defaultConfig: rawConfig;
const _write = function(path, data) { const _write = (path: string, data: string) => {
// This method will take text formatted as Unix line endings and transform it // 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 // 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. // text editor on Windows (notepad) doesn't Deal with LF files. Still. In 2017.
const crlfify = function(str) { const crlfify = (str: string) => {
return str.replace(/\r?\n/g, '\r\n'); return str.replace(/\r?\n/g, '\r\n');
}; };
const format = process.platform === 'win32' ? crlfify(data.toString()) : data; const format = process.platform === 'win32' ? crlfify(data.toString()) : data;
@ -19,20 +20,15 @@ const _write = function(path, data) {
// Saves a file as backup by appending '.backup' or '.backup2', '.backup3', etc. // Saves a file as backup by appending '.backup' or '.backup2', '.backup3', etc.
// so as to not override any existing files // so as to not override any existing files
const saveAsBackup = src => { const saveAsBackup = (src: string) => {
let attempt = 1; let attempt = 1;
while (attempt < 100) { while (attempt < 100) {
try { const backupPath = `${src}.backup${attempt === 1 ? '' : attempt}`;
const backupPath = src + '.backup' + (attempt === 1 ? '' : attempt); if (!existsSync(backupPath)) {
moveSync(src, backupPath); moveSync(src, backupPath);
return backupPath; return backupPath;
} catch (e) {
if (e.code === 'EEXIST') {
attempt++;
} else {
throw e;
}
} }
attempt++;
} }
throw new Error('Failed to create backup for config file. Too many backups'); throw new Error('Failed to create backup for config file. Too many backups');
}; };
@ -81,7 +77,7 @@ const migrateHyper2Config = () => {
); );
}; };
const _importConf = function() { const _importConf = () => {
// init plugin directories if not present // init plugin directories if not present
mkdirpSync(plugs.base); mkdirpSync(plugs.base);
mkdirpSync(plugs.local); mkdirpSync(plugs.local);
@ -89,47 +85,49 @@ const _importConf = function() {
try { try {
migrateHyper2Config(); migrateHyper2Config();
} catch (err) { } catch (err) {
//eslint-disable-next-line no-console
console.error(err); console.error(err);
} }
let defaultCfgRaw = '';
try { try {
const defaultCfgRaw = readFileSync(defaultCfg, 'utf8'); defaultCfgRaw = readFileSync(defaultCfg, 'utf8');
const _defaultCfg = _extractDefault(defaultCfgRaw);
// Importing platform specific keymap
try {
const content = readFileSync(defaultPlatformKeyPath(), 'utf8');
const mapping = JSON.parse(content);
_defaultCfg.keymaps = mapping;
} catch (err) {
//eslint-disable-next-line no-console
console.error(err);
}
// Import user config
try {
const userCfg = readFileSync(cfgPath, 'utf8');
return {userCfg, defaultCfg: _defaultCfg};
} catch (err) {
_write(cfgPath, defaultCfgRaw);
return {userCfg: defaultCfgRaw, defaultCfg: _defaultCfg};
}
} catch (err) { } catch (err) {
//eslint-disable-next-line no-console
console.log(err); console.log(err);
} }
const _defaultCfg = _extractDefault(defaultCfgRaw) as rawConfig;
// Importing platform specific keymap
let content = '{}';
try {
content = readFileSync(defaultPlatformKeyPath(), 'utf8');
} catch (err) {
console.error(err);
}
const mapping = JSON.parse(content) as Record<string, string | string[]>;
_defaultCfg.keymaps = mapping;
// Import user config
let userCfg: string;
try {
userCfg = readFileSync(cfgPath, 'utf8');
} catch (err) {
_write(cfgPath, defaultCfgRaw);
userCfg = defaultCfgRaw;
}
return {userCfg, defaultCfg: _defaultCfg};
}; };
exports._import = () => { export const _import = () => {
const imported = _importConf(); const imported = _importConf();
defaultConfig = imported.defaultCfg; defaultConfig = imported.defaultCfg;
const result = _init(imported); const result = _init(imported);
return result; return result;
}; };
exports.getDefaultConfig = () => { export const getDefaultConfig = () => {
if (!defaultConfig) { if (!defaultConfig) {
defaultConfig = _extractDefault(_importConf().defaultCfg); defaultConfig = _importConf().defaultCfg;
} }
return defaultConfig; return defaultConfig;
}; };

View file

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

49
app/config/init.ts Normal file
View file

@ -0,0 +1,49 @@
import vm from 'vm';
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});
if (!module.exports) {
throw new Error('Error reading configuration: `module.exports` not set');
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return module.exports;
};
const _syntaxValidation = (cfg: string) => {
try {
return new vm.Script(cfg, {filename: '.hyper.js', displayErrors: true});
} catch (err) {
notify(`Error loading config: ${err.name}`, `${err}`, {error: err});
}
};
const _extractDefault = (cfg: string) => {
return _extract(_syntaxValidation(cfg));
};
// init config
const _init = (cfg: {userCfg: string; defaultCfg: rawConfig}): parsedConfig => {
const script = _syntaxValidation(cfg.userCfg);
const _cfg = script && (_extract(script) as rawConfig);
return {
config: (() => {
if (_cfg?.config) {
return _cfg.config;
} else {
notify('Error reading configuration: `config` key is missing');
return cfg.defaultCfg.config || ({} as configOptions);
}
})(),
// Merging platform specific keymaps with user defined keymaps
keymaps: mapKeys({...cfg.defaultCfg.keymaps, ..._cfg?.keymaps}),
// Ignore undefined values in plugin and localPlugins array Issue #1862
plugins: (_cfg?.plugins && _cfg.plugins.filter(Boolean)) || [],
localPlugins: (_cfg?.localPlugins && _cfg.localPlugins.filter(Boolean)) || []
};
};
export {_init, _extractDefault};

View file

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

75
app/config/open.ts Normal file
View file

@ -0,0 +1,75 @@
import {shell} from 'electron';
import {cfgPath} from './paths';
import * as Registry from 'native-reg';
import {exec} from 'child_process';
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);
});
});
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');
} catch (err) {
console.error('Open config with default app error:', err);
}
return openNotepad(cfgPath);
}
return shell.openPath(cfgPath).then((error) => error === '');
};

View file

@ -1,9 +1,9 @@
// This module exports paths, names, and other metadata that is referenced // This module exports paths, names, and other metadata that is referenced
const {homedir} = require('os'); import {homedir} from 'os';
const {app} = require('electron'); import {app} from 'electron';
const {statSync} = require('fs'); import {statSync} from 'fs';
const {resolve, join} = require('path'); import {resolve, join} from 'path';
const isDev = require('electron-is-dev'); import isDev from 'electron-is-dev';
const cfgFile = '.hyper.js'; const cfgFile = '.hyper.js';
const defaultCfgFile = 'config-default.js'; const defaultCfgFile = 'config-default.js';
@ -14,11 +14,13 @@ const homeDirectory = homedir();
const applicationDirectory = const applicationDirectory =
process.env.XDG_CONFIG_HOME !== undefined process.env.XDG_CONFIG_HOME !== undefined
? join(process.env.XDG_CONFIG_HOME, 'hyper') ? join(process.env.XDG_CONFIG_HOME, 'hyper')
: process.platform == 'win32' ? app.getPath('userData') : homedir(); : process.platform == 'win32'
? app.getPath('userData')
: homedir();
let cfgDir = applicationDirectory; let cfgDir = applicationDirectory;
let cfgPath = join(applicationDirectory, cfgFile); let cfgPath = join(applicationDirectory, cfgFile);
let legacyCfgPath = join(homeDirectory, cfgFile); // Hyper 2 config location const legacyCfgPath = join(homeDirectory, cfgFile); // Hyper 2 config location
const devDir = resolve(__dirname, '../..'); const devDir = resolve(__dirname, '../..');
const devCfg = join(devDir, cfgFile); const devCfg = join(devDir, cfgFile);
@ -30,7 +32,6 @@ if (isDev) {
statSync(devCfg); statSync(devCfg);
cfgPath = devCfg; cfgPath = devCfg;
cfgDir = devDir; cfgDir = devDir;
//eslint-disable-next-line no-console
console.log('using config file:', cfgPath); console.log('using config file:', cfgPath);
} catch (err) { } catch (err) {
// ignore // ignore
@ -69,7 +70,7 @@ const defaultPlatformKeyPath = () => {
} }
}; };
module.exports = { export {
cfgDir, cfgDir,
cfgPath, cfgPath,
legacyCfgPath, legacyCfgPath,

View file

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

20
app/config/windows.ts Normal file
View file

@ -0,0 +1,20 @@
import Config from 'electron-store';
import {BrowserWindow} from 'electron';
export const defaults = {
windowPosition: [50, 50] as [number, number],
windowSize: [540, 380] as [number, number]
};
// local storage
const cfg = new Config({defaults});
export function get() {
const position = cfg.get('windowPosition', defaults.windowPosition);
const size = cfg.get('windowSize', defaults.windowSize);
return {position, size};
}
export function recordState(win: BrowserWindow) {
cfg.set('windowPosition', win.getPosition());
cfg.set('windowSize', win.getSize());
}

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

25
app/extend-electron.d.ts vendored Normal file
View file

@ -0,0 +1,25 @@
import type {Server} from './rpc';
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?: {size?: [number, number]; position?: [number, number]}
) => BrowserWindow;
setVersion: (version: string) => void;
}
// type Server = import('./rpc').Server;
interface BrowserWindow {
uid: string;
sessions: Map<any, any>;
focusTime: number;
clean: () => void;
rpc: Server;
}
}

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

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

View file

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

View file

@ -30,8 +30,8 @@
"tab:jump:prefix": "command", "tab:jump:prefix": "command",
"pane:next": "command+]", "pane:next": "command+]",
"pane:prev": "command+[", "pane:prev": "command+[",
"pane:splitVertical": "command+d", "pane:splitRight": "command+d",
"pane:splitHorizontal": "command+shift+d", "pane:splitDown": "command+shift+d",
"pane:close": "command+w", "pane:close": "command+w",
"editor:undo": "command+z", "editor:undo": "command+z",
"editor:redo": "command+y", "editor:redo": "command+y",
@ -39,6 +39,8 @@
"editor:copy": "command+c", "editor:copy": "command+c",
"editor:paste": "command+v", "editor:paste": "command+v",
"editor:selectAll": "command+a", "editor:selectAll": "command+a",
"editor:search": "command+f",
"editor:search-close": "esc",
"editor:movePreviousWord": "alt+left", "editor:movePreviousWord": "alt+left",
"editor:moveNextWord": "alt+right", "editor:moveNextWord": "alt+right",
"editor:moveBeginningLine": "command+left", "editor:moveBeginningLine": "command+left",

View file

@ -3,6 +3,7 @@
"window:reload": "ctrl+shift+r", "window:reload": "ctrl+shift+r",
"window:reloadFull": "ctrl+shift+f5", "window:reloadFull": "ctrl+shift+f5",
"window:preferences": "ctrl+,", "window:preferences": "ctrl+,",
"window:hamburgerMenu": "alt",
"zoom:reset": "ctrl+0", "zoom:reset": "ctrl+0",
"zoom:in": "ctrl+=", "zoom:in": "ctrl+=",
"zoom:out": "ctrl+-", "zoom:out": "ctrl+-",
@ -27,8 +28,8 @@
"tab:jump:prefix": "ctrl", "tab:jump:prefix": "ctrl",
"pane:next": "ctrl+pageup", "pane:next": "ctrl+pageup",
"pane:prev": "ctrl+pagedown", "pane:prev": "ctrl+pagedown",
"pane:splitVertical": "ctrl+shift+d", "pane:splitRight": "ctrl+shift+d",
"pane:splitHorizontal": "ctrl+shift+e", "pane:splitDown": "ctrl+shift+e",
"pane:close": "ctrl+shift+w", "pane:close": "ctrl+shift+w",
"editor:undo": "ctrl+shift+z", "editor:undo": "ctrl+shift+z",
"editor:redo": "ctrl+shift+y", "editor:redo": "ctrl+shift+y",
@ -36,6 +37,8 @@
"editor:copy": "ctrl+shift+c", "editor:copy": "ctrl+shift+c",
"editor:paste": "ctrl+shift+v", "editor:paste": "ctrl+shift+v",
"editor:selectAll": "ctrl+shift+a", "editor:selectAll": "ctrl+shift+a",
"editor:search": "ctrl+shift+f",
"editor:search-close": "esc",
"editor:movePreviousWord": "ctrl+left", "editor:movePreviousWord": "ctrl+left",
"editor:moveNextWord": "ctrl+right", "editor:moveNextWord": "ctrl+right",
"editor:moveBeginningLine": "home", "editor:moveBeginningLine": "home",

View file

@ -16,23 +16,11 @@
"alt+f4" "alt+f4"
], ],
"tab:new": "ctrl+shift+t", "tab:new": "ctrl+shift+t",
"tab:next": [
"ctrl+shift+]",
"ctrl+shift+right",
"ctrl+alt+right",
"ctrl+tab"
],
"tab:prev": [
"ctrl+shift+[",
"ctrl+shift+left",
"ctrl+alt+left",
"ctrl+shift+tab"
],
"tab:jump:prefix": "ctrl", "tab:jump:prefix": "ctrl",
"pane:next": "ctrl+pageup", "pane:next": "ctrl+pageup",
"pane:prev": "ctrl+pagedown", "pane:prev": "ctrl+pagedown",
"pane:splitVertical": "ctrl+shift+d", "pane:splitRight": "ctrl+shift+d",
"pane:splitHorizontal": "ctrl+shift+e", "pane:splitDown": "ctrl+shift+e",
"pane:close": "ctrl+shift+w", "pane:close": "ctrl+shift+w",
"editor:undo": "ctrl+shift+z", "editor:undo": "ctrl+shift+z",
"editor:redo": "ctrl+shift+y", "editor:redo": "ctrl+shift+y",
@ -40,8 +28,10 @@
"editor:copy": "ctrl+shift+c", "editor:copy": "ctrl+shift+c",
"editor:paste": "ctrl+shift+v", "editor:paste": "ctrl+shift+v",
"editor:selectAll": "ctrl+shift+a", "editor:selectAll": "ctrl+shift+a",
"editor:movePreviousWord": "ctrl+left", "editor:search": "ctrl+shift+f",
"editor:moveNextWord": "ctrl+right", "editor:search-close": "esc",
"editor:movePreviousWord": "",
"editor:moveNextWord": "",
"editor:moveBeginningLine": "Home", "editor:moveBeginningLine": "Home",
"editor:moveEndLine": "End", "editor:moveEndLine": "End",
"editor:deletePreviousWord": "ctrl+backspace", "editor:deletePreviousWord": "ctrl+backspace",

View file

@ -1,46 +1,49 @@
// Packages // Packages
const {app, dialog, Menu} = require('electron'); import {app, dialog, Menu, BrowserWindow} from 'electron';
// Utilities // Utilities
const {getConfig} = require('../config'); import {getConfig} from '../config';
const {icon} = require('../config/paths'); import {icon} from '../config/paths';
const viewMenu = require('./menus/view'); import viewMenu from './menus/view';
const shellMenu = require('./menus/shell'); import shellMenu from './menus/shell';
const editMenu = require('./menus/edit'); import editMenu from './menus/edit';
const pluginsMenu = require('./menus/plugins'); import toolsMenu from './menus/tools';
const windowMenu = require('./menus/window'); import windowMenu from './menus/window';
const helpMenu = require('./menus/help'); import helpMenu from './menus/help';
const darwinMenu = require('./menus/darwin'); import darwinMenu from './menus/darwin';
const {getDecoratedKeymaps} = require('../plugins'); import {getDecoratedKeymaps} from '../plugins';
const {execCommand} = require('../commands'); import {execCommand} from '../commands';
const {getRendererTypes} = require('../utils/renderer-utils'); import {getRendererTypes} from '../utils/renderer-utils';
const appName = app.getName(); const appName = app.name;
const appVersion = app.getVersion(); const appVersion = app.getVersion();
let menu_ = []; let menu_: Menu;
exports.createMenu = (createWindow, getLoadedPluginVersions) => { export const createMenu = (
createWindow: (fn?: (win: BrowserWindow) => void, options?: Record<string, any>) => BrowserWindow,
getLoadedPluginVersions: () => {name: string; version: string}[]
) => {
const config = getConfig(); const config = getConfig();
// We take only first shortcut in array for each command // We take only first shortcut in array for each command
const allCommandKeys = getDecoratedKeymaps(); const allCommandKeys = getDecoratedKeymaps();
const commandKeys = Object.keys(allCommandKeys).reduce((result, command) => { const commandKeys = Object.keys(allCommandKeys).reduce((result: Record<string, string>, command) => {
result[command] = allCommandKeys[command][0]; result[command] = allCommandKeys[command][0];
return result; return result;
}, {}); }, {});
let updateChannel = 'stable'; let updateChannel = 'stable';
if (config && config.updateChannel && config.updateChannel === 'canary') { if (config?.updateChannel && config.updateChannel === 'canary') {
updateChannel = 'canary'; updateChannel = 'canary';
} }
const showAbout = () => { const showAbout = () => {
const loadedPlugins = getLoadedPluginVersions(); const loadedPlugins = getLoadedPluginVersions();
const pluginList = const pluginList =
loadedPlugins.length === 0 ? 'none' : loadedPlugins.map(plugin => `\n ${plugin.name} (${plugin.version})`); loadedPlugins.length === 0 ? 'none' : loadedPlugins.map((plugin) => `\n ${plugin.name} (${plugin.version})`);
const rendererCounts = Object.values(getRendererTypes()).reduce((acc, type) => { const rendererCounts = Object.values(getRendererTypes()).reduce((acc: Record<string, number>, type) => {
acc[type] = acc[type] ? acc[type] + 1 : 1; acc[type] = acc[type] ? acc[type] + 1 : 1;
return acc; return acc;
}, {}); }, {});
@ -48,12 +51,12 @@ exports.createMenu = (createWindow, getLoadedPluginVersions) => {
.map(([type, count]) => type + (count > 1 ? ` (${count})` : '')) .map(([type, count]) => type + (count > 1 ? ` (${count})` : ''))
.join(', '); .join(', ');
dialog.showMessageBox({ void dialog.showMessageBox({
title: `About ${appName}`, title: `About ${appName}`,
message: `${appName} ${appVersion} (${updateChannel})`, message: `${appName} ${appVersion} (${updateChannel})`,
detail: `Renderers: ${renderers}\nPlugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright © 2019 ZEIT, Inc.`, detail: `Renderers: ${renderers}\nPlugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright © 2020 Vercel, Inc.`,
buttons: [], buttons: [],
icon icon: icon as any
}); });
}; };
const menu = [ const menu = [
@ -61,7 +64,7 @@ exports.createMenu = (createWindow, getLoadedPluginVersions) => {
shellMenu(commandKeys, execCommand), shellMenu(commandKeys, execCommand),
editMenu(commandKeys, execCommand), editMenu(commandKeys, execCommand),
viewMenu(commandKeys, execCommand), viewMenu(commandKeys, execCommand),
pluginsMenu(commandKeys, execCommand), toolsMenu(commandKeys, execCommand),
windowMenu(commandKeys, execCommand), windowMenu(commandKeys, execCommand),
helpMenu(commandKeys, showAbout) helpMenu(commandKeys, showAbout)
]; ];
@ -69,7 +72,7 @@ exports.createMenu = (createWindow, getLoadedPluginVersions) => {
return menu; return menu;
}; };
exports.buildMenu = template => { export const buildMenu = (template: Electron.MenuItemConstructorOptions[]): Electron.Menu => {
menu_ = Menu.buildFromTemplate(template); menu_ = Menu.buildFromTemplate(template);
return menu_; return menu_;
}; };

View file

@ -1,10 +1,14 @@
// This menu label is overrided by OSX to be the appName // This menu label is overrided by OSX to be the appName
// The label is set to appName here so it matches actual behavior // The label is set to appName here so it matches actual behavior
const {app} = require('electron'); import {app, BrowserWindow, MenuItemConstructorOptions} from 'electron';
module.exports = (commandKeys, execCommand, showAbout) => { export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void,
showAbout: () => void
): MenuItemConstructorOptions => {
return { return {
label: `${app.getName()}`, label: `${app.name}`,
submenu: [ submenu: [
{ {
label: 'About Hyper', label: 'About Hyper',
@ -36,7 +40,7 @@ module.exports = (commandKeys, execCommand, showAbout) => {
role: 'hide' role: 'hide'
}, },
{ {
role: 'hideothers' role: 'hideOthers'
}, },
{ {
role: 'unhide' role: 'unhide'

View file

@ -1,5 +1,10 @@
module.exports = (commandKeys, execCommand) => { import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
const submenu = [
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
) => {
const submenu: MenuItemConstructorOptions[] = [
{ {
label: 'Undo', label: 'Undo',
accelerator: commandKeys['editor:undo'], accelerator: commandKeys['editor:undo'],
@ -23,7 +28,7 @@ module.exports = (commandKeys, execCommand) => {
command: 'editor:copy', command: 'editor:copy',
accelerator: commandKeys['editor:copy'], accelerator: commandKeys['editor:copy'],
registerAccelerator: true registerAccelerator: true
}, } as any,
{ {
role: 'paste', role: 'paste',
accelerator: commandKeys['editor:paste'] accelerator: commandKeys['editor:paste']
@ -113,6 +118,13 @@ module.exports = (commandKeys, execCommand) => {
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('editor:clearBuffer', focusedWindow); execCommand('editor:clearBuffer', focusedWindow);
} }
},
{
label: 'Search',
accelerator: commandKeys['editor:search'],
click(item, focusedWindow) {
execCommand('editor:search', focusedWindow);
}
} }
]; ];

View file

@ -1,83 +0,0 @@
const {release} = require('os');
const {app, shell} = require('electron');
const {getConfig, getPlugins} = require('../../config');
const {arch, env, platform, versions} = process;
const {version} = require('../../package.json');
module.exports = (commands, showAbout) => {
const submenu = [
{
label: `${app.getName()} Website`,
click() {
shell.openExternal('https://hyper.is');
}
},
{
label: 'Report Issue',
click() {
const body = `
<!--
Hi there! Thank you for discovering and submitting an issue.
Before you submit this; let's make sure of a few things.
Please make sure the following boxes are if they are correct.
If not, please try and fulfil these first.
-->
<!-- 👉 Checked checkbox should look like this: [x] -->
- [ ] Your Hyper.app version is **${version}**. Please verify your using the [latest](https://github.com/zeit/hyper/releases/latest) Hyper.app version
- [ ] I have searched the [issues](https://github.com/zeit/hyper/issues) of this repo and believe that this is not a duplicate
---
- **Any relevant information from devtools?** _(CMD+ALT+I on macOS, CTRL+SHIFT+I elsewhere)_:
<!-- 👉 Replace with info if applicable, or N/A -->
- **Is the issue reproducible in vanilla Hyper.app?**
<!-- 👉 Replace with info if applicable, or Is Vanilla. (Vanilla means Hyper.app without any add-ons or extras. Straight out of the box.) -->
## Issue
<!-- 👉 Now feel free to write your issue, but please be descriptive! Thanks again 🙌 -->
<!-- ~/.hyper.js config -->
- **${app.getName()} version**: ${env.TERM_PROGRAM_VERSION} "${app.getVersion()}"
- **OS ARCH VERSION:** ${platform} ${arch} ${release()}
- **Electron:** ${versions.electron} **LANG:** ${env.LANG}
- **SHELL:** ${env.SHELL} **TERM:** ${env.TERM}
<details>
<summary><strong> ~/.hyper.js contents</strong></summary>
<pre>
<code>
${JSON.stringify(getConfig(), null, 2)}
${JSON.stringify(getPlugins(), null, 2)}
</code>
</pre>
</details>`;
shell.openExternal(`https://github.com/zeit/hyper/issues/new?body=${encodeURIComponent(body)}`);
}
}
];
if (process.platform !== 'darwin') {
submenu.push(
{type: 'separator'},
{
role: 'about',
click() {
showAbout();
}
}
);
}
return {
role: 'help',
submenu
};
};

108
app/menus/menus/help.ts Normal file
View file

@ -0,0 +1,108 @@
import {release} from 'os';
import {app, shell, MenuItemConstructorOptions, dialog, clipboard} from 'electron';
import {getConfig, getPlugins} from '../../config';
const {arch, env, platform, versions} = process;
import {version} from '../../package.json';
export default (commands: Record<string, string>, showAbout: () => void): MenuItemConstructorOptions => {
const submenu: MenuItemConstructorOptions[] = [
{
label: `${app.name} Website`,
click() {
void shell.openExternal('https://hyper.is');
}
},
{
label: 'Report Issue',
click(menuItem, focusedWindow) {
const body = `<!--
Hi there! Thank you for discovering and submitting an issue.
Before you submit this; let's make sure of a few things.
Please make sure the following boxes are if they are correct.
If not, please try and fulfil these first.
-->
<!-- 👉 Checked checkbox should look like this: [x] -->
- [ ] Your Hyper.app version is **${version}**. Please verify your using the [latest](https://github.com/vercel/hyper/releases/latest) Hyper.app version
- [ ] I have searched the [issues](https://github.com/vercel/hyper/issues) of this repo and believe that this is not a duplicate
---
- **Any relevant information from devtools?** _(CMD+OPTION+I on macOS, CTRL+SHIFT+I elsewhere)_:
<!-- 👉 Replace with info if applicable, or N/A -->
- **Is the issue reproducible in vanilla Hyper.app?**
<!-- 👉 Replace with info if applicable, or Is Vanilla. (Vanilla means Hyper.app without any add-ons or extras. Straight out of the box.) -->
## Issue
<!-- 👉 Now feel free to write your issue, but please be descriptive! Thanks again 🙌 ❤️ -->
---
<!-- ~/.hyper.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.js contents</strong></summary>
\`\`\`json
${JSON.stringify(getConfig(), null, 2)}
\`\`\`
</details>
<details><summary><strong>plugins</strong></summary>
\`\`\`json
${JSON.stringify(getPlugins(), null, 2)}
\`\`\`
</details>`;
const issueURL = `https://github.com/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
.showMessageBox(focusedWindow, {
message:
'There is too much data to send to GitHub directly. The data will be copied to the clipboard, ' +
'please paste it into the GitHub issue page that will open.',
type: 'warning',
buttons: ['OK', 'Cancel']
})
.then((result) => {
if (result.response === 0) {
copyAndSend();
}
});
} else {
void shell.openExternal(issueURL);
}
}
}
];
if (process.platform !== 'darwin') {
submenu.push(
{type: 'separator'},
{
label: 'About Hyper',
click() {
showAbout();
}
}
);
}
return {
role: 'help',
submenu
};
};

View file

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

View file

@ -1,4 +1,9 @@
module.exports = (commandKeys, execCommand) => { import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
const isMac = process.platform === 'darwin'; const isMac = process.platform === 'darwin';
return { return {
@ -22,17 +27,17 @@ module.exports = (commandKeys, execCommand) => {
type: 'separator' type: 'separator'
}, },
{ {
label: 'Split Horizontally', label: 'Split Down',
accelerator: commandKeys['pane:splitHorizontal'], accelerator: commandKeys['pane:splitDown'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('pane:splitHorizontal', focusedWindow); execCommand('pane:splitDown', focusedWindow);
} }
}, },
{ {
label: 'Split Vertically', label: 'Split Right',
accelerator: commandKeys['pane:splitVertical'], accelerator: commandKeys['pane:splitRight'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('pane:splitVertical', focusedWindow); execCommand('pane:splitRight', focusedWindow);
} }
}, },
{ {

47
app/menus/menus/tools.ts Normal file
View file

@ -0,0 +1,47 @@
import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
export default (
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'
}
]
: [])
]
};
};

View file

@ -1,4 +1,9 @@
module.exports = (commandKeys, execCommand) => { import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
return { return {
label: 'View', label: 'View',
submenu: [ submenu: [

View file

@ -1,11 +1,16 @@
module.exports = (commandKeys, execCommand) => { import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => {
// Generating tab:jump array // Generating tab:jump array
const tabJump = []; const tabJump = [];
for (let i = 1; i <= 9; i++) { for (let i = 1; i <= 9; i++) {
// 9 is a special number because it means 'last' // 9 is a special number because it means 'last'
const label = i === 9 ? 'Last' : `${i}`; const label = i === 9 ? 'Last' : `${i}`;
tabJump.push({ tabJump.push({
label: label, label,
accelerator: commandKeys[`tab:jump:${label.toLowerCase()}`] accelerator: commandKeys[`tab:jump:${label.toLowerCase()}`]
}); });
} }
@ -76,6 +81,12 @@ module.exports = (commandKeys, execCommand) => {
{ {
role: 'front' role: 'front'
}, },
{
label: 'Toggle Always on Top',
click: (item, focusedWindow) => {
execCommand('window:toggleKeepOnTop', focusedWindow);
}
},
{ {
role: 'togglefullscreen', role: 'togglefullscreen',
accelerator: commandKeys['window:toggleFullScreen'] accelerator: commandKeys['window:toggleFullScreen']

View file

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

View file

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

View file

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

20
app/notify.ts Normal file
View file

@ -0,0 +1,20 @@
import {app, Notification} from 'electron';
import {icon} from './config/paths';
export default function notify(title: string, body = '', details: {error?: any} = {}) {
console.log(`[Notification] ${title}: ${body}`);
if (details.error) {
console.error(details.error);
}
if (app.isReady()) {
_createNotification(title, body);
} else {
app.on('ready', () => {
_createNotification(title, body);
});
}
}
const _createNotification = (title: string, body: string) => {
new Notification({title, body, ...(process.platform === 'linux' && {icon})}).show();
};

View file

@ -10,29 +10,33 @@
}, },
"repository": "zeit/hyper", "repository": "zeit/hyper",
"dependencies": { "dependencies": {
"async-retry": "1.1.4", "async-retry": "1.3.1",
"color": "2.0.1", "chokidar": "^3.5.2",
"color": "3.1.3",
"convert-css-color-name-to-hex": "0.1.1", "convert-css-color-name-to-hex": "0.1.1",
"default-shell": "1.0.1", "default-shell": "1.0.1",
"electron-config": "2.0.0", "electron-fetch": "1.7.3",
"electron-fetch": "1.3.0", "electron-is-dev": "2.0.0",
"electron-is-dev": "1.0.1", "electron-store": "8.0.0",
"electron-squirrel-startup": "1.0.0", "file-uri-to-path": "2.0.0",
"file-uri-to-path": "1.0.0", "fs-extra": "10.0.0",
"fs-extra": "7.0.1", "git-describe": "4.0.4",
"git-describe": "4.0.2", "lodash": "4.17.21",
"lodash": "4.17.5", "mkdirp": "1.0.4",
"mkdirp": "0.5.1", "ms": "2.1.3",
"ms": "2.1.1", "node-pty": "0.10.1",
"node-pty": "0.8.0", "os-locale": "5.0.0",
"os-locale": "3.1.0", "parse-url": "5.0.7",
"parse-url": "3.0.2", "pify": "5.0.0",
"queue": "4.4.2", "queue": "6.0.2",
"react": "16.2.0", "react": "17.0.2",
"react-dom": "16.2.1", "react-dom": "17.0.2",
"semver": "5.5.0", "semver": "7.3.5",
"shell-env": "0.3.0", "shell-env": "3.0.1",
"uuid": "3.2.1", "sudo-prompt": "^9.2.1",
"winreg": "1.2.4" "uuid": "8.3.2"
},
"optionalDependencies": {
"native-reg": "0.3.5"
} }
} }

View file

@ -1,18 +1,20 @@
const {app, dialog} = require('electron'); /* eslint-disable eslint-comments/disable-enable-pair */
const {resolve, basename} = require('path'); /* eslint-disable @typescript-eslint/no-unsafe-return */
const {writeFileSync} = require('fs'); /* eslint-disable @typescript-eslint/no-unsafe-call */
const Config = require('electron-config'); import {app, dialog, BrowserWindow, App} from 'electron';
const ms = require('ms'); import {resolve, basename} from 'path';
import {writeFileSync} from 'fs';
const React = require('react'); import Config from 'electron-store';
const ReactDom = require('react-dom'); import ms from 'ms';
import React from 'react';
const config = require('./config'); import ReactDom from 'react-dom';
const notify = require('./notify'); import * as config from './config';
const {availableExtensions} = require('./plugins/extensions'); import notify from './notify';
const {install} = require('./plugins/install'); import {availableExtensions} from './plugins/extensions';
const {plugs} = require('./config/paths'); import {install} from './plugins/install';
const mapKeys = require('./utils/map-keys'); import {plugs} from './config/paths';
import mapKeys from './utils/map-keys';
import {configOptions} from '../lib/config';
// local storage // local storage
const cache = new Config(); const cache = new Config();
@ -28,11 +30,11 @@ let paths = getPaths();
let id = getId(plugins); let id = getId(plugins);
let modules = requirePlugins(); let modules = requirePlugins();
function getId(plugins_) { function getId(plugins_: any) {
return JSON.stringify(plugins_); return JSON.stringify(plugins_);
} }
const watchers = []; const watchers: Function[] = [];
// we listen on configuration updates to trigger // we listen on configuration updates to trigger
// plugin installation // plugin installation
@ -50,11 +52,12 @@ config.subscribe(() => {
// patching Module._load // patching Module._load
// so plugins can `require` them without needing their own version // so plugins can `require` them without needing their own version
// https://github.com/zeit/hyper/issues/619 // https://github.com/vercel/hyper/issues/619
function patchModuleLoad() { function patchModuleLoad() {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Module = require('module'); const Module = require('module');
const originalLoad = Module._load; const originalLoad = Module._load;
Module._load = function _load(modulePath) { Module._load = function _load(modulePath: string) {
// PLEASE NOTE: Code changes here, also need to be changed in // PLEASE NOTE: Code changes here, also need to be changed in
// lib/utils/plugins.js // lib/utils/plugins.js
switch (modulePath) { switch (modulePath) {
@ -74,13 +77,14 @@ function patchModuleLoad() {
case 'hyper/decorate': case 'hyper/decorate':
return Object; return Object;
default: default:
// eslint-disable-next-line prefer-rest-params
return originalLoad.apply(this, arguments); return originalLoad.apply(this, arguments);
} }
}; };
} }
function checkDeprecatedExtendKeymaps() { function checkDeprecatedExtendKeymaps() {
modules.forEach(plugin => { modules.forEach((plugin) => {
if (plugin.extendKeymaps) { if (plugin.extendKeymaps) {
notify('Plugin warning!', `"${plugin._name}" use deprecated "extendKeymaps" handler`); notify('Plugin warning!', `"${plugin._name}" use deprecated "extendKeymaps" handler`);
return; return;
@ -97,11 +101,10 @@ function updatePlugins({force = false} = {}) {
updating = true; updating = true;
syncPackageJSON(); syncPackageJSON();
const id_ = id; const id_ = id;
install(err => { install((err) => {
updating = false; updating = false;
if (err) { if (err) {
//eslint-disable-next-line no-console
notify('Error updating plugins.', err, {error: err}); notify('Error updating plugins.', err, {error: err});
} else { } else {
// flag successful plugin update // flag successful plugin update
@ -123,7 +126,9 @@ function updatePlugins({force = false} = {}) {
cache.set('hyper.plugin-versions', pluginVersions); cache.set('hyper.plugin-versions', pluginVersions);
// notify watchers // notify watchers
watchers.forEach(fn => fn(err, {force})); watchers.forEach((fn) => {
fn(err, {force});
});
if (force || changed) { if (force || changed) {
if (changed) { if (changed) {
@ -139,10 +144,10 @@ function updatePlugins({force = false} = {}) {
function getPluginVersions() { function getPluginVersions() {
const paths_ = paths.plugins.concat(paths.localPlugins); const paths_ = paths.plugins.concat(paths.localPlugins);
return paths_.map(path_ => { return paths_.map((path_) => {
let version = null; let version: string | null = null;
try { try {
//eslint-disable-next-line import/no-dynamic-require // eslint-disable-next-line @typescript-eslint/no-var-requires
version = require(resolve(path_, 'package.json')).version; version = require(resolve(path_, 'package.json')).version;
//eslint-disable-next-line no-empty //eslint-disable-next-line no-empty
} catch (err) {} } catch (err) {}
@ -152,7 +157,7 @@ function getPluginVersions() {
function clearCache() { function clearCache() {
// trigger unload hooks // trigger unload hooks
modules.forEach(mod => { modules.forEach((mod) => {
if (mod.onUnload) { if (mod.onUnload) {
mod.onUnload(app); mod.onUnload(app);
} }
@ -166,10 +171,10 @@ function clearCache() {
} }
} }
exports.updatePlugins = updatePlugins; export {updatePlugins};
exports.getLoadedPluginVersions = () => { export const getLoadedPluginVersions = () => {
return modules.map(mod => ({name: mod._name, version: mod._version})); return modules.map((mod) => ({name: mod._name, version: mod._version}));
}; };
// we schedule the initial plugins update // we schedule the initial plugins update
@ -177,15 +182,19 @@ exports.getLoadedPluginVersions = () => {
// to prevent slowness // to prevent slowness
if (cache.get('hyper.plugins') !== id || process.env.HYPER_FORCE_UPDATE) { if (cache.get('hyper.plugins') !== id || process.env.HYPER_FORCE_UPDATE) {
// install immediately if the user changed plugins // install immediately if the user changed plugins
//eslint-disable-next-line no-console
console.log('plugins have changed / not init, scheduling plugins installation'); console.log('plugins have changed / not init, scheduling plugins installation');
setTimeout(() => { setTimeout(() => {
updatePlugins(); updatePlugins();
}, 1000); }, 1000);
} }
// otherwise update plugins every 5 hours (() => {
setInterval(updatePlugins, ms('5h')); const baseConfig = config.getConfig();
if (baseConfig['autoUpdatePlugins']) {
// otherwise update plugins every 5 hours
setInterval(updatePlugins, ms(baseConfig['autoUpdatePlugins'] === true ? '5h' : baseConfig['autoUpdatePlugins']));
}
})();
function syncPackageJSON() { function syncPackageJSON() {
const dependencies = toDependencies(plugins); const dependencies = toDependencies(plugins);
@ -194,7 +203,7 @@ function syncPackageJSON() {
description: 'Auto-generated from `~/.hyper.js`!', description: 'Auto-generated from `~/.hyper.js`!',
private: true, private: true,
version: '0.0.1', version: '0.0.1',
repository: 'zeit/hyper', repository: 'vercel/hyper',
license: 'MIT', license: 'MIT',
homepage: 'https://hyper.is', homepage: 'https://hyper.is',
dependencies dependencies
@ -208,16 +217,16 @@ function syncPackageJSON() {
} }
} }
function alert(message) { function alert(message: string) {
dialog.showMessageBox({ void dialog.showMessageBox({
message, message,
buttons: ['Ok'] buttons: ['Ok']
}); });
} }
function toDependencies(plugins_) { function toDependencies(plugins_: {plugins: string[]}) {
const obj = {}; const obj: Record<string, string> = {};
plugins_.plugins.forEach(plugin => { plugins_.plugins.forEach((plugin) => {
const regex = /.(@|#)/; const regex = /.(@|#)/;
const match = regex.exec(plugin); const match = regex.exec(plugin);
@ -235,7 +244,7 @@ function toDependencies(plugins_) {
return obj; return obj;
} }
exports.subscribe = fn => { export const subscribe = (fn: Function) => {
watchers.push(fn); watchers.push(fn);
return () => { return () => {
watchers.splice(watchers.indexOf(fn), 1); watchers.splice(watchers.indexOf(fn), 1);
@ -244,53 +253,49 @@ exports.subscribe = fn => {
function getPaths() { function getPaths() {
return { return {
plugins: plugins.plugins.map(name => { plugins: plugins.plugins.map((name) => {
return resolve(path, 'node_modules', name.split('#')[0].split('@')[0]); return resolve(path, 'node_modules', name.split('#')[0]);
}), }),
localPlugins: plugins.localPlugins.map(name => { localPlugins: plugins.localPlugins.map((name) => {
return resolve(localPath, name); return resolve(localPath, name);
}) })
}; };
} }
// expose to renderer // expose to renderer
exports.getPaths = getPaths; export {getPaths};
// get paths from renderer // get paths from renderer
exports.getBasePaths = () => { export const getBasePaths = () => {
return {path, localPath}; return {path, localPath};
}; };
function requirePlugins() { function requirePlugins(): any[] {
const {plugins: plugins_, localPlugins} = paths; const {plugins: plugins_, localPlugins} = paths;
const load = path_ => { const load = (path_: string) => {
let mod; let mod: any;
try { try {
// eslint-disable-next-line import/no-dynamic-require
mod = require(path_); mod = require(path_);
const exposed = mod && Object.keys(mod).some(key => availableExtensions.has(key)); const exposed = mod && Object.keys(mod).some((key) => availableExtensions.has(key));
if (!exposed) { if (!exposed) {
notify('Plugin error!', `Plugin "${basename(path_)}" does not expose any ` + 'Hyper extension API methods'); notify('Plugin error!', `${`Plugin "${basename(path_)}" does not expose any `}Hyper extension API methods`);
return; return;
} }
// populate the name for internal errors here // populate the name for internal errors here
mod._name = basename(path_); mod._name = basename(path_);
try { try {
// eslint-disable-next-line import/no-dynamic-require // eslint-disable-next-line @typescript-eslint/no-var-requires
mod._version = require(resolve(path_, 'package.json')).version; mod._version = require(resolve(path_, 'package.json')).version;
} catch (err) { } catch (err) {
//eslint-disable-next-line no-console
console.warn(`No package.json found in ${path_}`); console.warn(`No package.json found in ${path_}`);
} }
//eslint-disable-next-line no-console
console.log(`Plugin ${mod._name} (${mod._version}) loaded.`); console.log(`Plugin ${mod._name} (${mod._version}) loaded.`);
return mod; return mod;
} catch (err) { } catch (err) {
if (err.code === 'MODULE_NOT_FOUND') { if (err.code === 'MODULE_NOT_FOUND') {
//eslint-disable-next-line no-console
console.warn(`Plugin error while loading "${basename(path_)}" (${path_}): ${err.message}`); console.warn(`Plugin error while loading "${basename(path_)}" (${path_}): ${err.message}`);
} else { } else {
notify('Plugin error!', `Plugin "${basename(path_)}" failed to load (${err.message})`, {error: err}); notify('Plugin error!', `Plugin "${basename(path_)}" failed to load (${err.message})`, {error: err});
@ -301,11 +306,11 @@ function requirePlugins() {
return plugins_ return plugins_
.map(load) .map(load)
.concat(localPlugins.map(load)) .concat(localPlugins.map(load))
.filter(v => Boolean(v)); .filter((v) => Boolean(v));
} }
exports.onApp = app_ => { export const onApp = (app_: App) => {
modules.forEach(plugin => { modules.forEach((plugin) => {
if (plugin.onApp) { if (plugin.onApp) {
try { try {
plugin.onApp(app_); plugin.onApp(app_);
@ -318,8 +323,8 @@ exports.onApp = app_ => {
}); });
}; };
exports.onWindowClass = win => { export const onWindowClass = (win: BrowserWindow) => {
modules.forEach(plugin => { modules.forEach((plugin) => {
if (plugin.onWindowClass) { if (plugin.onWindowClass) {
try { try {
plugin.onWindowClass(win); plugin.onWindowClass(win);
@ -332,8 +337,8 @@ exports.onWindowClass = win => {
}); });
}; };
exports.onWindow = win => { export const onWindow = (win: BrowserWindow) => {
modules.forEach(plugin => { modules.forEach((plugin) => {
if (plugin.onWindow) { if (plugin.onWindow) {
try { try {
plugin.onWindow(win); plugin.onWindow(win);
@ -348,9 +353,9 @@ exports.onWindow = win => {
// decorates the base entity by calling plugin[key] // decorates the base entity by calling plugin[key]
// for all the available plugins // for all the available plugins
function decorateEntity(base, key, type) { function decorateEntity(base: any, key: string, type: 'object' | 'function') {
let decorated = base; let decorated = base;
modules.forEach(plugin => { modules.forEach((plugin) => {
if (plugin[key]) { if (plugin[key]) {
let res; let res;
try { try {
@ -370,23 +375,23 @@ function decorateEntity(base, key, type) {
return decorated; return decorated;
} }
function decorateObject(base, key) { function decorateObject<T>(base: T, key: string): T {
return decorateEntity(base, key, 'object'); return decorateEntity(base, key, 'object');
} }
function decorateClass(base, key) { function decorateClass(base: any, key: string) {
return decorateEntity(base, key, 'function'); return decorateEntity(base, key, 'function');
} }
exports.getDeprecatedConfig = () => { export const getDeprecatedConfig = () => {
const deprecated = {}; const deprecated: Record<string, {css: string[]}> = {};
const baseConfig = config.getConfig(); const baseConfig = config.getConfig();
modules.forEach(plugin => { modules.forEach((plugin) => {
if (!plugin.decorateConfig) { if (!plugin.decorateConfig) {
return; return;
} }
// We need to clone config in case of plugin modifies config directly. // We need to clone config in case of plugin modifies config directly.
let configTmp; let configTmp: configOptions;
try { try {
configTmp = plugin.decorateConfig(JSON.parse(JSON.stringify(baseConfig))); configTmp = plugin.decorateConfig(JSON.parse(JSON.stringify(baseConfig)));
} catch (e) { } catch (e) {
@ -404,15 +409,15 @@ exports.getDeprecatedConfig = () => {
return deprecated; return deprecated;
}; };
exports.decorateMenu = tpl => { export const decorateMenu = (tpl: any) => {
return decorateObject(tpl, 'decorateMenu'); return decorateObject(tpl, 'decorateMenu');
}; };
exports.getDecoratedEnv = baseEnv => { export const getDecoratedEnv = (baseEnv: Record<string, string>) => {
return decorateObject(baseEnv, 'decorateEnv'); return decorateObject(baseEnv, 'decorateEnv');
}; };
exports.getDecoratedConfig = () => { export const getDecoratedConfig = () => {
const baseConfig = config.getConfig(); const baseConfig = config.getConfig();
const decoratedConfig = decorateObject(baseConfig, 'decorateConfig'); const decoratedConfig = decorateObject(baseConfig, 'decorateConfig');
const fixedConfig = config.fixConfigDefaults(decoratedConfig); const fixedConfig = config.fixConfigDefaults(decoratedConfig);
@ -420,27 +425,27 @@ exports.getDecoratedConfig = () => {
return translatedConfig; return translatedConfig;
}; };
exports.getDecoratedKeymaps = () => { export const getDecoratedKeymaps = () => {
const baseKeymaps = config.getKeymaps(); const baseKeymaps = config.getKeymaps();
// Ensure that all keys are in an array and don't use deprecated key combination` // Ensure that all keys are in an array and don't use deprecated key combination`
const decoratedKeymaps = mapKeys(decorateObject(baseKeymaps, 'decorateKeymaps')); const decoratedKeymaps = mapKeys(decorateObject(baseKeymaps, 'decorateKeymaps'));
return decoratedKeymaps; return decoratedKeymaps;
}; };
exports.getDecoratedBrowserOptions = defaults => { export const getDecoratedBrowserOptions = <T>(defaults: T): T => {
return decorateObject(defaults, 'decorateBrowserOptions'); return decorateObject(defaults, 'decorateBrowserOptions');
}; };
exports.decorateWindowClass = defaults => { export const decorateWindowClass = <T>(defaults: T): T => {
return decorateObject(defaults, 'decorateWindowClass'); return decorateObject(defaults, 'decorateWindowClass');
}; };
exports.decorateSessionOptions = defaults => { export const decorateSessionOptions = <T>(defaults: T): T => {
return decorateObject(defaults, 'decorateSessionOptions'); return decorateObject(defaults, 'decorateSessionOptions');
}; };
exports.decorateSessionClass = Session => { export const decorateSessionClass = <T>(Session: T): T => {
return decorateClass(Session, 'decorateSessionClass'); return decorateClass(Session, 'decorateSessionClass');
}; };
exports._toDependencies = toDependencies; export {toDependencies as _toDependencies};

View file

@ -1,44 +0,0 @@
module.exports = {
availableExtensions: new Set([
'onApp',
'onWindowClass',
'decorateWindowClass',
'onWindow',
'onRendererWindow',
'onUnload',
'decorateSessionClass',
'decorateSessionOptions',
'middleware',
'reduceUI',
'reduceSessions',
'reduceTermGroups',
'decorateBrowserOptions',
'decorateMenu',
'decorateTerm',
'decorateHyper',
'decorateHyperTerm', // for backwards compatibility with hyperterm
'decorateHeader',
'decorateTerms',
'decorateTab',
'decorateNotification',
'decorateNotifications',
'decorateTabs',
'decorateConfig',
'decorateKeymaps',
'decorateEnv',
'decorateTermGroup',
'decorateSplitPane',
'getTermProps',
'getTabProps',
'getTabsProps',
'getTermGroupProps',
'mapHyperTermState',
'mapTermsState',
'mapHeaderState',
'mapNotificationsState',
'mapHyperTermDispatch',
'mapTermsDispatch',
'mapHeaderDispatch',
'mapNotificationsDispatch'
])
};

42
app/plugins/extensions.ts Normal file
View file

@ -0,0 +1,42 @@
export const availableExtensions = new Set([
'onApp',
'onWindowClass',
'decorateWindowClass',
'onWindow',
'onRendererWindow',
'onUnload',
'decorateSessionClass',
'decorateSessionOptions',
'middleware',
'reduceUI',
'reduceSessions',
'reduceTermGroups',
'decorateBrowserOptions',
'decorateMenu',
'decorateTerm',
'decorateHyper',
'decorateHyperTerm', // for backwards compatibility with hyperterm
'decorateHeader',
'decorateTerms',
'decorateTab',
'decorateNotification',
'decorateNotifications',
'decorateTabs',
'decorateConfig',
'decorateKeymaps',
'decorateEnv',
'decorateTermGroup',
'decorateSplitPane',
'getTermProps',
'getTabProps',
'getTabsProps',
'getTermGroupProps',
'mapHyperTermState',
'mapTermsState',
'mapHeaderState',
'mapNotificationsState',
'mapHyperTermDispatch',
'mapTermsDispatch',
'mapHeaderDispatch',
'mapNotificationsDispatch'
]);

View file

@ -1,50 +0,0 @@
const cp = require('child_process');
const queue = require('queue');
const ms = require('ms');
const {yarn, plugs} = require('../config/paths');
module.exports = {
install: fn => {
const spawnQueue = queue({concurrency: 1});
function yarnFn(args, cb) {
const env = {
NODE_ENV: 'production',
ELECTRON_RUN_AS_NODE: 'true'
};
spawnQueue.push(end => {
const cmd = [process.execPath, yarn].concat(args).join(' ');
//eslint-disable-next-line no-console
console.log('Launching yarn:', cmd);
cp.execFile(
process.execPath,
[yarn].concat(args),
{
cwd: plugs.base,
env,
timeout: ms('5m'),
maxBuffer: 1024 * 1024
},
(err, stdout, stderr) => {
if (err) {
cb(stderr);
} else {
cb(null);
}
end();
spawnQueue.start();
}
);
});
spawnQueue.start();
}
yarnFn(['install', '--no-emoji', '--no-lockfile', '--cache-folder', plugs.cache], err => {
if (err) {
return fn(err);
}
fn(null);
});
}
};

47
app/plugins/install.ts Normal file
View file

@ -0,0 +1,47 @@
import cp from 'child_process';
import queue from 'queue';
import ms from 'ms';
import {yarn, plugs} from '../config/paths';
export const install = (fn: (err: string | null) => void) => {
const spawnQueue = queue({concurrency: 1});
function yarnFn(args: string[], cb: (err: string | null) => void) {
const env = {
NODE_ENV: 'production',
ELECTRON_RUN_AS_NODE: 'true'
};
spawnQueue.push((end) => {
const cmd = [process.execPath, yarn].concat(args).join(' ');
console.log('Launching yarn:', cmd);
cp.execFile(
process.execPath,
[yarn].concat(args),
{
cwd: plugs.base,
env,
timeout: ms('5m'),
maxBuffer: 1024 * 1024
},
(err, stdout, stderr) => {
if (err) {
cb(stderr);
} else {
cb(null);
}
end?.();
spawnQueue.start();
}
);
});
spawnQueue.start();
}
yarnFn(['install', '--no-emoji', '--no-lockfile', '--cache-folder', plugs.cache], (err) => {
if (err) {
return fn(err);
}
fn(null);
});
};

View file

@ -1,9 +1,12 @@
const {EventEmitter} = require('events'); import {EventEmitter} from 'events';
const {ipcMain} = require('electron'); import {ipcMain, BrowserWindow} from 'electron';
const uuid = require('uuid'); import {v4 as uuidv4} from 'uuid';
class Server extends EventEmitter { export class Server extends EventEmitter {
constructor(win) { destroyed = false;
win: BrowserWindow;
id!: string;
constructor(win: BrowserWindow) {
super(); super();
this.win = win; this.win = win;
this.ipcListener = this.ipcListener.bind(this); this.ipcListener = this.ipcListener.bind(this);
@ -12,9 +15,10 @@ class Server extends EventEmitter {
return; return;
} }
const uid = uuid.v4(); const uid = uuidv4();
this.id = uid; this.id = uid;
// eslint-disable-next-line @typescript-eslint/unbound-method
ipcMain.on(uid, this.ipcListener); ipcMain.on(uid, this.ipcListener);
// we intentionally subscribe to `on` instead of `once` // we intentionally subscribe to `on` instead of `once`
@ -29,11 +33,11 @@ class Server extends EventEmitter {
return this.win.webContents; return this.win.webContents;
} }
ipcListener(event, {ev, data}) { ipcListener(event: any, {ev, data}: {ev: string; data: any}) {
super.emit(ev, data); super.emit(ev, data);
} }
emit(ch, data) { emit(ch: string, data: any = {}): any {
// This check is needed because data-batching can cause extra data to be // This check is needed because data-batching can cause extra data to be
// emitted after the window has already closed // emitted after the window has already closed
if (!this.win.isDestroyed()) { if (!this.win.isDestroyed()) {
@ -45,6 +49,7 @@ class Server extends EventEmitter {
this.removeAllListeners(); this.removeAllListeners();
this.wc.removeAllListeners(); this.wc.removeAllListeners();
if (this.id) { if (this.id) {
// eslint-disable-next-line @typescript-eslint/unbound-method
ipcMain.removeListener(this.id, this.ipcListener); ipcMain.removeListener(this.id, this.ipcListener);
} else { } else {
// mark for `genUid` in constructor // mark for `genUid` in constructor
@ -53,6 +58,6 @@ class Server extends EventEmitter {
} }
} }
module.exports = win => { export default (win: BrowserWindow) => {
return new Server(win); return new Server(win);
}; };

View file

@ -1,25 +1,28 @@
const {EventEmitter} = require('events'); import {EventEmitter} from 'events';
const {StringDecoder} = require('string_decoder'); import {StringDecoder} from 'string_decoder';
import defaultShell from 'default-shell';
const defaultShell = require('default-shell'); import {getDecoratedEnv} from './plugins';
import {productName, version} from './package.json';
const {getDecoratedEnv} = require('./plugins'); import * as config from './config';
const {productName, version} = require('./package'); import {IPty, IWindowsPtyForkOptions, spawn as npSpawn} from 'node-pty';
const config = require('./config'); import {cliScriptPath} from './config/paths';
import {dirname} from 'path';
const createNodePtyError = () => const createNodePtyError = () =>
new Error( new Error(
'`node-pty` failed to load. Typically this means that it was built incorrectly. Please check the `readme.md` to more info.' '`node-pty` failed to load. Typically this means that it was built incorrectly. Please check the `readme.md` to more info.'
); );
let spawn; let spawn: typeof npSpawn;
try { try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
spawn = require('node-pty').spawn; spawn = require('node-pty').spawn;
} catch (err) { } catch (err) {
throw createNodePtyError(); throw createNodePtyError();
} }
const envFromConfig = config.getConfig().env || {}; const envFromConfig = config.getConfig().env || {};
const useConpty = config.getConfig().useConpty;
// Max duration to batch session data before sending it to the renderer process. // Max duration to batch session data before sending it to the renderer process.
const BATCH_DURATION_MS = 16; const BATCH_DURATION_MS = 16;
@ -34,7 +37,11 @@ const BATCH_MAX_SIZE = 200 * 1024;
// with the window ID which is then stripped on the renderer process and this // with the window ID which is then stripped on the renderer process and this
// overhead is reduced with batching. // overhead is reduced with batching.
class DataBatcher extends EventEmitter { class DataBatcher extends EventEmitter {
constructor(uid) { uid: string;
decoder: StringDecoder;
data!: string;
timeout!: NodeJS.Timeout | null;
constructor(uid: string) {
super(); super();
this.uid = uid; this.uid = uid;
this.decoder = new StringDecoder('utf8'); this.decoder = new StringDecoder('utf8');
@ -47,7 +54,7 @@ class DataBatcher extends EventEmitter {
this.timeout = null; this.timeout = null;
} }
write(chunk) { write(chunk: Buffer) {
if (this.data.length + chunk.length >= BATCH_MAX_SIZE) { if (this.data.length + chunk.length >= BATCH_MAX_SIZE) {
// We've reached the max batch size. Flush it and start another one // We've reached the max batch size. Flush it and start another one
if (this.timeout) { if (this.timeout) {
@ -73,23 +80,38 @@ class DataBatcher extends EventEmitter {
} }
} }
module.exports = class Session extends EventEmitter { interface SessionOptions {
constructor(options) { uid: 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;
constructor(options: SessionOptions) {
super(); super();
this.pty = null; this.pty = null;
this.batcher = null; this.batcher = null;
this.shell = null; this.shell = null;
this.ended = false; this.ended = false;
this.initTimestamp = new Date().getTime();
this.init(options); this.init(options);
} }
init({uid, rows, cols: columns, cwd, shell, shellArgs}) { init({uid, rows, cols: columns, cwd, shell: _shell, shellArgs: _shellArgs}: SessionOptions) {
const osLocale = require('os-locale'); // eslint-disable-next-line @typescript-eslint/no-var-requires
const osLocale = require('os-locale') as typeof import('os-locale');
const baseEnv = Object.assign( const baseEnv = Object.assign(
{}, {},
process.env, process.env,
{ {
LANG: osLocale.sync() + '.UTF-8', LANG: `${osLocale.sync().replace(/-/, '_')}.UTF-8`,
TERM: 'xterm-256color', TERM: 'xterm-256color',
COLORTERM: 'truecolor', COLORTERM: 'truecolor',
TERM_PROGRAM: productName, TERM_PROGRAM: productName,
@ -98,22 +120,40 @@ module.exports = class Session extends EventEmitter {
envFromConfig envFromConfig
); );
// path to AppImage mount point is added to PATH environment variable automatically
// which conflicts with the cli
if (baseEnv['APPIMAGE'] && baseEnv['APPDIR']) {
baseEnv['PATH'] = [dirname(cliScriptPath)]
.concat((baseEnv['PATH'] || '').split(':').filter((val) => !val.startsWith(baseEnv['APPDIR'])))
.join(':');
}
// Electron has a default value for process.env.GOOGLE_API_KEY // Electron has a default value for process.env.GOOGLE_API_KEY
// We don't want to leak this to the shell // We don't want to leak this to the shell
// See https://github.com/zeit/hyper/issues/696 // See https://github.com/vercel/hyper/issues/696
if (baseEnv.GOOGLE_API_KEY && process.env.GOOGLE_API_KEY === baseEnv.GOOGLE_API_KEY) { if (baseEnv.GOOGLE_API_KEY && process.env.GOOGLE_API_KEY === baseEnv.GOOGLE_API_KEY) {
delete baseEnv.GOOGLE_API_KEY; delete baseEnv.GOOGLE_API_KEY;
} }
const defaultShellArgs = ['--login']; const defaultShellArgs = ['--login'];
const options: IWindowsPtyForkOptions = {
cols: columns,
rows,
cwd,
env: getDecoratedEnv(baseEnv)
};
// if config do not set the useConpty, it will be judged by the node-pty
if (typeof useConpty === 'boolean') {
options.useConpty = useConpty;
}
const shell = _shell || defaultShell;
const shellArgs = _shellArgs || defaultShellArgs;
try { try {
this.pty = spawn(shell || defaultShell, shellArgs || defaultShellArgs, { this.pty = spawn(shell, shellArgs, options);
cols: columns,
rows,
cwd,
env: getDecoratedEnv(baseEnv)
});
} catch (err) { } catch (err) {
if (/is not a function/.test(err.message)) { if (/is not a function/.test(err.message)) {
throw createNodePtyError(); throw createNodePtyError();
@ -123,50 +163,62 @@ module.exports = class Session extends EventEmitter {
} }
this.batcher = new DataBatcher(uid); this.batcher = new DataBatcher(uid);
this.pty.on('data', chunk => { this.pty.onData((chunk) => {
if (this.ended) { if (this.ended) {
return; return;
} }
this.batcher.write(chunk); this.batcher?.write(chunk as any);
}); });
this.batcher.on('flush', data => { this.batcher.on('flush', (data: string) => {
this.emit('data', data); this.emit('data', data);
}); });
this.pty.on('exit', () => { this.pty.onExit((e) => {
if (!this.ended) { if (!this.ended) {
this.ended = true; // fall back to default shell config if the shell exits within 1 sec with non zero exit code
this.emit('exit'); // this will inform users in case there are errors in the config instead of instant exit
const runDuration = new Date().getTime() - this.initTimestamp;
if (e.exitCode > 0 && runDuration < 1000) {
const defaultShellConfig = {shell: defaultShell, shellArgs: defaultShellArgs};
const msg = `
shell exited in ${runDuration} ms with exit code ${e.exitCode}
please check the shell config: ${JSON.stringify({shell, shellArgs}, undefined, 2)}
fallback to default shell config: ${JSON.stringify(defaultShellConfig, undefined, 2)}
`;
console.warn(msg);
this.batcher?.write(msg.replace(/\n/g, '\r\n') as any);
this.init({uid, rows, cols: columns, cwd, ...defaultShellConfig});
} else {
this.ended = true;
this.emit('exit');
}
} }
}); });
this.shell = shell || defaultShell; this.shell = shell;
} }
exit() { exit() {
this.destroy(); this.destroy();
} }
write(data) { write(data: string) {
if (this.pty) { if (this.pty) {
this.pty.write(data); this.pty.write(data);
} else { } else {
//eslint-disable-next-line no-console
console.warn('Warning: Attempted to write to a session with no pty'); console.warn('Warning: Attempted to write to a session with no pty');
} }
} }
resize({cols, rows}) { resize({cols, rows}: {cols: number; rows: number}) {
if (this.pty) { if (this.pty) {
try { try {
this.pty.resize(cols, rows); this.pty.resize(cols, rows);
} catch (err) { } catch (err) {
//eslint-disable-next-line no-console
console.error(err.stack); console.error(err.stack);
} }
} else { } else {
//eslint-disable-next-line no-console
console.warn('Warning: Attempted to resize a session with no pty'); console.warn('Warning: Attempted to resize a session with no pty');
} }
} }
@ -176,14 +228,12 @@ module.exports = class Session extends EventEmitter {
try { try {
this.pty.kill(); this.pty.kill();
} catch (err) { } catch (err) {
//eslint-disable-next-line no-console
console.error('exit error', err.stack); console.error('exit error', err.stack);
} }
} else { } else {
//eslint-disable-next-line no-console
console.warn('Warning: Attempted to destroy a session with no pty'); console.warn('Warning: Attempted to destroy a session with no pty');
} }
this.emit('exit'); this.emit('exit');
this.ended = true; this.ended = true;
} }
}; }

View file

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

11
app/tsconfig.json Normal file
View file

@ -0,0 +1,11 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"declarationDir": "../dist/tmp/appdts/",
"outDir": "../target/"
},
"include": [
"./**/*",
"./package.json"
]
}

View file

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

33
app/ui/contextmenu.ts Normal file
View file

@ -0,0 +1,33 @@
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> =>
Object.keys(keymaps).reduce((commandKeys: Record<string, string>, command) => {
return Object.assign(commandKeys, {
[command]: keymaps[command][0]
});
}, {});
// only display cut/copy when there's a cursor selection
const filterCutCopy = (selection: string, menuItem: MenuItemConstructorOptions) => {
if (/^cut$|^copy$/.test(menuItem.role!) && !selection) {
return;
}
return menuItem;
};
export default (
createWindow: (fn?: (win: BrowserWindow) => void, options?: Record<string, any>) => BrowserWindow,
selection: string
) => {
const commandKeys = getCommandKeys(getDecoratedKeymaps());
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);
};

View file

@ -1,325 +0,0 @@
const {app, BrowserWindow, shell, Menu} = require('electron');
const {isAbsolute} = require('path');
const {parse: parseUrl} = require('url');
const uuid = require('uuid');
const fileUriToPath = require('file-uri-to-path');
const isDev = require('electron-is-dev');
const updater = require('../updater');
const toElectronBackgroundColor = require('../utils/to-electron-background-color');
const {icon, homeDirectory} = require('../config/paths');
const createRPC = require('../rpc');
const notify = require('../notify');
const fetchNotifications = require('../notifications');
const Session = require('../session');
const contextMenuTemplate = require('./contextmenu');
const {execCommand} = require('../commands');
const {setRendererType, unsetRendererType} = require('../utils/renderer-utils');
const {decorateSessionOptions, decorateSessionClass} = require('../plugins');
module.exports = class Window {
constructor(options_, cfg, fn) {
const classOpts = Object.assign({uid: uuid.v4()});
app.plugins.decorateWindowClass(classOpts);
this.uid = classOpts.uid;
app.plugins.onWindowClass(this);
const winOpts = Object.assign(
{
minWidth: 370,
minHeight: 190,
backgroundColor: toElectronBackgroundColor(cfg.backgroundColor || '#000'),
titleBarStyle: 'hiddenInset',
title: 'Hyper.app',
// we want to go frameless on Windows and Linux
frame: process.platform === 'darwin',
transparent: process.platform === 'darwin',
icon,
show: process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev,
acceptFirstMouse: true
},
options_
);
const window = new BrowserWindow(app.plugins.getDecoratedBrowserOptions(winOpts));
window.uid = classOpts.uid;
const rpc = createRPC(window);
const sessions = new Map();
const updateBackgroundColor = () => {
const cfg_ = app.plugins.getDecoratedConfig();
window.setBackgroundColor(toElectronBackgroundColor(cfg_.backgroundColor || '#000'));
};
// config changes
const cfgUnsubscribe = app.config.subscribe(() => {
const cfg_ = app.plugins.getDecoratedConfig();
// notify renderer
window.webContents.send('config change');
// notify user that shell changes require new sessions
if (cfg_.shell !== cfg.shell || JSON.stringify(cfg_.shellArgs) !== JSON.stringify(cfg.shellArgs)) {
notify('Shell configuration changed!', 'Open a new tab or window to start using the new shell');
}
// update background color if necessary
updateBackgroundColor();
cfg = cfg_;
});
rpc.on('init', () => {
window.show();
updateBackgroundColor();
// If no callback is passed to createWindow,
// a new session will be created by default.
if (!fn) {
fn = win => win.rpc.emit('termgroup add req');
}
// app.windowCallback is the createWindow callback
// that can be set before the 'ready' app event
// and createWindow definition. It's executed in place of
// the callback passed as parameter, and deleted right after.
(app.windowCallback || fn)(window);
delete app.windowCallback;
fetchNotifications(window);
// auto updates
if (!isDev) {
updater(window);
} else {
//eslint-disable-next-line no-console
console.log('ignoring auto updates during dev');
}
});
function createSession(extraOptions = {}) {
const uid = uuid.v4();
const defaultOptions = Object.assign(
{
rows: 40,
cols: 100,
cwd: process.argv[1] && isAbsolute(process.argv[1]) ? process.argv[1] : homeDirectory,
splitDirection: undefined,
shell: cfg.shell,
shellArgs: cfg.shellArgs && Array.from(cfg.shellArgs)
},
extraOptions,
{uid}
);
const options = decorateSessionOptions(defaultOptions);
const DecoratedSession = decorateSessionClass(Session);
const session = new DecoratedSession(options);
sessions.set(uid, session);
return {session, options};
}
// Optimistically create the initial session so that when the window sends
// the first "new" IPC message, there's a session already warmed up.
function createInitialSession() {
let {session, options} = createSession();
const initialEvents = [];
const handleData = data => initialEvents.push(['session data', data]);
const handleExit = () => initialEvents.push(['session exit']);
session.on('data', handleData);
session.on('exit', handleExit);
function flushEvents() {
for (let args of initialEvents) {
rpc.emit(...args);
}
session.removeListener('data', handleData);
session.removeListener('exit', handleExit);
}
return {session, options, flushEvents};
}
let initialSession = createInitialSession();
rpc.on('new', extraOptions => {
const {session, options} = initialSession || createSession(extraOptions);
sessions.set(options.uid, session);
rpc.emit('session add', {
rows: options.rows,
cols: options.cols,
uid: options.uid,
splitDirection: options.splitDirection,
shell: session.shell,
pid: session.pty.pid
});
// If this is the initial session, flush any events that might have
// occurred while the window was initializing
if (initialSession) {
initialSession.flushEvents();
initialSession = null;
}
session.on('data', data => {
rpc.emit('session data', data);
});
session.on('exit', () => {
rpc.emit('session exit', {uid: options.uid});
unsetRendererType(options.uid);
sessions.delete(options.uid);
});
});
rpc.on('exit', ({uid}) => {
const session = sessions.get(uid);
if (session) {
session.exit();
}
});
rpc.on('unmaximize', () => {
window.unmaximize();
});
rpc.on('maximize', () => {
window.maximize();
});
rpc.on('minimize', () => {
window.minimize();
});
rpc.on('resize', ({uid, cols, rows}) => {
const session = sessions.get(uid);
if (session) {
session.resize({cols, rows});
}
});
rpc.on('data', ({uid, data, escaped}) => {
const session = sessions.get(uid);
if (session) {
if (escaped) {
const escapedData = session.shell.endsWith('cmd.exe')
? `"${data}"` // This is how cmd.exe does it
: `'${data.replace(/'/g, `'\\''`)}'`; // Inside a single-quoted string nothing is interpreted
session.write(escapedData);
} else {
session.write(data);
}
}
});
rpc.on('info renderer', ({uid, type}) => {
// Used in the "About" dialog
setRendererType(uid, type);
});
rpc.on('open external', ({url}) => {
shell.openExternal(url);
});
rpc.on('open context menu', selection => {
const {createWindow} = app;
const {buildFromTemplate} = Menu;
buildFromTemplate(contextMenuTemplate(createWindow, selection)).popup(window);
});
rpc.on('open hamburger menu', ({x, y}) => {
Menu.getApplicationMenu().popup({x: Math.ceil(x), y: Math.ceil(y)});
});
// Same deal as above, grabbing the window titlebar when the window
// is maximized on Windows results in unmaximize, without hitting any
// app buttons
for (const ev of ['maximize', 'unmaximize', 'minimize', 'restore']) {
window.on(ev, () => rpc.emit('windowGeometry change'));
}
window.on('move', () => {
const position = window.getPosition();
rpc.emit('move', {bounds: {x: position[0], y: position[1]}});
});
rpc.on('close', () => {
window.close();
});
rpc.on('command', command => {
const focusedWindow = BrowserWindow.getFocusedWindow();
execCommand(command, focusedWindow);
});
const deleteSessions = () => {
sessions.forEach((session, key) => {
session.removeAllListeners();
session.destroy();
sessions.delete(key);
});
};
// we reset the rpc channel only upon
// subsequent refreshes (ie: F5)
let i = 0;
window.webContents.on('did-navigate', () => {
if (i++) {
deleteSessions();
}
});
// If file is dropped onto the terminal window, navigate event is prevented
// and his path is added to active session.
window.webContents.on('will-navigate', (event, url) => {
const protocol = typeof url === 'string' && parseUrl(url).protocol;
if (protocol === 'file:') {
event.preventDefault();
const path = fileUriToPath(url);
rpc.emit('session data send', {data: path, escaped: true});
} else if (protocol === 'http:' || protocol === 'https:') {
event.preventDefault();
rpc.emit('session data send', {data: url});
}
});
// xterm makes link clickable
window.webContents.on('new-window', (event, url) => {
const protocol = typeof url === 'string' && parseUrl(url).protocol;
if (protocol === 'http:' || protocol === 'https:') {
event.preventDefault();
shell.openExternal(url);
}
});
// expose internals to extension authors
window.rpc = rpc;
window.sessions = sessions;
const load = () => {
app.plugins.onWindow(window);
};
// load plugins
load();
const pluginsUnsubscribe = app.plugins.subscribe(err => {
if (!err) {
load();
window.webContents.send('plugins change');
updateBackgroundColor();
}
});
// Keep track of focus time of every window, to figure out
// which one of the existing window is the last focused.
// Works nicely even if a window is closed and removed.
const updateFocusTime = () => {
window.focusTime = process.uptime();
};
window.on('focus', () => {
updateFocusTime();
});
// the window can be closed by the browser process itself
window.clean = () => {
app.config.winRecord(window);
rpc.destroy();
deleteSessions();
cfgUnsubscribe();
pluginsUnsubscribe();
};
// Ensure focusTime is set on window open. The focus event doesn't
// fire from the dock (see bug #583)
updateFocusTime();
return window;
}
};

330
app/ui/window.ts Normal file
View file

@ -0,0 +1,330 @@
import {app, BrowserWindow, shell, Menu, BrowserWindowConstructorOptions} from 'electron';
import {isAbsolute, normalize, sep} from 'path';
import {parse as parseUrl} from 'url';
import {v4 as uuidv4} from 'uuid';
import fileUriToPath from 'file-uri-to-path';
import isDev from 'electron-is-dev';
import updater from '../updater';
import toElectronBackgroundColor from '../utils/to-electron-background-color';
import {icon, homeDirectory} from '../config/paths';
import createRPC from '../rpc';
import notify from '../notify';
import fetchNotifications from '../notifications';
import Session from '../session';
import contextMenuTemplate from './contextmenu';
import {execCommand} from '../commands';
import {setRendererType, unsetRendererType} from '../utils/renderer-utils';
import {decorateSessionOptions, decorateSessionClass} from '../plugins';
export function newWindow(
options_: BrowserWindowConstructorOptions,
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,
enableRemoteModule: true,
contextIsolation: false
},
...options_
};
const window = new BrowserWindow(app.plugins.getDecoratedBrowserOptions(winOpts));
window.uid = classOpts.uid;
app.plugins.onWindowClass(window);
window.uid = classOpts.uid;
const rpc = createRPC(window);
const sessions = new Map<string, Session>();
const updateBackgroundColor = () => {
const cfg_ = app.plugins.getDecoratedConfig();
window.setBackgroundColor(toElectronBackgroundColor(cfg_.backgroundColor || '#000'));
};
// set working directory
let argPath = process.argv[1];
if (argPath && process.platform === 'win32') {
if (/[a-zA-Z]:"/.test(argPath)) {
argPath = argPath.replace('"', sep);
}
argPath = normalize(argPath + sep);
}
let workingDirectory = homeDirectory;
if (argPath && isAbsolute(argPath)) {
workingDirectory = argPath;
} else if (cfg.workingDirectory && isAbsolute(cfg.workingDirectory)) {
workingDirectory = cfg.workingDirectory;
}
// config changes
const cfgUnsubscribe = app.config.subscribe(() => {
const cfg_ = app.plugins.getDecoratedConfig();
// notify renderer
window.webContents.send('config change');
// notify user that shell changes require new sessions
if (cfg_.shell !== cfg.shell || JSON.stringify(cfg_.shellArgs) !== JSON.stringify(cfg.shellArgs)) {
notify('Shell configuration changed!', 'Open a new tab or window to start using the new shell');
}
// update background color if necessary
updateBackgroundColor();
cfg = cfg_;
});
rpc.on('init', () => {
window.show();
updateBackgroundColor();
// If no callback is passed to createWindow,
// a new session will be created by default.
if (!fn) {
fn = (win: BrowserWindow) => {
win.rpc.emit('termgroup add req', {});
};
}
// app.windowCallback is the createWindow callback
// that can be set before the 'ready' app event
// and createWindow definition. It's executed in place of
// the callback passed as parameter, and deleted right after.
(app.windowCallback || fn)(window);
app.windowCallback = undefined;
fetchNotifications(window);
// auto updates
if (!isDev) {
updater(window);
} else {
console.log('ignoring auto updates during dev');
}
});
function createSession(extraOptions: any = {}) {
const uid = uuidv4();
const extraOptionsFiltered: any = {};
Object.keys(extraOptions).forEach((key) => {
if (extraOptions[key] !== undefined) extraOptionsFiltered[key] = extraOptions[key];
});
// remove the rows and cols, the wrong value of them will break layout when init create
const defaultOptions = Object.assign(
{
cwd: workingDirectory,
splitDirection: undefined,
shell: cfg.shell,
shellArgs: cfg.shellArgs && Array.from(cfg.shellArgs)
},
extraOptionsFiltered,
{uid}
);
const options = decorateSessionOptions(defaultOptions);
const DecoratedSession = decorateSessionClass(Session);
const session = new DecoratedSession(options);
sessions.set(uid, session);
return {session, options};
}
rpc.on('new', (extraOptions) => {
const {session, options} = createSession(extraOptions);
sessions.set(options.uid, session);
rpc.emit('session add', {
rows: options.rows,
cols: options.cols,
uid: options.uid,
splitDirection: options.splitDirection,
shell: session.shell,
pid: session.pty ? session.pty.pid : null,
activeUid: options.activeUid
});
session.on('data', (data: string) => {
rpc.emit('session data', data);
});
session.on('exit', () => {
rpc.emit('session exit', {uid: options.uid});
unsetRendererType(options.uid);
sessions.delete(options.uid);
});
});
rpc.on('exit', ({uid}) => {
const session = sessions.get(uid);
if (session) {
session.exit();
}
});
rpc.on('unmaximize', () => {
window.unmaximize();
});
rpc.on('maximize', () => {
window.maximize();
});
rpc.on('minimize', () => {
window.minimize();
});
rpc.on('resize', ({uid, cols, rows}) => {
const session = sessions.get(uid);
if (session) {
session.resize({cols, rows});
}
});
rpc.on('data', ({uid, data, escaped}: {uid: string; data: string; escaped: boolean}) => {
const session = sessions.get(uid);
if (session) {
if (escaped) {
const escapedData = session.shell?.endsWith('cmd.exe')
? `"${data}"` // This is how cmd.exe does it
: `'${data.replace(/'/g, `'\\''`)}'`; // Inside a single-quoted string nothing is interpreted
session.write(escapedData);
} else {
session.write(data);
}
}
});
rpc.on('info renderer', ({uid, type}) => {
// Used in the "About" dialog
setRendererType(uid, type);
});
rpc.on('open external', ({url}) => {
void shell.openExternal(url);
});
rpc.on('open context menu', (selection) => {
const {createWindow} = app;
Menu.buildFromTemplate(contextMenuTemplate(createWindow, selection)).popup({window});
});
rpc.on('open hamburger menu', ({x, y}) => {
Menu.getApplicationMenu()!.popup({x: Math.ceil(x), y: Math.ceil(y)});
});
// Same deal as above, grabbing the window titlebar when the window
// is maximized on Windows results in unmaximize, without hitting any
// app buttons
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]}});
});
rpc.on('close', () => {
window.close();
});
rpc.on('command', (command) => {
const focusedWindow = BrowserWindow.getFocusedWindow();
execCommand(command, focusedWindow!);
});
// pass on the full screen events from the window to react
rpc.win.on('enter-full-screen', () => {
rpc.emit('enter full screen', {});
});
rpc.win.on('leave-full-screen', () => {
rpc.emit('leave full screen', {});
});
const deleteSessions = () => {
sessions.forEach((session, key) => {
session.removeAllListeners();
session.destroy();
sessions.delete(key);
});
};
// we reset the rpc channel only upon
// subsequent refreshes (ie: F5)
let i = 0;
window.webContents.on('did-navigate', () => {
if (i++) {
deleteSessions();
}
});
// If file is dropped onto the terminal window, navigate event is prevented
// and his path is added to active session.
window.webContents.on('will-navigate', (event, url) => {
const protocol = typeof url === 'string' && parseUrl(url).protocol;
if (protocol === 'file:') {
event.preventDefault();
const path = fileUriToPath(url);
rpc.emit('session data send', {data: path, escaped: true});
} else if (protocol === 'http:' || protocol === 'https:') {
event.preventDefault();
rpc.emit('session data send', {data: url});
}
});
// xterm makes link clickable
window.webContents.on('new-window', (event, url) => {
const protocol = typeof url === 'string' && parseUrl(url).protocol;
if (protocol === 'http:' || protocol === 'https:') {
event.preventDefault();
void shell.openExternal(url);
}
});
// expose internals to extension authors
window.rpc = rpc;
window.sessions = sessions;
const load = () => {
app.plugins.onWindow(window);
};
// load plugins
load();
const pluginsUnsubscribe = app.plugins.subscribe((err: any) => {
if (!err) {
load();
window.webContents.send('plugins change');
updateBackgroundColor();
}
});
// Keep track of focus time of every window, to figure out
// which one of the existing window is the last focused.
// Works nicely even if a window is closed and removed.
const updateFocusTime = () => {
window.focusTime = process.uptime();
};
window.on('focus', () => {
updateFocusTime();
});
// the window can be closed by the browser process itself
window.clean = () => {
app.config.winRecord(window);
rpc.destroy();
deleteSessions();
cfgUnsubscribe();
pluginsUnsubscribe();
};
// Ensure focusTime is set on window open. The focus event doesn't
// fire from the dock (see bug #583)
updateFocusTime();
return window;
}

View file

@ -1,38 +1,36 @@
// Packages // Packages
const electron = require('electron'); import electron, {app, BrowserWindow, AutoUpdater} from 'electron';
const {app} = electron; import ms from 'ms';
const ms = require('ms'); import retry from 'async-retry';
const retry = require('async-retry');
// Utilities // Utilities
// eslint-disable-next-line no-unused-vars import {version} from './package.json';
const {version} = require('./package'); import {getDecoratedConfig} from './plugins';
const {getDecoratedConfig} = require('./plugins'); import autoUpdaterLinux from './auto-updater-linux';
const {platform} = process; const {platform} = process;
const isLinux = platform === 'linux'; const isLinux = platform === 'linux';
const autoUpdater = isLinux ? require('./auto-updater-linux') : electron.autoUpdater; const autoUpdater: AutoUpdater = isLinux ? autoUpdaterLinux : electron.autoUpdater;
let isInit = false; let isInit = false;
// Default to the "stable" update channel // Default to the "stable" update channel
let canaryUpdates = false; let canaryUpdates = false;
const buildFeedUrl = (canary, currentVersion) => { const buildFeedUrl = (canary: boolean, currentVersion: string) => {
const updatePrefix = canary ? 'releases-canary' : 'releases'; const updatePrefix = canary ? 'releases-canary' : 'releases';
return `https://${updatePrefix}.hyper.is/update/${isLinux ? 'deb' : platform}/${currentVersion}`; return `https://${updatePrefix}.hyper.is/update/${isLinux ? 'deb' : platform}/${currentVersion}`;
}; };
const isCanary = updateChannel => updateChannel === 'canary'; const isCanary = (updateChannel: string) => updateChannel === 'canary';
async function init() { async function init() {
autoUpdater.on('error', (err, msg) => { autoUpdater.on('error', (err) => {
//eslint-disable-next-line no-console console.error('Error fetching updates', `${err.message} (${err.stack})`);
console.error('Error fetching updates', msg + ' (' + err.stack + ')');
}); });
const config = await retry(async () => { const config = await retry(() => {
const content = await getDecoratedConfig(); const content = getDecoratedConfig();
if (!content) { if (!content) {
throw new Error('No config content loaded'); throw new Error('No config content loaded');
@ -48,7 +46,7 @@ async function init() {
const feedURL = buildFeedUrl(canaryUpdates, version); const feedURL = buildFeedUrl(canaryUpdates, version);
autoUpdater.setFeedURL(feedURL); autoUpdater.setFeedURL({url: feedURL});
setTimeout(() => { setTimeout(() => {
autoUpdater.checkForUpdates(); autoUpdater.checkForUpdates();
@ -61,19 +59,26 @@ async function init() {
isInit = true; isInit = true;
} }
module.exports = win => { export default (win: BrowserWindow) => {
if (!isInit) { if (!isInit) {
init(); void init();
} }
const {rpc} = win; const {rpc} = win;
const onupdate = (ev, releaseNotes, releaseName, date, updateUrl, onQuitAndInstall) => { const onupdate = (
const releaseUrl = updateUrl || `https://github.com/zeit/hyper/releases/tag/${releaseName}`; ev: Event,
releaseNotes: string,
releaseName: string,
date: Date,
updateUrl: string,
onQuitAndInstall: any
) => {
const releaseUrl = updateUrl || `https://github.com/vercel/hyper/releases/tag/${releaseName}`;
rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !!onQuitAndInstall}); rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !!onQuitAndInstall});
}; };
const eventName = isLinux ? 'update-available' : 'update-downloaded'; const eventName: any = isLinux ? 'update-available' : 'update-downloaded';
autoUpdater.on(eventName, onupdate); autoUpdater.on(eventName, onupdate);
@ -88,7 +93,7 @@ module.exports = win => {
if (newUpdateIsCanary !== canaryUpdates) { if (newUpdateIsCanary !== canaryUpdates) {
const feedURL = buildFeedUrl(newUpdateIsCanary, version); const feedURL = buildFeedUrl(newUpdateIsCanary, version);
autoUpdater.setFeedURL(feedURL); autoUpdater.setFeedURL({url: feedURL});
autoUpdater.checkForUpdates(); autoUpdater.checkForUpdates();
canaryUpdates = newUpdateIsCanary; canaryUpdates = newUpdateIsCanary;

View file

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

155
app/utils/cli-install.ts Normal file
View file

@ -0,0 +1,155 @@
import pify from 'pify';
import fs from 'fs';
import path from 'path';
import notify from '../notify';
import {cliScriptPath, cliLinkPath} from '../config/paths';
import * as Registry from 'native-reg';
import type {ValueType} from 'native-reg';
import sudoPrompt from 'sudo-prompt';
import {clipboard, dialog} from 'electron';
import {mkdirpSync} from 'fs-extra';
const readlink = pify(fs.readlink);
const symlink = pify(fs.symlink);
const sudoExec = pify(sudoPrompt.exec, {multiArgs: true});
const checkInstall = () => {
return readlink(cliLinkPath)
.then((link) => link === cliScriptPath)
.catch((err) => {
if (err.code === 'ENOENT') {
return false;
}
throw err;
});
};
const addSymlink = async (silent: boolean) => {
try {
const isInstalled = await checkInstall();
if (isInstalled) {
console.log('Hyper CLI already in PATH');
return;
}
console.log('Linking HyperCLI');
if (!fs.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) {
// '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[0];
}
} else if (result.response === 1) {
clipboard.writeText(`sudo ln -sf "${cliScriptPath}" "${cliLinkPath}"`);
}
}
throw error;
}
};
const addBinToUserPath = () => {
return new Promise<void>((resolve, reject) => {
try {
const envKey = Registry.openKey(Registry.HKCU, 'Environment', Registry.Access.ALL_ACCESS)!;
// C:\Users\<user>\AppData\Local\Programs\hyper\resources\bin
const binPath = path.dirname(cliScriptPath);
// C:\Users\<user>\AppData\Local\hyper
const oldPath = path.resolve(process.env.LOCALAPPDATA!, 'hyper');
const items = Registry.enumValueNames(envKey);
const pathItem = items.find((item) => item.toUpperCase() === 'PATH');
const pathItemName = pathItem || 'PATH';
let newPathValue = binPath;
let type: ValueType = Registry.ValueType.SZ;
if (pathItem) {
type = Registry.queryValueRaw(envKey, pathItem)!.type;
if (type !== Registry.ValueType.SZ && type !== Registry.ValueType.EXPAND_SZ) {
reject(`Registry key type is ${type}`);
return;
}
const value = Registry.queryValue(envKey, pathItem) as string;
let pathParts = value.split(';');
const existingPath = pathParts.includes(binPath);
const existingOldPath = pathParts.some((pathPart) => pathPart.startsWith(oldPath));
if (existingPath && !existingOldPath) {
console.log('Hyper CLI already in PATH');
Registry.closeKey(envKey);
resolve();
return;
}
// Because nsis install path is different from squirrel we need to remove old path if present
// and add current path if absent
if (existingOldPath) pathParts = pathParts.filter((pathPart) => !pathPart.startsWith(oldPath));
if (!pathParts.includes(binPath)) pathParts.push(binPath);
newPathValue = pathParts.join(';');
}
console.log('Adding HyperCLI path (registry)');
Registry.setValueRaw(envKey, pathItemName, type, Registry.formatString(newPathValue));
Registry.closeKey(envKey);
resolve();
} catch (error) {
reject(error);
}
});
};
const logNotify = (withNotification: boolean, title: string, body: string, details?: {error?: any}) => {
console.log(title, body, details);
withNotification && notify(title, body, details);
};
export const installCLI = async (withNotification: boolean) => {
if (process.platform === 'win32') {
try {
await addBinToUserPath();
logNotify(
withNotification,
'Hyper CLI installed',
'You may need to restart your computer to complete this installation process.'
);
} catch (err) {
logNotify(withNotification, 'Hyper CLI installation failed', `Failed to add Hyper CLI path to user PATH ${err}`);
}
} else if (process.platform === 'darwin' || process.platform === 'linux') {
// AppImages are mounted on run at a temporary path, don't create symlink
if (process.env['APPIMAGE']) {
console.log('Skipping CLI symlink creation as it is an AppImage install');
return;
}
try {
await addSymlink(!withNotification);
logNotify(withNotification, 'Hyper CLI installed', `Symlink created at ${cliLinkPath}`);
} catch (error) {
logNotify(withNotification, 'Hyper CLI installation failed', `${error}`);
}
} else {
logNotify(withNotification, 'Hyper CLI installation failed', `Unsupported platform ${process.platform}`);
}
};

View file

@ -19,14 +19,18 @@ const colorList = [
'grayscale' 'grayscale'
]; ];
exports.getColorMap = colors => { export const getColorMap: {
<T>(colors: T): T extends (infer U)[] ? {[k: string]: U} : T;
} = (colors) => {
if (!Array.isArray(colors)) { if (!Array.isArray(colors)) {
return colors; return colors;
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return colors.reduce((result, color, index) => { return colors.reduce((result, color, index) => {
if (index < colorList.length) { if (index < colorList.length) {
result[colorList[index]] = color; result[colorList[index]] = color;
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return result; return result;
}, {}); }, {});
}; };

View file

@ -1,29 +1,29 @@
const generatePrefixedCommand = (command, shortcuts) => { const generatePrefixedCommand = (command: string, shortcuts: string[]) => {
const result = {}; const result: Record<string, string[]> = {};
const baseCmd = command.replace(/:prefix$/, ''); const baseCmd = command.replace(/:prefix$/, '');
for (let i = 1; i <= 9; i++) { for (let i = 1; i <= 9; i++) {
// 9 is a special number because it means 'last' // 9 is a special number because it means 'last'
const index = i === 9 ? 'last' : i; const index = i === 9 ? 'last' : i;
const prefixedShortcuts = shortcuts.map(shortcut => `${shortcut}+${i}`); const prefixedShortcuts = shortcuts.map((shortcut) => `${shortcut}+${i}`);
result[`${baseCmd}:${index}`] = prefixedShortcuts; result[`${baseCmd}:${index}`] = prefixedShortcuts;
} }
return result; return result;
}; };
module.exports = config => { export default (config: Record<string, string[] | string>) => {
return Object.keys(config).reduce((keymap, command) => { return Object.keys(config).reduce((keymap: Record<string, string[]>, command: string) => {
if (!command) { if (!command) {
return; return keymap;
} }
// We can have different keys for a same command. // We can have different keys for a same command.
const shortcuts = Array.isArray(config[command]) ? config[command] : [config[command]]; const _shortcuts = config[command];
const fixedShortcuts = []; const shortcuts = Array.isArray(_shortcuts) ? _shortcuts : [_shortcuts];
shortcuts.forEach(shortcut => { const fixedShortcuts: string[] = [];
shortcuts.forEach((shortcut) => {
let newShortcut = shortcut; let newShortcut = shortcut;
if (newShortcut.indexOf('cmd') !== -1) { if (newShortcut.indexOf('cmd') !== -1) {
// Mousetrap use `command` and not `cmd` // Mousetrap use `command` and not `cmd`
//eslint-disable-next-line no-console
console.warn('Your config use deprecated `cmd` in key combination. Please use `command` instead.'); console.warn('Your config use deprecated `cmd` in key combination. Please use `command` instead.');
newShortcut = newShortcut.replace('cmd', 'command'); newShortcut = newShortcut.replace('cmd', 'command');
} }

View file

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

View file

@ -0,0 +1,15 @@
const rendererTypes: Record<string, string> = {};
function getRendererTypes() {
return rendererTypes;
}
function setRendererType(uid: string, type: string) {
rendererTypes[uid] = type;
}
function unsetRendererType(uid: string) {
delete rendererTypes[uid];
}
export {getRendererTypes, setRendererType, unsetRendererType};

View file

@ -0,0 +1,60 @@
import * as Registry from 'native-reg';
import type {HKEY} from 'native-reg';
const appPath = `"${process.execPath}"`;
const regKeys = [
`Software\\Classes\\Directory\\Background\\shell\\Hyper`,
`Software\\Classes\\Directory\\shell\\Hyper`,
`Software\\Classes\\Drive\\shell\\Hyper`
];
const regParts = [
{key: 'command', name: '', value: `${appPath} "%V"`},
{name: '', value: 'Open Hyper here'},
{name: 'Icon', value: `${appPath}`}
];
function addValues(hyperKey: HKEY, commandKey: HKEY) {
try {
Registry.setValueSZ(hyperKey, regParts[1].name, regParts[1].value);
} catch (error) {
console.error(error);
}
try {
Registry.setValueSZ(hyperKey, regParts[2].name, regParts[2].value);
} catch (err) {
console.error(err);
}
try {
Registry.setValueSZ(commandKey, regParts[0].name, regParts[0].value);
} catch (err_) {
console.error(err_);
}
}
export const add = () => {
regKeys.forEach((regKey) => {
try {
const hyperKey =
Registry.openKey(Registry.HKCU, regKey, Registry.Access.ALL_ACCESS) ||
Registry.createKey(Registry.HKCU, regKey, Registry.Access.ALL_ACCESS);
const commandKey =
Registry.openKey(Registry.HKCU, `${regKey}\\${regParts[0].key}`, Registry.Access.ALL_ACCESS) ||
Registry.createKey(Registry.HKCU, `${regKey}\\${regParts[0].key}`, Registry.Access.ALL_ACCESS);
addValues(hyperKey, commandKey);
Registry.closeKey(hyperKey);
Registry.closeKey(commandKey);
} catch (error) {
console.error(error);
}
});
};
export const remove = () => {
regKeys.forEach((regKey) => {
try {
Registry.deleteTree(Registry.HKCU, regKey);
} catch (err) {
console.error(err);
}
});
};

View file

@ -1,10 +1,10 @@
// Packages // Packages
const Color = require('color'); import Color from 'color';
// returns a background color that's in hex // returns a background color that's in hex
// format including the alpha channel (e.g.: `#00000050`) // format including the alpha channel (e.g.: `#00000050`)
// input can be any css value (rgb, hsl, string…) // input can be any css value (rgb, hsl, string…)
module.exports = bgColor => { export default (bgColor: string) => {
const color = Color(bgColor); const color = Color(bgColor);
if (color.alpha() === 1) { if (color.alpha() === 1) {
@ -13,12 +13,5 @@ module.exports = bgColor => {
// http://stackoverflow.com/a/11019879/1202488 // http://stackoverflow.com/a/11019879/1202488
const alphaHex = Math.round(color.alpha() * 255).toString(16); const alphaHex = Math.round(color.alpha() * 255).toString(16);
return ( return `#${alphaHex}${color.hex().toString().substr(1)}`;
'#' +
alphaHex +
color
.hex()
.toString()
.substr(1)
);
}; };

View file

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

File diff suppressed because it is too large Load diff

View file

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

29
assets/search-icons.svg Normal file
View file

@ -0,0 +1,29 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<symbol id="left-arrow" viewBox="0 0 10 10">
<title>left arrow</title>
<g stroke-linecap="round">
<line x1="0.5" y1="5" x2="8.5" y2="5" stroke="#000" />
<line x1="0.5" y1="5" x2="3.5" y2="2" stroke="#000" />
<line x1="0.5" y1="5" x2="3.5" y2="8" stroke="#000" />
</g>
</symbol>
<symbol id="right-arrow" viewBox="0 0 10 10">
<title>right arrow</title>
<g stroke-linecap="round">
<line x1="1.5" y1="5" x2="9.5" y2="5" stroke="#000" />
<line x1="9.5" y1="5" x2="6.5" y2="2" stroke="#000" />
<line x1="9.5" y1="5" x2="6.5" y2="8" stroke="#000" />
</g>
</symbol>
<symbol id="cancel" viewBox="0 0 10 10">
<title>cancel</title>
<g stroke-linecap="round">
<line x1="5" y1="5" x2="8" y2="8" stroke="#000" />
<line x1="5" y1="5" x2="8" y2="2" stroke="#000" />
<line x1="5" y1="5" x2="2" y2="2" stroke="#000" />
<line x1="5" y1="5" x2="2" y2="8" stroke="#000" />
</g>
</symbol>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

10
ava-spectron.config.js Normal file
View file

@ -0,0 +1,10 @@
export default {
files: ['test/*'],
babel: {
compileEnhancements: false,
compileAsTests: ['**/testUtils/**/*']
},
extensions: ['ts'],
require: ['ts-node/register/transpile-only'],
timeout: '30s'
};

9
ava.config.js Normal file
View file

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

18
babel.config.json Normal file
View file

@ -0,0 +1,18 @@
{
"presets": [
"@babel/react",
"@babel/typescript"
],
"plugins": [
[
"styled-jsx/babel",
{
"vendorPrefixes": false
}
],
"@babel/plugin-proposal-numeric-separator",
"@babel/proposal-class-properties",
"@babel/proposal-object-rest-spread",
"@babel/plugin-proposal-optional-chaining"
]
}

145321
bin/yarn-standalone.js vendored

File diff suppressed because one or more lines are too long

View file

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

BIN
build/icon.fig Normal file

Binary file not shown.

Binary file not shown.

28
build/win/installer.nsh Normal file
View file

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

View file

@ -1,39 +1,45 @@
const fs = require('fs'); // eslint-disable-next-line eslint-comments/disable-enable-pair
const os = require('os'); /* eslint-disable @typescript-eslint/no-unsafe-return */
const got = require('got'); import fs from 'fs';
const registryUrl = require('registry-url')(); import os from 'os';
const pify = require('pify'); import got from 'got';
const recast = require('recast'); import registryUrlModule from 'registry-url';
const path = require('path'); 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, // 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 // otherwise use the home directory in linux/mac and userdata in windows
const applicationDirectory = const applicationDirectory =
process.env.XDG_CONFIG_HOME !== undefined process.env.XDG_CONFIG_HOME !== undefined
? path.join(process.env.XDG_CONFIG_HOME, 'hyper') ? path.join(process.env.XDG_CONFIG_HOME, 'hyper')
: process.platform == 'win32' ? path.join(process.env.APPDATA, 'Hyper') : os.homedir(); : process.platform == 'win32'
? path.join(process.env.APPDATA!, 'Hyper')
: os.homedir();
const devConfigFileName = path.join(__dirname, `../.hyper.js`); const devConfigFileName = path.join(__dirname, `../.hyper.js`);
let fileName = const fileName =
process.env.NODE_ENV !== 'production' && fs.existsSync(devConfigFileName) process.env.NODE_ENV !== 'production' && fs.existsSync(devConfigFileName)
? devConfigFileName ? devConfigFileName
: path.join(applicationDirectory, '.hyper.js'); : path.join(applicationDirectory, '.hyper.js');
/** /**
* We need to make sure the file reading and parsing is lazy so that failure to * We need to make sure the file reading and parsing is lazy so that failure to
* statically analyze the hyper configuration isn't fatal for all kinds of * statically analyze the hyper configuration isn't fatal for all kinds of
* subcommands. We can use memoization to make reading and parsing lazy. * subcommands. We can use memoization to make reading and parsing lazy.
*/ */
function memoize(fn) { function memoize<T extends (...args: any[]) => any>(fn: T): T {
let hasResult = false; let hasResult = false;
let result; let result: any;
return (...args) => { return ((...args: any[]) => {
if (!hasResult) { if (!hasResult) {
result = fn(...args); result = fn(...args);
hasResult = true; hasResult = true;
} }
return result; return result;
}; }) as T;
} }
const getFileContents = memoize(() => { const getFileContents = memoize(() => {
@ -48,24 +54,39 @@ const getFileContents = memoize(() => {
return null; return null;
}); });
const getParsedFile = memoize(() => recast.parse(getFileContents())); const getParsedFile = memoize(() => recast.parse(getFileContents()!));
const getProperties = memoize(() => getParsedFile().program.body[0].expression.right.properties); const getProperties = memoize(
(): any[] =>
const getPlugins = memoize(() => getProperties().find(property => property.key.name === 'plugins').value.elements); ((getParsedFile()?.program?.body as any[]) || []).find(
(bodyItem) =>
const getLocalPlugins = memoize( bodyItem.type === 'ExpressionStatement' &&
() => getProperties().find(property => property.key.name === 'localPlugins').value.elements bodyItem.expression.type === 'AssignmentExpression' &&
bodyItem.expression.left.object.name === 'module' &&
bodyItem.expression.left.property.name === 'exports' &&
bodyItem.expression.right.type === 'ObjectExpression'
)?.expression?.right?.properties || []
); );
const getPluginsByKey = (key: string): any[] =>
getProperties().find((property) => property?.key?.name === key)?.value?.elements || [];
const getPlugins = memoize(() => {
return getPluginsByKey('plugins');
});
const getLocalPlugins = memoize(() => {
return getPluginsByKey('localPlugins');
});
function exists() { function exists() {
return getFileContents() !== undefined; return getFileContents() !== undefined;
} }
function isInstalled(plugin, locally) { function isInstalled(plugin: string, locally?: boolean) {
const array = locally ? getLocalPlugins() : getPlugins(); const array = locally ? getLocalPlugins() : getPlugins();
if (array && Array.isArray(array)) { if (array && Array.isArray(array)) {
return array.find(entry => entry.value === plugin) !== undefined; return array.some((entry) => entry.value === plugin);
} }
return false; return false;
} }
@ -74,30 +95,32 @@ function save() {
return pify(fs.writeFile)(fileName, recast.print(getParsedFile()).code, 'utf8'); return pify(fs.writeFile)(fileName, recast.print(getParsedFile()).code, 'utf8');
} }
function existsOnNpm(plugin) { function getPackageName(plugin: string) {
const name = getPackageName(plugin);
return got.get(registryUrl + name.toLowerCase(), {timeout: 10000, json: true}).then(res => {
if (!res.body.versions) {
return Promise.reject(res);
}
});
}
function getPackageName(plugin) {
const isScoped = plugin[0] === '@'; const isScoped = plugin[0] === '@';
const nameWithoutVersion = plugin.split('#')[0]; const nameWithoutVersion = plugin.split('#')[0];
if (isScoped) { if (isScoped) {
return '@' + nameWithoutVersion.split('@')[1].replace('/', '%2f'); return `@${nameWithoutVersion.split('@')[1].replace('/', '%2f')}`;
} }
return nameWithoutVersion.split('@')[0]; return nameWithoutVersion.split('@')[0];
} }
function install(plugin, locally) { function existsOnNpm(plugin: string) {
const name = getPackageName(plugin);
return got.get<any>(registryUrl + name.toLowerCase(), {timeout: 10000, responseType: 'json'}).then((res) => {
if (!res.body.versions) {
return Promise.reject(res);
} else {
return res;
}
});
}
function install(plugin: string, locally?: boolean) {
const array = locally ? getLocalPlugins() : getPlugins(); const array = locally ? getLocalPlugins() : getPlugins();
return existsOnNpm(plugin) return existsOnNpm(plugin)
.catch(err => { .catch((err: any) => {
const {statusCode} = err; const {statusCode} = err;
if (statusCode && (statusCode === 404 || statusCode === 200)) { if (statusCode && (statusCode === 404 || statusCode === 200)) {
return Promise.reject(`${plugin} not found on npm`); return Promise.reject(`${plugin} not found on npm`);
@ -114,29 +137,24 @@ function install(plugin, locally) {
}); });
} }
function uninstall(plugin) { function uninstall(plugin: string) {
if (!isInstalled(plugin)) { if (!isInstalled(plugin)) {
return Promise.reject(`${plugin} is not installed`); return Promise.reject(`${plugin} is not installed`);
} }
const index = getPlugins().findIndex(entry => entry.value === plugin); const index = getPlugins().findIndex((entry) => entry.value === plugin);
getPlugins().splice(index, 1); getPlugins().splice(index, 1);
return save(); return save();
} }
function list() { function list() {
if (Array.isArray(getPlugins())) { if (getPlugins().length > 0) {
return getPlugins() return getPlugins()
.map(plugin => plugin.value) .map((plugin) => plugin.value)
.join('\n'); .join('\n');
} }
return false; return false;
} }
module.exports.configPath = fileName; export const configPath = fileName;
module.exports.exists = exists; export {exists, existsOnNpm, isInstalled, install, uninstall, list};
module.exports.existsOnNpm = existsOnNpm;
module.exports.isInstalled = isInstalled;
module.exports.install = install;
module.exports.uninstall = uninstall;
module.exports.list = list;

View file

@ -1,219 +0,0 @@
// This is a CLI tool, using console is OK
/* eslint no-console: 0 */
const {spawn, exec} = require('child_process');
const {isAbsolute, resolve} = require('path');
const {existsSync} = require('fs');
const {version} = require('../app/package');
const pify = require('pify');
const args = require('args');
const chalk = require('chalk');
const opn = require('opn');
const columnify = require('columnify');
const got = require('got');
const ora = require('ora');
const api = require('./api');
const PLUGIN_PREFIX = 'hyper-';
let commandPromise;
const assertPluginName = pluginName => {
if (!pluginName) {
console.error(chalk.red('Plugin name is required'));
process.exit(1);
}
};
const checkConfig = () => {
if (api.exists()) {
return true;
}
let msg = chalk.red(`Error! Config file not found: ${api.configPath}\n`);
msg += 'Please launch Hyper and retry.';
console.error(msg);
process.exit(1);
};
args.command(['i', 'install'], 'Install a plugin', (name, args_) => {
checkConfig();
const pluginName = args_[0];
assertPluginName(pluginName);
commandPromise = api
.install(pluginName)
.then(() => console.log(chalk.green(`${pluginName} installed successfully!`)))
.catch(err => console.error(chalk.red(err)));
});
args.command(['u', 'uninstall', 'rm', 'remove'], 'Uninstall a plugin', (name, args_) => {
checkConfig();
const pluginName = args_[0];
assertPluginName(pluginName);
commandPromise = api
.uninstall(pluginName)
.then(() => console.log(chalk.green(`${pluginName} uninstalled successfully!`)))
.catch(err => console.log(chalk.red(err)));
});
args.command(['ls', 'list'], 'List installed plugins', () => {
checkConfig();
let plugins = api.list();
if (plugins) {
console.log(plugins);
} else {
console.log(chalk.red(`No plugins installed yet.`));
}
process.exit(0);
});
const lsRemote = pattern => {
// note that no errors are catched by this function
const URL = `https://api.npms.io/v2/search?q=${(pattern && `${pattern}+`) || ''}keywords:hyper-plugin,hyper-theme`;
return got(URL)
.then(response => JSON.parse(response.body).results)
.then(entries => entries.map(entry => entry.package))
.then(entries => entries.filter(entry => entry.name.indexOf(PLUGIN_PREFIX) === 0))
.then(entries =>
entries.map(({name, description}) => {
return {name, description};
})
)
.then(entries =>
entries.map(entry => {
entry.name = chalk.green(entry.name);
return entry;
})
);
};
args.command(['s', 'search'], 'Search for plugins on npm', (name, args_) => {
const spinner = ora('Searching').start();
const query = args_[0] ? args_[0].toLowerCase() : '';
commandPromise = lsRemote(query)
.then(entries => {
if (entries.length === 0) {
spinner.fail();
console.error(chalk.red(`Your search '${query}' did not match any plugins`));
console.error(`${chalk.red('Try')} ${chalk.green('hyper ls-remote')}`);
process.exit(1);
} else {
let msg = columnify(entries);
spinner.succeed();
msg = msg.substring(msg.indexOf('\n') + 1); // remove header
console.log(msg);
}
})
.catch(err => {
spinner.fail();
console.error(chalk.red(err)); // TODO
});
});
args.command(['lsr', 'list-remote', 'ls-remote'], 'List plugins available on npm', () => {
const spinner = ora('Searching').start();
commandPromise = lsRemote()
.then(entries => {
let msg = columnify(entries);
spinner.succeed();
msg = msg.substring(msg.indexOf('\n') + 1); // remove header
console.log(msg);
})
.catch(err => {
spinner.fail();
console.error(chalk.red(err)); // TODO
});
});
args.command(['d', 'docs', 'h', 'home'], 'Open the npm page of a plugin', (name, args_) => {
const pluginName = args_[0];
assertPluginName(pluginName);
opn(`http://ghub.io/${pluginName}`, {wait: false});
process.exit(0);
});
args.command(['version'], 'Show the version of hyper', () => {
console.log(version);
process.exit(0);
});
args.command(['<default>'], 'Launch Hyper');
args.option(['v', 'verbose'], 'Verbose mode', false);
const main = argv => {
const flags = args.parse(argv, {
name: 'hyper',
version: false,
mri: {
boolean: ['v', 'verbose']
}
});
if (commandPromise) {
return commandPromise;
}
const env = Object.assign({}, process.env, {
// this will signal Hyper that it was spawned from this module
HYPER_CLI: '1',
ELECTRON_NO_ATTACH_CONSOLE: '1'
});
delete env['ELECTRON_RUN_AS_NODE'];
if (flags.verbose) {
env['ELECTRON_ENABLE_LOGGING'] = '1';
}
const options = {
detached: true,
env
};
const args_ = args.sub.map(arg => {
const cwd = isAbsolute(arg) ? arg : resolve(process.cwd(), arg);
if (!existsSync(cwd)) {
console.error(chalk.red(`Error! Directory or file does not exist: ${cwd}`));
process.exit(1);
}
return cwd;
});
if (!flags.verbose) {
options['stdio'] = 'ignore';
if (process.platform === 'darwin') {
//Use `open` to prevent multiple Hyper process
const cmd = `open -b co.zeit.hyper ${args_}`;
const opts = {
env
};
return pify(exec)(cmd, opts);
}
}
const child = spawn(process.execPath, args_, options);
if (flags.verbose) {
child.stdout.on('data', data => console.log(data.toString('utf8')));
child.stderr.on('data', data => console.error(data.toString('utf8')));
}
if (flags.verbose) {
return new Promise(c => child.once('exit', () => c(null)));
}
child.unref();
return Promise.resolve();
};
function eventuallyExit(code) {
setTimeout(() => process.exit(code), 100);
}
main(process.argv)
.then(() => eventuallyExit(0))
.catch(err => {
console.error(err.stack ? err.stack : err);
eventuallyExit(1);
});

256
cli/index.ts Normal file
View file

@ -0,0 +1,256 @@
// This is a CLI tool, using console is OK
/* eslint no-console: 0 */
import {spawn, exec, SpawnOptions} from 'child_process';
import {isAbsolute, resolve} from 'path';
import {existsSync} from 'fs';
import {version} from '../app/package.json';
import pify from 'pify';
import args from 'args';
import chalk from 'chalk';
import open from 'open';
import columnify from 'columnify';
import got from 'got';
import ora from 'ora';
import * as api from './api';
let commandPromise: Promise<void> | undefined;
const assertPluginName = (pluginName: string) => {
if (!pluginName) {
console.error(chalk.red('Plugin name is required'));
process.exit(1);
}
};
const checkConfig = () => {
if (api.exists()) {
return true;
}
let msg = chalk.red(`Error! Config file not found: ${api.configPath}\n`);
msg += 'Please launch Hyper and retry.';
console.error(msg);
process.exit(1);
};
args.command(
'install',
'Install a plugin',
(name, args_) => {
checkConfig();
const pluginName = args_[0];
assertPluginName(pluginName);
commandPromise = api
.install(pluginName)
.then(() => console.log(chalk.green(`${pluginName} installed successfully!`)))
.catch((err) => console.error(chalk.red(err)));
},
['i']
);
args.command(
'uninstall',
'Uninstall a plugin',
(name, args_) => {
checkConfig();
const pluginName = args_[0];
assertPluginName(pluginName);
commandPromise = api
.uninstall(pluginName)
.then(() => console.log(chalk.green(`${pluginName} uninstalled successfully!`)))
.catch((err) => console.log(chalk.red(err)));
},
['u', 'rm', 'remove']
);
args.command(
'list',
'List installed plugins',
() => {
checkConfig();
const plugins = api.list();
if (plugins) {
console.log(plugins);
} else {
console.log(chalk.red(`No plugins installed yet.`));
}
process.exit(0);
},
['ls']
);
const lsRemote = (pattern?: string) => {
// note that no errors are catched by this function
const URL = `https://api.npms.io/v2/search?q=${
(pattern && `${pattern}+`) || ''
}keywords:hyper-plugin,hyper-theme&size=250`;
type npmResult = {package: {name: string; description: string}};
return got(URL)
.then((response) => JSON.parse(response.body).results as npmResult[])
.then((entries) => entries.map((entry) => entry.package))
.then((entries) =>
entries.map(({name, description}) => {
return {name, description};
})
)
.then((entries) =>
entries.map((entry) => {
entry.name = chalk.green(entry.name);
return entry;
})
);
};
args.command(
'search',
'Search for plugins on npm',
(name, args_) => {
const spinner = ora('Searching').start();
const query = args_[0] ? args_[0].toLowerCase() : '';
commandPromise = lsRemote(query)
.then((entries) => {
if (entries.length === 0) {
spinner.fail();
console.error(chalk.red(`Your search '${query}' did not match any plugins`));
console.error(`${chalk.red('Try')} ${chalk.green('hyper ls-remote')}`);
process.exit(1);
} else {
let msg = columnify(entries);
spinner.succeed();
msg = msg.substring(msg.indexOf('\n') + 1); // remove header
console.log(msg);
}
})
.catch((err) => {
spinner.fail();
console.error(chalk.red(err)); // TODO
});
},
['s']
);
args.command(
'list-remote',
'List plugins available on npm',
() => {
const spinner = ora('Searching').start();
commandPromise = lsRemote()
.then((entries) => {
let msg = columnify(entries);
spinner.succeed();
msg = msg.substring(msg.indexOf('\n') + 1); // remove header
console.log(msg);
})
.catch((err) => {
spinner.fail();
console.error(chalk.red(err)); // TODO
});
},
['lsr', 'ls-remote']
);
args.command(
'docs',
'Open the npm page of a plugin',
(name, args_) => {
const pluginName = args_[0];
assertPluginName(pluginName);
void open(`http://ghub.io/${pluginName}`, {wait: false});
process.exit(0);
},
['d', 'h', 'home']
);
args.command(
'version',
'Show the version of hyper',
() => {
console.log(version);
process.exit(0);
},
[]
);
args.command('<default>', 'Launch Hyper');
args.option(['v', 'verbose'], 'Verbose mode', false);
const main = (argv: string[]) => {
const flags = args.parse(argv, {
name: 'hyper',
version: false,
mri: {
boolean: ['v', 'verbose']
}
} as any);
if (commandPromise) {
return commandPromise;
}
const env = Object.assign({}, process.env, {
// this will signal Hyper that it was spawned from this module
HYPER_CLI: '1',
ELECTRON_NO_ATTACH_CONSOLE: '1'
});
delete env['ELECTRON_RUN_AS_NODE'];
if (flags.verbose) {
env['ELECTRON_ENABLE_LOGGING'] = '1';
}
const options: SpawnOptions = {
detached: true,
env
};
const args_ = args.sub.map((arg) => {
const cwd = isAbsolute(arg) ? arg : resolve(process.cwd(), arg);
if (!existsSync(cwd)) {
console.error(chalk.red(`Error! Directory or file does not exist: ${cwd}`));
process.exit(1);
}
return cwd;
});
if (!flags.verbose) {
options['stdio'] = 'ignore';
if (process.platform === 'darwin') {
//Use `open` to prevent multiple Hyper process
const cmd = `open -b co.zeit.hyper ${args_}`;
const opts = {
env
};
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')));
}
if (flags.verbose) {
return new Promise((c) => child.once('exit', () => c(null)));
}
child.unref();
return Promise.resolve();
};
function eventuallyExit(code: number) {
setTimeout(() => process.exit(code), 100);
}
main(process.argv)
.then(() => eventuallyExit(0))
.catch((err) => {
console.error(err.stack ? err.stack : err);
eventuallyExit(1);
});

132
electron-builder.json Normal file
View file

@ -0,0 +1,132 @@
{
"$schema": "http://json.schemastore.org/electron-builder",
"appId": "co.zeit.hyper",
"directories": {
"app": "target"
},
"extraResources": [
"./bin/yarn-standalone.js",
"./bin/cli.js",
{
"from": "./build/${os}/",
"to": "./bin/",
"filter": [
"hyper*"
]
}
],
"linux": {
"category": "TerminalEmulator",
"target": [
{
"target": "deb",
"arch": [
"x64",
"arm64"
]
},
{
"target": "AppImage",
"arch": [
"x64",
"arm64"
]
},
{
"target": "rpm",
"arch": [
"x64",
"arm64"
]
},
{
"target": "snap",
"arch": [
"x64"
]
}
]
},
"win": {
"target": [
"nsis"
],
"rfc3161TimeStampServer": "http://timestamp.comodoca.com"
},
"nsis": {
"include": "build/win/installer.nsh",
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true
},
"mac": {
"target": {
"target": "default",
"arch": [
"x64",
"arm64"
]
},
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
"category": "public.app-category.developer-tools",
"extendInfo": {
"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."
},
"darkModeSupport": true
},
"deb": {
"compression": "bzip2",
"afterInstall": "./build/linux/after-install.tpl"
},
"rpm": {
"afterInstall": "./build/linux/after-install.tpl"
},
"snap": {
"confinement": "classic",
"publish": "github"
},
"protocols": {
"name": "ssh URL",
"schemes": [
"ssh"
]
}
}

View file

@ -1,7 +0,0 @@
{
"compilerOptions": {
"jsx": "react",
"target": "es6"
},
"exclude": ["node_modules", "**/node_modules/*", "bin/*", "renderer/*"]
}

View file

@ -1,17 +0,0 @@
import {CONFIG_LOAD, CONFIG_RELOAD} from '../constants/config';
export function loadConfig(config) {
return {
type: CONFIG_LOAD,
config
};
}
export function reloadConfig(config) {
const now = Date.now();
return {
type: CONFIG_RELOAD,
config,
now
};
}

19
lib/actions/config.ts Normal file
View file

@ -0,0 +1,19 @@
import {CONFIG_LOAD, CONFIG_RELOAD} from '../constants/config';
import {HyperActions} from '../hyper';
import {configOptions} from '../config';
export function loadConfig(config: configOptions): HyperActions {
return {
type: CONFIG_LOAD,
config
};
}
export function reloadConfig(config: configOptions): HyperActions {
const now = Date.now();
return {
type: CONFIG_RELOAD,
config,
now
};
}

View file

@ -8,9 +8,10 @@ import {
} from '../constants/ui'; } from '../constants/ui';
import rpc from '../rpc'; import rpc from '../rpc';
import {userExitTermGroup, setActiveGroup} from './term-groups'; import {userExitTermGroup, setActiveGroup} from './term-groups';
import {HyperDispatch} from '../hyper';
export function closeTab(uid) { export function closeTab(uid: string) {
return dispatch => { return (dispatch: HyperDispatch) => {
dispatch({ dispatch({
type: CLOSE_TAB, type: CLOSE_TAB,
uid, uid,
@ -21,8 +22,8 @@ export function closeTab(uid) {
}; };
} }
export function changeTab(uid) { export function changeTab(uid: string) {
return dispatch => { return (dispatch: HyperDispatch) => {
dispatch({ dispatch({
type: CHANGE_TAB, type: CHANGE_TAB,
uid, uid,
@ -34,29 +35,29 @@ export function changeTab(uid) {
} }
export function maximize() { export function maximize() {
return dispatch => { return (dispatch: HyperDispatch) => {
dispatch({ dispatch({
type: UI_WINDOW_MAXIMIZE, type: UI_WINDOW_MAXIMIZE,
effect() { effect() {
rpc.emit('maximize'); rpc.emit('maximize', null);
} }
}); });
}; };
} }
export function unmaximize() { export function unmaximize() {
return dispatch => { return (dispatch: HyperDispatch) => {
dispatch({ dispatch({
type: UI_WINDOW_UNMAXIMIZE, type: UI_WINDOW_UNMAXIMIZE,
effect() { effect() {
rpc.emit('unmaximize'); rpc.emit('unmaximize', null);
} }
}); });
}; };
} }
export function openHamburgerMenu(coordinates) { export function openHamburgerMenu(coordinates: {x: number; y: number}) {
return dispatch => { return (dispatch: HyperDispatch) => {
dispatch({ dispatch({
type: UI_OPEN_HAMBURGER_MENU, type: UI_OPEN_HAMBURGER_MENU,
effect() { effect() {
@ -67,22 +68,22 @@ export function openHamburgerMenu(coordinates) {
} }
export function minimize() { export function minimize() {
return dispatch => { return (dispatch: HyperDispatch) => {
dispatch({ dispatch({
type: UI_WINDOW_MINIMIZE, type: UI_WINDOW_MINIMIZE,
effect() { effect() {
rpc.emit('minimize'); rpc.emit('minimize', null);
} }
}); });
}; };
} }
export function close() { export function close() {
return dispatch => { return (dispatch: HyperDispatch) => {
dispatch({ dispatch({
type: UI_WINDOW_CLOSE, type: UI_WINDOW_CLOSE,
effect() { effect() {
rpc.emit('close'); rpc.emit('close', null);
} }
}); });
}; };

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