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/dist
app/node_modules
app/typings
assets
website
bin
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
*.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.
Before you submit this; let's make sure of a few things.
Please make sure the following boxes are ticked if they are correct.
If not, please try and fulfil these first.
If not, please try and fulfill these first.
-->
<!-- Checked checkbox should look like this: [x] -->
- [ ] I am on the [latest](https://github.com/zeit/hyper/releases/latest) Hyper.app version
- [ ] I have searched the [issues](https://github.com/zeit/hyper/issues) of this repo and believe that this is not a duplicate
- [ ] I am on the [latest](https://github.com/vercel/hyper/releases/latest) Hyper.app version
- [ ] I have searched the [issues](https://github.com/vercel/hyper/issues) of this repo and believe that this is not a duplicate
<!--
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,
If there's anything left to do and if there are any related PRs
- It'd also be extremely helpful to enable us to update your PR incase we need to rebase or what-not by checking `Allow edits from maintainers`
- If your PR changes some API, please make a PR for hyper website too: https://github.com/zeit/hyper-site.
- If your PR changes some API, please make a PR for hyper website too: https://github.com/vercel/hyper-site.
Thanks, again! -->

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
dist
app/renderer
target
bin/cli.*
# dependencies
@ -15,3 +16,6 @@ yarn-error.log
.hyper_plugins
.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",
"name": "Launch Hyper",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"program": "${workspaceRoot}/app/index.js",
"program": "${workspaceRoot}/target/index.js",
"protocol": "inspector"
},
{

View file

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

View file

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

View file

@ -3,11 +3,11 @@
## Workflow
### Run Hyper in dev mode
Hyper can be run in dev mode by cloning this repository and following the ["Contributing" section of our README](https://github.com/zeit/hyper#contribute).
Hyper can be run in dev mode by cloning this repository and following the ["Contributing" section of our README](https://github.com/vercel/hyper#contribute).
In dev mode you'll get more ouput and access to React/Redux dev-tools in Electron.
Prerequisites and steps are described in the ["Contributing" section of our README](https://github.com/zeit/hyper#contribute).
Prerequisites and steps are described in the ["Contributing" section of our README](https://github.com/vercel/hyper#contribute).
Be sure to use the `canary` branch.
### Create a dev config file
@ -30,7 +30,7 @@ module.exports = {
```
### Running your plugin
To load, your plugin should expose at least one API method. All possible methods are listed [here](https://github.com/zeit/hyper/blob/canary/app/plugins/extensions.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.
@ -41,7 +41,7 @@ Almost all available API methods can be found on https://hyper.is.
If there's any missing, let us know or submit a PR to document it!
### Components
You can decorate almost all Hyper components with a Higher-Order Component (HOC). To understand their architecture, the easiest way is to use React dev-tools to dig in to their 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.
```js
@ -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 uses `xterm.js` instead of `hterm`. It means that PTY output renders now in a canvas element, not with a hackable DOM structure.
For example, plugins can't use TermCSS in order to modify text or link styles anymore. It is now required to use available configuration params that are passed down to `xterm.js`.

View file

@ -1,23 +1,39 @@
![](https://assets.zeit.co/image/upload/v1549723846/repositories/hyper/hyper-3-repo-banner.png)
![](https://assets.vercel.com/image/upload/v1549723846/repositories/hyper/hyper-3-repo-banner.png)
[![macOS CI Status](https://circleci.com/gh/zeit/hyper.svg?style=shield)](https://circleci.com/gh/zeit/hyper)
[![Windows CI status](https://ci.appveyor.com/api/projects/status/kqvb4oa772an58sc?svg=true)](https://ci.appveyor.com/project/zeit/hyper)
[![Linux CI status](https://travis-ci.org/zeit/hyper.svg?branch=master)](https://travis-ci.org/zeit/hyper)
<p align="center">
<a aria-label="Vercel logo" href="https://vercel.com">
<img src="https://img.shields.io/badge/MADE%20BY%20Vercel-000000.svg?style=for-the-badge&logo=vercel&labelColor=000000&logoWidth=20">
</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)
[![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/zeit/hyper)
For more details, head to: https://hyper.is
## Project goals
The goal of the project is to create a beautiful and extensible experience for command-line interface users, built on open web standards. In the beginning, our focus will be primarily around speed, stability and the development of the correct API for extension authors.
In the future, we anticipate the community will come up with innovative additions to enhance what could be the simplest, most powerful and well-tested interface for productivity.
## Usage
[Download the latest release!](https://hyper.is/#installation)
### Linux
#### Arch and derivatives
Hyper is available in the [AUR](https://aur.archlinux.org/packages/hyper/). Use an AUR package manager like [aurman](https://github.com/polygamma/aurman)
Hyper is available in the [AUR](https://aur.archlinux.org/packages/hyper/). Use an AUR [package manager](https://wiki.archlinux.org/index.php/AUR_helpers) e.g. [paru](https://github.com/Morganamilo/paru)
```sh
aurman -S hyper
paru -S hyper
```
#### NixOS
Hyper is available as [Nix package](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/hyper/default.nix), to install the app run this command:
```sh
nix-env -i hyper
```
### macOS
@ -26,7 +42,7 @@ Use [Homebrew Cask](https://brew.sh) to download the app by running these comman
```bash
brew update
brew cask install hyper
brew install --cask hyper
```
### Windows
@ -83,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
to the Terms of Service by running `sudo xcodebuild` after a fresh Xcode installation).
##### Error with `c++` on macOS when running `yarn`
If you are getting compiler errors when running `yarn` add the environment variable `export CXX=clang++`
##### Error with `codesign` on macOS when running `yarn run dist`
If you have issues in the `codesign` step when running `yarn run dist` on macOS, you can temporarily disable code signing locally by setting
@ -90,8 +110,8 @@ If you have issues in the `codesign` step when running `yarn run dist` on macOS,
## Related Repositories
- [Art](https://github.com/zeit/art/tree/master/hyper)
- [Website](https://github.com/zeit/hyper-site)
- [Sample Extension](https://github.com/zeit/hyperpower)
- [Sample Theme](https://github.com/zeit/hyperyellow)
- [Art](https://github.com/vercel/art/tree/master/hyper)
- [Website](https://github.com/vercel/hyper-site)
- [Sample Extension](https://github.com/vercel/hyperpower)
- [Sample Theme](https://github.com/vercel/hyperyellow)
- [Awesome Hyper](https://github.com/bnb/awesome-hyper)

1
app/.yarnrc Normal file
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;
const {EventEmitter} = require('events');
class AutoUpdater extends EventEmitter {
class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
updateURL!: string;
quitAndInstall() {
this.emitError('QuitAndInstall unimplemented');
}
@ -11,8 +10,8 @@ class AutoUpdater extends EventEmitter {
return this.updateURL;
}
setFeedURL(updateURL) {
this.updateURL = updateURL;
setFeedURL(options: Electron.FeedURLOptions) {
this.updateURL = options.url;
}
checkForUpdates() {
@ -22,9 +21,10 @@ class AutoUpdater extends EventEmitter {
this.emit('checking-for-update');
fetch(this.updateURL)
.then(res => {
.then((res) => {
if (res.status === 204) {
return this.emit('update-not-available');
this.emit('update-not-available');
return;
}
return res.json().then(({name, notes, pub_date}) => {
// Only name is mandatory, needed to construct release URL.
@ -39,12 +39,12 @@ class AutoUpdater extends EventEmitter {
.catch(this.emitError.bind(this));
}
emitError(error) {
emitError(error: string | Error) {
if (typeof error === 'string') {
error = new Error(error);
}
this.emit('error', error, error.message);
this.emit('error', error);
}
}
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
termCSS: '',
// set custom startup directory (must be an absolute path)
workingDirectory: '',
// if you're using a Linux setup which show native menus, set to false
// default: `true` on Linux, `true` on Windows, ignored on macOS
showHamburgerMenu: '',
@ -89,6 +92,8 @@ module.exports = {
lightMagenta: '#FD7CFC',
lightCyan: '#68FDFE',
lightWhite: '#FFFFFF',
limeGreen: '#32CD32',
lightCoral: '#F08080',
},
// the shell to run when spawning a new session (i.e. /usr/local/bin/fish)
@ -98,11 +103,17 @@ module.exports = {
// - Make sure to use a full path if the binary name doesn't work
// - Remove `--login` in shellArgs
//
// Bash on Windows
// - Example: `C:\\Windows\\System32\\bash.exe`
// Windows Subsystem for Linux (WSL) - previously Bash on Windows
// - Example: `C:\\Windows\\System32\\wsl.exe`
//
// Git-bash on Windows
// - Example: `C:\\Program Files\\Git\\bin\\bash.exe`
//
// PowerShell on Windows
// - Example: `C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`
//
// Cygwin
// - Example: `C:\\cygwin64\\bin\\bash.exe`
shell: '',
// for setting shell arguments (i.e. for using interactive shellArgs: `['-i']`)
@ -112,9 +123,14 @@ module.exports = {
// for environment variables
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',
// An absolute file path to a sound file on the machine.
// bellSoundURL: '/path/to/sound/file',
// if `true` (without backticks and without quotes), selected text will automatically be copied to the clipboard
copyOnSelect: false,
@ -130,12 +146,16 @@ module.exports = {
// (inside tmux or vim with mouse mode enabled for example).
macOptionSelectionMode: 'vertical',
// URL to custom bell
// bellSoundURL: 'http://example.com/bell.mp3',
// Whether to use the WebGL renderer. Set it to false to use canvas-based
// rendering (slower, but supports transparent backgrounds)
webGLRenderer: true,
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
},

View file

@ -1,16 +1,17 @@
const {moveSync, copySync, existsSync, writeFileSync, readFileSync, lstatSync} = require('fs-extra');
const {sync: mkdirpSync} = require('mkdirp');
const {defaultCfg, cfgPath, legacyCfgPath, plugs, defaultPlatformKeyPath} = require('./paths');
const {_init, _extractDefault} = require('./init');
const notify = require('../notify');
import {moveSync, copySync, existsSync, writeFileSync, readFileSync, lstatSync} from 'fs-extra';
import {sync as mkdirpSync} from 'mkdirp';
import {defaultCfg, cfgPath, legacyCfgPath, plugs, defaultPlatformKeyPath} from './paths';
import {_init, _extractDefault} from './init';
import notify from '../notify';
import {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
// to text formatted with DOS line endings. We do this because the default
// text editor on Windows (notepad) doesn't Deal with LF files. Still. In 2017.
const crlfify = function(str) {
const crlfify = (str: string) => {
return str.replace(/\r?\n/g, '\r\n');
};
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.
// so as to not override any existing files
const saveAsBackup = src => {
const saveAsBackup = (src: string) => {
let attempt = 1;
while (attempt < 100) {
try {
const backupPath = src + '.backup' + (attempt === 1 ? '' : attempt);
const backupPath = `${src}.backup${attempt === 1 ? '' : attempt}`;
if (!existsSync(backupPath)) {
moveSync(src, 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');
};
@ -81,7 +77,7 @@ const migrateHyper2Config = () => {
);
};
const _importConf = function() {
const _importConf = () => {
// init plugin directories if not present
mkdirpSync(plugs.base);
mkdirpSync(plugs.local);
@ -89,47 +85,49 @@ const _importConf = function() {
try {
migrateHyper2Config();
} catch (err) {
//eslint-disable-next-line no-console
console.error(err);
}
let defaultCfgRaw = '';
try {
const defaultCfgRaw = readFileSync(defaultCfg, 'utf8');
const _defaultCfg = _extractDefault(defaultCfgRaw);
// Importing platform specific keymap
try {
const content = readFileSync(defaultPlatformKeyPath(), 'utf8');
const mapping = JSON.parse(content);
_defaultCfg.keymaps = mapping;
} catch (err) {
//eslint-disable-next-line no-console
console.error(err);
}
// Import user config
try {
const userCfg = readFileSync(cfgPath, 'utf8');
return {userCfg, defaultCfg: _defaultCfg};
} catch (err) {
_write(cfgPath, defaultCfgRaw);
return {userCfg: defaultCfgRaw, defaultCfg: _defaultCfg};
}
defaultCfgRaw = readFileSync(defaultCfg, 'utf8');
} catch (err) {
//eslint-disable-next-line no-console
console.log(err);
}
const _defaultCfg = _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();
defaultConfig = imported.defaultCfg;
const result = _init(imported);
return result;
};
exports.getDefaultConfig = () => {
export const getDefaultConfig = () => {
if (!defaultConfig) {
defaultConfig = _extractDefault(_importConf().defaultCfg);
defaultConfig = _importConf().defaultCfg;
}
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
const {homedir} = require('os');
const {app} = require('electron');
const {statSync} = require('fs');
const {resolve, join} = require('path');
const isDev = require('electron-is-dev');
import {homedir} from 'os';
import {app} from 'electron';
import {statSync} from 'fs';
import {resolve, join} from 'path';
import isDev from 'electron-is-dev';
const cfgFile = '.hyper.js';
const defaultCfgFile = 'config-default.js';
@ -14,11 +14,13 @@ const homeDirectory = homedir();
const applicationDirectory =
process.env.XDG_CONFIG_HOME !== undefined
? 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 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 devCfg = join(devDir, cfgFile);
@ -30,7 +32,6 @@ if (isDev) {
statSync(devCfg);
cfgPath = devCfg;
cfgDir = devDir;
//eslint-disable-next-line no-console
console.log('using config file:', cfgPath);
} catch (err) {
// ignore
@ -69,7 +70,7 @@ const defaultPlatformKeyPath = () => {
}
};
module.exports = {
export {
cfgDir,
cfgPath,
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.
if (['--help', '-v', '--version'].includes(process.argv[1])) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {version} = require('./package');
const configLocation = process.platform === 'win32' ? process.env.userprofile + '\\.hyper.js' : '~/.hyper.js';
//eslint-disable-next-line no-console
const configLocation = process.platform === 'win32' ? `${process.env.userprofile}\\.hyper.js` : '~/.hyper.js';
console.log(`Hyper version ${version}`);
//eslint-disable-next-line no-console
console.log('Hyper does not accept any command line arguments. Please modify the config file instead.');
//eslint-disable-next-line no-console
console.log(`Hyper configuration file located at: ${configLocation}`);
// eslint-disable-next-line unicorn/no-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
const {resolve} = require('path');
import {resolve} from 'path';
// Packages
const {app, BrowserWindow, Menu} = require('electron');
const {gitDescribe} = require('git-describe');
const isDev = require('electron-is-dev');
const config = require('./config');
import {app, BrowserWindow, Menu} from 'electron';
import {gitDescribe} from 'git-describe';
import isDev from 'electron-is-dev';
import * as config from './config';
// set up config
config.setup();
const plugins = require('./plugins');
const {installCLI} = require('./utils/cli-install');
const AppMenu = require('./menus/menu');
const Window = require('./ui/window');
const windowUtils = require('./utils/window-utils');
import * as plugins from './plugins';
import {installCLI} from './utils/cli-install';
import * as AppMenu from './menus/menu';
import {newWindow} from './ui/window';
import * as windowUtils from './utils/window-utils';
const windowSet = new Set([]);
const windowSet = new Set<BrowserWindow>([]);
// expose to plugins
app.config = config;
@ -84,39 +45,56 @@ app.getLastFocusedWindow = () => {
});
};
//eslint-disable-next-line no-console
console.log('Disabling Chromium GPU blacklist');
app.commandLine.appendSwitch('ignore-gpu-blacklist');
if (isDev) {
//eslint-disable-next-line no-console
console.log('running in dev mode');
// Override default appVersion which is set from package.json
gitDescribe({customArguments: ['--tags']}, (error, gitInfo) => {
gitDescribe({customArguments: ['--tags']}, (error: any, gitInfo: any) => {
if (!error) {
app.setVersion(gitInfo.raw);
}
});
} else {
//eslint-disable-next-line no-console
console.log('running in prod mode');
}
const url = 'file://' + resolve(isDev ? __dirname : app.getAppPath(), 'index.html');
//eslint-disable-next-line no-console
const url = `file://${resolve(isDev ? __dirname : app.getAppPath(), 'index.html')}`;
console.log('electron will open', url);
async function installDevExtensions(isDev_: boolean) {
if (!isDev_) {
return [];
}
const 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', () =>
installDevExtensions(isDev)
.then(() => {
function createWindow(fn, options = {}) {
function createWindow(
fn?: (win: BrowserWindow) => void,
options: {size?: [number, number]; position?: [number, number]} = {}
) {
const cfg = plugins.getDecoratedConfig();
const winSet = config.getWin();
let [startX, startY] = winSet.position;
const [width, height] = options.size ? options.size : cfg.windowSize || winSet.size;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {screen} = require('electron');
const winPos = options.position;
@ -154,9 +132,9 @@ app.on('ready', () =>
[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);
hwin.loadURL(url);
void hwin.loadURL(url);
// the window can be closed by the browser process itself
hwin.on('close', () => {
@ -164,12 +142,6 @@ app.on('ready', () =>
windowSet.delete(hwin);
});
hwin.on('closed', () => {
if (process.platform !== 'darwin' && windowSet.size === 0) {
app.quit();
}
});
return hwin;
}
@ -188,6 +160,12 @@ app.on('ready', () =>
}
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
const makeMenu = () => {
const menu = plugins.decorateMenu(AppMenu.createMenu(createWindow, plugins.getLoadedPluginVersions));
@ -214,26 +192,26 @@ app.on('ready', () =>
if (!isDev) {
// check if should be set/removed as default ssh protocol client
if (config.getConfig().defaultSSHApp && !app.isDefaultProtocolClient('ssh')) {
//eslint-disable-next-line no-console
console.log('Setting Hyper as default client for ssh:// protocol');
app.setAsDefaultProtocolClient('ssh');
} else if (!config.getConfig().defaultSSHApp && app.isDefaultProtocolClient('ssh')) {
//eslint-disable-next-line no-console
console.log('Removing Hyper from default client for ssh:// protocol');
app.removeAsDefaultProtocolClient('ssh');
}
installCLI(false);
void installCLI(false);
}
})
.catch(err => {
//eslint-disable-next-line no-console
.catch((err) => {
console.error('Error while loading devtools extensions', err);
})
);
app.on('open-file', (event, path) => {
/**
* Get last focused BrowserWindow or create new if none and callback
* @param callback Function to call with the BrowserWindow
*/
function GetWindow(callback: (win: BrowserWindow) => void) {
const lastWindow = app.getLastFocusedWindow();
const callback = win => win.rpc.emit('open file', {path});
if (lastWindow) {
callback(lastWindow);
} 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.
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",
"pane:next": "command+]",
"pane:prev": "command+[",
"pane:splitVertical": "command+d",
"pane:splitHorizontal": "command+shift+d",
"pane:splitRight": "command+d",
"pane:splitDown": "command+shift+d",
"pane:close": "command+w",
"editor:undo": "command+z",
"editor:redo": "command+y",
@ -39,6 +39,8 @@
"editor:copy": "command+c",
"editor:paste": "command+v",
"editor:selectAll": "command+a",
"editor:search": "command+f",
"editor:search-close": "esc",
"editor:movePreviousWord": "alt+left",
"editor:moveNextWord": "alt+right",
"editor:moveBeginningLine": "command+left",

View file

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

View file

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

View file

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

View file

@ -1,10 +1,14 @@
// This menu label is overrided by OSX to be the appName
// 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 {
label: `${app.getName()}`,
label: `${app.name}`,
submenu: [
{
label: 'About Hyper',
@ -36,7 +40,7 @@ module.exports = (commandKeys, execCommand, showAbout) => {
role: 'hide'
},
{
role: 'hideothers'
role: 'hideOthers'
},
{
role: 'unhide'

View file

@ -1,5 +1,10 @@
module.exports = (commandKeys, execCommand) => {
const submenu = [
import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
export default (
commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
) => {
const submenu: MenuItemConstructorOptions[] = [
{
label: 'Undo',
accelerator: commandKeys['editor:undo'],
@ -23,7 +28,7 @@ module.exports = (commandKeys, execCommand) => {
command: 'editor:copy',
accelerator: commandKeys['editor:copy'],
registerAccelerator: true
},
} as any,
{
role: 'paste',
accelerator: commandKeys['editor:paste']
@ -113,6 +118,13 @@ module.exports = (commandKeys, execCommand) => {
click(item, 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';
return {
@ -22,17 +27,17 @@ module.exports = (commandKeys, execCommand) => {
type: 'separator'
},
{
label: 'Split Horizontally',
accelerator: commandKeys['pane:splitHorizontal'],
label: 'Split Down',
accelerator: commandKeys['pane:splitDown'],
click(item, focusedWindow) {
execCommand('pane:splitHorizontal', focusedWindow);
execCommand('pane:splitDown', focusedWindow);
}
},
{
label: 'Split Vertically',
accelerator: commandKeys['pane:splitVertical'],
label: 'Split Right',
accelerator: commandKeys['pane:splitRight'],
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 {
label: 'View',
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
const tabJump = [];
for (let i = 1; i <= 9; i++) {
// 9 is a special number because it means 'last'
const label = i === 9 ? 'Last' : `${i}`;
tabJump.push({
label: label,
label,
accelerator: commandKeys[`tab:jump:${label.toLowerCase()}`]
});
}
@ -76,6 +81,12 @@ module.exports = (commandKeys, execCommand) => {
{
role: 'front'
},
{
label: 'Toggle Always on Top',
click: (item, focusedWindow) => {
execCommand('window:toggleKeepOnTop', focusedWindow);
}
},
{
role: 'togglefullscreen',
accelerator: commandKeys['window:toggleFullScreen']

View file

@ -1,20 +1,18 @@
const ms = require('ms');
const fetch = require('electron-fetch').default;
const {version} = require('./package');
import ms from 'ms';
import fetch from 'electron-fetch';
import {version} from './package.json';
import {BrowserWindow} from 'electron';
const NEWS_URL = 'https://hyper-news.now.sh';
module.exports = function fetchNotifications(win) {
export default function fetchNotifications(win: BrowserWindow) {
const {rpc} = win;
const retry = err => {
const retry = (err?: Error) => {
setTimeout(() => fetchNotifications(win), ms('30m'));
if (err) {
//eslint-disable-next-line no-console
console.error('Notification messages fetch error', err.stack);
}
};
//eslint-disable-next-line no-console
console.log('Checking for notification messages');
fetch(NEWS_URL, {
headers: {
@ -22,14 +20,13 @@ module.exports = function fetchNotifications(win) {
'X-Hyper-Platform': process.platform
}
})
.then(res => res.json())
.then(data => {
.then((res) => res.json())
.then((data) => {
const {message} = data || {};
if (typeof message !== 'object' && message !== '') {
throw new Error('Bad response');
}
if (message === '') {
//eslint-disable-next-line no-console
console.log('No matching notification messages');
} else {
rpc.emit('add notification', message);
@ -38,4 +35,4 @@ module.exports = function fetchNotifications(win) {
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",
"dependencies": {
"async-retry": "1.1.4",
"color": "2.0.1",
"async-retry": "1.3.1",
"chokidar": "^3.5.2",
"color": "3.1.3",
"convert-css-color-name-to-hex": "0.1.1",
"default-shell": "1.0.1",
"electron-config": "2.0.0",
"electron-fetch": "1.3.0",
"electron-is-dev": "1.0.1",
"electron-squirrel-startup": "1.0.0",
"file-uri-to-path": "1.0.0",
"fs-extra": "7.0.1",
"git-describe": "4.0.2",
"lodash": "4.17.5",
"mkdirp": "0.5.1",
"ms": "2.1.1",
"node-pty": "0.8.0",
"os-locale": "3.1.0",
"parse-url": "3.0.2",
"queue": "4.4.2",
"react": "16.2.0",
"react-dom": "16.2.1",
"semver": "5.5.0",
"shell-env": "0.3.0",
"uuid": "3.2.1",
"winreg": "1.2.4"
"electron-fetch": "1.7.3",
"electron-is-dev": "2.0.0",
"electron-store": "8.0.0",
"file-uri-to-path": "2.0.0",
"fs-extra": "10.0.0",
"git-describe": "4.0.4",
"lodash": "4.17.21",
"mkdirp": "1.0.4",
"ms": "2.1.3",
"node-pty": "0.10.1",
"os-locale": "5.0.0",
"parse-url": "5.0.7",
"pify": "5.0.0",
"queue": "6.0.2",
"react": "17.0.2",
"react-dom": "17.0.2",
"semver": "7.3.5",
"shell-env": "3.0.1",
"sudo-prompt": "^9.2.1",
"uuid": "8.3.2"
},
"optionalDependencies": {
"native-reg": "0.3.5"
}
}

View file

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

View file

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

View file

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

View file

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

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
const Color = require('color');
import Color from 'color';
// returns a background color that's in hex
// format including the alpha channel (e.g.: `#00000050`)
// input can be any css value (rgb, hsl, string…)
module.exports = bgColor => {
export default (bgColor: string) => {
const color = Color(bgColor);
if (color.alpha() === 1) {
@ -13,12 +13,5 @@ module.exports = bgColor => {
// http://stackoverflow.com/a/11019879/1202488
const alphaHex = Math.round(color.alpha() * 255).toString(16);
return (
'#' +
alphaHex +
color
.hex()
.toString()
.substr(1)
);
return `#${alphaHex}${color.hex().toString().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 [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;
});
}
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');
const os = require('os');
const got = require('got');
const registryUrl = require('registry-url')();
const pify = require('pify');
const recast = require('recast');
const path = require('path');
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable @typescript-eslint/no-unsafe-return */
import fs from 'fs';
import os from 'os';
import got from 'got';
import registryUrlModule from 'registry-url';
const registryUrl = registryUrlModule();
import pify from 'pify';
import * as recast from 'recast';
import path from 'path';
// If the user defines XDG_CONFIG_HOME they definitely want their config there,
// otherwise use the home directory in linux/mac and userdata in windows
const applicationDirectory =
process.env.XDG_CONFIG_HOME !== undefined
? path.join(process.env.XDG_CONFIG_HOME, 'hyper')
: process.platform == 'win32' ? 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`);
let fileName =
const fileName =
process.env.NODE_ENV !== 'production' && fs.existsSync(devConfigFileName)
? devConfigFileName
: path.join(applicationDirectory, '.hyper.js');
/**
* We need to make sure the file reading and parsing is lazy so that failure to
* statically analyze the hyper configuration isn't fatal for all kinds of
* subcommands. We can use memoization to make reading and parsing lazy.
*/
function memoize(fn) {
function memoize<T extends (...args: any[]) => any>(fn: T): T {
let hasResult = false;
let result;
return (...args) => {
let result: any;
return ((...args: any[]) => {
if (!hasResult) {
result = fn(...args);
hasResult = true;
}
return result;
};
}) as T;
}
const getFileContents = memoize(() => {
@ -48,24 +54,39 @@ const getFileContents = memoize(() => {
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 getPlugins = memoize(() => getProperties().find(property => property.key.name === 'plugins').value.elements);
const getLocalPlugins = memoize(
() => getProperties().find(property => property.key.name === 'localPlugins').value.elements
const getProperties = memoize(
(): any[] =>
((getParsedFile()?.program?.body as any[]) || []).find(
(bodyItem) =>
bodyItem.type === 'ExpressionStatement' &&
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() {
return getFileContents() !== undefined;
}
function isInstalled(plugin, locally) {
function isInstalled(plugin: string, locally?: boolean) {
const array = locally ? getLocalPlugins() : getPlugins();
if (array && Array.isArray(array)) {
return array.find(entry => entry.value === plugin) !== undefined;
return array.some((entry) => entry.value === plugin);
}
return false;
}
@ -74,30 +95,32 @@ function save() {
return pify(fs.writeFile)(fileName, recast.print(getParsedFile()).code, 'utf8');
}
function existsOnNpm(plugin) {
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) {
function getPackageName(plugin: string) {
const isScoped = plugin[0] === '@';
const nameWithoutVersion = plugin.split('#')[0];
if (isScoped) {
return '@' + nameWithoutVersion.split('@')[1].replace('/', '%2f');
return `@${nameWithoutVersion.split('@')[1].replace('/', '%2f')}`;
}
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();
return existsOnNpm(plugin)
.catch(err => {
.catch((err: any) => {
const {statusCode} = err;
if (statusCode && (statusCode === 404 || statusCode === 200)) {
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)) {
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);
return save();
}
function list() {
if (Array.isArray(getPlugins())) {
if (getPlugins().length > 0) {
return getPlugins()
.map(plugin => plugin.value)
.map((plugin) => plugin.value)
.join('\n');
}
return false;
}
module.exports.configPath = fileName;
module.exports.exists = exists;
module.exports.existsOnNpm = existsOnNpm;
module.exports.isInstalled = isInstalled;
module.exports.install = install;
module.exports.uninstall = uninstall;
module.exports.list = list;
export const configPath = fileName;
export {exists, existsOnNpm, isInstalled, install, uninstall, 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';
import rpc from '../rpc';
import {userExitTermGroup, setActiveGroup} from './term-groups';
import {HyperDispatch} from '../hyper';
export function closeTab(uid) {
return dispatch => {
export function closeTab(uid: string) {
return (dispatch: HyperDispatch) => {
dispatch({
type: CLOSE_TAB,
uid,
@ -21,8 +22,8 @@ export function closeTab(uid) {
};
}
export function changeTab(uid) {
return dispatch => {
export function changeTab(uid: string) {
return (dispatch: HyperDispatch) => {
dispatch({
type: CHANGE_TAB,
uid,
@ -34,29 +35,29 @@ export function changeTab(uid) {
}
export function maximize() {
return dispatch => {
return (dispatch: HyperDispatch) => {
dispatch({
type: UI_WINDOW_MAXIMIZE,
effect() {
rpc.emit('maximize');
rpc.emit('maximize', null);
}
});
};
}
export function unmaximize() {
return dispatch => {
return (dispatch: HyperDispatch) => {
dispatch({
type: UI_WINDOW_UNMAXIMIZE,
effect() {
rpc.emit('unmaximize');
rpc.emit('unmaximize', null);
}
});
};
}
export function openHamburgerMenu(coordinates) {
return dispatch => {
export function openHamburgerMenu(coordinates: {x: number; y: number}) {
return (dispatch: HyperDispatch) => {
dispatch({
type: UI_OPEN_HAMBURGER_MENU,
effect() {
@ -67,22 +68,22 @@ export function openHamburgerMenu(coordinates) {
}
export function minimize() {
return dispatch => {
return (dispatch: HyperDispatch) => {
dispatch({
type: UI_WINDOW_MINIMIZE,
effect() {
rpc.emit('minimize');
rpc.emit('minimize', null);
}
});
};
}
export function close() {
return dispatch => {
return (dispatch: HyperDispatch) => {
dispatch({
type: UI_WINDOW_CLOSE,
effect() {
rpc.emit('close');
rpc.emit('close', null);
}
});
};

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