mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-12 20:18:41 -09:00
Merge branch 'canary'
This commit is contained in:
commit
e1d57077c5
188 changed files with 91172 additions and 76893 deletions
|
|
@ -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: /.*/
|
||||
|
|
@ -4,7 +4,9 @@ app/static
|
|||
app/bin
|
||||
app/dist
|
||||
app/node_modules
|
||||
app/typings
|
||||
assets
|
||||
website
|
||||
bin
|
||||
dist
|
||||
dist
|
||||
target
|
||||
113
.eslintrc.json
Normal file
113
.eslintrc.json
Normal 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
3
.gitattributes
vendored
|
|
@ -1,2 +1,5 @@
|
|||
* text=auto
|
||||
*.js text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
bin/* linguist-vendored
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea/feature for Hyper
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
16
.github/dependabot.yml
vendored
Normal file
16
.github/dependabot.yml
vendored
Normal 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
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
|
|
@ -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
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ canary ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ canary ]
|
||||
schedule:
|
||||
- cron: '37 6 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@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
76
.github/workflows/nodejs.yml
vendored
Normal 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
56
.github/workflows/spectron_comment.yml
vendored
Normal 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
4
.gitignore
vendored
|
|
@ -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
1
.husky/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
_
|
||||
4
.husky/pre-push
Executable file
4
.husky/pre-push
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn test
|
||||
41
.travis.yml
41
.travis.yml
|
|
@ -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
2
.vscode/launch.json
vendored
|
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
2
.yarnrc
2
.yarnrc
|
|
@ -1 +1 @@
|
|||
save-exact true
|
||||
registry "https://registry.npmjs.org/"
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -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
|
||||
|
|
|
|||
21
PLUGINS.md
21
PLUGINS.md
|
|
@ -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
|
||||
|
|
@ -70,7 +70,7 @@ exports.decorateTerms = (Terms, {React}) => {
|
|||
// <Terms onDecorated={this.onDecorated} />
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
:warning: Note that you have to execute `this.props.onDecorated` to not break the handler chain. Without this, you could break other plugins that decorate the same component.
|
||||
|
||||
### Keymaps
|
||||
|
|
@ -190,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`.
|
||||
|
|
|
|||
44
README.md
44
README.md
|
|
@ -1,23 +1,39 @@
|
|||

|
||||

|
||||
|
||||
[](https://circleci.com/gh/zeit/hyper)
|
||||
[](https://ci.appveyor.com/project/zeit/hyper)
|
||||
[](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>
|
||||
|
||||
[](https://github.com/vercel/hyper/actions?query=workflow%3A%22Node+CI%22+branch%3Acanary+event%3Apush)
|
||||
[](https://changelog.com/213)
|
||||
[](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
1
app/.yarnrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
registry "https://registry.npmjs.org/"
|
||||
|
|
@ -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();
|
||||
127
app/commands.js
127
app/commands.js
|
|
@ -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
152
app/commands.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
154
app/config.js
154
app/config.js
|
|
@ -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
137
app/config.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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
49
app/config/init.ts
Normal 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};
|
||||
|
|
@ -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
75
app/config/open.ts
Normal 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 === '');
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
@ -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
20
app/config/windows.ts
Normal 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
8
app/ext-modules.d.ts
vendored
Normal 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
25
app/extend-electron.d.ts
vendored
Normal 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
1
app/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
// Dummy file, required by tsc
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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_;
|
||||
};
|
||||
|
|
@ -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'
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -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
108
app/menus/menus/help.ts
Normal 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
|
||||
};
|
||||
};
|
||||
|
|
@ -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'
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
|
@ -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
47
app/menus/menus/tools.ts
Normal 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'
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
};
|
||||
};
|
||||
|
|
@ -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: [
|
||||
|
|
@ -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']
|
||||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
require('electron').ipcRenderer.on('notification', (ev, { title, body }) => {
|
||||
new Notification(title, { body });
|
||||
});
|
||||
</script>
|
||||
|
|
@ -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
20
app/notify.ts
Normal 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();
|
||||
};
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
@ -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
42
app/plugins/extensions.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
export const availableExtensions = new Set([
|
||||
'onApp',
|
||||
'onWindowClass',
|
||||
'decorateWindowClass',
|
||||
'onWindow',
|
||||
'onRendererWindow',
|
||||
'onUnload',
|
||||
'decorateSessionClass',
|
||||
'decorateSessionOptions',
|
||||
'middleware',
|
||||
'reduceUI',
|
||||
'reduceSessions',
|
||||
'reduceTermGroups',
|
||||
'decorateBrowserOptions',
|
||||
'decorateMenu',
|
||||
'decorateTerm',
|
||||
'decorateHyper',
|
||||
'decorateHyperTerm', // for backwards compatibility with hyperterm
|
||||
'decorateHeader',
|
||||
'decorateTerms',
|
||||
'decorateTab',
|
||||
'decorateNotification',
|
||||
'decorateNotifications',
|
||||
'decorateTabs',
|
||||
'decorateConfig',
|
||||
'decorateKeymaps',
|
||||
'decorateEnv',
|
||||
'decorateTermGroup',
|
||||
'decorateSplitPane',
|
||||
'getTermProps',
|
||||
'getTabProps',
|
||||
'getTabsProps',
|
||||
'getTermGroupProps',
|
||||
'mapHyperTermState',
|
||||
'mapTermsState',
|
||||
'mapHeaderState',
|
||||
'mapNotificationsState',
|
||||
'mapHyperTermDispatch',
|
||||
'mapTermsDispatch',
|
||||
'mapHeaderDispatch',
|
||||
'mapNotificationsDispatch'
|
||||
]);
|
||||
|
|
@ -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
47
app/plugins/install.ts
Normal 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);
|
||||
});
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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
11
app/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"declarationDir": "../dist/tmp/appdts/",
|
||||
"outDir": "../target/"
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"./package.json"
|
||||
]
|
||||
}
|
||||
|
|
@ -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
33
app/ui/contextmenu.ts
Normal 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);
|
||||
};
|
||||
325
app/ui/window.js
325
app/ui/window.js
|
|
@ -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
330
app/ui/window.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
155
app/utils/cli-install.ts
Normal 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}`);
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
}, {});
|
||||
};
|
||||
|
|
@ -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');
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
15
app/utils/renderer-utils.ts
Normal file
15
app/utils/renderer-utils.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
const rendererTypes: Record<string, string> = {};
|
||||
|
||||
function getRendererTypes() {
|
||||
return rendererTypes;
|
||||
}
|
||||
|
||||
function setRendererType(uid: string, type: string) {
|
||||
rendererTypes[uid] = type;
|
||||
}
|
||||
|
||||
function unsetRendererType(uid: string) {
|
||||
delete rendererTypes[uid];
|
||||
}
|
||||
|
||||
export {getRendererTypes, setRendererType, unsetRendererType};
|
||||
60
app/utils/system-context-menu.ts
Normal file
60
app/utils/system-context-menu.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import * as Registry from 'native-reg';
|
||||
import type {HKEY} from 'native-reg';
|
||||
|
||||
const appPath = `"${process.execPath}"`;
|
||||
const regKeys = [
|
||||
`Software\\Classes\\Directory\\Background\\shell\\Hyper`,
|
||||
`Software\\Classes\\Directory\\shell\\Hyper`,
|
||||
`Software\\Classes\\Drive\\shell\\Hyper`
|
||||
];
|
||||
const regParts = [
|
||||
{key: 'command', name: '', value: `${appPath} "%V"`},
|
||||
{name: '', value: 'Open Hyper here'},
|
||||
{name: 'Icon', value: `${appPath}`}
|
||||
];
|
||||
|
||||
function addValues(hyperKey: HKEY, commandKey: HKEY) {
|
||||
try {
|
||||
Registry.setValueSZ(hyperKey, regParts[1].name, regParts[1].value);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
Registry.setValueSZ(hyperKey, regParts[2].name, regParts[2].value);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
try {
|
||||
Registry.setValueSZ(commandKey, regParts[0].name, regParts[0].value);
|
||||
} catch (err_) {
|
||||
console.error(err_);
|
||||
}
|
||||
}
|
||||
|
||||
export const add = () => {
|
||||
regKeys.forEach((regKey) => {
|
||||
try {
|
||||
const hyperKey =
|
||||
Registry.openKey(Registry.HKCU, regKey, Registry.Access.ALL_ACCESS) ||
|
||||
Registry.createKey(Registry.HKCU, regKey, Registry.Access.ALL_ACCESS);
|
||||
const commandKey =
|
||||
Registry.openKey(Registry.HKCU, `${regKey}\\${regParts[0].key}`, Registry.Access.ALL_ACCESS) ||
|
||||
Registry.createKey(Registry.HKCU, `${regKey}\\${regParts[0].key}`, Registry.Access.ALL_ACCESS);
|
||||
addValues(hyperKey, commandKey);
|
||||
Registry.closeKey(hyperKey);
|
||||
Registry.closeKey(commandKey);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const remove = () => {
|
||||
regKeys.forEach((regKey) => {
|
||||
try {
|
||||
Registry.deleteTree(Registry.HKCU, regKey);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -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)}`;
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
1060
app/yarn.lock
1060
app/yarn.lock
File diff suppressed because it is too large
Load diff
32
appveyor.yml
32
appveyor.yml
|
|
@ -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
29
assets/search-icons.svg
Normal 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
10
ava-spectron.config.js
Normal 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
9
ava.config.js
Normal 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
18
babel.config.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"presets": [
|
||||
"@babel/react",
|
||||
"@babel/typescript"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"styled-jsx/babel",
|
||||
{
|
||||
"vendorPrefixes": false
|
||||
}
|
||||
],
|
||||
"@babel/plugin-proposal-numeric-separator",
|
||||
"@babel/proposal-class-properties",
|
||||
"@babel/proposal-object-rest-spread",
|
||||
"@babel/plugin-proposal-optional-chaining"
|
||||
]
|
||||
}
|
||||
145321
bin/yarn-standalone.js
vendored
145321
bin/yarn-standalone.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -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
BIN
build/icon.fig
Normal file
Binary file not shown.
BIN
build/icon.icns
BIN
build/icon.icns
Binary file not shown.
28
build/win/installer.nsh
Normal file
28
build/win/installer.nsh
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
!macro customInstall
|
||||
WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper" "" "Open Hyper here"
|
||||
WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper" "Icon" "$appExe"
|
||||
WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper\command" "" `$appExe "%V"`
|
||||
|
||||
WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper" "" "Open Hyper here"
|
||||
WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper" "Icon" "$appExe"
|
||||
WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper\command" "" `$appExe "%V"`
|
||||
|
||||
WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper" "" "Open Hyper here"
|
||||
WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper" "Icon" "$appExe"
|
||||
WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper\command" "" `$appExe "%V"`
|
||||
!macroend
|
||||
|
||||
!macro customUnInstall
|
||||
DeleteRegKey HKCU "Software\Classes\Directory\Background\shell\Hyper"
|
||||
DeleteRegKey HKCU "Software\Classes\Directory\shell\Hyper"
|
||||
DeleteRegKey HKCU "Software\Classes\Drive\shell\Hyper"
|
||||
!macroend
|
||||
|
||||
!macro customInstallMode
|
||||
StrCpy $isForceCurrentInstall "1"
|
||||
!macroend
|
||||
|
||||
!macro customInit
|
||||
IfFileExists $LOCALAPPDATA\Hyper\Update.exe 0 +2
|
||||
nsExec::Exec '"$LOCALAPPDATA\Hyper\Update.exe" --uninstall -s'
|
||||
!macroend
|
||||
|
|
@ -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};
|
||||
219
cli/index.js
219
cli/index.js
|
|
@ -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
256
cli/index.ts
Normal 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
132
electron-builder.json
Normal 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 device’s 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 Apple’s 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"target": "es6"
|
||||
},
|
||||
"exclude": ["node_modules", "**/node_modules/*", "bin/*", "renderer/*"]
|
||||
}
|
||||
|
|
@ -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
19
lib/actions/config.ts
Normal 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
|
||||
};
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue