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/bin
|
||||||
app/dist
|
app/dist
|
||||||
app/node_modules
|
app/node_modules
|
||||||
|
app/typings
|
||||||
assets
|
assets
|
||||||
website
|
website
|
||||||
bin
|
bin
|
||||||
dist
|
dist
|
||||||
|
target
|
||||||
113
.eslintrc.json
Normal file
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
|
* text=auto
|
||||||
*.js text eol=lf
|
*.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.
|
Hi there! Thank you for discovering and submitting an issue.
|
||||||
|
|
||||||
Before you submit this; let's make sure of a few things.
|
Before you submit this; let's make sure of a few things.
|
||||||
Please make sure the following boxes are ticked if they are correct.
|
Please make sure the following boxes are ticked if they are correct.
|
||||||
If not, please try and fulfil these first.
|
If not, please try and fulfill these first.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- Checked checkbox should look like this: [x] -->
|
<!-- Checked checkbox should look like this: [x] -->
|
||||||
- [ ] I am on the [latest](https://github.com/zeit/hyper/releases/latest) Hyper.app version
|
- [ ] I am on the [latest](https://github.com/vercel/hyper/releases/latest) Hyper.app version
|
||||||
- [ ] I have searched the [issues](https://github.com/zeit/hyper/issues) of this repo and believe that this is not a duplicate
|
- [ ] I have searched the [issues](https://github.com/vercel/hyper/issues) of this repo and believe that this is not a duplicate
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Once those are done, if you're able to fill in the following list with your information,
|
Once those are done, if you're able to fill in the following list with your information,
|
||||||
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,
|
- To help whoever reviews your PR, it'd be extremely helpful for you to list whether your PR is ready to be merged,
|
||||||
If there's anything left to do and if there are any related PRs
|
If there's anything left to do and if there are any related PRs
|
||||||
- It'd also be extremely helpful to enable us to update your PR incase we need to rebase or what-not by checking `Allow edits from maintainers`
|
- It'd also be extremely helpful to enable us to update your PR incase we need to rebase or what-not by checking `Allow edits from maintainers`
|
||||||
- If your PR changes some API, please make a PR for hyper website too: https://github.com/zeit/hyper-site.
|
- If your PR changes some API, please make a PR for hyper website too: https://github.com/vercel/hyper-site.
|
||||||
|
|
||||||
Thanks, again! -->
|
Thanks, again! -->
|
||||||
|
|
|
||||||
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
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
|
# build output
|
||||||
dist
|
dist
|
||||||
app/renderer
|
app/renderer
|
||||||
|
target
|
||||||
bin/cli.*
|
bin/cli.*
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
|
|
@ -15,3 +16,6 @@ yarn-error.log
|
||||||
.hyper_plugins
|
.hyper_plugins
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/launch.json
|
||||||
|
.idea
|
||||||
|
|
|
||||||
1
.husky/.gitignore
vendored
Normal file
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",
|
"request": "launch",
|
||||||
"name": "Launch Hyper",
|
"name": "Launch Hyper",
|
||||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
||||||
"program": "${workspaceRoot}/app/index.js",
|
"program": "${workspaceRoot}/target/index.js",
|
||||||
"protocol": "inspector"
|
"protocol": "inspector"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
2
.yarnrc
2
.yarnrc
|
|
@ -1 +1 @@
|
||||||
save-exact true
|
registry "https://registry.npmjs.org/"
|
||||||
|
|
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
# MIT License
|
# MIT License
|
||||||
|
|
||||||
Copyright (c) 2018 ZEIT, Inc.
|
Copyright (c) 2018 Vercel, Inc.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
||||||
21
PLUGINS.md
21
PLUGINS.md
|
|
@ -3,11 +3,11 @@
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
### Run Hyper in dev mode
|
### Run Hyper in dev mode
|
||||||
Hyper can be run in dev mode by cloning this repository and following the ["Contributing" section of our README](https://github.com/zeit/hyper#contribute).
|
Hyper can be run in dev mode by cloning this repository and following the ["Contributing" section of our README](https://github.com/vercel/hyper#contribute).
|
||||||
|
|
||||||
In dev mode you'll get more ouput and access to React/Redux dev-tools in Electron.
|
In dev mode you'll get more ouput and access to React/Redux dev-tools in Electron.
|
||||||
|
|
||||||
Prerequisites and steps are described in the ["Contributing" section of our README](https://github.com/zeit/hyper#contribute).
|
Prerequisites and steps are described in the ["Contributing" section of our README](https://github.com/vercel/hyper#contribute).
|
||||||
Be sure to use the `canary` branch.
|
Be sure to use the `canary` branch.
|
||||||
|
|
||||||
### Create a dev config file
|
### Create a dev config file
|
||||||
|
|
@ -30,7 +30,7 @@ module.exports = {
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running your plugin
|
### Running your plugin
|
||||||
To load, your plugin should expose at least one API method. All possible methods are listed [here](https://github.com/zeit/hyper/blob/canary/app/plugins/extensions.js).
|
To load, your plugin should expose at least one API method. All possible methods are listed [here](https://github.com/vercel/hyper/blob/canary/app/plugins/extensions.ts).
|
||||||
|
|
||||||
After launching Hyper in dev mode, run `yarn run app`, it should log that your plugin has been correcty loaded: `Plugin hyper-awesome-plugin (0.1.0) loaded.`. Name and version printed are the ones in your plugins `package.json` file.
|
After launching Hyper in dev mode, run `yarn run app`, it should log that your plugin has been correcty loaded: `Plugin hyper-awesome-plugin (0.1.0) loaded.`. Name and version printed are the ones in your plugins `package.json` file.
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ Almost all available API methods can be found on https://hyper.is.
|
||||||
If there's any missing, let us know or submit a PR to document it!
|
If there's any missing, let us know or submit a PR to document it!
|
||||||
|
|
||||||
### Components
|
### Components
|
||||||
You can decorate almost all Hyper components with a Higher-Order Component (HOC). To understand their architecture, the easiest way is to use React dev-tools to dig in to their hierachy.
|
You can decorate almost all Hyper components with a Higher-Order Component (HOC). To understand their architecture, the easiest way is to use React dev-tools to dig in to their hierarchy.
|
||||||
|
|
||||||
Multiple plugins can decorate the same Hyper component. Thus, `Component` passed as first argument to your decorator function could possibly not be an original Hyper component but a HOC of a previous plugin. If you need to retrieve a reference to a real Hyper component, you can pass down a `onDecorated` handler.
|
Multiple plugins can decorate the same Hyper component. Thus, `Component` passed as first argument to your decorator function could possibly not be an original Hyper component but a HOC of a previous plugin. If you need to retrieve a reference to a real Hyper component, you can pass down a `onDecorated` handler.
|
||||||
```js
|
```js
|
||||||
|
|
@ -70,7 +70,7 @@ exports.decorateTerms = (Terms, {React}) => {
|
||||||
// <Terms onDecorated={this.onDecorated} />
|
// <Terms onDecorated={this.onDecorated} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
:warning: Note that you have to execute `this.props.onDecorated` to not break the handler chain. Without this, you could break other plugins that decorate the same component.
|
:warning: Note that you have to execute `this.props.onDecorated` to not break the handler chain. Without this, you could break other plugins that decorate the same component.
|
||||||
|
|
||||||
### Keymaps
|
### Keymaps
|
||||||
|
|
@ -190,6 +190,17 @@ exports.decorateTerm = (Term, { React, notify }) => {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Require Electron
|
||||||
|
Hyper doesn't provide a reference to electron. However plugins can directly require electron.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const electron = require('electron')
|
||||||
|
// or
|
||||||
|
const { dialog, Menu } = require('electron')
|
||||||
|
```
|
||||||
|
|
||||||
|
This is needed in order to allow show/hide to have proper return of focus.
|
||||||
|
|
||||||
## Hyper v2 breaking changes
|
## Hyper v2 breaking changes
|
||||||
Hyper v2 uses `xterm.js` instead of `hterm`. It means that PTY output renders now in a canvas element, not with a hackable DOM structure.
|
Hyper v2 uses `xterm.js` instead of `hterm`. It means that PTY output renders now in a canvas element, not with a hackable DOM structure.
|
||||||
For example, plugins can't use TermCSS in order to modify text or link styles anymore. It is now required to use available configuration params that are passed down to `xterm.js`.
|
For example, plugins can't use TermCSS in order to modify text or link styles anymore. It is now required to use available configuration params that are passed down to `xterm.js`.
|
||||||
|
|
|
||||||
44
README.md
44
README.md
|
|
@ -1,23 +1,39 @@
|
||||||

|

|
||||||
|
|
||||||
[](https://circleci.com/gh/zeit/hyper)
|
<p align="center">
|
||||||
[](https://ci.appveyor.com/project/zeit/hyper)
|
<a aria-label="Vercel logo" href="https://vercel.com">
|
||||||
[](https://travis-ci.org/zeit/hyper)
|
<img src="https://img.shields.io/badge/MADE%20BY%20Vercel-000000.svg?style=for-the-badge&logo=vercel&labelColor=000000&logoWidth=20">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[](https://github.com/vercel/hyper/actions?query=workflow%3A%22Node+CI%22+branch%3Acanary+event%3Apush)
|
||||||
[](https://changelog.com/213)
|
[](https://changelog.com/213)
|
||||||
[](https://spectrum.chat/zeit/hyper)
|
|
||||||
|
|
||||||
For more details, head to: https://hyper.is
|
For more details, head to: https://hyper.is
|
||||||
|
|
||||||
|
## Project goals
|
||||||
|
|
||||||
|
The goal of the project is to create a beautiful and extensible experience for command-line interface users, built on open web standards. In the beginning, our focus will be primarily around speed, stability and the development of the correct API for extension authors.
|
||||||
|
|
||||||
|
In the future, we anticipate the community will come up with innovative additions to enhance what could be the simplest, most powerful and well-tested interface for productivity.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
[Download the latest release!](https://hyper.is/#installation)
|
[Download the latest release!](https://hyper.is/#installation)
|
||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
#### Arch and derivatives
|
#### Arch and derivatives
|
||||||
Hyper is available in the [AUR](https://aur.archlinux.org/packages/hyper/). Use an AUR package manager like [aurman](https://github.com/polygamma/aurman)
|
Hyper is available in the [AUR](https://aur.archlinux.org/packages/hyper/). Use an AUR [package manager](https://wiki.archlinux.org/index.php/AUR_helpers) e.g. [paru](https://github.com/Morganamilo/paru)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
aurman -S hyper
|
paru -S hyper
|
||||||
|
```
|
||||||
|
|
||||||
|
#### NixOS
|
||||||
|
Hyper is available as [Nix package](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/misc/hyper/default.nix), to install the app run this command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix-env -i hyper
|
||||||
```
|
```
|
||||||
|
|
||||||
### macOS
|
### macOS
|
||||||
|
|
@ -26,7 +42,7 @@ Use [Homebrew Cask](https://brew.sh) to download the app by running these comman
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew update
|
brew update
|
||||||
brew cask install hyper
|
brew install --cask hyper
|
||||||
```
|
```
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
@ -83,6 +99,10 @@ make sure its build process is working correctly by running `yarn run rebuild-no
|
||||||
If you are on macOS, this typically is related to Xcode issues (like not having agreed
|
If you are on macOS, this typically is related to Xcode issues (like not having agreed
|
||||||
to the Terms of Service by running `sudo xcodebuild` after a fresh Xcode installation).
|
to the Terms of Service by running `sudo xcodebuild` after a fresh Xcode installation).
|
||||||
|
|
||||||
|
##### Error with `c++` on macOS when running `yarn`
|
||||||
|
|
||||||
|
If you are getting compiler errors when running `yarn` add the environment variable `export CXX=clang++`
|
||||||
|
|
||||||
##### Error with `codesign` on macOS when running `yarn run dist`
|
##### Error with `codesign` on macOS when running `yarn run dist`
|
||||||
|
|
||||||
If you have issues in the `codesign` step when running `yarn run dist` on macOS, you can temporarily disable code signing locally by setting
|
If you have issues in the `codesign` step when running `yarn run dist` on macOS, you can temporarily disable code signing locally by setting
|
||||||
|
|
@ -90,8 +110,8 @@ If you have issues in the `codesign` step when running `yarn run dist` on macOS,
|
||||||
|
|
||||||
## Related Repositories
|
## Related Repositories
|
||||||
|
|
||||||
- [Art](https://github.com/zeit/art/tree/master/hyper)
|
- [Art](https://github.com/vercel/art/tree/master/hyper)
|
||||||
- [Website](https://github.com/zeit/hyper-site)
|
- [Website](https://github.com/vercel/hyper-site)
|
||||||
- [Sample Extension](https://github.com/zeit/hyperpower)
|
- [Sample Extension](https://github.com/vercel/hyperpower)
|
||||||
- [Sample Theme](https://github.com/zeit/hyperyellow)
|
- [Sample Theme](https://github.com/vercel/hyperyellow)
|
||||||
- [Awesome Hyper](https://github.com/bnb/awesome-hyper)
|
- [Awesome Hyper](https://github.com/bnb/awesome-hyper)
|
||||||
|
|
|
||||||
1
app/.yarnrc
Normal file
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;
|
class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
|
||||||
const {EventEmitter} = require('events');
|
updateURL!: string;
|
||||||
|
|
||||||
class AutoUpdater extends EventEmitter {
|
|
||||||
quitAndInstall() {
|
quitAndInstall() {
|
||||||
this.emitError('QuitAndInstall unimplemented');
|
this.emitError('QuitAndInstall unimplemented');
|
||||||
}
|
}
|
||||||
|
|
@ -11,8 +10,8 @@ class AutoUpdater extends EventEmitter {
|
||||||
return this.updateURL;
|
return this.updateURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
setFeedURL(updateURL) {
|
setFeedURL(options: Electron.FeedURLOptions) {
|
||||||
this.updateURL = updateURL;
|
this.updateURL = options.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkForUpdates() {
|
checkForUpdates() {
|
||||||
|
|
@ -22,9 +21,10 @@ class AutoUpdater extends EventEmitter {
|
||||||
this.emit('checking-for-update');
|
this.emit('checking-for-update');
|
||||||
|
|
||||||
fetch(this.updateURL)
|
fetch(this.updateURL)
|
||||||
.then(res => {
|
.then((res) => {
|
||||||
if (res.status === 204) {
|
if (res.status === 204) {
|
||||||
return this.emit('update-not-available');
|
this.emit('update-not-available');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return res.json().then(({name, notes, pub_date}) => {
|
return res.json().then(({name, notes, pub_date}) => {
|
||||||
// Only name is mandatory, needed to construct release URL.
|
// Only name is mandatory, needed to construct release URL.
|
||||||
|
|
@ -39,12 +39,12 @@ class AutoUpdater extends EventEmitter {
|
||||||
.catch(this.emitError.bind(this));
|
.catch(this.emitError.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
emitError(error) {
|
emitError(error: string | Error) {
|
||||||
if (typeof error === 'string') {
|
if (typeof error === 'string') {
|
||||||
error = new Error(error);
|
error = new Error(error);
|
||||||
}
|
}
|
||||||
this.emit('error', error, error.message);
|
this.emit('error', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new AutoUpdater();
|
export default new AutoUpdater();
|
||||||
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
|
// custom CSS to embed in the terminal window
|
||||||
termCSS: '',
|
termCSS: '',
|
||||||
|
|
||||||
|
// set custom startup directory (must be an absolute path)
|
||||||
|
workingDirectory: '',
|
||||||
|
|
||||||
// if you're using a Linux setup which show native menus, set to false
|
// if you're using a Linux setup which show native menus, set to false
|
||||||
// default: `true` on Linux, `true` on Windows, ignored on macOS
|
// default: `true` on Linux, `true` on Windows, ignored on macOS
|
||||||
showHamburgerMenu: '',
|
showHamburgerMenu: '',
|
||||||
|
|
@ -89,6 +92,8 @@ module.exports = {
|
||||||
lightMagenta: '#FD7CFC',
|
lightMagenta: '#FD7CFC',
|
||||||
lightCyan: '#68FDFE',
|
lightCyan: '#68FDFE',
|
||||||
lightWhite: '#FFFFFF',
|
lightWhite: '#FFFFFF',
|
||||||
|
limeGreen: '#32CD32',
|
||||||
|
lightCoral: '#F08080',
|
||||||
},
|
},
|
||||||
|
|
||||||
// the shell to run when spawning a new session (i.e. /usr/local/bin/fish)
|
// the shell to run when spawning a new session (i.e. /usr/local/bin/fish)
|
||||||
|
|
@ -98,11 +103,17 @@ module.exports = {
|
||||||
// - Make sure to use a full path if the binary name doesn't work
|
// - Make sure to use a full path if the binary name doesn't work
|
||||||
// - Remove `--login` in shellArgs
|
// - Remove `--login` in shellArgs
|
||||||
//
|
//
|
||||||
// Bash on Windows
|
// Windows Subsystem for Linux (WSL) - previously Bash on Windows
|
||||||
// - Example: `C:\\Windows\\System32\\bash.exe`
|
// - Example: `C:\\Windows\\System32\\wsl.exe`
|
||||||
|
//
|
||||||
|
// Git-bash on Windows
|
||||||
|
// - Example: `C:\\Program Files\\Git\\bin\\bash.exe`
|
||||||
//
|
//
|
||||||
// PowerShell on Windows
|
// PowerShell on Windows
|
||||||
// - Example: `C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`
|
// - Example: `C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`
|
||||||
|
//
|
||||||
|
// Cygwin
|
||||||
|
// - Example: `C:\\cygwin64\\bin\\bash.exe`
|
||||||
shell: '',
|
shell: '',
|
||||||
|
|
||||||
// for setting shell arguments (i.e. for using interactive shellArgs: `['-i']`)
|
// for setting shell arguments (i.e. for using interactive shellArgs: `['-i']`)
|
||||||
|
|
@ -112,9 +123,14 @@ module.exports = {
|
||||||
// for environment variables
|
// for environment variables
|
||||||
env: {},
|
env: {},
|
||||||
|
|
||||||
// set to `false` for no bell
|
// Supported Options:
|
||||||
|
// 1. 'SOUND' -> Enables the bell as a sound
|
||||||
|
// 2. false: turns off the bell
|
||||||
bell: 'SOUND',
|
bell: 'SOUND',
|
||||||
|
|
||||||
|
// An absolute file path to a sound file on the machine.
|
||||||
|
// bellSoundURL: '/path/to/sound/file',
|
||||||
|
|
||||||
// if `true` (without backticks and without quotes), selected text will automatically be copied to the clipboard
|
// if `true` (without backticks and without quotes), selected text will automatically be copied to the clipboard
|
||||||
copyOnSelect: false,
|
copyOnSelect: false,
|
||||||
|
|
||||||
|
|
@ -130,12 +146,16 @@ module.exports = {
|
||||||
// (inside tmux or vim with mouse mode enabled for example).
|
// (inside tmux or vim with mouse mode enabled for example).
|
||||||
macOptionSelectionMode: 'vertical',
|
macOptionSelectionMode: 'vertical',
|
||||||
|
|
||||||
// URL to custom bell
|
|
||||||
// bellSoundURL: 'http://example.com/bell.mp3',
|
|
||||||
|
|
||||||
// Whether to use the WebGL renderer. Set it to false to use canvas-based
|
// Whether to use the WebGL renderer. Set it to false to use canvas-based
|
||||||
// rendering (slower, but supports transparent backgrounds)
|
// rendering (slower, but supports transparent backgrounds)
|
||||||
webGLRenderer: true,
|
webGLRenderer: false,
|
||||||
|
|
||||||
|
// keypress required for weblink activation: [ctrl|alt|meta|shift]
|
||||||
|
// todo: does not pick up config changes automatically, need to restart terminal :/
|
||||||
|
webLinksActivationKey: '',
|
||||||
|
|
||||||
|
// if `false` (without backticks and without quotes), Hyper will use ligatures provided by some fonts
|
||||||
|
disableLigatures: true,
|
||||||
|
|
||||||
// for advanced config flags please refer to https://hyper.is/#cfg
|
// for advanced config flags please refer to https://hyper.is/#cfg
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
const {moveSync, copySync, existsSync, writeFileSync, readFileSync, lstatSync} = require('fs-extra');
|
import {moveSync, copySync, existsSync, writeFileSync, readFileSync, lstatSync} from 'fs-extra';
|
||||||
const {sync: mkdirpSync} = require('mkdirp');
|
import {sync as mkdirpSync} from 'mkdirp';
|
||||||
const {defaultCfg, cfgPath, legacyCfgPath, plugs, defaultPlatformKeyPath} = require('./paths');
|
import {defaultCfg, cfgPath, legacyCfgPath, plugs, defaultPlatformKeyPath} from './paths';
|
||||||
const {_init, _extractDefault} = require('./init');
|
import {_init, _extractDefault} from './init';
|
||||||
const notify = require('../notify');
|
import notify from '../notify';
|
||||||
|
import {rawConfig} from '../../lib/config';
|
||||||
|
|
||||||
let defaultConfig;
|
let defaultConfig: rawConfig;
|
||||||
|
|
||||||
const _write = function(path, data) {
|
const _write = (path: string, data: string) => {
|
||||||
// This method will take text formatted as Unix line endings and transform it
|
// This method will take text formatted as Unix line endings and transform it
|
||||||
// to text formatted with DOS line endings. We do this because the default
|
// to text formatted with DOS line endings. We do this because the default
|
||||||
// text editor on Windows (notepad) doesn't Deal with LF files. Still. In 2017.
|
// text editor on Windows (notepad) doesn't Deal with LF files. Still. In 2017.
|
||||||
const crlfify = function(str) {
|
const crlfify = (str: string) => {
|
||||||
return str.replace(/\r?\n/g, '\r\n');
|
return str.replace(/\r?\n/g, '\r\n');
|
||||||
};
|
};
|
||||||
const format = process.platform === 'win32' ? crlfify(data.toString()) : data;
|
const format = process.platform === 'win32' ? crlfify(data.toString()) : data;
|
||||||
|
|
@ -19,20 +20,15 @@ const _write = function(path, data) {
|
||||||
|
|
||||||
// Saves a file as backup by appending '.backup' or '.backup2', '.backup3', etc.
|
// Saves a file as backup by appending '.backup' or '.backup2', '.backup3', etc.
|
||||||
// so as to not override any existing files
|
// so as to not override any existing files
|
||||||
const saveAsBackup = src => {
|
const saveAsBackup = (src: string) => {
|
||||||
let attempt = 1;
|
let attempt = 1;
|
||||||
while (attempt < 100) {
|
while (attempt < 100) {
|
||||||
try {
|
const backupPath = `${src}.backup${attempt === 1 ? '' : attempt}`;
|
||||||
const backupPath = src + '.backup' + (attempt === 1 ? '' : attempt);
|
if (!existsSync(backupPath)) {
|
||||||
moveSync(src, backupPath);
|
moveSync(src, backupPath);
|
||||||
return backupPath;
|
return backupPath;
|
||||||
} catch (e) {
|
|
||||||
if (e.code === 'EEXIST') {
|
|
||||||
attempt++;
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
attempt++;
|
||||||
}
|
}
|
||||||
throw new Error('Failed to create backup for config file. Too many backups');
|
throw new Error('Failed to create backup for config file. Too many backups');
|
||||||
};
|
};
|
||||||
|
|
@ -81,7 +77,7 @@ const migrateHyper2Config = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const _importConf = function() {
|
const _importConf = () => {
|
||||||
// init plugin directories if not present
|
// init plugin directories if not present
|
||||||
mkdirpSync(plugs.base);
|
mkdirpSync(plugs.base);
|
||||||
mkdirpSync(plugs.local);
|
mkdirpSync(plugs.local);
|
||||||
|
|
@ -89,47 +85,49 @@ const _importConf = function() {
|
||||||
try {
|
try {
|
||||||
migrateHyper2Config();
|
migrateHyper2Config();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let defaultCfgRaw = '';
|
||||||
try {
|
try {
|
||||||
const defaultCfgRaw = readFileSync(defaultCfg, 'utf8');
|
defaultCfgRaw = readFileSync(defaultCfg, 'utf8');
|
||||||
const _defaultCfg = _extractDefault(defaultCfgRaw);
|
|
||||||
// Importing platform specific keymap
|
|
||||||
try {
|
|
||||||
const content = readFileSync(defaultPlatformKeyPath(), 'utf8');
|
|
||||||
const mapping = JSON.parse(content);
|
|
||||||
_defaultCfg.keymaps = mapping;
|
|
||||||
} catch (err) {
|
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import user config
|
|
||||||
try {
|
|
||||||
const userCfg = readFileSync(cfgPath, 'utf8');
|
|
||||||
return {userCfg, defaultCfg: _defaultCfg};
|
|
||||||
} catch (err) {
|
|
||||||
_write(cfgPath, defaultCfgRaw);
|
|
||||||
return {userCfg: defaultCfgRaw, defaultCfg: _defaultCfg};
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
|
const _defaultCfg = _extractDefault(defaultCfgRaw) as rawConfig;
|
||||||
|
|
||||||
|
// Importing platform specific keymap
|
||||||
|
let content = '{}';
|
||||||
|
try {
|
||||||
|
content = readFileSync(defaultPlatformKeyPath(), 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
const mapping = JSON.parse(content) as Record<string, string | string[]>;
|
||||||
|
_defaultCfg.keymaps = mapping;
|
||||||
|
|
||||||
|
// Import user config
|
||||||
|
let userCfg: string;
|
||||||
|
try {
|
||||||
|
userCfg = readFileSync(cfgPath, 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
_write(cfgPath, defaultCfgRaw);
|
||||||
|
userCfg = defaultCfgRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {userCfg, defaultCfg: _defaultCfg};
|
||||||
};
|
};
|
||||||
|
|
||||||
exports._import = () => {
|
export const _import = () => {
|
||||||
const imported = _importConf();
|
const imported = _importConf();
|
||||||
defaultConfig = imported.defaultCfg;
|
defaultConfig = imported.defaultCfg;
|
||||||
const result = _init(imported);
|
const result = _init(imported);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getDefaultConfig = () => {
|
export const getDefaultConfig = () => {
|
||||||
if (!defaultConfig) {
|
if (!defaultConfig) {
|
||||||
defaultConfig = _extractDefault(_importConf().defaultCfg);
|
defaultConfig = _importConf().defaultCfg;
|
||||||
}
|
}
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
};
|
};
|
||||||
|
|
@ -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
|
// This module exports paths, names, and other metadata that is referenced
|
||||||
const {homedir} = require('os');
|
import {homedir} from 'os';
|
||||||
const {app} = require('electron');
|
import {app} from 'electron';
|
||||||
const {statSync} = require('fs');
|
import {statSync} from 'fs';
|
||||||
const {resolve, join} = require('path');
|
import {resolve, join} from 'path';
|
||||||
const isDev = require('electron-is-dev');
|
import isDev from 'electron-is-dev';
|
||||||
|
|
||||||
const cfgFile = '.hyper.js';
|
const cfgFile = '.hyper.js';
|
||||||
const defaultCfgFile = 'config-default.js';
|
const defaultCfgFile = 'config-default.js';
|
||||||
|
|
@ -14,11 +14,13 @@ const homeDirectory = homedir();
|
||||||
const applicationDirectory =
|
const applicationDirectory =
|
||||||
process.env.XDG_CONFIG_HOME !== undefined
|
process.env.XDG_CONFIG_HOME !== undefined
|
||||||
? join(process.env.XDG_CONFIG_HOME, 'hyper')
|
? join(process.env.XDG_CONFIG_HOME, 'hyper')
|
||||||
: process.platform == 'win32' ? app.getPath('userData') : homedir();
|
: process.platform == 'win32'
|
||||||
|
? app.getPath('userData')
|
||||||
|
: homedir();
|
||||||
|
|
||||||
let cfgDir = applicationDirectory;
|
let cfgDir = applicationDirectory;
|
||||||
let cfgPath = join(applicationDirectory, cfgFile);
|
let cfgPath = join(applicationDirectory, cfgFile);
|
||||||
let legacyCfgPath = join(homeDirectory, cfgFile); // Hyper 2 config location
|
const legacyCfgPath = join(homeDirectory, cfgFile); // Hyper 2 config location
|
||||||
|
|
||||||
const devDir = resolve(__dirname, '../..');
|
const devDir = resolve(__dirname, '../..');
|
||||||
const devCfg = join(devDir, cfgFile);
|
const devCfg = join(devDir, cfgFile);
|
||||||
|
|
@ -30,7 +32,6 @@ if (isDev) {
|
||||||
statSync(devCfg);
|
statSync(devCfg);
|
||||||
cfgPath = devCfg;
|
cfgPath = devCfg;
|
||||||
cfgDir = devDir;
|
cfgDir = devDir;
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log('using config file:', cfgPath);
|
console.log('using config file:', cfgPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignore
|
// ignore
|
||||||
|
|
@ -69,7 +70,7 @@ const defaultPlatformKeyPath = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
export {
|
||||||
cfgDir,
|
cfgDir,
|
||||||
cfgPath,
|
cfgPath,
|
||||||
legacyCfgPath,
|
legacyCfgPath,
|
||||||
|
|
@ -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.
|
// Print diagnostic information for a few arguments instead of running Hyper.
|
||||||
if (['--help', '-v', '--version'].includes(process.argv[1])) {
|
if (['--help', '-v', '--version'].includes(process.argv[1])) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const {version} = require('./package');
|
const {version} = require('./package');
|
||||||
const configLocation = process.platform === 'win32' ? process.env.userprofile + '\\.hyper.js' : '~/.hyper.js';
|
const configLocation = process.platform === 'win32' ? `${process.env.userprofile}\\.hyper.js` : '~/.hyper.js';
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log(`Hyper version ${version}`);
|
console.log(`Hyper version ${version}`);
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log('Hyper does not accept any command line arguments. Please modify the config file instead.');
|
console.log('Hyper does not accept any command line arguments. Please modify the config file instead.');
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log(`Hyper configuration file located at: ${configLocation}`);
|
console.log(`Hyper configuration file located at: ${configLocation}`);
|
||||||
// eslint-disable-next-line unicorn/no-process-exit
|
|
||||||
process.exit();
|
process.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkSquirrel = () => {
|
|
||||||
let squirrel;
|
|
||||||
|
|
||||||
try {
|
|
||||||
squirrel = require('electron-squirrel-startup');
|
|
||||||
//eslint-disable-next-line no-empty
|
|
||||||
} catch (err) {}
|
|
||||||
if (squirrel) {
|
|
||||||
// eslint-disable-next-line unicorn/no-process-exit
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// handle startup squirrel events
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
// eslint-disable-next-line import/order
|
|
||||||
const systemContextMenu = require('./system-context-menu');
|
|
||||||
|
|
||||||
switch (process.argv[1]) {
|
|
||||||
case '--squirrel-install':
|
|
||||||
case '--squirrel-updated':
|
|
||||||
systemContextMenu.add(() => {
|
|
||||||
checkSquirrel();
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case '--squirrel-uninstall':
|
|
||||||
systemContextMenu.remove(() => {
|
|
||||||
checkSquirrel();
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
checkSquirrel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Native
|
// Native
|
||||||
const {resolve} = require('path');
|
import {resolve} from 'path';
|
||||||
|
|
||||||
// Packages
|
// Packages
|
||||||
const {app, BrowserWindow, Menu} = require('electron');
|
import {app, BrowserWindow, Menu} from 'electron';
|
||||||
const {gitDescribe} = require('git-describe');
|
import {gitDescribe} from 'git-describe';
|
||||||
const isDev = require('electron-is-dev');
|
import isDev from 'electron-is-dev';
|
||||||
|
import * as config from './config';
|
||||||
const config = require('./config');
|
|
||||||
|
|
||||||
// set up config
|
// set up config
|
||||||
config.setup();
|
config.setup();
|
||||||
|
|
||||||
const plugins = require('./plugins');
|
import * as plugins from './plugins';
|
||||||
const {installCLI} = require('./utils/cli-install');
|
import {installCLI} from './utils/cli-install';
|
||||||
const AppMenu = require('./menus/menu');
|
import * as AppMenu from './menus/menu';
|
||||||
const Window = require('./ui/window');
|
import {newWindow} from './ui/window';
|
||||||
const windowUtils = require('./utils/window-utils');
|
import * as windowUtils from './utils/window-utils';
|
||||||
|
|
||||||
const windowSet = new Set([]);
|
const windowSet = new Set<BrowserWindow>([]);
|
||||||
|
|
||||||
// expose to plugins
|
// expose to plugins
|
||||||
app.config = config;
|
app.config = config;
|
||||||
|
|
@ -84,39 +45,56 @@ app.getLastFocusedWindow = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log('Disabling Chromium GPU blacklist');
|
console.log('Disabling Chromium GPU blacklist');
|
||||||
app.commandLine.appendSwitch('ignore-gpu-blacklist');
|
app.commandLine.appendSwitch('ignore-gpu-blacklist');
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log('running in dev mode');
|
console.log('running in dev mode');
|
||||||
|
|
||||||
// Override default appVersion which is set from package.json
|
// Override default appVersion which is set from package.json
|
||||||
gitDescribe({customArguments: ['--tags']}, (error, gitInfo) => {
|
gitDescribe({customArguments: ['--tags']}, (error: any, gitInfo: any) => {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
app.setVersion(gitInfo.raw);
|
app.setVersion(gitInfo.raw);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log('running in prod mode');
|
console.log('running in prod mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = 'file://' + resolve(isDev ? __dirname : app.getAppPath(), 'index.html');
|
const url = `file://${resolve(isDev ? __dirname : app.getAppPath(), 'index.html')}`;
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log('electron will open', url);
|
console.log('electron will open', url);
|
||||||
|
|
||||||
|
async function installDevExtensions(isDev_: boolean) {
|
||||||
|
if (!isDev_) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const installer = await import('electron-devtools-installer');
|
||||||
|
|
||||||
|
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'] as const;
|
||||||
|
const forceDownload = Boolean(process.env.UPGRADE_EXTENSIONS);
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
extensions.map((name) =>
|
||||||
|
installer.default(installer[name], {forceDownload, loadExtensionOptions: {allowFileAccess: true}})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
app.on('ready', () =>
|
app.on('ready', () =>
|
||||||
installDevExtensions(isDev)
|
installDevExtensions(isDev)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
function createWindow(fn, options = {}) {
|
function createWindow(
|
||||||
|
fn?: (win: BrowserWindow) => void,
|
||||||
|
options: {size?: [number, number]; position?: [number, number]} = {}
|
||||||
|
) {
|
||||||
const cfg = plugins.getDecoratedConfig();
|
const cfg = plugins.getDecoratedConfig();
|
||||||
|
|
||||||
const winSet = config.getWin();
|
const winSet = config.getWin();
|
||||||
let [startX, startY] = winSet.position;
|
let [startX, startY] = winSet.position;
|
||||||
|
|
||||||
const [width, height] = options.size ? options.size : cfg.windowSize || winSet.size;
|
const [width, height] = options.size ? options.size : cfg.windowSize || winSet.size;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const {screen} = require('electron');
|
const {screen} = require('electron');
|
||||||
|
|
||||||
const winPos = options.position;
|
const winPos = options.position;
|
||||||
|
|
@ -154,9 +132,9 @@ app.on('ready', () =>
|
||||||
[startX, startY] = config.windowDefaults.windowPosition;
|
[startX, startY] = config.windowDefaults.windowPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hwin = new Window({width, height, x: startX, y: startY}, cfg, fn);
|
const hwin = newWindow({width, height, x: startX, y: startY}, cfg, fn);
|
||||||
windowSet.add(hwin);
|
windowSet.add(hwin);
|
||||||
hwin.loadURL(url);
|
void hwin.loadURL(url);
|
||||||
|
|
||||||
// the window can be closed by the browser process itself
|
// the window can be closed by the browser process itself
|
||||||
hwin.on('close', () => {
|
hwin.on('close', () => {
|
||||||
|
|
@ -164,12 +142,6 @@ app.on('ready', () =>
|
||||||
windowSet.delete(hwin);
|
windowSet.delete(hwin);
|
||||||
});
|
});
|
||||||
|
|
||||||
hwin.on('closed', () => {
|
|
||||||
if (process.platform !== 'darwin' && windowSet.size === 0) {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return hwin;
|
return hwin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,6 +160,12 @@ app.on('ready', () =>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const makeMenu = () => {
|
const makeMenu = () => {
|
||||||
const menu = plugins.decorateMenu(AppMenu.createMenu(createWindow, plugins.getLoadedPluginVersions));
|
const menu = plugins.decorateMenu(AppMenu.createMenu(createWindow, plugins.getLoadedPluginVersions));
|
||||||
|
|
||||||
|
|
@ -214,26 +192,26 @@ app.on('ready', () =>
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
// check if should be set/removed as default ssh protocol client
|
// check if should be set/removed as default ssh protocol client
|
||||||
if (config.getConfig().defaultSSHApp && !app.isDefaultProtocolClient('ssh')) {
|
if (config.getConfig().defaultSSHApp && !app.isDefaultProtocolClient('ssh')) {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log('Setting Hyper as default client for ssh:// protocol');
|
console.log('Setting Hyper as default client for ssh:// protocol');
|
||||||
app.setAsDefaultProtocolClient('ssh');
|
app.setAsDefaultProtocolClient('ssh');
|
||||||
} else if (!config.getConfig().defaultSSHApp && app.isDefaultProtocolClient('ssh')) {
|
} else if (!config.getConfig().defaultSSHApp && app.isDefaultProtocolClient('ssh')) {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log('Removing Hyper from default client for ssh:// protocol');
|
console.log('Removing Hyper from default client for ssh:// protocol');
|
||||||
app.removeAsDefaultProtocolClient('ssh');
|
app.removeAsDefaultProtocolClient('ssh');
|
||||||
}
|
}
|
||||||
installCLI(false);
|
void installCLI(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.error('Error while loading devtools extensions', err);
|
console.error('Error while loading devtools extensions', err);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
app.on('open-file', (event, path) => {
|
/**
|
||||||
|
* Get last focused BrowserWindow or create new if none and callback
|
||||||
|
* @param callback Function to call with the BrowserWindow
|
||||||
|
*/
|
||||||
|
function GetWindow(callback: (win: BrowserWindow) => void) {
|
||||||
const lastWindow = app.getLastFocusedWindow();
|
const lastWindow = app.getLastFocusedWindow();
|
||||||
const callback = win => win.rpc.emit('open file', {path});
|
|
||||||
if (lastWindow) {
|
if (lastWindow) {
|
||||||
callback(lastWindow);
|
callback(lastWindow);
|
||||||
} else if (!lastWindow && {}.hasOwnProperty.call(app, 'createWindow')) {
|
} else if (!lastWindow && {}.hasOwnProperty.call(app, 'createWindow')) {
|
||||||
|
|
@ -243,31 +221,16 @@ app.on('open-file', (event, path) => {
|
||||||
// sets his callback to an app.windowCallback property.
|
// sets his callback to an app.windowCallback property.
|
||||||
app.windowCallback = callback;
|
app.windowCallback = callback;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
app.on('open-url', (event, sshUrl) => {
|
|
||||||
const lastWindow = app.getLastFocusedWindow();
|
|
||||||
const callback = win => win.rpc.emit('open ssh', sshUrl);
|
|
||||||
if (lastWindow) {
|
|
||||||
callback(lastWindow);
|
|
||||||
} else if (!lastWindow && {}.hasOwnProperty.call(app, 'createWindow')) {
|
|
||||||
app.createWindow(callback);
|
|
||||||
} else {
|
|
||||||
// If createWindow doesn't exist yet ('ready' event was not fired),
|
|
||||||
// sets his callback to an app.windowCallback property.
|
|
||||||
app.windowCallback = callback;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function installDevExtensions(isDev_) {
|
|
||||||
if (!isDev_) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
||||||
const installer = require('electron-devtools-installer');
|
|
||||||
|
|
||||||
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
|
|
||||||
const forceDownload = Boolean(process.env.UPGRADE_EXTENSIONS);
|
|
||||||
|
|
||||||
return Promise.all(extensions.map(name => installer.default(installer[name], forceDownload)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.on('open-file', (_event, path) => {
|
||||||
|
GetWindow((win: BrowserWindow) => {
|
||||||
|
win.rpc.emit('open file', {path});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('open-url', (_event, sshUrl) => {
|
||||||
|
GetWindow((win: BrowserWindow) => {
|
||||||
|
win.rpc.emit('open ssh', sshUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -30,8 +30,8 @@
|
||||||
"tab:jump:prefix": "command",
|
"tab:jump:prefix": "command",
|
||||||
"pane:next": "command+]",
|
"pane:next": "command+]",
|
||||||
"pane:prev": "command+[",
|
"pane:prev": "command+[",
|
||||||
"pane:splitVertical": "command+d",
|
"pane:splitRight": "command+d",
|
||||||
"pane:splitHorizontal": "command+shift+d",
|
"pane:splitDown": "command+shift+d",
|
||||||
"pane:close": "command+w",
|
"pane:close": "command+w",
|
||||||
"editor:undo": "command+z",
|
"editor:undo": "command+z",
|
||||||
"editor:redo": "command+y",
|
"editor:redo": "command+y",
|
||||||
|
|
@ -39,6 +39,8 @@
|
||||||
"editor:copy": "command+c",
|
"editor:copy": "command+c",
|
||||||
"editor:paste": "command+v",
|
"editor:paste": "command+v",
|
||||||
"editor:selectAll": "command+a",
|
"editor:selectAll": "command+a",
|
||||||
|
"editor:search": "command+f",
|
||||||
|
"editor:search-close": "esc",
|
||||||
"editor:movePreviousWord": "alt+left",
|
"editor:movePreviousWord": "alt+left",
|
||||||
"editor:moveNextWord": "alt+right",
|
"editor:moveNextWord": "alt+right",
|
||||||
"editor:moveBeginningLine": "command+left",
|
"editor:moveBeginningLine": "command+left",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"window:reload": "ctrl+shift+r",
|
"window:reload": "ctrl+shift+r",
|
||||||
"window:reloadFull": "ctrl+shift+f5",
|
"window:reloadFull": "ctrl+shift+f5",
|
||||||
"window:preferences": "ctrl+,",
|
"window:preferences": "ctrl+,",
|
||||||
|
"window:hamburgerMenu": "alt",
|
||||||
"zoom:reset": "ctrl+0",
|
"zoom:reset": "ctrl+0",
|
||||||
"zoom:in": "ctrl+=",
|
"zoom:in": "ctrl+=",
|
||||||
"zoom:out": "ctrl+-",
|
"zoom:out": "ctrl+-",
|
||||||
|
|
@ -27,8 +28,8 @@
|
||||||
"tab:jump:prefix": "ctrl",
|
"tab:jump:prefix": "ctrl",
|
||||||
"pane:next": "ctrl+pageup",
|
"pane:next": "ctrl+pageup",
|
||||||
"pane:prev": "ctrl+pagedown",
|
"pane:prev": "ctrl+pagedown",
|
||||||
"pane:splitVertical": "ctrl+shift+d",
|
"pane:splitRight": "ctrl+shift+d",
|
||||||
"pane:splitHorizontal": "ctrl+shift+e",
|
"pane:splitDown": "ctrl+shift+e",
|
||||||
"pane:close": "ctrl+shift+w",
|
"pane:close": "ctrl+shift+w",
|
||||||
"editor:undo": "ctrl+shift+z",
|
"editor:undo": "ctrl+shift+z",
|
||||||
"editor:redo": "ctrl+shift+y",
|
"editor:redo": "ctrl+shift+y",
|
||||||
|
|
@ -36,6 +37,8 @@
|
||||||
"editor:copy": "ctrl+shift+c",
|
"editor:copy": "ctrl+shift+c",
|
||||||
"editor:paste": "ctrl+shift+v",
|
"editor:paste": "ctrl+shift+v",
|
||||||
"editor:selectAll": "ctrl+shift+a",
|
"editor:selectAll": "ctrl+shift+a",
|
||||||
|
"editor:search": "ctrl+shift+f",
|
||||||
|
"editor:search-close": "esc",
|
||||||
"editor:movePreviousWord": "ctrl+left",
|
"editor:movePreviousWord": "ctrl+left",
|
||||||
"editor:moveNextWord": "ctrl+right",
|
"editor:moveNextWord": "ctrl+right",
|
||||||
"editor:moveBeginningLine": "home",
|
"editor:moveBeginningLine": "home",
|
||||||
|
|
|
||||||
|
|
@ -16,23 +16,11 @@
|
||||||
"alt+f4"
|
"alt+f4"
|
||||||
],
|
],
|
||||||
"tab:new": "ctrl+shift+t",
|
"tab:new": "ctrl+shift+t",
|
||||||
"tab:next": [
|
|
||||||
"ctrl+shift+]",
|
|
||||||
"ctrl+shift+right",
|
|
||||||
"ctrl+alt+right",
|
|
||||||
"ctrl+tab"
|
|
||||||
],
|
|
||||||
"tab:prev": [
|
|
||||||
"ctrl+shift+[",
|
|
||||||
"ctrl+shift+left",
|
|
||||||
"ctrl+alt+left",
|
|
||||||
"ctrl+shift+tab"
|
|
||||||
],
|
|
||||||
"tab:jump:prefix": "ctrl",
|
"tab:jump:prefix": "ctrl",
|
||||||
"pane:next": "ctrl+pageup",
|
"pane:next": "ctrl+pageup",
|
||||||
"pane:prev": "ctrl+pagedown",
|
"pane:prev": "ctrl+pagedown",
|
||||||
"pane:splitVertical": "ctrl+shift+d",
|
"pane:splitRight": "ctrl+shift+d",
|
||||||
"pane:splitHorizontal": "ctrl+shift+e",
|
"pane:splitDown": "ctrl+shift+e",
|
||||||
"pane:close": "ctrl+shift+w",
|
"pane:close": "ctrl+shift+w",
|
||||||
"editor:undo": "ctrl+shift+z",
|
"editor:undo": "ctrl+shift+z",
|
||||||
"editor:redo": "ctrl+shift+y",
|
"editor:redo": "ctrl+shift+y",
|
||||||
|
|
@ -40,8 +28,10 @@
|
||||||
"editor:copy": "ctrl+shift+c",
|
"editor:copy": "ctrl+shift+c",
|
||||||
"editor:paste": "ctrl+shift+v",
|
"editor:paste": "ctrl+shift+v",
|
||||||
"editor:selectAll": "ctrl+shift+a",
|
"editor:selectAll": "ctrl+shift+a",
|
||||||
"editor:movePreviousWord": "ctrl+left",
|
"editor:search": "ctrl+shift+f",
|
||||||
"editor:moveNextWord": "ctrl+right",
|
"editor:search-close": "esc",
|
||||||
|
"editor:movePreviousWord": "",
|
||||||
|
"editor:moveNextWord": "",
|
||||||
"editor:moveBeginningLine": "Home",
|
"editor:moveBeginningLine": "Home",
|
||||||
"editor:moveEndLine": "End",
|
"editor:moveEndLine": "End",
|
||||||
"editor:deletePreviousWord": "ctrl+backspace",
|
"editor:deletePreviousWord": "ctrl+backspace",
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,49 @@
|
||||||
// Packages
|
// Packages
|
||||||
const {app, dialog, Menu} = require('electron');
|
import {app, dialog, Menu, BrowserWindow} from 'electron';
|
||||||
|
|
||||||
// Utilities
|
// Utilities
|
||||||
const {getConfig} = require('../config');
|
import {getConfig} from '../config';
|
||||||
const {icon} = require('../config/paths');
|
import {icon} from '../config/paths';
|
||||||
const viewMenu = require('./menus/view');
|
import viewMenu from './menus/view';
|
||||||
const shellMenu = require('./menus/shell');
|
import shellMenu from './menus/shell';
|
||||||
const editMenu = require('./menus/edit');
|
import editMenu from './menus/edit';
|
||||||
const pluginsMenu = require('./menus/plugins');
|
import toolsMenu from './menus/tools';
|
||||||
const windowMenu = require('./menus/window');
|
import windowMenu from './menus/window';
|
||||||
const helpMenu = require('./menus/help');
|
import helpMenu from './menus/help';
|
||||||
const darwinMenu = require('./menus/darwin');
|
import darwinMenu from './menus/darwin';
|
||||||
const {getDecoratedKeymaps} = require('../plugins');
|
import {getDecoratedKeymaps} from '../plugins';
|
||||||
const {execCommand} = require('../commands');
|
import {execCommand} from '../commands';
|
||||||
const {getRendererTypes} = require('../utils/renderer-utils');
|
import {getRendererTypes} from '../utils/renderer-utils';
|
||||||
|
|
||||||
const appName = app.getName();
|
const appName = app.name;
|
||||||
const appVersion = app.getVersion();
|
const appVersion = app.getVersion();
|
||||||
|
|
||||||
let menu_ = [];
|
let menu_: Menu;
|
||||||
|
|
||||||
exports.createMenu = (createWindow, getLoadedPluginVersions) => {
|
export const createMenu = (
|
||||||
|
createWindow: (fn?: (win: BrowserWindow) => void, options?: Record<string, any>) => BrowserWindow,
|
||||||
|
getLoadedPluginVersions: () => {name: string; version: string}[]
|
||||||
|
) => {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
// We take only first shortcut in array for each command
|
// We take only first shortcut in array for each command
|
||||||
const allCommandKeys = getDecoratedKeymaps();
|
const allCommandKeys = getDecoratedKeymaps();
|
||||||
const commandKeys = Object.keys(allCommandKeys).reduce((result, command) => {
|
const commandKeys = Object.keys(allCommandKeys).reduce((result: Record<string, string>, command) => {
|
||||||
result[command] = allCommandKeys[command][0];
|
result[command] = allCommandKeys[command][0];
|
||||||
return result;
|
return result;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
let updateChannel = 'stable';
|
let updateChannel = 'stable';
|
||||||
|
|
||||||
if (config && config.updateChannel && config.updateChannel === 'canary') {
|
if (config?.updateChannel && config.updateChannel === 'canary') {
|
||||||
updateChannel = 'canary';
|
updateChannel = 'canary';
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAbout = () => {
|
const showAbout = () => {
|
||||||
const loadedPlugins = getLoadedPluginVersions();
|
const loadedPlugins = getLoadedPluginVersions();
|
||||||
const pluginList =
|
const pluginList =
|
||||||
loadedPlugins.length === 0 ? 'none' : loadedPlugins.map(plugin => `\n ${plugin.name} (${plugin.version})`);
|
loadedPlugins.length === 0 ? 'none' : loadedPlugins.map((plugin) => `\n ${plugin.name} (${plugin.version})`);
|
||||||
|
|
||||||
const rendererCounts = Object.values(getRendererTypes()).reduce((acc, type) => {
|
const rendererCounts = Object.values(getRendererTypes()).reduce((acc: Record<string, number>, type) => {
|
||||||
acc[type] = acc[type] ? acc[type] + 1 : 1;
|
acc[type] = acc[type] ? acc[type] + 1 : 1;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
@ -48,12 +51,12 @@ exports.createMenu = (createWindow, getLoadedPluginVersions) => {
|
||||||
.map(([type, count]) => type + (count > 1 ? ` (${count})` : ''))
|
.map(([type, count]) => type + (count > 1 ? ` (${count})` : ''))
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
dialog.showMessageBox({
|
void dialog.showMessageBox({
|
||||||
title: `About ${appName}`,
|
title: `About ${appName}`,
|
||||||
message: `${appName} ${appVersion} (${updateChannel})`,
|
message: `${appName} ${appVersion} (${updateChannel})`,
|
||||||
detail: `Renderers: ${renderers}\nPlugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright © 2019 ZEIT, Inc.`,
|
detail: `Renderers: ${renderers}\nPlugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright © 2020 Vercel, Inc.`,
|
||||||
buttons: [],
|
buttons: [],
|
||||||
icon
|
icon: icon as any
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const menu = [
|
const menu = [
|
||||||
|
|
@ -61,7 +64,7 @@ exports.createMenu = (createWindow, getLoadedPluginVersions) => {
|
||||||
shellMenu(commandKeys, execCommand),
|
shellMenu(commandKeys, execCommand),
|
||||||
editMenu(commandKeys, execCommand),
|
editMenu(commandKeys, execCommand),
|
||||||
viewMenu(commandKeys, execCommand),
|
viewMenu(commandKeys, execCommand),
|
||||||
pluginsMenu(commandKeys, execCommand),
|
toolsMenu(commandKeys, execCommand),
|
||||||
windowMenu(commandKeys, execCommand),
|
windowMenu(commandKeys, execCommand),
|
||||||
helpMenu(commandKeys, showAbout)
|
helpMenu(commandKeys, showAbout)
|
||||||
];
|
];
|
||||||
|
|
@ -69,7 +72,7 @@ exports.createMenu = (createWindow, getLoadedPluginVersions) => {
|
||||||
return menu;
|
return menu;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.buildMenu = template => {
|
export const buildMenu = (template: Electron.MenuItemConstructorOptions[]): Electron.Menu => {
|
||||||
menu_ = Menu.buildFromTemplate(template);
|
menu_ = Menu.buildFromTemplate(template);
|
||||||
return menu_;
|
return menu_;
|
||||||
};
|
};
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
// This menu label is overrided by OSX to be the appName
|
// This menu label is overrided by OSX to be the appName
|
||||||
// The label is set to appName here so it matches actual behavior
|
// The label is set to appName here so it matches actual behavior
|
||||||
const {app} = require('electron');
|
import {app, BrowserWindow, MenuItemConstructorOptions} from 'electron';
|
||||||
|
|
||||||
module.exports = (commandKeys, execCommand, showAbout) => {
|
export default (
|
||||||
|
commandKeys: Record<string, string>,
|
||||||
|
execCommand: (command: string, focusedWindow?: BrowserWindow) => void,
|
||||||
|
showAbout: () => void
|
||||||
|
): MenuItemConstructorOptions => {
|
||||||
return {
|
return {
|
||||||
label: `${app.getName()}`,
|
label: `${app.name}`,
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'About Hyper',
|
label: 'About Hyper',
|
||||||
|
|
@ -36,7 +40,7 @@ module.exports = (commandKeys, execCommand, showAbout) => {
|
||||||
role: 'hide'
|
role: 'hide'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'hideothers'
|
role: 'hideOthers'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'unhide'
|
role: 'unhide'
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
module.exports = (commandKeys, execCommand) => {
|
import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
|
||||||
const submenu = [
|
|
||||||
|
export default (
|
||||||
|
commandKeys: Record<string, string>,
|
||||||
|
execCommand: (command: string, focusedWindow?: BrowserWindow) => void
|
||||||
|
) => {
|
||||||
|
const submenu: MenuItemConstructorOptions[] = [
|
||||||
{
|
{
|
||||||
label: 'Undo',
|
label: 'Undo',
|
||||||
accelerator: commandKeys['editor:undo'],
|
accelerator: commandKeys['editor:undo'],
|
||||||
|
|
@ -23,7 +28,7 @@ module.exports = (commandKeys, execCommand) => {
|
||||||
command: 'editor:copy',
|
command: 'editor:copy',
|
||||||
accelerator: commandKeys['editor:copy'],
|
accelerator: commandKeys['editor:copy'],
|
||||||
registerAccelerator: true
|
registerAccelerator: true
|
||||||
},
|
} as any,
|
||||||
{
|
{
|
||||||
role: 'paste',
|
role: 'paste',
|
||||||
accelerator: commandKeys['editor:paste']
|
accelerator: commandKeys['editor:paste']
|
||||||
|
|
@ -113,6 +118,13 @@ module.exports = (commandKeys, execCommand) => {
|
||||||
click(item, focusedWindow) {
|
click(item, focusedWindow) {
|
||||||
execCommand('editor:clearBuffer', focusedWindow);
|
execCommand('editor:clearBuffer', focusedWindow);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Search',
|
||||||
|
accelerator: commandKeys['editor:search'],
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
execCommand('editor:search', focusedWindow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -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';
|
const isMac = process.platform === 'darwin';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -22,17 +27,17 @@ module.exports = (commandKeys, execCommand) => {
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Split Horizontally',
|
label: 'Split Down',
|
||||||
accelerator: commandKeys['pane:splitHorizontal'],
|
accelerator: commandKeys['pane:splitDown'],
|
||||||
click(item, focusedWindow) {
|
click(item, focusedWindow) {
|
||||||
execCommand('pane:splitHorizontal', focusedWindow);
|
execCommand('pane:splitDown', focusedWindow);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Split Vertically',
|
label: 'Split Right',
|
||||||
accelerator: commandKeys['pane:splitVertical'],
|
accelerator: commandKeys['pane:splitRight'],
|
||||||
click(item, focusedWindow) {
|
click(item, focusedWindow) {
|
||||||
execCommand('pane:splitVertical', focusedWindow);
|
execCommand('pane:splitRight', focusedWindow);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
47
app/menus/menus/tools.ts
Normal file
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 {
|
return {
|
||||||
label: 'View',
|
label: 'View',
|
||||||
submenu: [
|
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
|
// Generating tab:jump array
|
||||||
const tabJump = [];
|
const tabJump = [];
|
||||||
for (let i = 1; i <= 9; i++) {
|
for (let i = 1; i <= 9; i++) {
|
||||||
// 9 is a special number because it means 'last'
|
// 9 is a special number because it means 'last'
|
||||||
const label = i === 9 ? 'Last' : `${i}`;
|
const label = i === 9 ? 'Last' : `${i}`;
|
||||||
tabJump.push({
|
tabJump.push({
|
||||||
label: label,
|
label,
|
||||||
accelerator: commandKeys[`tab:jump:${label.toLowerCase()}`]
|
accelerator: commandKeys[`tab:jump:${label.toLowerCase()}`]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -76,6 +81,12 @@ module.exports = (commandKeys, execCommand) => {
|
||||||
{
|
{
|
||||||
role: 'front'
|
role: 'front'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Toggle Always on Top',
|
||||||
|
click: (item, focusedWindow) => {
|
||||||
|
execCommand('window:toggleKeepOnTop', focusedWindow);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
role: 'togglefullscreen',
|
role: 'togglefullscreen',
|
||||||
accelerator: commandKeys['window:toggleFullScreen']
|
accelerator: commandKeys['window:toggleFullScreen']
|
||||||
|
|
@ -1,20 +1,18 @@
|
||||||
const ms = require('ms');
|
import ms from 'ms';
|
||||||
const fetch = require('electron-fetch').default;
|
import fetch from 'electron-fetch';
|
||||||
|
import {version} from './package.json';
|
||||||
const {version} = require('./package');
|
import {BrowserWindow} from 'electron';
|
||||||
|
|
||||||
const NEWS_URL = 'https://hyper-news.now.sh';
|
const NEWS_URL = 'https://hyper-news.now.sh';
|
||||||
|
|
||||||
module.exports = function fetchNotifications(win) {
|
export default function fetchNotifications(win: BrowserWindow) {
|
||||||
const {rpc} = win;
|
const {rpc} = win;
|
||||||
const retry = err => {
|
const retry = (err?: Error) => {
|
||||||
setTimeout(() => fetchNotifications(win), ms('30m'));
|
setTimeout(() => fetchNotifications(win), ms('30m'));
|
||||||
if (err) {
|
if (err) {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.error('Notification messages fetch error', err.stack);
|
console.error('Notification messages fetch error', err.stack);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log('Checking for notification messages');
|
console.log('Checking for notification messages');
|
||||||
fetch(NEWS_URL, {
|
fetch(NEWS_URL, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -22,14 +20,13 @@ module.exports = function fetchNotifications(win) {
|
||||||
'X-Hyper-Platform': process.platform
|
'X-Hyper-Platform': process.platform
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then((res) => res.json())
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
const {message} = data || {};
|
const {message} = data || {};
|
||||||
if (typeof message !== 'object' && message !== '') {
|
if (typeof message !== 'object' && message !== '') {
|
||||||
throw new Error('Bad response');
|
throw new Error('Bad response');
|
||||||
}
|
}
|
||||||
if (message === '') {
|
if (message === '') {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log('No matching notification messages');
|
console.log('No matching notification messages');
|
||||||
} else {
|
} else {
|
||||||
rpc.emit('add notification', message);
|
rpc.emit('add notification', message);
|
||||||
|
|
@ -38,4 +35,4 @@ module.exports = function fetchNotifications(win) {
|
||||||
retry();
|
retry();
|
||||||
})
|
})
|
||||||
.catch(retry);
|
.catch(retry);
|
||||||
};
|
}
|
||||||
|
|
@ -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",
|
"repository": "zeit/hyper",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async-retry": "1.1.4",
|
"async-retry": "1.3.1",
|
||||||
"color": "2.0.1",
|
"chokidar": "^3.5.2",
|
||||||
|
"color": "3.1.3",
|
||||||
"convert-css-color-name-to-hex": "0.1.1",
|
"convert-css-color-name-to-hex": "0.1.1",
|
||||||
"default-shell": "1.0.1",
|
"default-shell": "1.0.1",
|
||||||
"electron-config": "2.0.0",
|
"electron-fetch": "1.7.3",
|
||||||
"electron-fetch": "1.3.0",
|
"electron-is-dev": "2.0.0",
|
||||||
"electron-is-dev": "1.0.1",
|
"electron-store": "8.0.0",
|
||||||
"electron-squirrel-startup": "1.0.0",
|
"file-uri-to-path": "2.0.0",
|
||||||
"file-uri-to-path": "1.0.0",
|
"fs-extra": "10.0.0",
|
||||||
"fs-extra": "7.0.1",
|
"git-describe": "4.0.4",
|
||||||
"git-describe": "4.0.2",
|
"lodash": "4.17.21",
|
||||||
"lodash": "4.17.5",
|
"mkdirp": "1.0.4",
|
||||||
"mkdirp": "0.5.1",
|
"ms": "2.1.3",
|
||||||
"ms": "2.1.1",
|
"node-pty": "0.10.1",
|
||||||
"node-pty": "0.8.0",
|
"os-locale": "5.0.0",
|
||||||
"os-locale": "3.1.0",
|
"parse-url": "5.0.7",
|
||||||
"parse-url": "3.0.2",
|
"pify": "5.0.0",
|
||||||
"queue": "4.4.2",
|
"queue": "6.0.2",
|
||||||
"react": "16.2.0",
|
"react": "17.0.2",
|
||||||
"react-dom": "16.2.1",
|
"react-dom": "17.0.2",
|
||||||
"semver": "5.5.0",
|
"semver": "7.3.5",
|
||||||
"shell-env": "0.3.0",
|
"shell-env": "3.0.1",
|
||||||
"uuid": "3.2.1",
|
"sudo-prompt": "^9.2.1",
|
||||||
"winreg": "1.2.4"
|
"uuid": "8.3.2"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"native-reg": "0.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
const {app, dialog} = require('electron');
|
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||||
const {resolve, basename} = require('path');
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
const {writeFileSync} = require('fs');
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
const Config = require('electron-config');
|
import {app, dialog, BrowserWindow, App} from 'electron';
|
||||||
const ms = require('ms');
|
import {resolve, basename} from 'path';
|
||||||
|
import {writeFileSync} from 'fs';
|
||||||
const React = require('react');
|
import Config from 'electron-store';
|
||||||
const ReactDom = require('react-dom');
|
import ms from 'ms';
|
||||||
|
import React from 'react';
|
||||||
const config = require('./config');
|
import ReactDom from 'react-dom';
|
||||||
const notify = require('./notify');
|
import * as config from './config';
|
||||||
const {availableExtensions} = require('./plugins/extensions');
|
import notify from './notify';
|
||||||
const {install} = require('./plugins/install');
|
import {availableExtensions} from './plugins/extensions';
|
||||||
const {plugs} = require('./config/paths');
|
import {install} from './plugins/install';
|
||||||
const mapKeys = require('./utils/map-keys');
|
import {plugs} from './config/paths';
|
||||||
|
import mapKeys from './utils/map-keys';
|
||||||
|
import {configOptions} from '../lib/config';
|
||||||
|
|
||||||
// local storage
|
// local storage
|
||||||
const cache = new Config();
|
const cache = new Config();
|
||||||
|
|
@ -28,11 +30,11 @@ let paths = getPaths();
|
||||||
let id = getId(plugins);
|
let id = getId(plugins);
|
||||||
let modules = requirePlugins();
|
let modules = requirePlugins();
|
||||||
|
|
||||||
function getId(plugins_) {
|
function getId(plugins_: any) {
|
||||||
return JSON.stringify(plugins_);
|
return JSON.stringify(plugins_);
|
||||||
}
|
}
|
||||||
|
|
||||||
const watchers = [];
|
const watchers: Function[] = [];
|
||||||
|
|
||||||
// we listen on configuration updates to trigger
|
// we listen on configuration updates to trigger
|
||||||
// plugin installation
|
// plugin installation
|
||||||
|
|
@ -50,11 +52,12 @@ config.subscribe(() => {
|
||||||
|
|
||||||
// patching Module._load
|
// patching Module._load
|
||||||
// so plugins can `require` them without needing their own version
|
// so plugins can `require` them without needing their own version
|
||||||
// https://github.com/zeit/hyper/issues/619
|
// https://github.com/vercel/hyper/issues/619
|
||||||
function patchModuleLoad() {
|
function patchModuleLoad() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const Module = require('module');
|
const Module = require('module');
|
||||||
const originalLoad = Module._load;
|
const originalLoad = Module._load;
|
||||||
Module._load = function _load(modulePath) {
|
Module._load = function _load(modulePath: string) {
|
||||||
// PLEASE NOTE: Code changes here, also need to be changed in
|
// PLEASE NOTE: Code changes here, also need to be changed in
|
||||||
// lib/utils/plugins.js
|
// lib/utils/plugins.js
|
||||||
switch (modulePath) {
|
switch (modulePath) {
|
||||||
|
|
@ -74,13 +77,14 @@ function patchModuleLoad() {
|
||||||
case 'hyper/decorate':
|
case 'hyper/decorate':
|
||||||
return Object;
|
return Object;
|
||||||
default:
|
default:
|
||||||
|
// eslint-disable-next-line prefer-rest-params
|
||||||
return originalLoad.apply(this, arguments);
|
return originalLoad.apply(this, arguments);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkDeprecatedExtendKeymaps() {
|
function checkDeprecatedExtendKeymaps() {
|
||||||
modules.forEach(plugin => {
|
modules.forEach((plugin) => {
|
||||||
if (plugin.extendKeymaps) {
|
if (plugin.extendKeymaps) {
|
||||||
notify('Plugin warning!', `"${plugin._name}" use deprecated "extendKeymaps" handler`);
|
notify('Plugin warning!', `"${plugin._name}" use deprecated "extendKeymaps" handler`);
|
||||||
return;
|
return;
|
||||||
|
|
@ -97,11 +101,10 @@ function updatePlugins({force = false} = {}) {
|
||||||
updating = true;
|
updating = true;
|
||||||
syncPackageJSON();
|
syncPackageJSON();
|
||||||
const id_ = id;
|
const id_ = id;
|
||||||
install(err => {
|
install((err) => {
|
||||||
updating = false;
|
updating = false;
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
notify('Error updating plugins.', err, {error: err});
|
notify('Error updating plugins.', err, {error: err});
|
||||||
} else {
|
} else {
|
||||||
// flag successful plugin update
|
// flag successful plugin update
|
||||||
|
|
@ -123,7 +126,9 @@ function updatePlugins({force = false} = {}) {
|
||||||
cache.set('hyper.plugin-versions', pluginVersions);
|
cache.set('hyper.plugin-versions', pluginVersions);
|
||||||
|
|
||||||
// notify watchers
|
// notify watchers
|
||||||
watchers.forEach(fn => fn(err, {force}));
|
watchers.forEach((fn) => {
|
||||||
|
fn(err, {force});
|
||||||
|
});
|
||||||
|
|
||||||
if (force || changed) {
|
if (force || changed) {
|
||||||
if (changed) {
|
if (changed) {
|
||||||
|
|
@ -139,10 +144,10 @@ function updatePlugins({force = false} = {}) {
|
||||||
|
|
||||||
function getPluginVersions() {
|
function getPluginVersions() {
|
||||||
const paths_ = paths.plugins.concat(paths.localPlugins);
|
const paths_ = paths.plugins.concat(paths.localPlugins);
|
||||||
return paths_.map(path_ => {
|
return paths_.map((path_) => {
|
||||||
let version = null;
|
let version: string | null = null;
|
||||||
try {
|
try {
|
||||||
//eslint-disable-next-line import/no-dynamic-require
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
version = require(resolve(path_, 'package.json')).version;
|
version = require(resolve(path_, 'package.json')).version;
|
||||||
//eslint-disable-next-line no-empty
|
//eslint-disable-next-line no-empty
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
|
|
@ -152,7 +157,7 @@ function getPluginVersions() {
|
||||||
|
|
||||||
function clearCache() {
|
function clearCache() {
|
||||||
// trigger unload hooks
|
// trigger unload hooks
|
||||||
modules.forEach(mod => {
|
modules.forEach((mod) => {
|
||||||
if (mod.onUnload) {
|
if (mod.onUnload) {
|
||||||
mod.onUnload(app);
|
mod.onUnload(app);
|
||||||
}
|
}
|
||||||
|
|
@ -166,10 +171,10 @@ function clearCache() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.updatePlugins = updatePlugins;
|
export {updatePlugins};
|
||||||
|
|
||||||
exports.getLoadedPluginVersions = () => {
|
export const getLoadedPluginVersions = () => {
|
||||||
return modules.map(mod => ({name: mod._name, version: mod._version}));
|
return modules.map((mod) => ({name: mod._name, version: mod._version}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// we schedule the initial plugins update
|
// we schedule the initial plugins update
|
||||||
|
|
@ -177,15 +182,19 @@ exports.getLoadedPluginVersions = () => {
|
||||||
// to prevent slowness
|
// to prevent slowness
|
||||||
if (cache.get('hyper.plugins') !== id || process.env.HYPER_FORCE_UPDATE) {
|
if (cache.get('hyper.plugins') !== id || process.env.HYPER_FORCE_UPDATE) {
|
||||||
// install immediately if the user changed plugins
|
// install immediately if the user changed plugins
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log('plugins have changed / not init, scheduling plugins installation');
|
console.log('plugins have changed / not init, scheduling plugins installation');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updatePlugins();
|
updatePlugins();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise update plugins every 5 hours
|
(() => {
|
||||||
setInterval(updatePlugins, ms('5h'));
|
const baseConfig = config.getConfig();
|
||||||
|
if (baseConfig['autoUpdatePlugins']) {
|
||||||
|
// otherwise update plugins every 5 hours
|
||||||
|
setInterval(updatePlugins, ms(baseConfig['autoUpdatePlugins'] === true ? '5h' : baseConfig['autoUpdatePlugins']));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
function syncPackageJSON() {
|
function syncPackageJSON() {
|
||||||
const dependencies = toDependencies(plugins);
|
const dependencies = toDependencies(plugins);
|
||||||
|
|
@ -194,7 +203,7 @@ function syncPackageJSON() {
|
||||||
description: 'Auto-generated from `~/.hyper.js`!',
|
description: 'Auto-generated from `~/.hyper.js`!',
|
||||||
private: true,
|
private: true,
|
||||||
version: '0.0.1',
|
version: '0.0.1',
|
||||||
repository: 'zeit/hyper',
|
repository: 'vercel/hyper',
|
||||||
license: 'MIT',
|
license: 'MIT',
|
||||||
homepage: 'https://hyper.is',
|
homepage: 'https://hyper.is',
|
||||||
dependencies
|
dependencies
|
||||||
|
|
@ -208,16 +217,16 @@ function syncPackageJSON() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function alert(message) {
|
function alert(message: string) {
|
||||||
dialog.showMessageBox({
|
void dialog.showMessageBox({
|
||||||
message,
|
message,
|
||||||
buttons: ['Ok']
|
buttons: ['Ok']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDependencies(plugins_) {
|
function toDependencies(plugins_: {plugins: string[]}) {
|
||||||
const obj = {};
|
const obj: Record<string, string> = {};
|
||||||
plugins_.plugins.forEach(plugin => {
|
plugins_.plugins.forEach((plugin) => {
|
||||||
const regex = /.(@|#)/;
|
const regex = /.(@|#)/;
|
||||||
const match = regex.exec(plugin);
|
const match = regex.exec(plugin);
|
||||||
|
|
||||||
|
|
@ -235,7 +244,7 @@ function toDependencies(plugins_) {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.subscribe = fn => {
|
export const subscribe = (fn: Function) => {
|
||||||
watchers.push(fn);
|
watchers.push(fn);
|
||||||
return () => {
|
return () => {
|
||||||
watchers.splice(watchers.indexOf(fn), 1);
|
watchers.splice(watchers.indexOf(fn), 1);
|
||||||
|
|
@ -244,53 +253,49 @@ exports.subscribe = fn => {
|
||||||
|
|
||||||
function getPaths() {
|
function getPaths() {
|
||||||
return {
|
return {
|
||||||
plugins: plugins.plugins.map(name => {
|
plugins: plugins.plugins.map((name) => {
|
||||||
return resolve(path, 'node_modules', name.split('#')[0].split('@')[0]);
|
return resolve(path, 'node_modules', name.split('#')[0]);
|
||||||
}),
|
}),
|
||||||
localPlugins: plugins.localPlugins.map(name => {
|
localPlugins: plugins.localPlugins.map((name) => {
|
||||||
return resolve(localPath, name);
|
return resolve(localPath, name);
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// expose to renderer
|
// expose to renderer
|
||||||
exports.getPaths = getPaths;
|
export {getPaths};
|
||||||
|
|
||||||
// get paths from renderer
|
// get paths from renderer
|
||||||
exports.getBasePaths = () => {
|
export const getBasePaths = () => {
|
||||||
return {path, localPath};
|
return {path, localPath};
|
||||||
};
|
};
|
||||||
|
|
||||||
function requirePlugins() {
|
function requirePlugins(): any[] {
|
||||||
const {plugins: plugins_, localPlugins} = paths;
|
const {plugins: plugins_, localPlugins} = paths;
|
||||||
|
|
||||||
const load = path_ => {
|
const load = (path_: string) => {
|
||||||
let mod;
|
let mod: any;
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line import/no-dynamic-require
|
|
||||||
mod = require(path_);
|
mod = require(path_);
|
||||||
const exposed = mod && Object.keys(mod).some(key => availableExtensions.has(key));
|
const exposed = mod && Object.keys(mod).some((key) => availableExtensions.has(key));
|
||||||
if (!exposed) {
|
if (!exposed) {
|
||||||
notify('Plugin error!', `Plugin "${basename(path_)}" does not expose any ` + 'Hyper extension API methods');
|
notify('Plugin error!', `${`Plugin "${basename(path_)}" does not expose any `}Hyper extension API methods`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// populate the name for internal errors here
|
// populate the name for internal errors here
|
||||||
mod._name = basename(path_);
|
mod._name = basename(path_);
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line import/no-dynamic-require
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
mod._version = require(resolve(path_, 'package.json')).version;
|
mod._version = require(resolve(path_, 'package.json')).version;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.warn(`No package.json found in ${path_}`);
|
console.warn(`No package.json found in ${path_}`);
|
||||||
}
|
}
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.log(`Plugin ${mod._name} (${mod._version}) loaded.`);
|
console.log(`Plugin ${mod._name} (${mod._version}) loaded.`);
|
||||||
|
|
||||||
return mod;
|
return mod;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'MODULE_NOT_FOUND') {
|
if (err.code === 'MODULE_NOT_FOUND') {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.warn(`Plugin error while loading "${basename(path_)}" (${path_}): ${err.message}`);
|
console.warn(`Plugin error while loading "${basename(path_)}" (${path_}): ${err.message}`);
|
||||||
} else {
|
} else {
|
||||||
notify('Plugin error!', `Plugin "${basename(path_)}" failed to load (${err.message})`, {error: err});
|
notify('Plugin error!', `Plugin "${basename(path_)}" failed to load (${err.message})`, {error: err});
|
||||||
|
|
@ -301,11 +306,11 @@ function requirePlugins() {
|
||||||
return plugins_
|
return plugins_
|
||||||
.map(load)
|
.map(load)
|
||||||
.concat(localPlugins.map(load))
|
.concat(localPlugins.map(load))
|
||||||
.filter(v => Boolean(v));
|
.filter((v) => Boolean(v));
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.onApp = app_ => {
|
export const onApp = (app_: App) => {
|
||||||
modules.forEach(plugin => {
|
modules.forEach((plugin) => {
|
||||||
if (plugin.onApp) {
|
if (plugin.onApp) {
|
||||||
try {
|
try {
|
||||||
plugin.onApp(app_);
|
plugin.onApp(app_);
|
||||||
|
|
@ -318,8 +323,8 @@ exports.onApp = app_ => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.onWindowClass = win => {
|
export const onWindowClass = (win: BrowserWindow) => {
|
||||||
modules.forEach(plugin => {
|
modules.forEach((plugin) => {
|
||||||
if (plugin.onWindowClass) {
|
if (plugin.onWindowClass) {
|
||||||
try {
|
try {
|
||||||
plugin.onWindowClass(win);
|
plugin.onWindowClass(win);
|
||||||
|
|
@ -332,8 +337,8 @@ exports.onWindowClass = win => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.onWindow = win => {
|
export const onWindow = (win: BrowserWindow) => {
|
||||||
modules.forEach(plugin => {
|
modules.forEach((plugin) => {
|
||||||
if (plugin.onWindow) {
|
if (plugin.onWindow) {
|
||||||
try {
|
try {
|
||||||
plugin.onWindow(win);
|
plugin.onWindow(win);
|
||||||
|
|
@ -348,9 +353,9 @@ exports.onWindow = win => {
|
||||||
|
|
||||||
// decorates the base entity by calling plugin[key]
|
// decorates the base entity by calling plugin[key]
|
||||||
// for all the available plugins
|
// for all the available plugins
|
||||||
function decorateEntity(base, key, type) {
|
function decorateEntity(base: any, key: string, type: 'object' | 'function') {
|
||||||
let decorated = base;
|
let decorated = base;
|
||||||
modules.forEach(plugin => {
|
modules.forEach((plugin) => {
|
||||||
if (plugin[key]) {
|
if (plugin[key]) {
|
||||||
let res;
|
let res;
|
||||||
try {
|
try {
|
||||||
|
|
@ -370,23 +375,23 @@ function decorateEntity(base, key, type) {
|
||||||
return decorated;
|
return decorated;
|
||||||
}
|
}
|
||||||
|
|
||||||
function decorateObject(base, key) {
|
function decorateObject<T>(base: T, key: string): T {
|
||||||
return decorateEntity(base, key, 'object');
|
return decorateEntity(base, key, 'object');
|
||||||
}
|
}
|
||||||
|
|
||||||
function decorateClass(base, key) {
|
function decorateClass(base: any, key: string) {
|
||||||
return decorateEntity(base, key, 'function');
|
return decorateEntity(base, key, 'function');
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getDeprecatedConfig = () => {
|
export const getDeprecatedConfig = () => {
|
||||||
const deprecated = {};
|
const deprecated: Record<string, {css: string[]}> = {};
|
||||||
const baseConfig = config.getConfig();
|
const baseConfig = config.getConfig();
|
||||||
modules.forEach(plugin => {
|
modules.forEach((plugin) => {
|
||||||
if (!plugin.decorateConfig) {
|
if (!plugin.decorateConfig) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// We need to clone config in case of plugin modifies config directly.
|
// We need to clone config in case of plugin modifies config directly.
|
||||||
let configTmp;
|
let configTmp: configOptions;
|
||||||
try {
|
try {
|
||||||
configTmp = plugin.decorateConfig(JSON.parse(JSON.stringify(baseConfig)));
|
configTmp = plugin.decorateConfig(JSON.parse(JSON.stringify(baseConfig)));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -404,15 +409,15 @@ exports.getDeprecatedConfig = () => {
|
||||||
return deprecated;
|
return deprecated;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.decorateMenu = tpl => {
|
export const decorateMenu = (tpl: any) => {
|
||||||
return decorateObject(tpl, 'decorateMenu');
|
return decorateObject(tpl, 'decorateMenu');
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getDecoratedEnv = baseEnv => {
|
export const getDecoratedEnv = (baseEnv: Record<string, string>) => {
|
||||||
return decorateObject(baseEnv, 'decorateEnv');
|
return decorateObject(baseEnv, 'decorateEnv');
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getDecoratedConfig = () => {
|
export const getDecoratedConfig = () => {
|
||||||
const baseConfig = config.getConfig();
|
const baseConfig = config.getConfig();
|
||||||
const decoratedConfig = decorateObject(baseConfig, 'decorateConfig');
|
const decoratedConfig = decorateObject(baseConfig, 'decorateConfig');
|
||||||
const fixedConfig = config.fixConfigDefaults(decoratedConfig);
|
const fixedConfig = config.fixConfigDefaults(decoratedConfig);
|
||||||
|
|
@ -420,27 +425,27 @@ exports.getDecoratedConfig = () => {
|
||||||
return translatedConfig;
|
return translatedConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getDecoratedKeymaps = () => {
|
export const getDecoratedKeymaps = () => {
|
||||||
const baseKeymaps = config.getKeymaps();
|
const baseKeymaps = config.getKeymaps();
|
||||||
// Ensure that all keys are in an array and don't use deprecated key combination`
|
// Ensure that all keys are in an array and don't use deprecated key combination`
|
||||||
const decoratedKeymaps = mapKeys(decorateObject(baseKeymaps, 'decorateKeymaps'));
|
const decoratedKeymaps = mapKeys(decorateObject(baseKeymaps, 'decorateKeymaps'));
|
||||||
return decoratedKeymaps;
|
return decoratedKeymaps;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getDecoratedBrowserOptions = defaults => {
|
export const getDecoratedBrowserOptions = <T>(defaults: T): T => {
|
||||||
return decorateObject(defaults, 'decorateBrowserOptions');
|
return decorateObject(defaults, 'decorateBrowserOptions');
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.decorateWindowClass = defaults => {
|
export const decorateWindowClass = <T>(defaults: T): T => {
|
||||||
return decorateObject(defaults, 'decorateWindowClass');
|
return decorateObject(defaults, 'decorateWindowClass');
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.decorateSessionOptions = defaults => {
|
export const decorateSessionOptions = <T>(defaults: T): T => {
|
||||||
return decorateObject(defaults, 'decorateSessionOptions');
|
return decorateObject(defaults, 'decorateSessionOptions');
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.decorateSessionClass = Session => {
|
export const decorateSessionClass = <T>(Session: T): T => {
|
||||||
return decorateClass(Session, 'decorateSessionClass');
|
return decorateClass(Session, 'decorateSessionClass');
|
||||||
};
|
};
|
||||||
|
|
||||||
exports._toDependencies = toDependencies;
|
export {toDependencies as _toDependencies};
|
||||||
|
|
@ -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');
|
import {EventEmitter} from 'events';
|
||||||
const {ipcMain} = require('electron');
|
import {ipcMain, BrowserWindow} from 'electron';
|
||||||
const uuid = require('uuid');
|
import {v4 as uuidv4} from 'uuid';
|
||||||
|
|
||||||
class Server extends EventEmitter {
|
export class Server extends EventEmitter {
|
||||||
constructor(win) {
|
destroyed = false;
|
||||||
|
win: BrowserWindow;
|
||||||
|
id!: string;
|
||||||
|
constructor(win: BrowserWindow) {
|
||||||
super();
|
super();
|
||||||
this.win = win;
|
this.win = win;
|
||||||
this.ipcListener = this.ipcListener.bind(this);
|
this.ipcListener = this.ipcListener.bind(this);
|
||||||
|
|
@ -12,9 +15,10 @@ class Server extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uid = uuid.v4();
|
const uid = uuidv4();
|
||||||
this.id = uid;
|
this.id = uid;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
ipcMain.on(uid, this.ipcListener);
|
ipcMain.on(uid, this.ipcListener);
|
||||||
|
|
||||||
// we intentionally subscribe to `on` instead of `once`
|
// we intentionally subscribe to `on` instead of `once`
|
||||||
|
|
@ -29,11 +33,11 @@ class Server extends EventEmitter {
|
||||||
return this.win.webContents;
|
return this.win.webContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcListener(event, {ev, data}) {
|
ipcListener(event: any, {ev, data}: {ev: string; data: any}) {
|
||||||
super.emit(ev, data);
|
super.emit(ev, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(ch, data) {
|
emit(ch: string, data: any = {}): any {
|
||||||
// This check is needed because data-batching can cause extra data to be
|
// This check is needed because data-batching can cause extra data to be
|
||||||
// emitted after the window has already closed
|
// emitted after the window has already closed
|
||||||
if (!this.win.isDestroyed()) {
|
if (!this.win.isDestroyed()) {
|
||||||
|
|
@ -45,6 +49,7 @@ class Server extends EventEmitter {
|
||||||
this.removeAllListeners();
|
this.removeAllListeners();
|
||||||
this.wc.removeAllListeners();
|
this.wc.removeAllListeners();
|
||||||
if (this.id) {
|
if (this.id) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
ipcMain.removeListener(this.id, this.ipcListener);
|
ipcMain.removeListener(this.id, this.ipcListener);
|
||||||
} else {
|
} else {
|
||||||
// mark for `genUid` in constructor
|
// mark for `genUid` in constructor
|
||||||
|
|
@ -53,6 +58,6 @@ class Server extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = win => {
|
export default (win: BrowserWindow) => {
|
||||||
return new Server(win);
|
return new Server(win);
|
||||||
};
|
};
|
||||||
|
|
@ -1,25 +1,28 @@
|
||||||
const {EventEmitter} = require('events');
|
import {EventEmitter} from 'events';
|
||||||
const {StringDecoder} = require('string_decoder');
|
import {StringDecoder} from 'string_decoder';
|
||||||
|
import defaultShell from 'default-shell';
|
||||||
const defaultShell = require('default-shell');
|
import {getDecoratedEnv} from './plugins';
|
||||||
|
import {productName, version} from './package.json';
|
||||||
const {getDecoratedEnv} = require('./plugins');
|
import * as config from './config';
|
||||||
const {productName, version} = require('./package');
|
import {IPty, IWindowsPtyForkOptions, spawn as npSpawn} from 'node-pty';
|
||||||
const config = require('./config');
|
import {cliScriptPath} from './config/paths';
|
||||||
|
import {dirname} from 'path';
|
||||||
|
|
||||||
const createNodePtyError = () =>
|
const createNodePtyError = () =>
|
||||||
new Error(
|
new Error(
|
||||||
'`node-pty` failed to load. Typically this means that it was built incorrectly. Please check the `readme.md` to more info.'
|
'`node-pty` failed to load. Typically this means that it was built incorrectly. Please check the `readme.md` to more info.'
|
||||||
);
|
);
|
||||||
|
|
||||||
let spawn;
|
let spawn: typeof npSpawn;
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
spawn = require('node-pty').spawn;
|
spawn = require('node-pty').spawn;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw createNodePtyError();
|
throw createNodePtyError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const envFromConfig = config.getConfig().env || {};
|
const envFromConfig = config.getConfig().env || {};
|
||||||
|
const useConpty = config.getConfig().useConpty;
|
||||||
|
|
||||||
// Max duration to batch session data before sending it to the renderer process.
|
// Max duration to batch session data before sending it to the renderer process.
|
||||||
const BATCH_DURATION_MS = 16;
|
const BATCH_DURATION_MS = 16;
|
||||||
|
|
@ -34,7 +37,11 @@ const BATCH_MAX_SIZE = 200 * 1024;
|
||||||
// with the window ID which is then stripped on the renderer process and this
|
// with the window ID which is then stripped on the renderer process and this
|
||||||
// overhead is reduced with batching.
|
// overhead is reduced with batching.
|
||||||
class DataBatcher extends EventEmitter {
|
class DataBatcher extends EventEmitter {
|
||||||
constructor(uid) {
|
uid: string;
|
||||||
|
decoder: StringDecoder;
|
||||||
|
data!: string;
|
||||||
|
timeout!: NodeJS.Timeout | null;
|
||||||
|
constructor(uid: string) {
|
||||||
super();
|
super();
|
||||||
this.uid = uid;
|
this.uid = uid;
|
||||||
this.decoder = new StringDecoder('utf8');
|
this.decoder = new StringDecoder('utf8');
|
||||||
|
|
@ -47,7 +54,7 @@ class DataBatcher extends EventEmitter {
|
||||||
this.timeout = null;
|
this.timeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
write(chunk) {
|
write(chunk: Buffer) {
|
||||||
if (this.data.length + chunk.length >= BATCH_MAX_SIZE) {
|
if (this.data.length + chunk.length >= BATCH_MAX_SIZE) {
|
||||||
// We've reached the max batch size. Flush it and start another one
|
// We've reached the max batch size. Flush it and start another one
|
||||||
if (this.timeout) {
|
if (this.timeout) {
|
||||||
|
|
@ -73,23 +80,38 @@ class DataBatcher extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = class Session extends EventEmitter {
|
interface SessionOptions {
|
||||||
constructor(options) {
|
uid: string;
|
||||||
|
rows: number;
|
||||||
|
cols: number;
|
||||||
|
cwd: string;
|
||||||
|
shell: string;
|
||||||
|
shellArgs: string[];
|
||||||
|
}
|
||||||
|
export default class Session extends EventEmitter {
|
||||||
|
pty: IPty | null;
|
||||||
|
batcher: DataBatcher | null;
|
||||||
|
shell: string | null;
|
||||||
|
ended: boolean;
|
||||||
|
initTimestamp: number;
|
||||||
|
constructor(options: SessionOptions) {
|
||||||
super();
|
super();
|
||||||
this.pty = null;
|
this.pty = null;
|
||||||
this.batcher = null;
|
this.batcher = null;
|
||||||
this.shell = null;
|
this.shell = null;
|
||||||
this.ended = false;
|
this.ended = false;
|
||||||
|
this.initTimestamp = new Date().getTime();
|
||||||
this.init(options);
|
this.init(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
init({uid, rows, cols: columns, cwd, shell, shellArgs}) {
|
init({uid, rows, cols: columns, cwd, shell: _shell, shellArgs: _shellArgs}: SessionOptions) {
|
||||||
const osLocale = require('os-locale');
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const osLocale = require('os-locale') as typeof import('os-locale');
|
||||||
const baseEnv = Object.assign(
|
const baseEnv = Object.assign(
|
||||||
{},
|
{},
|
||||||
process.env,
|
process.env,
|
||||||
{
|
{
|
||||||
LANG: osLocale.sync() + '.UTF-8',
|
LANG: `${osLocale.sync().replace(/-/, '_')}.UTF-8`,
|
||||||
TERM: 'xterm-256color',
|
TERM: 'xterm-256color',
|
||||||
COLORTERM: 'truecolor',
|
COLORTERM: 'truecolor',
|
||||||
TERM_PROGRAM: productName,
|
TERM_PROGRAM: productName,
|
||||||
|
|
@ -98,22 +120,40 @@ module.exports = class Session extends EventEmitter {
|
||||||
envFromConfig
|
envFromConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// path to AppImage mount point is added to PATH environment variable automatically
|
||||||
|
// which conflicts with the cli
|
||||||
|
if (baseEnv['APPIMAGE'] && baseEnv['APPDIR']) {
|
||||||
|
baseEnv['PATH'] = [dirname(cliScriptPath)]
|
||||||
|
.concat((baseEnv['PATH'] || '').split(':').filter((val) => !val.startsWith(baseEnv['APPDIR'])))
|
||||||
|
.join(':');
|
||||||
|
}
|
||||||
|
|
||||||
// Electron has a default value for process.env.GOOGLE_API_KEY
|
// Electron has a default value for process.env.GOOGLE_API_KEY
|
||||||
// We don't want to leak this to the shell
|
// We don't want to leak this to the shell
|
||||||
// See https://github.com/zeit/hyper/issues/696
|
// See https://github.com/vercel/hyper/issues/696
|
||||||
if (baseEnv.GOOGLE_API_KEY && process.env.GOOGLE_API_KEY === baseEnv.GOOGLE_API_KEY) {
|
if (baseEnv.GOOGLE_API_KEY && process.env.GOOGLE_API_KEY === baseEnv.GOOGLE_API_KEY) {
|
||||||
delete baseEnv.GOOGLE_API_KEY;
|
delete baseEnv.GOOGLE_API_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultShellArgs = ['--login'];
|
const defaultShellArgs = ['--login'];
|
||||||
|
|
||||||
|
const options: IWindowsPtyForkOptions = {
|
||||||
|
cols: columns,
|
||||||
|
rows,
|
||||||
|
cwd,
|
||||||
|
env: getDecoratedEnv(baseEnv)
|
||||||
|
};
|
||||||
|
|
||||||
|
// if config do not set the useConpty, it will be judged by the node-pty
|
||||||
|
if (typeof useConpty === 'boolean') {
|
||||||
|
options.useConpty = useConpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shell = _shell || defaultShell;
|
||||||
|
const shellArgs = _shellArgs || defaultShellArgs;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.pty = spawn(shell || defaultShell, shellArgs || defaultShellArgs, {
|
this.pty = spawn(shell, shellArgs, options);
|
||||||
cols: columns,
|
|
||||||
rows,
|
|
||||||
cwd,
|
|
||||||
env: getDecoratedEnv(baseEnv)
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (/is not a function/.test(err.message)) {
|
if (/is not a function/.test(err.message)) {
|
||||||
throw createNodePtyError();
|
throw createNodePtyError();
|
||||||
|
|
@ -123,50 +163,62 @@ module.exports = class Session extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.batcher = new DataBatcher(uid);
|
this.batcher = new DataBatcher(uid);
|
||||||
this.pty.on('data', chunk => {
|
this.pty.onData((chunk) => {
|
||||||
if (this.ended) {
|
if (this.ended) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.batcher.write(chunk);
|
this.batcher?.write(chunk as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.batcher.on('flush', data => {
|
this.batcher.on('flush', (data: string) => {
|
||||||
this.emit('data', data);
|
this.emit('data', data);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.pty.on('exit', () => {
|
this.pty.onExit((e) => {
|
||||||
if (!this.ended) {
|
if (!this.ended) {
|
||||||
this.ended = true;
|
// fall back to default shell config if the shell exits within 1 sec with non zero exit code
|
||||||
this.emit('exit');
|
// this will inform users in case there are errors in the config instead of instant exit
|
||||||
|
const runDuration = new Date().getTime() - this.initTimestamp;
|
||||||
|
if (e.exitCode > 0 && runDuration < 1000) {
|
||||||
|
const defaultShellConfig = {shell: defaultShell, shellArgs: defaultShellArgs};
|
||||||
|
const msg = `
|
||||||
|
shell exited in ${runDuration} ms with exit code ${e.exitCode}
|
||||||
|
please check the shell config: ${JSON.stringify({shell, shellArgs}, undefined, 2)}
|
||||||
|
fallback to default shell config: ${JSON.stringify(defaultShellConfig, undefined, 2)}
|
||||||
|
`;
|
||||||
|
console.warn(msg);
|
||||||
|
this.batcher?.write(msg.replace(/\n/g, '\r\n') as any);
|
||||||
|
this.init({uid, rows, cols: columns, cwd, ...defaultShellConfig});
|
||||||
|
} else {
|
||||||
|
this.ended = true;
|
||||||
|
this.emit('exit');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.shell = shell || defaultShell;
|
this.shell = shell;
|
||||||
}
|
}
|
||||||
|
|
||||||
exit() {
|
exit() {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
write(data) {
|
write(data: string) {
|
||||||
if (this.pty) {
|
if (this.pty) {
|
||||||
this.pty.write(data);
|
this.pty.write(data);
|
||||||
} else {
|
} else {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.warn('Warning: Attempted to write to a session with no pty');
|
console.warn('Warning: Attempted to write to a session with no pty');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resize({cols, rows}) {
|
resize({cols, rows}: {cols: number; rows: number}) {
|
||||||
if (this.pty) {
|
if (this.pty) {
|
||||||
try {
|
try {
|
||||||
this.pty.resize(cols, rows);
|
this.pty.resize(cols, rows);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.warn('Warning: Attempted to resize a session with no pty');
|
console.warn('Warning: Attempted to resize a session with no pty');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -176,14 +228,12 @@ module.exports = class Session extends EventEmitter {
|
||||||
try {
|
try {
|
||||||
this.pty.kill();
|
this.pty.kill();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.error('exit error', err.stack);
|
console.error('exit error', err.stack);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.warn('Warning: Attempted to destroy a session with no pty');
|
console.warn('Warning: Attempted to destroy a session with no pty');
|
||||||
}
|
}
|
||||||
this.emit('exit');
|
this.emit('exit');
|
||||||
this.ended = true;
|
this.ended = true;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
@ -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
|
// Packages
|
||||||
const electron = require('electron');
|
import electron, {app, BrowserWindow, AutoUpdater} from 'electron';
|
||||||
const {app} = electron;
|
import ms from 'ms';
|
||||||
const ms = require('ms');
|
import retry from 'async-retry';
|
||||||
const retry = require('async-retry');
|
|
||||||
|
|
||||||
// Utilities
|
// Utilities
|
||||||
// eslint-disable-next-line no-unused-vars
|
import {version} from './package.json';
|
||||||
const {version} = require('./package');
|
import {getDecoratedConfig} from './plugins';
|
||||||
const {getDecoratedConfig} = require('./plugins');
|
import autoUpdaterLinux from './auto-updater-linux';
|
||||||
|
|
||||||
const {platform} = process;
|
const {platform} = process;
|
||||||
const isLinux = platform === 'linux';
|
const isLinux = platform === 'linux';
|
||||||
|
|
||||||
const autoUpdater = isLinux ? require('./auto-updater-linux') : electron.autoUpdater;
|
const autoUpdater: AutoUpdater = isLinux ? autoUpdaterLinux : electron.autoUpdater;
|
||||||
|
|
||||||
let isInit = false;
|
let isInit = false;
|
||||||
// Default to the "stable" update channel
|
// Default to the "stable" update channel
|
||||||
let canaryUpdates = false;
|
let canaryUpdates = false;
|
||||||
|
|
||||||
const buildFeedUrl = (canary, currentVersion) => {
|
const buildFeedUrl = (canary: boolean, currentVersion: string) => {
|
||||||
const updatePrefix = canary ? 'releases-canary' : 'releases';
|
const updatePrefix = canary ? 'releases-canary' : 'releases';
|
||||||
return `https://${updatePrefix}.hyper.is/update/${isLinux ? 'deb' : platform}/${currentVersion}`;
|
return `https://${updatePrefix}.hyper.is/update/${isLinux ? 'deb' : platform}/${currentVersion}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isCanary = updateChannel => updateChannel === 'canary';
|
const isCanary = (updateChannel: string) => updateChannel === 'canary';
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
autoUpdater.on('error', (err, msg) => {
|
autoUpdater.on('error', (err) => {
|
||||||
//eslint-disable-next-line no-console
|
console.error('Error fetching updates', `${err.message} (${err.stack})`);
|
||||||
console.error('Error fetching updates', msg + ' (' + err.stack + ')');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const config = await retry(async () => {
|
const config = await retry(() => {
|
||||||
const content = await getDecoratedConfig();
|
const content = getDecoratedConfig();
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
throw new Error('No config content loaded');
|
throw new Error('No config content loaded');
|
||||||
|
|
@ -48,7 +46,7 @@ async function init() {
|
||||||
|
|
||||||
const feedURL = buildFeedUrl(canaryUpdates, version);
|
const feedURL = buildFeedUrl(canaryUpdates, version);
|
||||||
|
|
||||||
autoUpdater.setFeedURL(feedURL);
|
autoUpdater.setFeedURL({url: feedURL});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
autoUpdater.checkForUpdates();
|
autoUpdater.checkForUpdates();
|
||||||
|
|
@ -61,19 +59,26 @@ async function init() {
|
||||||
isInit = true;
|
isInit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = win => {
|
export default (win: BrowserWindow) => {
|
||||||
if (!isInit) {
|
if (!isInit) {
|
||||||
init();
|
void init();
|
||||||
}
|
}
|
||||||
|
|
||||||
const {rpc} = win;
|
const {rpc} = win;
|
||||||
|
|
||||||
const onupdate = (ev, releaseNotes, releaseName, date, updateUrl, onQuitAndInstall) => {
|
const onupdate = (
|
||||||
const releaseUrl = updateUrl || `https://github.com/zeit/hyper/releases/tag/${releaseName}`;
|
ev: Event,
|
||||||
|
releaseNotes: string,
|
||||||
|
releaseName: string,
|
||||||
|
date: Date,
|
||||||
|
updateUrl: string,
|
||||||
|
onQuitAndInstall: any
|
||||||
|
) => {
|
||||||
|
const releaseUrl = updateUrl || `https://github.com/vercel/hyper/releases/tag/${releaseName}`;
|
||||||
rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !!onQuitAndInstall});
|
rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !!onQuitAndInstall});
|
||||||
};
|
};
|
||||||
|
|
||||||
const eventName = isLinux ? 'update-available' : 'update-downloaded';
|
const eventName: any = isLinux ? 'update-available' : 'update-downloaded';
|
||||||
|
|
||||||
autoUpdater.on(eventName, onupdate);
|
autoUpdater.on(eventName, onupdate);
|
||||||
|
|
||||||
|
|
@ -88,7 +93,7 @@ module.exports = win => {
|
||||||
if (newUpdateIsCanary !== canaryUpdates) {
|
if (newUpdateIsCanary !== canaryUpdates) {
|
||||||
const feedURL = buildFeedUrl(newUpdateIsCanary, version);
|
const feedURL = buildFeedUrl(newUpdateIsCanary, version);
|
||||||
|
|
||||||
autoUpdater.setFeedURL(feedURL);
|
autoUpdater.setFeedURL({url: feedURL});
|
||||||
autoUpdater.checkForUpdates();
|
autoUpdater.checkForUpdates();
|
||||||
|
|
||||||
canaryUpdates = newUpdateIsCanary;
|
canaryUpdates = newUpdateIsCanary;
|
||||||
|
|
@ -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'
|
'grayscale'
|
||||||
];
|
];
|
||||||
|
|
||||||
exports.getColorMap = colors => {
|
export const getColorMap: {
|
||||||
|
<T>(colors: T): T extends (infer U)[] ? {[k: string]: U} : T;
|
||||||
|
} = (colors) => {
|
||||||
if (!Array.isArray(colors)) {
|
if (!Array.isArray(colors)) {
|
||||||
return colors;
|
return colors;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
return colors.reduce((result, color, index) => {
|
return colors.reduce((result, color, index) => {
|
||||||
if (index < colorList.length) {
|
if (index < colorList.length) {
|
||||||
result[colorList[index]] = color;
|
result[colorList[index]] = color;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
return result;
|
return result;
|
||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
|
@ -1,29 +1,29 @@
|
||||||
const generatePrefixedCommand = (command, shortcuts) => {
|
const generatePrefixedCommand = (command: string, shortcuts: string[]) => {
|
||||||
const result = {};
|
const result: Record<string, string[]> = {};
|
||||||
const baseCmd = command.replace(/:prefix$/, '');
|
const baseCmd = command.replace(/:prefix$/, '');
|
||||||
for (let i = 1; i <= 9; i++) {
|
for (let i = 1; i <= 9; i++) {
|
||||||
// 9 is a special number because it means 'last'
|
// 9 is a special number because it means 'last'
|
||||||
const index = i === 9 ? 'last' : i;
|
const index = i === 9 ? 'last' : i;
|
||||||
const prefixedShortcuts = shortcuts.map(shortcut => `${shortcut}+${i}`);
|
const prefixedShortcuts = shortcuts.map((shortcut) => `${shortcut}+${i}`);
|
||||||
result[`${baseCmd}:${index}`] = prefixedShortcuts;
|
result[`${baseCmd}:${index}`] = prefixedShortcuts;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config => {
|
export default (config: Record<string, string[] | string>) => {
|
||||||
return Object.keys(config).reduce((keymap, command) => {
|
return Object.keys(config).reduce((keymap: Record<string, string[]>, command: string) => {
|
||||||
if (!command) {
|
if (!command) {
|
||||||
return;
|
return keymap;
|
||||||
}
|
}
|
||||||
// We can have different keys for a same command.
|
// We can have different keys for a same command.
|
||||||
const shortcuts = Array.isArray(config[command]) ? config[command] : [config[command]];
|
const _shortcuts = config[command];
|
||||||
const fixedShortcuts = [];
|
const shortcuts = Array.isArray(_shortcuts) ? _shortcuts : [_shortcuts];
|
||||||
shortcuts.forEach(shortcut => {
|
const fixedShortcuts: string[] = [];
|
||||||
|
shortcuts.forEach((shortcut) => {
|
||||||
let newShortcut = shortcut;
|
let newShortcut = shortcut;
|
||||||
if (newShortcut.indexOf('cmd') !== -1) {
|
if (newShortcut.indexOf('cmd') !== -1) {
|
||||||
// Mousetrap use `command` and not `cmd`
|
// Mousetrap use `command` and not `cmd`
|
||||||
//eslint-disable-next-line no-console
|
|
||||||
console.warn('Your config use deprecated `cmd` in key combination. Please use `command` instead.');
|
console.warn('Your config use deprecated `cmd` in key combination. Please use `command` instead.');
|
||||||
newShortcut = newShortcut.replace('cmd', 'command');
|
newShortcut = newShortcut.replace('cmd', 'command');
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
// Packages
|
||||||
const Color = require('color');
|
import Color from 'color';
|
||||||
|
|
||||||
// returns a background color that's in hex
|
// returns a background color that's in hex
|
||||||
// format including the alpha channel (e.g.: `#00000050`)
|
// format including the alpha channel (e.g.: `#00000050`)
|
||||||
// input can be any css value (rgb, hsl, string…)
|
// input can be any css value (rgb, hsl, string…)
|
||||||
module.exports = bgColor => {
|
export default (bgColor: string) => {
|
||||||
const color = Color(bgColor);
|
const color = Color(bgColor);
|
||||||
|
|
||||||
if (color.alpha() === 1) {
|
if (color.alpha() === 1) {
|
||||||
|
|
@ -13,12 +13,5 @@ module.exports = bgColor => {
|
||||||
|
|
||||||
// http://stackoverflow.com/a/11019879/1202488
|
// http://stackoverflow.com/a/11019879/1202488
|
||||||
const alphaHex = Math.round(color.alpha() * 255).toString(16);
|
const alphaHex = Math.round(color.alpha() * 255).toString(16);
|
||||||
return (
|
return `#${alphaHex}${color.hex().toString().substr(1)}`;
|
||||||
'#' +
|
|
||||||
alphaHex +
|
|
||||||
color
|
|
||||||
.hex()
|
|
||||||
.toString()
|
|
||||||
.substr(1)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const electron = require('electron');
|
import electron from 'electron';
|
||||||
|
|
||||||
function positionIsValid(position) {
|
export function positionIsValid(position: [number, number]) {
|
||||||
const displays = electron.screen.getAllDisplays();
|
const displays = electron.screen.getAllDisplays();
|
||||||
const [x, y] = position;
|
const [x, y] = position;
|
||||||
|
|
||||||
|
|
@ -8,7 +8,3 @@ function positionIsValid(position) {
|
||||||
return x >= workArea.x && x <= workArea.x + workArea.width && y >= workArea.y && y <= workArea.y + workArea.height;
|
return x >= workArea.x && x <= workArea.x + workArea.width && y >= workArea.y && y <= workArea.y + workArea.height;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
positionIsValid
|
|
||||||
};
|
|
||||||
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');
|
// eslint-disable-next-line eslint-comments/disable-enable-pair
|
||||||
const os = require('os');
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
const got = require('got');
|
import fs from 'fs';
|
||||||
const registryUrl = require('registry-url')();
|
import os from 'os';
|
||||||
const pify = require('pify');
|
import got from 'got';
|
||||||
const recast = require('recast');
|
import registryUrlModule from 'registry-url';
|
||||||
const path = require('path');
|
const registryUrl = registryUrlModule();
|
||||||
|
import pify from 'pify';
|
||||||
|
import * as recast from 'recast';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
// If the user defines XDG_CONFIG_HOME they definitely want their config there,
|
// If the user defines XDG_CONFIG_HOME they definitely want their config there,
|
||||||
// otherwise use the home directory in linux/mac and userdata in windows
|
// otherwise use the home directory in linux/mac and userdata in windows
|
||||||
const applicationDirectory =
|
const applicationDirectory =
|
||||||
process.env.XDG_CONFIG_HOME !== undefined
|
process.env.XDG_CONFIG_HOME !== undefined
|
||||||
? path.join(process.env.XDG_CONFIG_HOME, 'hyper')
|
? path.join(process.env.XDG_CONFIG_HOME, 'hyper')
|
||||||
: process.platform == 'win32' ? path.join(process.env.APPDATA, 'Hyper') : os.homedir();
|
: process.platform == 'win32'
|
||||||
|
? path.join(process.env.APPDATA!, 'Hyper')
|
||||||
|
: os.homedir();
|
||||||
|
|
||||||
const devConfigFileName = path.join(__dirname, `../.hyper.js`);
|
const devConfigFileName = path.join(__dirname, `../.hyper.js`);
|
||||||
|
|
||||||
let fileName =
|
const fileName =
|
||||||
process.env.NODE_ENV !== 'production' && fs.existsSync(devConfigFileName)
|
process.env.NODE_ENV !== 'production' && fs.existsSync(devConfigFileName)
|
||||||
? devConfigFileName
|
? devConfigFileName
|
||||||
: path.join(applicationDirectory, '.hyper.js');
|
: path.join(applicationDirectory, '.hyper.js');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We need to make sure the file reading and parsing is lazy so that failure to
|
* We need to make sure the file reading and parsing is lazy so that failure to
|
||||||
* statically analyze the hyper configuration isn't fatal for all kinds of
|
* statically analyze the hyper configuration isn't fatal for all kinds of
|
||||||
* subcommands. We can use memoization to make reading and parsing lazy.
|
* subcommands. We can use memoization to make reading and parsing lazy.
|
||||||
*/
|
*/
|
||||||
function memoize(fn) {
|
function memoize<T extends (...args: any[]) => any>(fn: T): T {
|
||||||
let hasResult = false;
|
let hasResult = false;
|
||||||
let result;
|
let result: any;
|
||||||
return (...args) => {
|
return ((...args: any[]) => {
|
||||||
if (!hasResult) {
|
if (!hasResult) {
|
||||||
result = fn(...args);
|
result = fn(...args);
|
||||||
hasResult = true;
|
hasResult = true;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
}) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFileContents = memoize(() => {
|
const getFileContents = memoize(() => {
|
||||||
|
|
@ -48,24 +54,39 @@ const getFileContents = memoize(() => {
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const getParsedFile = memoize(() => recast.parse(getFileContents()));
|
const getParsedFile = memoize(() => recast.parse(getFileContents()!));
|
||||||
|
|
||||||
const getProperties = memoize(() => getParsedFile().program.body[0].expression.right.properties);
|
const getProperties = memoize(
|
||||||
|
(): any[] =>
|
||||||
const getPlugins = memoize(() => getProperties().find(property => property.key.name === 'plugins').value.elements);
|
((getParsedFile()?.program?.body as any[]) || []).find(
|
||||||
|
(bodyItem) =>
|
||||||
const getLocalPlugins = memoize(
|
bodyItem.type === 'ExpressionStatement' &&
|
||||||
() => getProperties().find(property => property.key.name === 'localPlugins').value.elements
|
bodyItem.expression.type === 'AssignmentExpression' &&
|
||||||
|
bodyItem.expression.left.object.name === 'module' &&
|
||||||
|
bodyItem.expression.left.property.name === 'exports' &&
|
||||||
|
bodyItem.expression.right.type === 'ObjectExpression'
|
||||||
|
)?.expression?.right?.properties || []
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getPluginsByKey = (key: string): any[] =>
|
||||||
|
getProperties().find((property) => property?.key?.name === key)?.value?.elements || [];
|
||||||
|
|
||||||
|
const getPlugins = memoize(() => {
|
||||||
|
return getPluginsByKey('plugins');
|
||||||
|
});
|
||||||
|
|
||||||
|
const getLocalPlugins = memoize(() => {
|
||||||
|
return getPluginsByKey('localPlugins');
|
||||||
|
});
|
||||||
|
|
||||||
function exists() {
|
function exists() {
|
||||||
return getFileContents() !== undefined;
|
return getFileContents() !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInstalled(plugin, locally) {
|
function isInstalled(plugin: string, locally?: boolean) {
|
||||||
const array = locally ? getLocalPlugins() : getPlugins();
|
const array = locally ? getLocalPlugins() : getPlugins();
|
||||||
if (array && Array.isArray(array)) {
|
if (array && Array.isArray(array)) {
|
||||||
return array.find(entry => entry.value === plugin) !== undefined;
|
return array.some((entry) => entry.value === plugin);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -74,30 +95,32 @@ function save() {
|
||||||
return pify(fs.writeFile)(fileName, recast.print(getParsedFile()).code, 'utf8');
|
return pify(fs.writeFile)(fileName, recast.print(getParsedFile()).code, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
function existsOnNpm(plugin) {
|
function getPackageName(plugin: string) {
|
||||||
const name = getPackageName(plugin);
|
|
||||||
return got.get(registryUrl + name.toLowerCase(), {timeout: 10000, json: true}).then(res => {
|
|
||||||
if (!res.body.versions) {
|
|
||||||
return Promise.reject(res);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPackageName(plugin) {
|
|
||||||
const isScoped = plugin[0] === '@';
|
const isScoped = plugin[0] === '@';
|
||||||
const nameWithoutVersion = plugin.split('#')[0];
|
const nameWithoutVersion = plugin.split('#')[0];
|
||||||
|
|
||||||
if (isScoped) {
|
if (isScoped) {
|
||||||
return '@' + nameWithoutVersion.split('@')[1].replace('/', '%2f');
|
return `@${nameWithoutVersion.split('@')[1].replace('/', '%2f')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return nameWithoutVersion.split('@')[0];
|
return nameWithoutVersion.split('@')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function install(plugin, locally) {
|
function existsOnNpm(plugin: string) {
|
||||||
|
const name = getPackageName(plugin);
|
||||||
|
return got.get<any>(registryUrl + name.toLowerCase(), {timeout: 10000, responseType: 'json'}).then((res) => {
|
||||||
|
if (!res.body.versions) {
|
||||||
|
return Promise.reject(res);
|
||||||
|
} else {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function install(plugin: string, locally?: boolean) {
|
||||||
const array = locally ? getLocalPlugins() : getPlugins();
|
const array = locally ? getLocalPlugins() : getPlugins();
|
||||||
return existsOnNpm(plugin)
|
return existsOnNpm(plugin)
|
||||||
.catch(err => {
|
.catch((err: any) => {
|
||||||
const {statusCode} = err;
|
const {statusCode} = err;
|
||||||
if (statusCode && (statusCode === 404 || statusCode === 200)) {
|
if (statusCode && (statusCode === 404 || statusCode === 200)) {
|
||||||
return Promise.reject(`${plugin} not found on npm`);
|
return Promise.reject(`${plugin} not found on npm`);
|
||||||
|
|
@ -114,29 +137,24 @@ function install(plugin, locally) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function uninstall(plugin) {
|
function uninstall(plugin: string) {
|
||||||
if (!isInstalled(plugin)) {
|
if (!isInstalled(plugin)) {
|
||||||
return Promise.reject(`${plugin} is not installed`);
|
return Promise.reject(`${plugin} is not installed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = getPlugins().findIndex(entry => entry.value === plugin);
|
const index = getPlugins().findIndex((entry) => entry.value === plugin);
|
||||||
getPlugins().splice(index, 1);
|
getPlugins().splice(index, 1);
|
||||||
return save();
|
return save();
|
||||||
}
|
}
|
||||||
|
|
||||||
function list() {
|
function list() {
|
||||||
if (Array.isArray(getPlugins())) {
|
if (getPlugins().length > 0) {
|
||||||
return getPlugins()
|
return getPlugins()
|
||||||
.map(plugin => plugin.value)
|
.map((plugin) => plugin.value)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.configPath = fileName;
|
export const configPath = fileName;
|
||||||
module.exports.exists = exists;
|
export {exists, existsOnNpm, isInstalled, install, uninstall, list};
|
||||||
module.exports.existsOnNpm = existsOnNpm;
|
|
||||||
module.exports.isInstalled = isInstalled;
|
|
||||||
module.exports.install = install;
|
|
||||||
module.exports.uninstall = uninstall;
|
|
||||||
module.exports.list = list;
|
|
||||||
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';
|
} from '../constants/ui';
|
||||||
import rpc from '../rpc';
|
import rpc from '../rpc';
|
||||||
import {userExitTermGroup, setActiveGroup} from './term-groups';
|
import {userExitTermGroup, setActiveGroup} from './term-groups';
|
||||||
|
import {HyperDispatch} from '../hyper';
|
||||||
|
|
||||||
export function closeTab(uid) {
|
export function closeTab(uid: string) {
|
||||||
return dispatch => {
|
return (dispatch: HyperDispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: CLOSE_TAB,
|
type: CLOSE_TAB,
|
||||||
uid,
|
uid,
|
||||||
|
|
@ -21,8 +22,8 @@ export function closeTab(uid) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeTab(uid) {
|
export function changeTab(uid: string) {
|
||||||
return dispatch => {
|
return (dispatch: HyperDispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: CHANGE_TAB,
|
type: CHANGE_TAB,
|
||||||
uid,
|
uid,
|
||||||
|
|
@ -34,29 +35,29 @@ export function changeTab(uid) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function maximize() {
|
export function maximize() {
|
||||||
return dispatch => {
|
return (dispatch: HyperDispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UI_WINDOW_MAXIMIZE,
|
type: UI_WINDOW_MAXIMIZE,
|
||||||
effect() {
|
effect() {
|
||||||
rpc.emit('maximize');
|
rpc.emit('maximize', null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unmaximize() {
|
export function unmaximize() {
|
||||||
return dispatch => {
|
return (dispatch: HyperDispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UI_WINDOW_UNMAXIMIZE,
|
type: UI_WINDOW_UNMAXIMIZE,
|
||||||
effect() {
|
effect() {
|
||||||
rpc.emit('unmaximize');
|
rpc.emit('unmaximize', null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openHamburgerMenu(coordinates) {
|
export function openHamburgerMenu(coordinates: {x: number; y: number}) {
|
||||||
return dispatch => {
|
return (dispatch: HyperDispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UI_OPEN_HAMBURGER_MENU,
|
type: UI_OPEN_HAMBURGER_MENU,
|
||||||
effect() {
|
effect() {
|
||||||
|
|
@ -67,22 +68,22 @@ export function openHamburgerMenu(coordinates) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function minimize() {
|
export function minimize() {
|
||||||
return dispatch => {
|
return (dispatch: HyperDispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UI_WINDOW_MINIMIZE,
|
type: UI_WINDOW_MINIMIZE,
|
||||||
effect() {
|
effect() {
|
||||||
rpc.emit('minimize');
|
rpc.emit('minimize', null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function close() {
|
export function close() {
|
||||||
return dispatch => {
|
return (dispatch: HyperDispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UI_WINDOW_CLOSE,
|
type: UI_WINDOW_CLOSE,
|
||||||
effect() {
|
effect() {
|
||||||
rpc.emit('close');
|
rpc.emit('close', null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue