├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── codeql-analysis.yml │ ├── e2e_comment.yml │ └── nodejs.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-push ├── .vscode └── launch.json ├── .yarnrc ├── LICENSE ├── PLUGINS.md ├── README.md ├── app ├── .yarnrc ├── auto-updater-linux.ts ├── commands.ts ├── config.ts ├── config │ ├── config-default.json │ ├── import.ts │ ├── init.ts │ ├── migrate.ts │ ├── open.ts │ ├── paths.ts │ ├── schema.json │ └── windows.ts ├── index.html ├── index.ts ├── keymaps │ ├── darwin.json │ ├── linux.json │ └── win32.json ├── menus │ ├── menu.ts │ └── menus │ │ ├── darwin.ts │ │ ├── edit.ts │ │ ├── help.ts │ │ ├── shell.ts │ │ ├── tools.ts │ │ ├── view.ts │ │ └── window.ts ├── notifications.ts ├── notify.ts ├── package.json ├── patches │ └── node-pty+1.0.0.patch ├── plugins.ts ├── plugins │ ├── extensions.ts │ └── install.ts ├── rpc.ts ├── session.ts ├── static │ ├── icon.png │ └── icon96x96.png ├── tsconfig.json ├── ui │ ├── contextmenu.ts │ └── window.ts ├── updater.ts ├── utils │ ├── cli-install.ts │ ├── colors.ts │ ├── map-keys.ts │ ├── renderer-utils.ts │ ├── shell-fallback.ts │ ├── system-context-menu.ts │ ├── to-electron-background-color.ts │ └── window-utils.ts └── yarn.lock ├── assets └── icons.svg ├── ava-e2e.config.js ├── ava.config.js ├── babel.config.json ├── bin ├── cp-snapshot.js ├── mk-snapshot.js ├── notarize.js ├── rimraf-standalone.js ├── snapshot-libs.js └── yarn-standalone.js ├── build ├── canary.icns ├── canary.ico ├── icon.fig ├── icon.icns ├── icon.ico ├── linux │ ├── after-install.tpl │ └── hyper ├── mac │ ├── entitlements.plist │ └── hyper └── win │ ├── hyper │ ├── hyper.cmd │ └── installer.nsh ├── cli ├── api.ts └── index.ts ├── electron-builder-linux-ci.json ├── electron-builder.json ├── lib ├── actions │ ├── config.ts │ ├── header.ts │ ├── index.ts │ ├── notifications.ts │ ├── sessions.ts │ ├── term-groups.ts │ ├── ui.ts │ └── updater.ts ├── command-registry.ts ├── components │ ├── header.tsx │ ├── new-tab.tsx │ ├── notification.tsx │ ├── notifications.tsx │ ├── searchBox.tsx │ ├── split-pane.tsx │ ├── style-sheet.tsx │ ├── tab.tsx │ ├── tabs.tsx │ ├── term-group.tsx │ ├── term.tsx │ └── terms.tsx ├── containers │ ├── header.ts │ ├── hyper.tsx │ ├── notifications.ts │ └── terms.ts ├── index.tsx ├── reducers │ ├── index.ts │ ├── sessions.ts │ ├── term-groups.ts │ └── ui.ts ├── rpc.ts ├── selectors.ts ├── store │ ├── configure-store.dev.ts │ ├── configure-store.prod.ts │ ├── configure-store.ts │ └── write-middleware.ts ├── terms.ts ├── utils │ ├── config.ts │ ├── effects.ts │ ├── file.ts │ ├── ipc-child-process.ts │ ├── ipc.ts │ ├── notify.ts │ ├── object.ts │ ├── paste.ts │ ├── plugins.ts │ ├── rpc.ts │ └── term-groups.ts └── v8-snapshot-util.ts ├── package.json ├── release.js ├── test ├── index.ts ├── testUtils │ └── is-hex-color.ts └── unit │ ├── cli-api.test.ts │ ├── to-electron-background-color.test.ts │ └── window-utils.test.ts ├── tsconfig.base.json ├── tsconfig.eslint.json ├── tsconfig.json ├── typings ├── common.d.ts ├── config.d.ts ├── constants │ ├── config.d.ts │ ├── index.d.ts │ ├── notifications.d.ts │ ├── sessions.d.ts │ ├── tabs.d.ts │ ├── term-groups.d.ts │ ├── ui.d.ts │ └── updater.d.ts ├── ext-modules.d.ts ├── extend-electron.d.ts └── hyper.d.ts ├── webpack.config.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | app/renderer 3 | app/static 4 | app/bin 5 | app/dist 6 | app/node_modules 7 | app/typings 8 | assets 9 | website 10 | bin 11 | dist 12 | target 13 | cache 14 | schema.json -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "react", 4 | "prettier", 5 | "@typescript-eslint", 6 | "eslint-comments", 7 | "lodash", 8 | "import" 9 | ], 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:react/recommended", 13 | "plugin:prettier/recommended", 14 | "plugin:eslint-comments/recommended" 15 | ], 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { 18 | "sourceType": "module", 19 | "ecmaFeatures": { 20 | "jsx": true, 21 | "impliedStrict": true, 22 | "experimentalObjectRestSpread": true 23 | }, 24 | "allowImportExportEverywhere": true, 25 | "project": [ 26 | "./tsconfig.eslint.json" 27 | ] 28 | }, 29 | "env": { 30 | "es6": true, 31 | "browser": true, 32 | "node": true 33 | }, 34 | "settings": { 35 | "react": { 36 | "version": "detect" 37 | }, 38 | "import/resolver": { 39 | "typescript": {} 40 | }, 41 | "import/internal-regex": "^(electron|react)$" 42 | }, 43 | "rules": { 44 | "func-names": [ 45 | "error", 46 | "as-needed" 47 | ], 48 | "no-shadow": "error", 49 | "no-extra-semi": 0, 50 | "react/prop-types": 0, 51 | "react/react-in-jsx-scope": 0, 52 | "react/no-unescaped-entities": 0, 53 | "react/jsx-no-target-blank": 0, 54 | "react/no-string-refs": 0, 55 | "prettier/prettier": [ 56 | "error", 57 | { 58 | "printWidth": 120, 59 | "tabWidth": 2, 60 | "singleQuote": true, 61 | "trailingComma": "none", 62 | "bracketSpacing": false, 63 | "semi": true, 64 | "useTabs": false, 65 | "bracketSameLine": false 66 | } 67 | ], 68 | "eslint-comments/no-unused-disable": "error", 69 | "react/no-unknown-property":[ 70 | "error", 71 | { 72 | "ignore": [ 73 | "jsx", 74 | "global" 75 | ] 76 | } 77 | ] 78 | }, 79 | "overrides": [ 80 | { 81 | "files": [ 82 | "**.ts", 83 | "**.tsx" 84 | ], 85 | "extends": [ 86 | "plugin:@typescript-eslint/recommended", 87 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 88 | "prettier" 89 | ], 90 | "rules": { 91 | "@typescript-eslint/explicit-function-return-type": "off", 92 | "@typescript-eslint/explicit-module-boundary-types": "off", 93 | "@typescript-eslint/no-explicit-any": "off", 94 | "@typescript-eslint/no-non-null-assertion": "off", 95 | "@typescript-eslint/prefer-optional-chain": "error", 96 | "@typescript-eslint/ban-types": "off", 97 | "no-shadow": "off", 98 | "@typescript-eslint/no-shadow": ["error"], 99 | "@typescript-eslint/no-unsafe-assignment": "off", 100 | "@typescript-eslint/no-unsafe-member-access": "off", 101 | "@typescript-eslint/restrict-template-expressions": "off", 102 | "@typescript-eslint/consistent-type-imports": [ "error", { "disallowTypeAnnotations": false } ], 103 | "lodash/prop-shorthand": [ "error", "always" ], 104 | "lodash/import-scope": [ "error", "method" ], 105 | "lodash/collection-return": "error", 106 | "lodash/collection-method-value": "error", 107 | "import/no-extraneous-dependencies": "error", 108 | "import/no-anonymous-default-export": "error", 109 | "import/order": [ 110 | "error", 111 | { 112 | "groups": [ 113 | "builtin", 114 | "internal", 115 | "external", 116 | "parent", 117 | "sibling", 118 | "index" 119 | ], 120 | "newlines-between": "always", 121 | "alphabetize": { 122 | "order": "asc", 123 | "orderImportKind": "desc", 124 | "caseInsensitive": true 125 | } 126 | } 127 | ] 128 | } 129 | }, 130 | { 131 | "extends": [ 132 | "plugin:jsonc/recommended-with-json", 133 | "plugin:json-schema-validator/recommended" 134 | ], 135 | "files": [ 136 | "*.json" 137 | ], 138 | "parser": "jsonc-eslint-parser", 139 | "plugins": [ 140 | "jsonc", 141 | "json-schema-validator" 142 | ], 143 | "rules": { 144 | "jsonc/array-element-newline": [ 145 | "error", 146 | "consistent" 147 | ], 148 | "jsonc/array-bracket-newline": [ 149 | "error", 150 | "consistent" 151 | ], 152 | "jsonc/indent": [ 153 | "error", 154 | 2 155 | ], 156 | "prettier/prettier": "off", 157 | "json-schema-validator/no-invalid": "error" 158 | } 159 | } 160 | ] 161 | } 162 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | *.ts text eol=lf 4 | *.tsx text eol=lf 5 | bin/* linguist-vendored 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help Hyper improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 17 | 18 | 19 | - [ ] I am on the [latest](https://github.com/vercel/hyper/releases/latest) Hyper.app version 20 | - [ ] I have searched the [issues](https://github.com/vercel/hyper/issues) of this repo and believe that this is not a duplicate 21 | 22 | 26 | 27 | - **OS version and name**: 28 | - **Hyper.app version**: 29 | - **Link of a [Gist](https://gist.github.com/) with the contents of your hyper.json**: 30 | - **Relevant information from devtools** _(CMD+ALT+I on macOS, CTRL+SHIFT+I elsewhere)_: 31 | - **The issue is reproducible in vanilla Hyper.app**: 32 | 33 | ## Issue 34 | 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea/feature for Hyper 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: '11:00' 8 | open-pull-requests-limit: 30 9 | target-branch: canary 10 | versioning-strategy: increase 11 | - package-ecosystem: npm 12 | directory: "/app" 13 | schedule: 14 | interval: weekly 15 | time: '11:00' 16 | open-pull-requests-limit: 30 17 | target-branch: canary 18 | versioning-strategy: increase 19 | - package-ecosystem: github-actions 20 | directory: "/" 21 | schedule: 22 | interval: weekly 23 | time: '11:00' 24 | open-pull-requests-limit: 30 25 | target-branch: canary 26 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ canary ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ canary ] 20 | schedule: 21 | - cron: '37 6 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # 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 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v3 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v3 68 | -------------------------------------------------------------------------------- /.github/workflows/e2e_comment.yml: -------------------------------------------------------------------------------- 1 | name: Comment e2e test screenshots on PR 2 | on: 3 | workflow_run: 4 | workflows: ['Node CI'] 5 | types: 6 | - completed 7 | jobs: 8 | e2e_comment: 9 | runs-on: ubuntu-latest 10 | if: github.event.workflow_run.event == 'pull_request' 11 | steps: 12 | - name: Dump Workflow run info from GitHub context 13 | env: 14 | WORKFLOW_RUN_INFO: ${{ toJSON(github.event.workflow_run) }} 15 | run: echo "$WORKFLOW_RUN_INFO" 16 | - name: Download Artifacts 17 | uses: dawidd6/action-download-artifact@v3.1.4 18 | with: 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | workflow: nodejs.yml 21 | run_id: ${{ github.event.workflow_run.id }} 22 | name: e2e 23 | - name: Get PR number 24 | uses: dawidd6/action-download-artifact@v3.1.4 25 | with: 26 | github_token: ${{ secrets.GITHUB_TOKEN }} 27 | workflow: nodejs.yml 28 | run_id: ${{ github.event.workflow_run.id }} 29 | name: pr_num 30 | - name: Read the pr_num file 31 | id: pr_num_reader 32 | uses: juliangruber/read-file-action@v1.1.7 33 | with: 34 | path: ./pr_num.txt 35 | - name: List images 36 | run: ls -al 37 | - name: Upload images to imgur 38 | id: upload_screenshots 39 | uses: devicons/public-upload-to-imgur@v2.2.2 40 | with: 41 | path: ./*.png 42 | client_id: ${{ secrets.IMGUR_CLIENT_ID }} 43 | - name: Comment on the PR 44 | uses: jungwinter/comment@v1 45 | env: 46 | IMG_MARKDOWN: ${{ join(fromJSON(steps.upload_screenshots.outputs.markdown_urls), '') }} 47 | MESSAGE: | 48 | Hi there, 49 | Thank you for contributing to Hyper! 50 | You can get the build artifacts from [here](https://nightly.link/{1}/actions/runs/{2}). 51 | Here are screenshots of Hyper built from this pr. 52 | {0} 53 | with: 54 | type: create 55 | issue_number: ${{ steps.pr_num_reader.outputs.content }} 56 | token: ${{ secrets.GITHUB_TOKEN }} 57 | body: ${{ format(env.MESSAGE, env.IMG_MARKDOWN, github.repository, github.event.workflow_run.id) }} 58 | - name: Hide older comments 59 | uses: kanga333/comment-hider@v0.4.0 60 | with: 61 | github_token: ${{ secrets.GITHUB_TOKEN }} 62 | leave_visible: 1 63 | issue_number: ${{ steps.pr_num_reader.outputs.content }} 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist 3 | app/renderer 4 | target 5 | bin/cli.* 6 | cache 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # logs 12 | npm-debug.log 13 | yarn-error.log 14 | 15 | # optional dev config file and plugins directory 16 | hyper.json 17 | schema.json 18 | plugins 19 | 20 | .DS_Store 21 | .vscode/* 22 | !.vscode/launch.json 23 | .idea 24 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn test 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Launch Hyper", 8 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 9 | "program": "${workspaceRoot}/target/index.js", 10 | "protocol": "inspector" 11 | }, 12 | { 13 | "type": "node", 14 | "request": "launch", 15 | "name": "cli", 16 | "runtimeExecutable": "node", 17 | "program": "${workspaceRoot}/bin/cli.js", 18 | "args": ["--help"], 19 | "protocol": "inspector" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npmjs.org/" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2018 Vercel, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://assets.vercel.com/image/upload/v1549723846/repositories/hyper/hyper-3-repo-banner.png) 2 | 3 |

4 | 5 | 6 | 7 |

8 | 9 | [![Node CI](https://github.com/vercel/hyper/workflows/Node%20CI/badge.svg?event=push)](https://github.com/vercel/hyper/actions?query=workflow%3A%22Node+CI%22+branch%3Acanary+event%3Apush) 10 | [![Changelog #213](https://img.shields.io/badge/changelog-%23213-lightgrey.svg)](https://changelog.com/213) 11 | 12 | For more details, head to: https://hyper.is 13 | 14 | ## Project goals 15 | 16 | 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. 17 | 18 | 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. 19 | 20 | ## Usage 21 | 22 | [Download the latest release!](https://hyper.is/#installation) 23 | 24 | ### Linux 25 | #### Arch and derivatives 26 | 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) 27 | 28 | ```sh 29 | paru -S hyper 30 | ``` 31 | 32 | #### NixOS 33 | 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: 34 | 35 | ```sh 36 | nix-env -i hyper 37 | ``` 38 | 39 | ### macOS 40 | 41 | Use [Homebrew Cask](https://brew.sh) to download the app by running these commands: 42 | 43 | ```bash 44 | brew update 45 | brew install --cask hyper 46 | ``` 47 | 48 | ### Windows 49 | 50 | Use [chocolatey](https://chocolatey.org/) to install the app by running the following command (package information can be found [here](https://chocolatey.org/packages/hyper/)): 51 | 52 | ```bash 53 | choco install hyper 54 | ``` 55 | 56 | **Note:** The version available on [Homebrew Cask](https://brew.sh), [Chocolatey](https://chocolatey.org), [Snapcraft](https://snapcraft.io/store) or the [AUR](https://aur.archlinux.org) may not be the latest. Please consider downloading it from [here](https://hyper.is/#installation) if that's the case. 57 | 58 | ## Contribute 59 | 60 | Regardless of the platform you are working on, you will need to have Yarn installed. If you have never installed Yarn before, you can find out how at: https://yarnpkg.com/en/docs/install. 61 | 62 | 1. Install necessary packages: 63 | * Windows 64 | - Be sure to run `yarn global add windows-build-tools` from an elevated prompt (as an administrator) to install `windows-build-tools`. 65 | * macOS 66 | - Once you have installed Yarn, you can skip this section! 67 | * Linux (You can see [here](https://en.wikipedia.org/wiki/List_of_Linux_distributions) what your Linux is based on.) 68 | - RPM-based 69 | + `GraphicsMagick` 70 | + `libicns-utils` 71 | + `xz` (Installed by default on some distributions.) 72 | - Debian-based 73 | + `graphicsmagick` 74 | + `icnsutils` 75 | + `xz-utils` 76 | 2. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device 77 | 3. Install the dependencies: `yarn` 78 | 4. Build the code and watch for changes: `yarn run dev` 79 | 5. To run `hyper` 80 | * `yarn run app` from another terminal tab/window/pane 81 | * If you are using **Visual Studio Code**, select `Launch Hyper` in debugger configuration to launch a new Hyper instance with debugger attached. 82 | * If you interrupt `yarn run dev`, you'll need to relaunch it each time you want to test something. Webpack will watch changes and will rebuild renderer code when needed (and only what have changed). You'll just have to relaunch electron by using yarn run app or VSCode launch task. 83 | 84 | To make sure that your code works in the finished application, you can generate the binaries like this: 85 | 86 | ```bash 87 | yarn run dist 88 | ``` 89 | 90 | After that, you will see the binary in the `./dist` folder! 91 | 92 | #### Known issues that can happen during development 93 | 94 | ##### Error building `node-pty` 95 | 96 | If after building during development you get an alert dialog related to `node-pty` issues, 97 | make sure its build process is working correctly by running `yarn run rebuild-node-pty`. 98 | 99 | If you are on macOS, this typically is related to Xcode issues (like not having agreed 100 | to the Terms of Service by running `sudo xcodebuild` after a fresh Xcode installation). 101 | 102 | ##### Error with `C++` on macOS when running `yarn` 103 | 104 | If you are getting compiler errors when running `yarn` add the environment variable `export CXX=clang++` 105 | 106 | ##### Error with `codesign` on macOS when running `yarn run dist` 107 | 108 | If you have issues in the `codesign` step when running `yarn run dist` on macOS, you can temporarily disable code signing locally by setting 109 | `export CSC_IDENTITY_AUTO_DISCOVERY=false` for the current terminal session. 110 | 111 | ## Related Repositories 112 | 113 | - [Website](https://github.com/vercel/hyper-site) 114 | - [Sample Extension](https://github.com/vercel/hyperpower) 115 | - [Sample Theme](https://github.com/vercel/hyperyellow) 116 | - [Awesome Hyper](https://github.com/bnb/awesome-hyper) 117 | -------------------------------------------------------------------------------- /app/.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npmjs.org/" 2 | -------------------------------------------------------------------------------- /app/auto-updater-linux.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | 3 | import fetch from 'electron-fetch'; 4 | 5 | class AutoUpdater extends EventEmitter implements Electron.AutoUpdater { 6 | updateURL!: string; 7 | quitAndInstall() { 8 | this.emitError('QuitAndInstall unimplemented'); 9 | } 10 | getFeedURL() { 11 | return this.updateURL; 12 | } 13 | 14 | setFeedURL(options: Electron.FeedURLOptions) { 15 | this.updateURL = options.url; 16 | } 17 | 18 | checkForUpdates() { 19 | if (!this.updateURL) { 20 | return this.emitError('Update URL is not set'); 21 | } 22 | this.emit('checking-for-update'); 23 | 24 | fetch(this.updateURL) 25 | .then((res) => { 26 | if (res.status === 204) { 27 | this.emit('update-not-available'); 28 | return; 29 | } 30 | return res.json().then(({name, notes, pub_date}: {name: string; notes: string; pub_date: string}) => { 31 | // Only name is mandatory, needed to construct release URL. 32 | if (!name) { 33 | throw new Error('Malformed server response: release name is missing.'); 34 | } 35 | const date = pub_date ? new Date(pub_date) : new Date(); 36 | this.emit('update-available', {}, notes, name, date); 37 | }); 38 | }) 39 | .catch(this.emitError.bind(this)); 40 | } 41 | 42 | emitError(error: string | Error) { 43 | if (typeof error === 'string') { 44 | error = new Error(error); 45 | } 46 | this.emit('error', error); 47 | } 48 | } 49 | 50 | const autoUpdaterLinux = new AutoUpdater(); 51 | 52 | export default autoUpdaterLinux; 53 | -------------------------------------------------------------------------------- /app/config.ts: -------------------------------------------------------------------------------- 1 | import {app} from 'electron'; 2 | 3 | import chokidar from 'chokidar'; 4 | 5 | import type {parsedConfig, configOptions} from '../typings/config'; 6 | 7 | import {_import, getDefaultConfig} from './config/import'; 8 | import _openConfig from './config/open'; 9 | import {cfgPath, cfgDir} from './config/paths'; 10 | import notify from './notify'; 11 | import {getColorMap} from './utils/colors'; 12 | 13 | const watchers: Function[] = []; 14 | let cfg: parsedConfig = {} as any; 15 | let _watcher: chokidar.FSWatcher; 16 | 17 | export const getDeprecatedCSS = (config: configOptions) => { 18 | const deprecated: string[] = []; 19 | const deprecatedCSS = ['x-screen', 'x-row', 'cursor-node', '::selection']; 20 | deprecatedCSS.forEach((css) => { 21 | if (config.css?.includes(css) || config.termCSS?.includes(css)) { 22 | deprecated.push(css); 23 | } 24 | }); 25 | return deprecated; 26 | }; 27 | 28 | const checkDeprecatedConfig = () => { 29 | if (!cfg.config) { 30 | return; 31 | } 32 | const deprecated = getDeprecatedCSS(cfg.config); 33 | if (deprecated.length === 0) { 34 | return; 35 | } 36 | const deprecatedStr = deprecated.join(', '); 37 | notify('Configuration warning', `Your configuration uses some deprecated CSS classes (${deprecatedStr})`); 38 | }; 39 | 40 | const _watch = () => { 41 | if (_watcher) { 42 | return; 43 | } 44 | 45 | const onChange = () => { 46 | // Need to wait 100ms to ensure that write is complete 47 | setTimeout(() => { 48 | cfg = _import(); 49 | notify('Configuration updated', 'Hyper configuration reloaded!'); 50 | watchers.forEach((fn) => { 51 | fn(); 52 | }); 53 | checkDeprecatedConfig(); 54 | }, 100); 55 | }; 56 | 57 | _watcher = chokidar.watch(cfgPath); 58 | _watcher.on('change', onChange); 59 | _watcher.on('error', (error) => { 60 | console.error('error watching config', error); 61 | }); 62 | 63 | app.on('before-quit', () => { 64 | if (Object.keys(_watcher.getWatched()).length > 0) { 65 | _watcher.close().catch((err) => { 66 | console.warn(err); 67 | }); 68 | } 69 | }); 70 | }; 71 | 72 | export const subscribe = (fn: Function) => { 73 | watchers.push(fn); 74 | return () => { 75 | watchers.splice(watchers.indexOf(fn), 1); 76 | }; 77 | }; 78 | 79 | export const getConfigDir = () => { 80 | // expose config directory to load plugin from the right place 81 | return cfgDir; 82 | }; 83 | 84 | export const getDefaultProfile = () => { 85 | return cfg.config.defaultProfile || cfg.config.profiles[0]?.name || 'default'; 86 | }; 87 | 88 | // get config for the default profile, keeping it for backward compatibility 89 | export const getConfig = () => { 90 | return getProfileConfig(getDefaultProfile()); 91 | }; 92 | 93 | export const getProfiles = () => { 94 | return cfg.config.profiles; 95 | }; 96 | 97 | export const getProfileConfig = (profileName: string): configOptions => { 98 | const {profiles, defaultProfile, ...baseConfig} = cfg.config; 99 | const profileConfig = profiles.find((p) => p.name === profileName)?.config || {}; 100 | for (const key in profileConfig) { 101 | if (typeof baseConfig[key] === 'object' && !Array.isArray(baseConfig[key])) { 102 | baseConfig[key] = {...baseConfig[key], ...profileConfig[key]}; 103 | } else { 104 | baseConfig[key] = profileConfig[key]; 105 | } 106 | } 107 | return {...baseConfig, defaultProfile, profiles}; 108 | }; 109 | 110 | export const openConfig = () => { 111 | return _openConfig(); 112 | }; 113 | 114 | export const getPlugins = (): {plugins: string[]; localPlugins: string[]} => { 115 | return { 116 | plugins: cfg.plugins, 117 | localPlugins: cfg.localPlugins 118 | }; 119 | }; 120 | 121 | export const getKeymaps = () => { 122 | return cfg.keymaps; 123 | }; 124 | 125 | export const setup = () => { 126 | cfg = _import(); 127 | _watch(); 128 | checkDeprecatedConfig(); 129 | }; 130 | 131 | export {get as getWin, recordState as winRecord, defaults as windowDefaults} from './config/windows'; 132 | 133 | export const fixConfigDefaults = (decoratedConfig: configOptions) => { 134 | const defaultConfig = getDefaultConfig().config!; 135 | decoratedConfig.colors = getColorMap(decoratedConfig.colors) || {}; 136 | // We must have default colors for xterm css. 137 | decoratedConfig.colors = {...defaultConfig.colors, ...decoratedConfig.colors}; 138 | return decoratedConfig; 139 | }; 140 | 141 | export const htermConfigTranslate = (config: configOptions) => { 142 | const cssReplacements: Record = { 143 | 'x-screen x-row([ {.[])': '.xterm-rows > div$1', 144 | '.cursor-node([ {.[])': '.terminal-cursor$1', 145 | '::selection([ {.[])': '.terminal .xterm-selection div$1', 146 | 'x-screen a([ {.[])': '.terminal a$1', 147 | 'x-row a([ {.[])': '.terminal a$1' 148 | }; 149 | Object.keys(cssReplacements).forEach((pattern) => { 150 | const searchvalue = new RegExp(pattern, 'g'); 151 | const newvalue = cssReplacements[pattern]; 152 | config.css = config.css?.replace(searchvalue, newvalue); 153 | config.termCSS = config.termCSS?.replace(searchvalue, newvalue); 154 | }); 155 | return config; 156 | }; 157 | -------------------------------------------------------------------------------- /app/config/config-default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./schema.json", 3 | "config": { 4 | "updateChannel": "stable", 5 | "fontSize": 12, 6 | "fontFamily": "Menlo, \"DejaVu Sans Mono\", Consolas, \"Lucida Console\", monospace", 7 | "fontWeight": "normal", 8 | "fontWeightBold": "bold", 9 | "lineHeight": 1, 10 | "letterSpacing": 0, 11 | "scrollback": 1000, 12 | "cursorColor": "rgba(248,28,229,0.8)", 13 | "cursorAccentColor": "#000", 14 | "cursorShape": "BLOCK", 15 | "cursorBlink": false, 16 | "foregroundColor": "#fff", 17 | "backgroundColor": "#000", 18 | "selectionColor": "rgba(248,28,229,0.3)", 19 | "borderColor": "#333", 20 | "css": "", 21 | "termCSS": "", 22 | "workingDirectory": "", 23 | "showHamburgerMenu": "", 24 | "showWindowControls": "", 25 | "padding": "12px 14px", 26 | "colors": { 27 | "black": "#000000", 28 | "red": "#C51E14", 29 | "green": "#1DC121", 30 | "yellow": "#C7C329", 31 | "blue": "#0A2FC4", 32 | "magenta": "#C839C5", 33 | "cyan": "#20C5C6", 34 | "white": "#C7C7C7", 35 | "lightBlack": "#686868", 36 | "lightRed": "#FD6F6B", 37 | "lightGreen": "#67F86F", 38 | "lightYellow": "#FFFA72", 39 | "lightBlue": "#6A76FB", 40 | "lightMagenta": "#FD7CFC", 41 | "lightCyan": "#68FDFE", 42 | "lightWhite": "#FFFFFF", 43 | "limeGreen": "#32CD32", 44 | "lightCoral": "#F08080" 45 | }, 46 | "shell": "", 47 | "shellArgs": [ 48 | "--login" 49 | ], 50 | "env": {}, 51 | "bell": "SOUND", 52 | "bellSound": null, 53 | "bellSoundURL": null, 54 | "copyOnSelect": false, 55 | "defaultSSHApp": true, 56 | "quickEdit": false, 57 | "macOptionSelectionMode": "vertical", 58 | "webGLRenderer": false, 59 | "webLinksActivationKey": "", 60 | "disableLigatures": true, 61 | "disableAutoUpdates": false, 62 | "autoUpdatePlugins": true, 63 | "preserveCWD": true, 64 | "screenReaderMode": false, 65 | "imageSupport": true, 66 | "defaultProfile": "default", 67 | "profiles": [ 68 | { 69 | "name": "default", 70 | "config": {} 71 | } 72 | ] 73 | }, 74 | "plugins": [], 75 | "localPlugins": [], 76 | "keymaps": {} 77 | } 78 | -------------------------------------------------------------------------------- /app/config/import.ts: -------------------------------------------------------------------------------- 1 | import {readFileSync, mkdirpSync} from 'fs-extra'; 2 | 3 | import type {rawConfig} from '../../typings/config'; 4 | import notify from '../notify'; 5 | 6 | import {_init} from './init'; 7 | import {migrateHyper3Config} from './migrate'; 8 | import {defaultCfg, cfgPath, plugs, defaultPlatformKeyPath} from './paths'; 9 | 10 | let defaultConfig: rawConfig; 11 | 12 | const _importConf = () => { 13 | // init plugin directories if not present 14 | mkdirpSync(plugs.base); 15 | mkdirpSync(plugs.local); 16 | 17 | try { 18 | migrateHyper3Config(); 19 | } catch (err) { 20 | console.error(err); 21 | } 22 | 23 | let defaultCfgRaw = '{}'; 24 | try { 25 | defaultCfgRaw = readFileSync(defaultCfg, 'utf8'); 26 | } catch (err) { 27 | console.log(err); 28 | } 29 | const _defaultCfg = JSON.parse(defaultCfgRaw) as rawConfig; 30 | 31 | // Importing platform specific keymap 32 | let content = '{}'; 33 | try { 34 | content = readFileSync(defaultPlatformKeyPath(), 'utf8'); 35 | } catch (err) { 36 | console.error(err); 37 | } 38 | const mapping = JSON.parse(content) as Record; 39 | _defaultCfg.keymaps = mapping; 40 | 41 | // Import user config 42 | let userCfg: rawConfig; 43 | try { 44 | userCfg = JSON.parse(readFileSync(cfgPath, 'utf8')); 45 | } catch (err) { 46 | notify("Couldn't parse config file. Using default config instead."); 47 | userCfg = JSON.parse(defaultCfgRaw); 48 | } 49 | 50 | return {userCfg, defaultCfg: _defaultCfg}; 51 | }; 52 | 53 | export const _import = () => { 54 | const imported = _importConf(); 55 | defaultConfig = imported.defaultCfg; 56 | const result = _init(imported.userCfg, imported.defaultCfg); 57 | return result; 58 | }; 59 | 60 | export const getDefaultConfig = () => { 61 | if (!defaultConfig) { 62 | defaultConfig = _importConf().defaultCfg; 63 | } 64 | return defaultConfig; 65 | }; 66 | -------------------------------------------------------------------------------- /app/config/init.ts: -------------------------------------------------------------------------------- 1 | import vm from 'vm'; 2 | 3 | import merge from 'lodash/merge'; 4 | 5 | import type {parsedConfig, rawConfig, configOptions} from '../../typings/config'; 6 | import notify from '../notify'; 7 | import mapKeys from '../utils/map-keys'; 8 | 9 | const _extract = (script?: vm.Script): Record => { 10 | const module: Record = {}; 11 | script?.runInNewContext({module}, {displayErrors: true}); 12 | if (!module.exports) { 13 | throw new Error('Error reading configuration: `module.exports` not set'); 14 | } 15 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 16 | return module.exports; 17 | }; 18 | 19 | const _syntaxValidation = (cfg: string) => { 20 | try { 21 | return new vm.Script(cfg, {filename: '.hyper.js'}); 22 | } catch (_err) { 23 | const err = _err as {name: string}; 24 | notify(`Error loading config: ${err.name}`, JSON.stringify(err), {error: err}); 25 | } 26 | }; 27 | 28 | const _extractDefault = (cfg: string) => { 29 | return _extract(_syntaxValidation(cfg)); 30 | }; 31 | 32 | // init config 33 | const _init = (userCfg: rawConfig, defaultCfg: rawConfig): parsedConfig => { 34 | return { 35 | config: (() => { 36 | if (userCfg?.config) { 37 | const conf = userCfg.config; 38 | conf.defaultProfile = conf.defaultProfile || 'default'; 39 | conf.profiles = conf.profiles || []; 40 | conf.profiles = conf.profiles.length > 0 ? conf.profiles : [{name: 'default', config: {}}]; 41 | conf.profiles = conf.profiles.map((p, i) => ({ 42 | ...p, 43 | name: p.name || `profile-${i + 1}`, 44 | config: p.config || {} 45 | })); 46 | if (!conf.profiles.map((p) => p.name).includes(conf.defaultProfile)) { 47 | conf.defaultProfile = conf.profiles[0].name; 48 | } 49 | return merge({}, defaultCfg.config, conf); 50 | } else { 51 | notify('Error reading configuration: `config` key is missing'); 52 | return defaultCfg.config || ({} as configOptions); 53 | } 54 | })(), 55 | // Merging platform specific keymaps with user defined keymaps 56 | keymaps: mapKeys({...defaultCfg.keymaps, ...userCfg?.keymaps}), 57 | // Ignore undefined values in plugin and localPlugins array Issue #1862 58 | plugins: userCfg?.plugins?.filter(Boolean) || [], 59 | localPlugins: userCfg?.localPlugins?.filter(Boolean) || [] 60 | }; 61 | }; 62 | 63 | export {_init, _extractDefault}; 64 | -------------------------------------------------------------------------------- /app/config/open.ts: -------------------------------------------------------------------------------- 1 | import {exec} from 'child_process'; 2 | 3 | import {shell} from 'electron'; 4 | 5 | import * as Registry from 'native-reg'; 6 | 7 | import {cfgPath} from './paths'; 8 | 9 | const getUserChoiceKey = () => { 10 | try { 11 | // Load FileExts keys for .js files 12 | const fileExtsKeys = Registry.openKey( 13 | Registry.HKCU, 14 | 'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.js', 15 | Registry.Access.READ 16 | ); 17 | const keys = fileExtsKeys ? Registry.enumKeyNames(fileExtsKeys) : []; 18 | Registry.closeKey(fileExtsKeys); 19 | 20 | // Find UserChoice key 21 | const userChoice = keys.find((k) => k.endsWith('UserChoice')); 22 | return userChoice 23 | ? `Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.js\\${userChoice}` 24 | : userChoice; 25 | } catch (error) { 26 | console.error(error); 27 | return; 28 | } 29 | }; 30 | 31 | const hasDefaultSet = () => { 32 | const userChoice = getUserChoiceKey(); 33 | if (!userChoice) return false; 34 | 35 | try { 36 | // Load key values 37 | const userChoiceKey = Registry.openKey(Registry.HKCU, userChoice, Registry.Access.READ)!; 38 | const values: string[] = Registry.enumValueNames(userChoiceKey).map( 39 | (x) => (Registry.queryValue(userChoiceKey, x) as string) || '' 40 | ); 41 | Registry.closeKey(userChoiceKey); 42 | 43 | // Look for default program 44 | const hasDefaultProgramConfigured = values.every( 45 | (value) => value && typeof value === 'string' && !value.includes('WScript.exe') && !value.includes('JSFile') 46 | ); 47 | 48 | return hasDefaultProgramConfigured; 49 | } catch (error) { 50 | console.error(error); 51 | return false; 52 | } 53 | }; 54 | 55 | // This mimics shell.openItem, true if it worked, false if not. 56 | const openNotepad = (file: string) => 57 | new Promise((resolve) => { 58 | exec(`start notepad.exe ${file}`, (error) => { 59 | resolve(!error); 60 | }); 61 | }); 62 | 63 | const openConfig = () => { 64 | // Windows opens .js files with WScript.exe by default 65 | // If the user hasn't set up an editor for .js files, we fallback to notepad. 66 | if (process.platform === 'win32') { 67 | try { 68 | if (hasDefaultSet()) { 69 | return shell.openPath(cfgPath).then((error) => error === ''); 70 | } 71 | console.warn('No default app set for .js files, using notepad.exe fallback'); 72 | } catch (err) { 73 | console.error('Open config with default app error:', err); 74 | } 75 | return openNotepad(cfgPath); 76 | } 77 | return shell.openPath(cfgPath).then((error) => error === ''); 78 | }; 79 | 80 | export default openConfig; 81 | -------------------------------------------------------------------------------- /app/config/paths.ts: -------------------------------------------------------------------------------- 1 | // This module exports paths, names, and other metadata that is referenced 2 | import {statSync} from 'fs'; 3 | import {homedir} from 'os'; 4 | import {resolve, join} from 'path'; 5 | 6 | import {app} from 'electron'; 7 | 8 | import isDev from 'electron-is-dev'; 9 | 10 | const cfgFile = 'hyper.json'; 11 | const defaultCfgFile = 'config-default.json'; 12 | const schemaFile = 'schema.json'; 13 | const homeDirectory = homedir(); 14 | 15 | // If the user defines XDG_CONFIG_HOME they definitely want their config there, 16 | // otherwise use the home directory in linux/mac and userdata in windows 17 | let cfgDir = process.env.XDG_CONFIG_HOME 18 | ? join(process.env.XDG_CONFIG_HOME, 'Hyper') 19 | : process.platform === 'win32' 20 | ? app.getPath('userData') 21 | : join(homeDirectory, '.config', 'Hyper'); 22 | 23 | const legacyCfgPath = join( 24 | process.env.XDG_CONFIG_HOME !== undefined 25 | ? join(process.env.XDG_CONFIG_HOME, 'hyper') 26 | : process.platform == 'win32' 27 | ? app.getPath('userData') 28 | : homedir(), 29 | '.hyper.js' 30 | ); 31 | 32 | let cfgPath = join(cfgDir, cfgFile); 33 | const schemaPath = resolve(__dirname, schemaFile); 34 | 35 | const devDir = resolve(__dirname, '../..'); 36 | const devCfg = join(devDir, cfgFile); 37 | const defaultCfg = resolve(__dirname, defaultCfgFile); 38 | 39 | if (isDev) { 40 | // if a local config file exists, use it 41 | try { 42 | statSync(devCfg); 43 | cfgPath = devCfg; 44 | cfgDir = devDir; 45 | console.log('using config file:', cfgPath); 46 | } catch (err) { 47 | // ignore 48 | } 49 | } 50 | 51 | const plugins = resolve(cfgDir, 'plugins'); 52 | const plugs = { 53 | base: plugins, 54 | local: resolve(plugins, 'local'), 55 | cache: resolve(plugins, 'cache') 56 | }; 57 | const yarn = resolve(__dirname, '../../bin/yarn-standalone.js'); 58 | const cliScriptPath = resolve(__dirname, '../../bin/hyper'); 59 | const cliLinkPath = '/usr/local/bin/hyper'; 60 | 61 | const icon = resolve(__dirname, '../static/icon96x96.png'); 62 | 63 | const keymapPath = resolve(__dirname, '../keymaps'); 64 | const darwinKeys = join(keymapPath, 'darwin.json'); 65 | const win32Keys = join(keymapPath, 'win32.json'); 66 | const linuxKeys = join(keymapPath, 'linux.json'); 67 | 68 | const defaultPlatformKeyPath = () => { 69 | switch (process.platform) { 70 | case 'darwin': 71 | return darwinKeys; 72 | case 'win32': 73 | return win32Keys; 74 | case 'linux': 75 | return linuxKeys; 76 | default: 77 | return darwinKeys; 78 | } 79 | }; 80 | 81 | export { 82 | cfgDir, 83 | cfgPath, 84 | legacyCfgPath, 85 | cfgFile, 86 | defaultCfg, 87 | icon, 88 | defaultPlatformKeyPath, 89 | plugs, 90 | yarn, 91 | cliScriptPath, 92 | cliLinkPath, 93 | homeDirectory, 94 | schemaFile, 95 | schemaPath 96 | }; 97 | -------------------------------------------------------------------------------- /app/config/windows.ts: -------------------------------------------------------------------------------- 1 | import type {BrowserWindow} from 'electron'; 2 | 3 | import Config from 'electron-store'; 4 | 5 | export const defaults = { 6 | windowPosition: [50, 50] as [number, number], 7 | windowSize: [540, 380] as [number, number] 8 | }; 9 | 10 | // local storage 11 | const cfg = new Config({defaults}); 12 | 13 | export function get() { 14 | const position = cfg.get('windowPosition', defaults.windowPosition); 15 | const size = cfg.get('windowSize', defaults.windowSize); 16 | return {position, size}; 17 | } 18 | export function recordState(win: BrowserWindow) { 19 | cfg.set('windowPosition', win.getPosition()); 20 | cfg.set('windowSize', win.getSize()); 21 | } 22 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hyper 5 | 6 | 7 | 8 | 9 | 10 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/keymaps/darwin.json: -------------------------------------------------------------------------------- 1 | { 2 | "window:devtools": "command+alt+i", 3 | "window:reload": "command+shift+r", 4 | "window:reloadFull": "command+shift+f5", 5 | "window:preferences": "command+,", 6 | "zoom:reset": "command+0", 7 | "zoom:in": [ 8 | "command+plus", 9 | "command+=" 10 | ], 11 | "zoom:out": "command+-", 12 | "window:new": "command+n", 13 | "window:minimize": "command+m", 14 | "window:zoom": "ctrl+alt+command+m", 15 | "window:toggleFullScreen": "command+ctrl+f", 16 | "window:close": "command+shift+w", 17 | "tab:new": "command+t", 18 | "tab:next": [ 19 | "command+shift+]", 20 | "command+shift+right", 21 | "command+alt+right", 22 | "ctrl+tab" 23 | ], 24 | "tab:prev": [ 25 | "command+shift+[", 26 | "command+shift+left", 27 | "command+alt+left", 28 | "ctrl+shift+tab" 29 | ], 30 | "tab:jump:prefix": "command", 31 | "pane:next": "command+]", 32 | "pane:prev": "command+[", 33 | "pane:splitRight": "command+d", 34 | "pane:splitDown": "command+shift+d", 35 | "pane:close": "command+w", 36 | "editor:undo": "command+z", 37 | "editor:redo": "command+y", 38 | "editor:cut": "command+x", 39 | "editor:copy": "command+c", 40 | "editor:paste": "command+v", 41 | "editor:selectAll": "command+a", 42 | "editor:search": "command+f", 43 | "editor:search-close": "esc", 44 | "editor:movePreviousWord": "alt+left", 45 | "editor:moveNextWord": "alt+right", 46 | "editor:moveBeginningLine": "command+left", 47 | "editor:moveEndLine": "command+right", 48 | "editor:deletePreviousWord": "alt+backspace", 49 | "editor:deleteNextWord": "alt+delete", 50 | "editor:deleteBeginningLine": "command+backspace", 51 | "editor:deleteEndLine": "command+delete", 52 | "editor:clearBuffer": "command+k", 53 | "editor:break": "ctrl+c", 54 | "plugins:update": "command+shift+u" 55 | } 56 | -------------------------------------------------------------------------------- /app/keymaps/linux.json: -------------------------------------------------------------------------------- 1 | { 2 | "window:devtools": "ctrl+shift+i", 3 | "window:reload": "ctrl+shift+r", 4 | "window:reloadFull": "ctrl+shift+f5", 5 | "window:preferences": "ctrl+,", 6 | "window:hamburgerMenu": "alt+f", 7 | "zoom:reset": "ctrl+0", 8 | "zoom:in": "ctrl+=", 9 | "zoom:out": "ctrl+-", 10 | "window:new": "ctrl+shift+n", 11 | "window:minimize": "ctrl+shift+m", 12 | "window:zoom": "ctrl+shift+alt+m", 13 | "window:toggleFullScreen": "f11", 14 | "window:close": "ctrl+shift+q", 15 | "tab:new": "ctrl+shift+t", 16 | "tab:next": [ 17 | "ctrl+shift+]", 18 | "ctrl+shift+right", 19 | "ctrl+alt+right", 20 | "ctrl+tab" 21 | ], 22 | "tab:prev": [ 23 | "ctrl+shift+[", 24 | "ctrl+shift+left", 25 | "ctrl+alt+left", 26 | "ctrl+shift+tab" 27 | ], 28 | "tab:jump:prefix": "ctrl", 29 | "pane:next": "ctrl+pageup", 30 | "pane:prev": "ctrl+pagedown", 31 | "pane:splitRight": "ctrl+shift+d", 32 | "pane:splitDown": "ctrl+shift+e", 33 | "pane:close": "ctrl+shift+w", 34 | "editor:undo": "ctrl+shift+z", 35 | "editor:redo": "ctrl+shift+y", 36 | "editor:cut": "ctrl+shift+x", 37 | "editor:copy": "ctrl+shift+c", 38 | "editor:paste": "ctrl+shift+v", 39 | "editor:selectAll": "ctrl+shift+a", 40 | "editor:search": "ctrl+shift+f", 41 | "editor:search-close": "esc", 42 | "editor:movePreviousWord": "ctrl+left", 43 | "editor:moveNextWord": "ctrl+right", 44 | "editor:moveBeginningLine": "home", 45 | "editor:moveEndLine": "end", 46 | "editor:deletePreviousWord": "ctrl+backspace", 47 | "editor:deleteNextWord": "ctrl+del", 48 | "editor:deleteBeginningLine": "ctrl+home", 49 | "editor:deleteEndLine": "ctrl+end", 50 | "editor:clearBuffer": "ctrl+shift+k", 51 | "editor:break": "ctrl+c", 52 | "plugins:update": "ctrl+shift+u" 53 | } 54 | -------------------------------------------------------------------------------- /app/keymaps/win32.json: -------------------------------------------------------------------------------- 1 | { 2 | "window:devtools": "ctrl+shift+i", 3 | "window:reload": "ctrl+shift+r", 4 | "window:reloadFull": "ctrl+shift+f5", 5 | "window:preferences": "ctrl+,", 6 | "window:hamburgerMenu": "alt+f", 7 | "zoom:reset": "ctrl+0", 8 | "zoom:in": "ctrl+=", 9 | "zoom:out": "ctrl+-", 10 | "window:new": "ctrl+shift+n", 11 | "window:minimize": "ctrl+shift+m", 12 | "window:zoom": "ctrl+shift+alt+m", 13 | "window:toggleFullScreen": "f11", 14 | "window:close": [ 15 | "ctrl+shift+q", 16 | "alt+f4" 17 | ], 18 | "tab:new": "ctrl+shift+t", 19 | "tab:next": [ 20 | "ctrl+tab" 21 | ], 22 | "tab:prev": [ 23 | "ctrl+shift+tab" 24 | ], 25 | "tab:jump:prefix": "ctrl", 26 | "pane:next": "ctrl+pageup", 27 | "pane:prev": "ctrl+pagedown", 28 | "pane:splitRight": "ctrl+shift+d", 29 | "pane:splitDown": "ctrl+shift+e", 30 | "pane:close": "ctrl+shift+w", 31 | "editor:undo": "ctrl+shift+z", 32 | "editor:redo": "ctrl+shift+y", 33 | "editor:cut": "ctrl+shift+x", 34 | "editor:copy": "ctrl+shift+c", 35 | "editor:paste": "ctrl+shift+v", 36 | "editor:selectAll": "ctrl+shift+a", 37 | "editor:search": "ctrl+shift+f", 38 | "editor:search-close": "esc", 39 | "editor:movePreviousWord": "", 40 | "editor:moveNextWord": "", 41 | "editor:moveBeginningLine": "Home", 42 | "editor:moveEndLine": "End", 43 | "editor:deletePreviousWord": "ctrl+backspace", 44 | "editor:deleteNextWord": "ctrl+del", 45 | "editor:deleteBeginningLine": "ctrl+home", 46 | "editor:deleteEndLine": "ctrl+end", 47 | "editor:clearBuffer": "ctrl+shift+k", 48 | "editor:break": "ctrl+c", 49 | "plugins:update": "ctrl+shift+u" 50 | } 51 | -------------------------------------------------------------------------------- /app/menus/menu.ts: -------------------------------------------------------------------------------- 1 | // Packages 2 | import {app, dialog, Menu} from 'electron'; 3 | import type {BrowserWindow} from 'electron'; 4 | 5 | // Utilities 6 | import {execCommand} from '../commands'; 7 | import {getConfig} from '../config'; 8 | import {icon} from '../config/paths'; 9 | import {getDecoratedKeymaps} from '../plugins'; 10 | import {getRendererTypes} from '../utils/renderer-utils'; 11 | 12 | import darwinMenu from './menus/darwin'; 13 | import editMenu from './menus/edit'; 14 | import helpMenu from './menus/help'; 15 | import shellMenu from './menus/shell'; 16 | import toolsMenu from './menus/tools'; 17 | import viewMenu from './menus/view'; 18 | import windowMenu from './menus/window'; 19 | 20 | const appName = app.name; 21 | const appVersion = app.getVersion(); 22 | 23 | let menu_: Menu; 24 | 25 | export const createMenu = ( 26 | createWindow: (fn?: (win: BrowserWindow) => void, options?: Record) => BrowserWindow, 27 | getLoadedPluginVersions: () => {name: string; version: string}[] 28 | ) => { 29 | const config = getConfig(); 30 | // We take only first shortcut in array for each command 31 | const allCommandKeys = getDecoratedKeymaps(); 32 | const commandKeys = Object.keys(allCommandKeys).reduce((result: Record, command) => { 33 | result[command] = allCommandKeys[command][0]; 34 | return result; 35 | }, {}); 36 | 37 | let updateChannel = 'stable'; 38 | 39 | if (config?.updateChannel && config.updateChannel === 'canary') { 40 | updateChannel = 'canary'; 41 | } 42 | 43 | const showAbout = () => { 44 | const loadedPlugins = getLoadedPluginVersions(); 45 | const pluginList = 46 | loadedPlugins.length === 0 ? 'none' : loadedPlugins.map((plugin) => `\n ${plugin.name} (${plugin.version})`); 47 | 48 | const rendererCounts = Object.values(getRendererTypes()).reduce((acc: Record, type) => { 49 | acc[type] = acc[type] ? acc[type] + 1 : 1; 50 | return acc; 51 | }, {}); 52 | const renderers = Object.entries(rendererCounts) 53 | .map(([type, count]) => type + (count > 1 ? ` (${count})` : '')) 54 | .join(', '); 55 | 56 | void dialog.showMessageBox({ 57 | title: `About ${appName}`, 58 | message: `${appName} ${appVersion} (${updateChannel})`, 59 | detail: `Renderers: ${renderers}\nPlugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright © 2022 Vercel, Inc.`, 60 | buttons: [], 61 | icon: icon as any 62 | }); 63 | }; 64 | const menu = [ 65 | ...(process.platform === 'darwin' ? [darwinMenu(commandKeys, execCommand, showAbout)] : []), 66 | shellMenu( 67 | commandKeys, 68 | execCommand, 69 | getConfig().profiles.map((p) => p.name) 70 | ), 71 | editMenu(commandKeys, execCommand), 72 | viewMenu(commandKeys, execCommand), 73 | toolsMenu(commandKeys, execCommand), 74 | windowMenu(commandKeys, execCommand), 75 | helpMenu(commandKeys, showAbout) 76 | ]; 77 | 78 | return menu; 79 | }; 80 | 81 | export const buildMenu = (template: Electron.MenuItemConstructorOptions[]): Electron.Menu => { 82 | menu_ = Menu.buildFromTemplate(template); 83 | return menu_; 84 | }; 85 | -------------------------------------------------------------------------------- /app/menus/menus/darwin.ts: -------------------------------------------------------------------------------- 1 | // This menu label is overrided by OSX to be the appName 2 | // The label is set to appName here so it matches actual behavior 3 | import {app} from 'electron'; 4 | import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; 5 | 6 | const darwinMenu = ( 7 | commandKeys: Record, 8 | execCommand: (command: string, focusedWindow?: BrowserWindow) => void, 9 | showAbout: () => void 10 | ): MenuItemConstructorOptions => { 11 | return { 12 | label: `${app.name}`, 13 | submenu: [ 14 | { 15 | label: 'About Hyper', 16 | click() { 17 | showAbout(); 18 | } 19 | }, 20 | { 21 | type: 'separator' 22 | }, 23 | { 24 | label: 'Preferences...', 25 | accelerator: commandKeys['window:preferences'], 26 | click() { 27 | execCommand('window:preferences'); 28 | } 29 | }, 30 | { 31 | type: 'separator' 32 | }, 33 | { 34 | role: 'services', 35 | submenu: [] 36 | }, 37 | { 38 | type: 'separator' 39 | }, 40 | { 41 | role: 'hide' 42 | }, 43 | { 44 | role: 'hideOthers' 45 | }, 46 | { 47 | role: 'unhide' 48 | }, 49 | { 50 | type: 'separator' 51 | }, 52 | { 53 | role: 'quit' 54 | } 55 | ] 56 | }; 57 | }; 58 | 59 | export default darwinMenu; 60 | -------------------------------------------------------------------------------- /app/menus/menus/edit.ts: -------------------------------------------------------------------------------- 1 | import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; 2 | 3 | const editMenu = ( 4 | commandKeys: Record, 5 | execCommand: (command: string, focusedWindow?: BrowserWindow) => void 6 | ) => { 7 | const submenu: MenuItemConstructorOptions[] = [ 8 | { 9 | label: 'Undo', 10 | accelerator: commandKeys['editor:undo'], 11 | enabled: false 12 | }, 13 | { 14 | label: 'Redo', 15 | accelerator: commandKeys['editor:redo'], 16 | enabled: false 17 | }, 18 | { 19 | type: 'separator' 20 | }, 21 | { 22 | label: 'Cut', 23 | accelerator: commandKeys['editor:cut'], 24 | enabled: false 25 | }, 26 | { 27 | role: 'copy', 28 | command: 'editor:copy', 29 | accelerator: commandKeys['editor:copy'], 30 | registerAccelerator: true 31 | } as any, 32 | { 33 | role: 'paste', 34 | accelerator: commandKeys['editor:paste'], 35 | registerAccelerator: true 36 | }, 37 | { 38 | label: 'Select All', 39 | accelerator: commandKeys['editor:selectAll'], 40 | click(item, focusedWindow) { 41 | execCommand('editor:selectAll', focusedWindow); 42 | } 43 | }, 44 | { 45 | type: 'separator' 46 | }, 47 | { 48 | label: 'Move to...', 49 | submenu: [ 50 | { 51 | label: 'Previous word', 52 | accelerator: commandKeys['editor:movePreviousWord'], 53 | click(item, focusedWindow) { 54 | execCommand('editor:movePreviousWord', focusedWindow); 55 | } 56 | }, 57 | { 58 | label: 'Next word', 59 | accelerator: commandKeys['editor:moveNextWord'], 60 | click(item, focusedWindow) { 61 | execCommand('editor:moveNextWord', focusedWindow); 62 | } 63 | }, 64 | { 65 | label: 'Line beginning', 66 | accelerator: commandKeys['editor:moveBeginningLine'], 67 | click(item, focusedWindow) { 68 | execCommand('editor:moveBeginningLine', focusedWindow); 69 | } 70 | }, 71 | { 72 | label: 'Line end', 73 | accelerator: commandKeys['editor:moveEndLine'], 74 | click(item, focusedWindow) { 75 | execCommand('editor:moveEndLine', focusedWindow); 76 | } 77 | } 78 | ] 79 | }, 80 | { 81 | label: 'Delete...', 82 | submenu: [ 83 | { 84 | label: 'Previous word', 85 | accelerator: commandKeys['editor:deletePreviousWord'], 86 | click(item, focusedWindow) { 87 | execCommand('editor:deletePreviousWord', focusedWindow); 88 | } 89 | }, 90 | { 91 | label: 'Next word', 92 | accelerator: commandKeys['editor:deleteNextWord'], 93 | click(item, focusedWindow) { 94 | execCommand('editor:deleteNextWord', focusedWindow); 95 | } 96 | }, 97 | { 98 | label: 'Line beginning', 99 | accelerator: commandKeys['editor:deleteBeginningLine'], 100 | click(item, focusedWindow) { 101 | execCommand('editor:deleteBeginningLine', focusedWindow); 102 | } 103 | }, 104 | { 105 | label: 'Line end', 106 | accelerator: commandKeys['editor:deleteEndLine'], 107 | click(item, focusedWindow) { 108 | execCommand('editor:deleteEndLine', focusedWindow); 109 | } 110 | } 111 | ] 112 | }, 113 | { 114 | type: 'separator' 115 | }, 116 | { 117 | label: 'Clear Buffer', 118 | accelerator: commandKeys['editor:clearBuffer'], 119 | click(item, focusedWindow) { 120 | execCommand('editor:clearBuffer', focusedWindow); 121 | } 122 | }, 123 | { 124 | label: 'Search', 125 | accelerator: commandKeys['editor:search'], 126 | click(item, focusedWindow) { 127 | execCommand('editor:search', focusedWindow); 128 | } 129 | } 130 | ]; 131 | 132 | if (process.platform !== 'darwin') { 133 | submenu.push( 134 | {type: 'separator'}, 135 | { 136 | label: 'Preferences...', 137 | accelerator: commandKeys['window:preferences'], 138 | click() { 139 | execCommand('window:preferences'); 140 | } 141 | } 142 | ); 143 | } 144 | 145 | return { 146 | label: 'Edit', 147 | submenu 148 | }; 149 | }; 150 | 151 | export default editMenu; 152 | -------------------------------------------------------------------------------- /app/menus/menus/help.ts: -------------------------------------------------------------------------------- 1 | import {release} from 'os'; 2 | 3 | import {app, shell, dialog, clipboard} from 'electron'; 4 | import type {MenuItemConstructorOptions} from 'electron'; 5 | 6 | import {getConfig, getPlugins} from '../../config'; 7 | import {version} from '../../package.json'; 8 | 9 | const {arch, env, platform, versions} = process; 10 | 11 | const helpMenu = (commands: Record, showAbout: () => void): MenuItemConstructorOptions => { 12 | const submenu: MenuItemConstructorOptions[] = [ 13 | { 14 | label: `${app.name} Website`, 15 | click() { 16 | void shell.openExternal('https://hyper.is'); 17 | } 18 | }, 19 | { 20 | label: 'Report Issue', 21 | click(menuItem, focusedWindow) { 22 | const body = ` 28 | 29 | - [ ] Your Hyper.app version is **${version}**. Please verify you're using the [latest](https://github.com/vercel/hyper/releases/latest) Hyper.app version 30 | - [ ] I have searched the [issues](https://github.com/vercel/hyper/issues) of this repo and believe that this is not a duplicate 31 | --- 32 | - **Any relevant information from devtools?** _(CMD+OPTION+I on macOS, CTRL+SHIFT+I elsewhere)_: 33 | 34 | 35 | - **Is the issue reproducible in vanilla Hyper.app?** 36 | 37 | 38 | ## Issue 39 | 40 | 41 | 42 | 43 | 44 | 45 | --- 46 | 47 | - **${app.name} version**: ${env.TERM_PROGRAM_VERSION} "${app.getVersion()}" 48 | - **OS ARCH VERSION:** ${platform} ${arch} ${release()} 49 | - **Electron:** ${versions.electron} **LANG:** ${env.LANG} 50 | - **SHELL:** ${env.SHELL} **TERM:** ${env.TERM} 51 |
hyper.json contents 52 | 53 | \`\`\`json 54 | ${JSON.stringify(getConfig(), null, 2)} 55 | \`\`\` 56 |
57 |
plugins 58 | 59 | \`\`\`json 60 | ${JSON.stringify(getPlugins(), null, 2)} 61 | \`\`\` 62 |
`; 63 | 64 | const issueURL = `https://github.com/vercel/hyper/issues/new?body=${encodeURIComponent(body)}`; 65 | const copyAndSend = () => { 66 | clipboard.writeText(body); 67 | void shell.openExternal( 68 | `https://github.com/vercel/hyper/issues/new?body=${encodeURIComponent( 69 | '\n' 71 | )}` 72 | ); 73 | }; 74 | if (!focusedWindow) { 75 | copyAndSend(); 76 | } else if (issueURL.length > 6144) { 77 | void dialog 78 | .showMessageBox(focusedWindow, { 79 | message: 80 | 'There is too much data to send to GitHub directly. The data will be copied to the clipboard, ' + 81 | 'please paste it into the GitHub issue page that will open.', 82 | type: 'warning', 83 | buttons: ['OK', 'Cancel'] 84 | }) 85 | .then((result) => { 86 | if (result.response === 0) { 87 | copyAndSend(); 88 | } 89 | }); 90 | } else { 91 | void shell.openExternal(issueURL); 92 | } 93 | } 94 | } 95 | ]; 96 | 97 | if (process.platform !== 'darwin') { 98 | submenu.push( 99 | {type: 'separator'}, 100 | { 101 | label: 'About Hyper', 102 | click() { 103 | showAbout(); 104 | } 105 | } 106 | ); 107 | } 108 | return { 109 | role: 'help', 110 | submenu 111 | }; 112 | }; 113 | 114 | export default helpMenu; 115 | -------------------------------------------------------------------------------- /app/menus/menus/shell.ts: -------------------------------------------------------------------------------- 1 | import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; 2 | 3 | const shellMenu = ( 4 | commandKeys: Record, 5 | execCommand: (command: string, focusedWindow?: BrowserWindow) => void, 6 | profiles: string[] 7 | ): MenuItemConstructorOptions => { 8 | const isMac = process.platform === 'darwin'; 9 | 10 | return { 11 | label: isMac ? 'Shell' : 'File', 12 | submenu: [ 13 | { 14 | label: 'New Tab', 15 | accelerator: commandKeys['tab:new'], 16 | click(item, focusedWindow) { 17 | execCommand('tab:new', focusedWindow); 18 | } 19 | }, 20 | { 21 | label: 'New Window', 22 | accelerator: commandKeys['window:new'], 23 | click(item, focusedWindow) { 24 | execCommand('window:new', focusedWindow); 25 | } 26 | }, 27 | { 28 | type: 'separator' 29 | }, 30 | { 31 | label: 'Split Down', 32 | accelerator: commandKeys['pane:splitDown'], 33 | click(item, focusedWindow) { 34 | execCommand('pane:splitDown', focusedWindow); 35 | } 36 | }, 37 | { 38 | label: 'Split Right', 39 | accelerator: commandKeys['pane:splitRight'], 40 | click(item, focusedWindow) { 41 | execCommand('pane:splitRight', focusedWindow); 42 | } 43 | }, 44 | { 45 | type: 'separator' 46 | }, 47 | ...profiles.map( 48 | (profile): MenuItemConstructorOptions => ({ 49 | label: profile, 50 | submenu: [ 51 | { 52 | label: 'New Tab', 53 | accelerator: commandKeys[`tab:new:${profile}`], 54 | click(item, focusedWindow) { 55 | execCommand(`tab:new:${profile}`, focusedWindow); 56 | } 57 | }, 58 | { 59 | label: 'New Window', 60 | accelerator: commandKeys[`window:new:${profile}`], 61 | click(item, focusedWindow) { 62 | execCommand(`window:new:${profile}`, focusedWindow); 63 | } 64 | }, 65 | { 66 | type: 'separator' 67 | }, 68 | { 69 | label: 'Split Down', 70 | accelerator: commandKeys[`pane:splitDown:${profile}`], 71 | click(item, focusedWindow) { 72 | execCommand(`pane:splitDown:${profile}`, focusedWindow); 73 | } 74 | }, 75 | { 76 | label: 'Split Right', 77 | accelerator: commandKeys[`pane:splitRight:${profile}`], 78 | click(item, focusedWindow) { 79 | execCommand(`pane:splitRight:${profile}`, focusedWindow); 80 | } 81 | } 82 | ] 83 | }) 84 | ), 85 | { 86 | type: 'separator' 87 | }, 88 | { 89 | label: 'Close', 90 | accelerator: commandKeys['pane:close'], 91 | click(item, focusedWindow) { 92 | execCommand('pane:close', focusedWindow); 93 | } 94 | }, 95 | { 96 | label: isMac ? 'Close Window' : 'Quit', 97 | role: 'close', 98 | accelerator: commandKeys['window:close'] 99 | } 100 | ] 101 | }; 102 | }; 103 | 104 | export default shellMenu; 105 | -------------------------------------------------------------------------------- /app/menus/menus/tools.ts: -------------------------------------------------------------------------------- 1 | import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; 2 | 3 | const toolsMenu = ( 4 | commands: Record, 5 | execCommand: (command: string, focusedWindow?: BrowserWindow) => void 6 | ): MenuItemConstructorOptions => { 7 | return { 8 | label: 'Tools', 9 | submenu: [ 10 | { 11 | label: 'Update plugins', 12 | accelerator: commands['plugins:update'], 13 | click() { 14 | execCommand('plugins:update'); 15 | } 16 | }, 17 | { 18 | label: 'Install Hyper CLI command in PATH', 19 | click() { 20 | execCommand('cli:install'); 21 | } 22 | }, 23 | { 24 | type: 'separator' 25 | }, 26 | ...(process.platform === 'win32' 27 | ? [ 28 | { 29 | label: 'Add Hyper to system context menu', 30 | click() { 31 | execCommand('systemContextMenu:add'); 32 | } 33 | }, 34 | { 35 | label: 'Remove Hyper from system context menu', 36 | click() { 37 | execCommand('systemContextMenu:remove'); 38 | } 39 | }, 40 | { 41 | type: 'separator' 42 | } 43 | ] 44 | : []) 45 | ] 46 | }; 47 | }; 48 | 49 | export default toolsMenu; 50 | -------------------------------------------------------------------------------- /app/menus/menus/view.ts: -------------------------------------------------------------------------------- 1 | import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; 2 | 3 | const viewMenu = ( 4 | commandKeys: Record, 5 | execCommand: (command: string, focusedWindow?: BrowserWindow) => void 6 | ): MenuItemConstructorOptions => { 7 | return { 8 | label: 'View', 9 | submenu: [ 10 | { 11 | label: 'Reload', 12 | accelerator: commandKeys['window:reload'], 13 | click(item, focusedWindow) { 14 | execCommand('window:reload', focusedWindow); 15 | } 16 | }, 17 | { 18 | label: 'Full Reload', 19 | accelerator: commandKeys['window:reloadFull'], 20 | click(item, focusedWindow) { 21 | execCommand('window:reloadFull', focusedWindow); 22 | } 23 | }, 24 | { 25 | label: 'Developer Tools', 26 | accelerator: commandKeys['window:devtools'], 27 | click: (item, focusedWindow) => { 28 | execCommand('window:devtools', focusedWindow); 29 | } 30 | }, 31 | { 32 | type: 'separator' 33 | }, 34 | { 35 | label: 'Reset Zoom Level', 36 | accelerator: commandKeys['zoom:reset'], 37 | click(item, focusedWindow) { 38 | execCommand('zoom:reset', focusedWindow); 39 | } 40 | }, 41 | { 42 | label: 'Zoom In', 43 | accelerator: commandKeys['zoom:in'], 44 | click(item, focusedWindow) { 45 | execCommand('zoom:in', focusedWindow); 46 | } 47 | }, 48 | { 49 | label: 'Zoom Out', 50 | accelerator: commandKeys['zoom:out'], 51 | click(item, focusedWindow) { 52 | execCommand('zoom:out', focusedWindow); 53 | } 54 | } 55 | ] 56 | }; 57 | }; 58 | 59 | export default viewMenu; 60 | -------------------------------------------------------------------------------- /app/menus/menus/window.ts: -------------------------------------------------------------------------------- 1 | import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; 2 | 3 | const windowMenu = ( 4 | commandKeys: Record, 5 | execCommand: (command: string, focusedWindow?: BrowserWindow) => void 6 | ): MenuItemConstructorOptions => { 7 | // Generating tab:jump array 8 | const tabJump: MenuItemConstructorOptions[] = []; 9 | for (let i = 1; i <= 9; i++) { 10 | // 9 is a special number because it means 'last' 11 | const label = i === 9 ? 'Last' : `${i}`; 12 | tabJump.push({ 13 | label, 14 | accelerator: commandKeys[`tab:jump:${label.toLowerCase()}`] 15 | }); 16 | } 17 | 18 | return { 19 | role: 'window', 20 | submenu: [ 21 | { 22 | role: 'minimize', 23 | accelerator: commandKeys['window:minimize'] 24 | }, 25 | { 26 | type: 'separator' 27 | }, 28 | { 29 | // It's the same thing as clicking the green traffc-light on macOS 30 | role: 'zoom', 31 | accelerator: commandKeys['window:zoom'] 32 | }, 33 | { 34 | label: 'Select Tab', 35 | submenu: [ 36 | { 37 | label: 'Previous', 38 | accelerator: commandKeys['tab:prev'], 39 | click: (item, focusedWindow) => { 40 | execCommand('tab:prev', focusedWindow); 41 | } 42 | }, 43 | { 44 | label: 'Next', 45 | accelerator: commandKeys['tab:next'], 46 | click: (item, focusedWindow) => { 47 | execCommand('tab:next', focusedWindow); 48 | } 49 | }, 50 | { 51 | type: 'separator' 52 | }, 53 | ...tabJump 54 | ] 55 | }, 56 | { 57 | type: 'separator' 58 | }, 59 | { 60 | label: 'Select Pane', 61 | submenu: [ 62 | { 63 | label: 'Previous', 64 | accelerator: commandKeys['pane:prev'], 65 | click: (item, focusedWindow) => { 66 | execCommand('pane:prev', focusedWindow); 67 | } 68 | }, 69 | { 70 | label: 'Next', 71 | accelerator: commandKeys['pane:next'], 72 | click: (item, focusedWindow) => { 73 | execCommand('pane:next', focusedWindow); 74 | } 75 | } 76 | ] 77 | }, 78 | { 79 | type: 'separator' 80 | }, 81 | { 82 | role: 'front' 83 | }, 84 | { 85 | label: 'Toggle Always on Top', 86 | click: (item, focusedWindow) => { 87 | execCommand('window:toggleKeepOnTop', focusedWindow); 88 | } 89 | }, 90 | { 91 | role: 'togglefullscreen', 92 | accelerator: commandKeys['window:toggleFullScreen'] 93 | } 94 | ] 95 | }; 96 | }; 97 | 98 | export default windowMenu; 99 | -------------------------------------------------------------------------------- /app/notifications.ts: -------------------------------------------------------------------------------- 1 | import type {BrowserWindow} from 'electron'; 2 | 3 | import fetch from 'electron-fetch'; 4 | import ms from 'ms'; 5 | 6 | import {version} from './package.json'; 7 | 8 | const NEWS_URL = 'https://hyper-news.now.sh'; 9 | 10 | export default function fetchNotifications(win: BrowserWindow) { 11 | const {rpc} = win; 12 | const retry = (err?: Error) => { 13 | setTimeout(() => fetchNotifications(win), ms('30m')); 14 | if (err) { 15 | console.error('Notification messages fetch error', err.stack); 16 | } 17 | }; 18 | console.log('Checking for notification messages'); 19 | fetch(NEWS_URL, { 20 | headers: { 21 | 'X-Hyper-Version': version, 22 | 'X-Hyper-Platform': process.platform 23 | } 24 | }) 25 | .then((res) => res.json()) 26 | .then((data) => { 27 | const message: {text: string; url: string; dismissable: boolean} | '' = data.message || ''; 28 | if (typeof message !== 'object' && message !== '') { 29 | throw new Error('Bad response'); 30 | } 31 | if (message === '') { 32 | console.log('No matching notification messages'); 33 | } else { 34 | rpc.emit('add notification', message); 35 | } 36 | 37 | retry(); 38 | }) 39 | .catch(retry); 40 | } 41 | -------------------------------------------------------------------------------- /app/notify.ts: -------------------------------------------------------------------------------- 1 | import {app, Notification} from 'electron'; 2 | 3 | import {icon} from './config/paths'; 4 | 5 | export default function notify(title: string, body = '', details: {error?: any} = {}) { 6 | console.log(`[Notification] ${title}: ${body}`); 7 | if (details.error) { 8 | console.error(details.error); 9 | } 10 | if (app.isReady()) { 11 | _createNotification(title, body); 12 | } else { 13 | app.on('ready', () => { 14 | _createNotification(title, body); 15 | }); 16 | } 17 | } 18 | 19 | const _createNotification = (title: string, body: string) => { 20 | new Notification({title, body, ...(process.platform === 'linux' && {icon})}).show(); 21 | }; 22 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyper", 3 | "productName": "Hyper", 4 | "description": "A terminal built on web technologies", 5 | "version": "4.0.0-canary.5", 6 | "license": "MIT", 7 | "author": { 8 | "name": "ZEIT, Inc.", 9 | "email": "team@zeit.co" 10 | }, 11 | "repository": "zeit/hyper", 12 | "scripts": { 13 | "postinstall": "npx patch-package" 14 | }, 15 | "dependencies": { 16 | "@babel/parser": "7.24.4", 17 | "@electron/remote": "2.1.2", 18 | "ast-types": "^0.16.1", 19 | "async-retry": "1.3.3", 20 | "chokidar": "^3.6.0", 21 | "color": "4.2.3", 22 | "default-shell": "1.0.1", 23 | "electron-devtools-installer": "3.2.0", 24 | "electron-fetch": "1.9.1", 25 | "electron-is-dev": "2.0.0", 26 | "electron-store": "8.2.0", 27 | "fs-extra": "11.2.0", 28 | "git-describe": "4.1.1", 29 | "lodash": "4.17.21", 30 | "ms": "2.1.3", 31 | "native-process-working-directory": "^1.0.2", 32 | "node-pty": "1.0.0", 33 | "os-locale": "5.0.0", 34 | "parse-url": "8.1.0", 35 | "queue": "6.0.2", 36 | "react": "18.2.0", 37 | "react-dom": "18.2.0", 38 | "recast": "0.23.6", 39 | "semver": "7.6.0", 40 | "shell-env": "3.0.1", 41 | "sudo-prompt": "^9.2.1", 42 | "uuid": "9.0.1" 43 | }, 44 | "optionalDependencies": { 45 | "native-reg": "1.1.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/patches/node-pty+1.0.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/node-pty/src/win/conpty.cc b/node_modules/node-pty/src/win/conpty.cc 2 | index 47af75c..884d542 100644 3 | --- a/node_modules/node-pty/src/win/conpty.cc 4 | +++ b/node_modules/node-pty/src/win/conpty.cc 5 | @@ -472,10 +472,6 @@ static NAN_METHOD(PtyKill) { 6 | } 7 | } 8 | 9 | - DisconnectNamedPipe(handle->hIn); 10 | - DisconnectNamedPipe(handle->hOut); 11 | - CloseHandle(handle->hIn); 12 | - CloseHandle(handle->hOut); 13 | CloseHandle(handle->hShell); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /app/plugins/extensions.ts: -------------------------------------------------------------------------------- 1 | export const availableExtensions = new Set([ 2 | 'onApp', 3 | 'onWindowClass', 4 | 'decorateWindowClass', 5 | 'onWindow', 6 | 'onRendererWindow', 7 | 'onUnload', 8 | 'decorateSessionClass', 9 | 'decorateSessionOptions', 10 | 'middleware', 11 | 'reduceUI', 12 | 'reduceSessions', 13 | 'reduceTermGroups', 14 | 'decorateBrowserOptions', 15 | 'decorateMenu', 16 | 'decorateTerm', 17 | 'decorateHyper', 18 | 'decorateHyperTerm', // for backwards compatibility with hyperterm 19 | 'decorateHeader', 20 | 'decorateTerms', 21 | 'decorateTab', 22 | 'decorateNotification', 23 | 'decorateNotifications', 24 | 'decorateTabs', 25 | 'decorateConfig', 26 | 'decorateKeymaps', 27 | 'decorateEnv', 28 | 'decorateTermGroup', 29 | 'decorateSplitPane', 30 | 'getTermProps', 31 | 'getTabProps', 32 | 'getTabsProps', 33 | 'getTermGroupProps', 34 | 'mapHyperTermState', 35 | 'mapTermsState', 36 | 'mapHeaderState', 37 | 'mapNotificationsState', 38 | 'mapHyperTermDispatch', 39 | 'mapTermsDispatch', 40 | 'mapHeaderDispatch', 41 | 'mapNotificationsDispatch' 42 | ]); 43 | -------------------------------------------------------------------------------- /app/plugins/install.ts: -------------------------------------------------------------------------------- 1 | import cp from 'child_process'; 2 | 3 | import ms from 'ms'; 4 | import queue from 'queue'; 5 | 6 | import {yarn, plugs} from '../config/paths'; 7 | 8 | export const install = (fn: (err: string | null) => void) => { 9 | const spawnQueue = queue({concurrency: 1}); 10 | function yarnFn(args: string[], cb: (err: string | null) => void) { 11 | const env = { 12 | NODE_ENV: 'production', 13 | ELECTRON_RUN_AS_NODE: 'true' 14 | }; 15 | spawnQueue.push((end) => { 16 | const cmd = [process.execPath, yarn].concat(args).join(' '); 17 | console.log('Launching yarn:', cmd); 18 | 19 | cp.execFile( 20 | process.execPath, 21 | [yarn].concat(args), 22 | { 23 | cwd: plugs.base, 24 | env, 25 | timeout: ms('5m'), 26 | maxBuffer: 1024 * 1024 27 | }, 28 | (err, stdout, stderr) => { 29 | if (err) { 30 | cb(stderr); 31 | } else { 32 | cb(null); 33 | } 34 | end?.(); 35 | spawnQueue.start(); 36 | } 37 | ); 38 | }); 39 | 40 | spawnQueue.start(); 41 | } 42 | 43 | yarnFn(['install', '--no-emoji', '--no-lockfile', '--cache-folder', plugs.cache], (err) => { 44 | if (err) { 45 | return fn(err); 46 | } 47 | fn(null); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /app/rpc.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | 3 | import {ipcMain} from 'electron'; 4 | import type {BrowserWindow, IpcMainEvent} from 'electron'; 5 | 6 | import {v4 as uuidv4} from 'uuid'; 7 | 8 | import type {TypedEmitter, MainEvents, RendererEvents, FilterNever} from '../typings/common'; 9 | 10 | export class Server { 11 | emitter: TypedEmitter; 12 | destroyed = false; 13 | win: BrowserWindow; 14 | id!: string; 15 | 16 | constructor(win: BrowserWindow) { 17 | this.emitter = new EventEmitter(); 18 | this.win = win; 19 | this.emit = this.emit.bind(this); 20 | 21 | if (this.destroyed) { 22 | return; 23 | } 24 | 25 | const uid = uuidv4(); 26 | this.id = uid; 27 | 28 | ipcMain.on(uid, this.ipcListener); 29 | 30 | // we intentionally subscribe to `on` instead of `once` 31 | // to support reloading the window and re-initializing 32 | // the channel 33 | this.wc.on('did-finish-load', () => { 34 | this.wc.send('init', uid, win.profileName); 35 | }); 36 | } 37 | 38 | get wc() { 39 | return this.win.webContents; 40 | } 41 | 42 | ipcListener = (event: IpcMainEvent, {ev, data}: {ev: U; data: MainEvents[U]}) => 43 | this.emitter.emit(ev, data); 44 | 45 | on = (ev: U, fn: (arg0: MainEvents[U]) => void) => { 46 | this.emitter.on(ev, fn); 47 | return this; 48 | }; 49 | 50 | once = (ev: U, fn: (arg0: MainEvents[U]) => void) => { 51 | this.emitter.once(ev, fn); 52 | return this; 53 | }; 54 | 55 | emit>>(ch: U): boolean; 56 | emit>(ch: U, data: RendererEvents[U]): boolean; 57 | emit(ch: U, data?: RendererEvents[U]) { 58 | // This check is needed because data-batching can cause extra data to be 59 | // emitted after the window has already closed 60 | if (!this.win.isDestroyed()) { 61 | this.wc.send(this.id, {ch, data}); 62 | return true; 63 | } 64 | return false; 65 | } 66 | 67 | destroy() { 68 | this.emitter.removeAllListeners(); 69 | this.wc.removeAllListeners(); 70 | if (this.id) { 71 | ipcMain.removeListener(this.id, this.ipcListener); 72 | } else { 73 | // mark for `genUid` in constructor 74 | this.destroyed = true; 75 | } 76 | } 77 | } 78 | 79 | const createRPC = (win: BrowserWindow) => { 80 | return new Server(win); 81 | }; 82 | 83 | export default createRPC; 84 | -------------------------------------------------------------------------------- /app/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/hyper/2a7bb18259d975f27b30b502af1be7576f6f5656/app/static/icon.png -------------------------------------------------------------------------------- /app/static/icon96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/hyper/2a7bb18259d975f27b30b502af1be7576f6f5656/app/static/icon96x96.png -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "declarationDir": "../dist/tmp/appdts/", 5 | "outDir": "../target/", 6 | "noImplicitAny": false 7 | }, 8 | "include": [ 9 | "./**/*", 10 | "./package.json", 11 | "../typings/extend-electron.d.ts", 12 | "../typings/ext-modules.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /app/ui/contextmenu.ts: -------------------------------------------------------------------------------- 1 | import type {MenuItemConstructorOptions, BrowserWindow} from 'electron'; 2 | 3 | import {execCommand} from '../commands'; 4 | import {getProfiles} from '../config'; 5 | import editMenu from '../menus/menus/edit'; 6 | import shellMenu from '../menus/menus/shell'; 7 | import {getDecoratedKeymaps} from '../plugins'; 8 | 9 | const separator: MenuItemConstructorOptions = {type: 'separator'}; 10 | 11 | const getCommandKeys = (keymaps: Record): Record => 12 | Object.keys(keymaps).reduce((commandKeys: Record, command) => { 13 | return Object.assign(commandKeys, { 14 | [command]: keymaps[command][0] 15 | }); 16 | }, {}); 17 | 18 | // only display cut/copy when there's a cursor selection 19 | const filterCutCopy = (selection: string, menuItem: MenuItemConstructorOptions) => { 20 | if (/^cut$|^copy$/.test(menuItem.role!) && !selection) { 21 | return; 22 | } 23 | return menuItem; 24 | }; 25 | 26 | const contextMenuTemplate = ( 27 | createWindow: (fn?: (win: BrowserWindow) => void, options?: Record) => BrowserWindow, 28 | selection: string 29 | ) => { 30 | const commandKeys = getCommandKeys(getDecoratedKeymaps()); 31 | const _shell = shellMenu( 32 | commandKeys, 33 | execCommand, 34 | getProfiles().map((p) => p.name) 35 | ).submenu as MenuItemConstructorOptions[]; 36 | const _edit = editMenu(commandKeys, execCommand).submenu.filter(filterCutCopy.bind(null, selection)); 37 | return _edit 38 | .concat(separator, _shell) 39 | .filter((menuItem) => !Object.prototype.hasOwnProperty.call(menuItem, 'enabled') || menuItem.enabled); 40 | }; 41 | 42 | export default contextMenuTemplate; 43 | -------------------------------------------------------------------------------- /app/updater.ts: -------------------------------------------------------------------------------- 1 | // Packages 2 | import electron, {app} from 'electron'; 3 | import type {BrowserWindow, AutoUpdater} from 'electron'; 4 | 5 | import retry from 'async-retry'; 6 | import ms from 'ms'; 7 | 8 | // Utilities 9 | import autoUpdaterLinux from './auto-updater-linux'; 10 | import {getDefaultProfile} from './config'; 11 | import {version} from './package.json'; 12 | import {getDecoratedConfig} from './plugins'; 13 | 14 | const {platform} = process; 15 | const isLinux = platform === 'linux'; 16 | 17 | const autoUpdater: AutoUpdater = isLinux ? autoUpdaterLinux : electron.autoUpdater; 18 | 19 | const getDecoratedConfigWithRetry = async () => { 20 | return await retry(() => { 21 | const content = getDecoratedConfig(getDefaultProfile()); 22 | if (!content) { 23 | throw new Error('No config content loaded'); 24 | } 25 | return content; 26 | }); 27 | }; 28 | 29 | const checkForUpdates = async () => { 30 | const config = await getDecoratedConfigWithRetry(); 31 | if (!config.disableAutoUpdates) { 32 | autoUpdater.checkForUpdates(); 33 | } 34 | }; 35 | 36 | let isInit = false; 37 | // Default to the "stable" update channel 38 | let canaryUpdates = false; 39 | 40 | const buildFeedUrl = (canary: boolean, currentVersion: string) => { 41 | const updatePrefix = canary ? 'releases-canary' : 'releases'; 42 | const archSuffix = process.arch === 'arm64' || app.runningUnderARM64Translation ? '_arm64' : ''; 43 | return `https://${updatePrefix}.hyper.is/update/${isLinux ? 'deb' : platform}${archSuffix}/${currentVersion}`; 44 | }; 45 | 46 | const isCanary = (updateChannel: string) => updateChannel === 'canary'; 47 | 48 | async function init() { 49 | autoUpdater.on('error', (err) => { 50 | console.error('Error fetching updates', `${err.message} (${err.stack})`); 51 | }); 52 | 53 | const config = await getDecoratedConfigWithRetry(); 54 | 55 | // If defined in the config, switch to the "canary" channel 56 | if (config.updateChannel && isCanary(config.updateChannel)) { 57 | canaryUpdates = true; 58 | } 59 | 60 | const feedURL = buildFeedUrl(canaryUpdates, version); 61 | 62 | autoUpdater.setFeedURL({url: feedURL}); 63 | 64 | setTimeout(() => { 65 | void checkForUpdates(); 66 | }, ms('10s')); 67 | 68 | setInterval(() => { 69 | void checkForUpdates(); 70 | }, ms('30m')); 71 | 72 | isInit = true; 73 | } 74 | 75 | const updater = (win: BrowserWindow) => { 76 | if (!isInit) { 77 | void init(); 78 | } 79 | 80 | const {rpc} = win; 81 | 82 | const onupdate = (ev: Event, releaseNotes: string, releaseName: string, date: Date, updateUrl: string) => { 83 | const releaseUrl = updateUrl || `https://github.com/vercel/hyper/releases/tag/${releaseName}`; 84 | rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !isLinux}); 85 | }; 86 | 87 | if (isLinux) { 88 | autoUpdater.on('update-available', onupdate); 89 | } else { 90 | autoUpdater.on('update-downloaded', onupdate); 91 | } 92 | 93 | rpc.once('quit and install', () => { 94 | autoUpdater.quitAndInstall(); 95 | }); 96 | 97 | app.config.subscribe(async () => { 98 | const {updateChannel} = await getDecoratedConfigWithRetry(); 99 | const newUpdateIsCanary = isCanary(updateChannel); 100 | 101 | if (newUpdateIsCanary !== canaryUpdates) { 102 | const feedURL = buildFeedUrl(newUpdateIsCanary, version); 103 | 104 | autoUpdater.setFeedURL({url: feedURL}); 105 | void checkForUpdates(); 106 | 107 | canaryUpdates = newUpdateIsCanary; 108 | } 109 | }); 110 | 111 | win.on('close', () => { 112 | if (isLinux) { 113 | autoUpdater.removeListener('update-available', onupdate); 114 | } else { 115 | autoUpdater.removeListener('update-downloaded', onupdate); 116 | } 117 | }); 118 | }; 119 | 120 | export default updater; 121 | -------------------------------------------------------------------------------- /app/utils/colors.ts: -------------------------------------------------------------------------------- 1 | const colorList = [ 2 | 'black', 3 | 'red', 4 | 'green', 5 | 'yellow', 6 | 'blue', 7 | 'magenta', 8 | 'cyan', 9 | 'white', 10 | 'lightBlack', 11 | 'lightRed', 12 | 'lightGreen', 13 | 'lightYellow', 14 | 'lightBlue', 15 | 'lightMagenta', 16 | 'lightCyan', 17 | 'lightWhite', 18 | 'colorCubes', 19 | 'grayscale' 20 | ]; 21 | 22 | export const getColorMap: { 23 | (colors: T): T extends (infer U)[] ? {[k: string]: U} : T; 24 | } = (colors) => { 25 | if (!Array.isArray(colors)) { 26 | return colors; 27 | } 28 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 29 | return colors.reduce((result, color, index) => { 30 | if (index < colorList.length) { 31 | result[colorList[index]] = color; 32 | } 33 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 34 | return result; 35 | }, {}); 36 | }; 37 | -------------------------------------------------------------------------------- /app/utils/map-keys.ts: -------------------------------------------------------------------------------- 1 | const generatePrefixedCommand = (command: string, shortcuts: string[]) => { 2 | const result: Record = {}; 3 | const baseCmd = command.replace(/:prefix$/, ''); 4 | for (let i = 1; i <= 9; i++) { 5 | // 9 is a special number because it means 'last' 6 | const index = i === 9 ? 'last' : i; 7 | const prefixedShortcuts = shortcuts.map((shortcut) => `${shortcut}+${i}`); 8 | result[`${baseCmd}:${index}`] = prefixedShortcuts; 9 | } 10 | 11 | return result; 12 | }; 13 | 14 | const mapKeys = (config: Record) => { 15 | return Object.keys(config).reduce((keymap: Record, command: string) => { 16 | if (!command) { 17 | return keymap; 18 | } 19 | // We can have different keys for a same command. 20 | const _shortcuts = config[command]; 21 | const shortcuts = Array.isArray(_shortcuts) ? _shortcuts : [_shortcuts]; 22 | const fixedShortcuts: string[] = []; 23 | shortcuts.forEach((shortcut) => { 24 | let newShortcut = shortcut; 25 | if (newShortcut.indexOf('cmd') !== -1) { 26 | // Mousetrap use `command` and not `cmd` 27 | console.warn('Your config use deprecated `cmd` in key combination. Please use `command` instead.'); 28 | newShortcut = newShortcut.replace('cmd', 'command'); 29 | } 30 | fixedShortcuts.push(newShortcut); 31 | }); 32 | 33 | if (command.endsWith(':prefix')) { 34 | return Object.assign(keymap, generatePrefixedCommand(command, fixedShortcuts)); 35 | } 36 | 37 | keymap[command] = fixedShortcuts; 38 | 39 | return keymap; 40 | }, {}); 41 | }; 42 | 43 | export default mapKeys; 44 | -------------------------------------------------------------------------------- /app/utils/renderer-utils.ts: -------------------------------------------------------------------------------- 1 | const rendererTypes: Record = {}; 2 | 3 | function getRendererTypes() { 4 | return rendererTypes; 5 | } 6 | 7 | function setRendererType(uid: string, type: string) { 8 | rendererTypes[uid] = type; 9 | } 10 | 11 | function unsetRendererType(uid: string) { 12 | delete rendererTypes[uid]; 13 | } 14 | 15 | export {getRendererTypes, setRendererType, unsetRendererType}; 16 | -------------------------------------------------------------------------------- /app/utils/shell-fallback.ts: -------------------------------------------------------------------------------- 1 | export const getFallBackShellConfig = ( 2 | shell: string, 3 | shellArgs: string[], 4 | defaultShell: string, 5 | defaultShellArgs: string[] 6 | ): { 7 | shell: string; 8 | shellArgs: string[]; 9 | } | null => { 10 | if (shellArgs.length > 0) { 11 | return { 12 | shell, 13 | shellArgs: [] 14 | }; 15 | } 16 | 17 | if (shell != defaultShell) { 18 | return { 19 | shell: defaultShell, 20 | shellArgs: defaultShellArgs 21 | }; 22 | } 23 | 24 | return null; 25 | }; 26 | -------------------------------------------------------------------------------- /app/utils/system-context-menu.ts: -------------------------------------------------------------------------------- 1 | import * as Registry from 'native-reg'; 2 | import type {HKEY} from 'native-reg'; 3 | 4 | const appPath = `"${process.execPath}"`; 5 | const regKeys = [ 6 | `Software\\Classes\\Directory\\Background\\shell\\Hyper`, 7 | `Software\\Classes\\Directory\\shell\\Hyper`, 8 | `Software\\Classes\\Drive\\shell\\Hyper` 9 | ]; 10 | const regParts = [ 11 | {key: 'command', name: '', value: `${appPath} "%V"`}, 12 | {name: '', value: 'Open &Hyper here'}, 13 | {name: 'Icon', value: `${appPath}`} 14 | ]; 15 | 16 | function addValues(hyperKey: HKEY, commandKey: HKEY) { 17 | try { 18 | Registry.setValueSZ(hyperKey, regParts[1].name, regParts[1].value); 19 | } catch (error) { 20 | console.error(error); 21 | } 22 | try { 23 | Registry.setValueSZ(hyperKey, regParts[2].name, regParts[2].value); 24 | } catch (err) { 25 | console.error(err); 26 | } 27 | try { 28 | Registry.setValueSZ(commandKey, regParts[0].name, regParts[0].value); 29 | } catch (err_) { 30 | console.error(err_); 31 | } 32 | } 33 | 34 | export const add = () => { 35 | regKeys.forEach((regKey) => { 36 | try { 37 | const hyperKey = 38 | Registry.openKey(Registry.HKCU, regKey, Registry.Access.ALL_ACCESS) || 39 | Registry.createKey(Registry.HKCU, regKey, Registry.Access.ALL_ACCESS); 40 | const commandKey = 41 | Registry.openKey(Registry.HKCU, `${regKey}\\${regParts[0].key}`, Registry.Access.ALL_ACCESS) || 42 | Registry.createKey(Registry.HKCU, `${regKey}\\${regParts[0].key}`, Registry.Access.ALL_ACCESS); 43 | addValues(hyperKey, commandKey); 44 | Registry.closeKey(hyperKey); 45 | Registry.closeKey(commandKey); 46 | } catch (error) { 47 | console.error(error); 48 | } 49 | }); 50 | }; 51 | 52 | export const remove = () => { 53 | regKeys.forEach((regKey) => { 54 | try { 55 | Registry.deleteTree(Registry.HKCU, regKey); 56 | } catch (err) { 57 | console.error(err); 58 | } 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /app/utils/to-electron-background-color.ts: -------------------------------------------------------------------------------- 1 | // Packages 2 | import Color from 'color'; 3 | 4 | // returns a background color that's in hex 5 | // format including the alpha channel (e.g.: `#00000050`) 6 | // input can be any css value (rgb, hsl, string…) 7 | const toElectronBackgroundColor = (bgColor: string) => { 8 | const color = Color(bgColor); 9 | 10 | if (color.alpha() === 1) { 11 | return color.hex().toString(); 12 | } 13 | 14 | // http://stackoverflow.com/a/11019879/1202488 15 | const alphaHex = Math.round(color.alpha() * 255).toString(16); 16 | return `#${alphaHex}${color.hex().toString().slice(1)}`; 17 | }; 18 | 19 | export default toElectronBackgroundColor; 20 | -------------------------------------------------------------------------------- /app/utils/window-utils.ts: -------------------------------------------------------------------------------- 1 | import electron from 'electron'; 2 | 3 | export function positionIsValid(position: [number, number]) { 4 | const displays = electron.screen.getAllDisplays(); 5 | const [x, y] = position; 6 | 7 | return displays.some(({workArea}) => { 8 | return x >= workArea.x && x <= workArea.x + workArea.width && y >= workArea.y && y <= workArea.y + workArea.height; 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /assets/icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | close tab 5 | 6 | 7 | 8 | hamburger menu 9 | 10 | 11 | 12 | 13 | 14 | minimize window 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | maximize window 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | restore window 39 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | close window 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /ava-e2e.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | files: ['test/*'], 3 | extensions: ['ts'], 4 | require: ['ts-node/register/transpile-only'], 5 | timeout: '30s' 6 | }; 7 | -------------------------------------------------------------------------------- /ava.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | files: ['test/unit/*'], 3 | extensions: ['ts'], 4 | require: ['ts-node/register/transpile-only'] 5 | }; 6 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/react", 4 | "@babel/typescript" 5 | ], 6 | "plugins": [ 7 | [ 8 | "styled-jsx/babel", 9 | { 10 | "vendorPrefixes": false 11 | } 12 | ], 13 | "@babel/plugin-proposal-numeric-separator", 14 | "@babel/proposal-class-properties", 15 | "@babel/proposal-object-rest-spread", 16 | "@babel/plugin-proposal-optional-chaining" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /bin/cp-snapshot.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const {Arch} = require('electron-builder'); 4 | 5 | function copySnapshot(pathToElectron, archToCopy) { 6 | const snapshotFileName = 'snapshot_blob.bin'; 7 | const v8ContextFileName = getV8ContextFileName(archToCopy); 8 | const pathToBlob = path.resolve(__dirname, '..', 'cache', archToCopy, snapshotFileName); 9 | const pathToBlobV8 = path.resolve(__dirname, '..', 'cache', archToCopy, v8ContextFileName); 10 | 11 | console.log('Copying v8 snapshots from', pathToBlob, 'to', pathToElectron); 12 | fs.copyFileSync(pathToBlob, path.join(pathToElectron, snapshotFileName)); 13 | fs.copyFileSync(pathToBlobV8, path.join(pathToElectron, v8ContextFileName)); 14 | } 15 | 16 | function getPathToElectron() { 17 | switch (process.platform) { 18 | case 'darwin': 19 | return path.resolve( 20 | __dirname, 21 | '..', 22 | 'node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources' 23 | ); 24 | case 'win32': 25 | case 'linux': 26 | return path.resolve(__dirname, '..', 'node_modules', 'electron', 'dist'); 27 | } 28 | } 29 | 30 | function getV8ContextFileName(archToCopy) { 31 | if (process.platform === 'darwin') { 32 | return `v8_context_snapshot${archToCopy === 'arm64' ? '.arm64' : '.x86_64'}.bin`; 33 | } else { 34 | return `v8_context_snapshot.bin`; 35 | } 36 | } 37 | 38 | exports.default = async (context) => { 39 | const archToCopy = Arch[context.arch]; 40 | const pathToElectron = 41 | process.platform === 'darwin' 42 | ? `${context.appOutDir}/Hyper.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources` 43 | : context.appOutDir; 44 | copySnapshot(pathToElectron, archToCopy); 45 | }; 46 | 47 | if (require.main === module) { 48 | const archToCopy = process.env.npm_config_arch; 49 | const pathToElectron = getPathToElectron(); 50 | if ((process.arch.startsWith('arm') ? 'arm64' : 'x64') === archToCopy) { 51 | copySnapshot(pathToElectron, archToCopy); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /bin/mk-snapshot.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process'); 2 | const vm = require('vm'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const electronLink = require('electron-link'); 6 | const {mkdirp} = require('fs-extra'); 7 | 8 | const excludedModules = {}; 9 | 10 | const crossArchDirs = ['clang_x86_v8_arm', 'clang_x64_v8_arm64', 'win_clang_x64']; 11 | 12 | async function main() { 13 | const baseDirPath = path.resolve(__dirname, '..'); 14 | 15 | console.log('Creating a linked script..'); 16 | const result = await electronLink({ 17 | baseDirPath: baseDirPath, 18 | mainPath: `${__dirname}/snapshot-libs.js`, 19 | cachePath: `${baseDirPath}/cache`, 20 | // eslint-disable-next-line no-prototype-builtins 21 | shouldExcludeModule: (modulePath) => excludedModules.hasOwnProperty(modulePath) 22 | }); 23 | 24 | const snapshotScriptPath = `${baseDirPath}/cache/snapshot-libs.js`; 25 | fs.writeFileSync(snapshotScriptPath, result.snapshotScript); 26 | 27 | // Verify if we will be able to use this in `mksnapshot` 28 | vm.runInNewContext(result.snapshotScript, undefined, {filename: snapshotScriptPath, displayErrors: true}); 29 | 30 | const outputBlobPath = `${baseDirPath}/cache/${process.env.npm_config_arch}`; 31 | await mkdirp(outputBlobPath); 32 | 33 | if (process.platform !== 'darwin') { 34 | const mksnapshotBinPath = `${baseDirPath}/node_modules/electron-mksnapshot/bin`; 35 | const matchingDirs = crossArchDirs.map((dir) => `${mksnapshotBinPath}/${dir}`).filter((dir) => fs.existsSync(dir)); 36 | for (const dir of matchingDirs) { 37 | if (fs.existsSync(`${mksnapshotBinPath}/gen/v8/embedded.S`)) { 38 | await mkdirp(`${dir}/gen/v8`); 39 | fs.copyFileSync(`${mksnapshotBinPath}/gen/v8/embedded.S`, `${dir}/gen/v8/embedded.S`); 40 | } 41 | } 42 | } 43 | 44 | console.log(`Generating startup blob in "${outputBlobPath}"`); 45 | childProcess.execFileSync( 46 | path.resolve(__dirname, '..', 'node_modules', '.bin', 'mksnapshot' + (process.platform === 'win32' ? '.cmd' : '')), 47 | [snapshotScriptPath, '--output_dir', outputBlobPath] 48 | ); 49 | } 50 | 51 | main().catch((err) => console.error(err)); 52 | -------------------------------------------------------------------------------- /bin/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require("@electron/notarize"); 2 | 3 | exports.default = async function notarizing(context) { 4 | const { electronPlatformName, appOutDir } = context; 5 | if (electronPlatformName !== "darwin" || !process.env.APPLE_ID || !process.env.APPLE_PASSWORD) { 6 | return; 7 | } 8 | 9 | const appName = context.packager.appInfo.productFilename; 10 | return await notarize({ 11 | appBundleId: "co.zeit.hyper", 12 | appPath: `${appOutDir}/${appName}.app`, 13 | appleId: process.env.APPLE_ID, 14 | appleIdPassword: process.env.APPLE_PASSWORD 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /bin/snapshot-libs.js: -------------------------------------------------------------------------------- 1 | require('color-convert'); 2 | require('color-string'); 3 | require('columnify'); 4 | require('lodash'); 5 | require('ms'); 6 | require('normalize-url'); 7 | require('parse-url'); 8 | require('php-escape-shell'); 9 | require('plist'); 10 | require('redux-thunk'); 11 | require('redux'); 12 | require('reselect'); 13 | require('seamless-immutable'); 14 | require('stylis'); 15 | require('xterm-addon-unicode11'); 16 | // eslint-disable-next-line no-constant-condition 17 | if (false) { 18 | require('args'); 19 | require('mousetrap'); 20 | require('open'); 21 | require('react-dom'); 22 | require('react-redux'); 23 | require('react'); 24 | require('xterm-addon-fit'); 25 | require('xterm-addon-image'); 26 | require('xterm-addon-search'); 27 | require('xterm-addon-web-links'); 28 | require('xterm-addon-webgl'); 29 | require('xterm-addon-canvas'); 30 | require('xterm'); 31 | } 32 | -------------------------------------------------------------------------------- /build/canary.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/hyper/2a7bb18259d975f27b30b502af1be7576f6f5656/build/canary.icns -------------------------------------------------------------------------------- /build/canary.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/hyper/2a7bb18259d975f27b30b502af1be7576f6f5656/build/canary.ico -------------------------------------------------------------------------------- /build/icon.fig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/hyper/2a7bb18259d975f27b30b502af1be7576f6f5656/build/icon.fig -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/hyper/2a7bb18259d975f27b30b502af1be7576f6f5656/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/hyper/2a7bb18259d975f27b30b502af1be7576f6f5656/build/icon.ico -------------------------------------------------------------------------------- /build/linux/after-install.tpl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /usr/local/bin 4 | 5 | # Link to the CLI bootstrap 6 | ln -sf '/opt/${productFilename}/resources/bin/${executable}' '/usr/local/bin/${executable}' 7 | -------------------------------------------------------------------------------- /build/linux/hyper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/linux/bin/code.sh 3 | 4 | # If root, ensure that --user-data-dir is specified 5 | if [ "$(id -u)" = "0" ]; then 6 | for i in $@ 7 | do 8 | if [[ $i == --user-data-dir=* ]]; then 9 | DATA_DIR_SET=1 10 | fi 11 | done 12 | if [ -z $DATA_DIR_SET ]; then 13 | echo "It is recommended to start hyper as a normal user. To run as root, you must specify an alternate user data directory with the --user-data-dir argument." 1>&2 14 | exit 1 15 | fi 16 | fi 17 | 18 | if [ ! -L $0 ]; then 19 | # if path is not a symlink, find relatively 20 | HYPER_PATH="$(dirname $0)/../.." 21 | else 22 | if which readlink >/dev/null; then 23 | # if readlink exists, follow the symlink and find relatively 24 | HYPER_PATH="$(dirname $(readlink -f $0))/../.." 25 | else 26 | # else use the standard install location 27 | HYPER_PATH="/opt/Hyper" 28 | fi 29 | fi 30 | 31 | ELECTRON="$HYPER_PATH/hyper" 32 | CLI="$HYPER_PATH/resources/bin/cli.js" 33 | ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@" 34 | exit $? -------------------------------------------------------------------------------- /build/mac/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.automation.apple-events 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.cs.allow-unsigned-executable-memory 10 | 11 | com.apple.security.cs.disable-library-validation 12 | 13 | com.apple.security.device.audio-input 14 | 15 | com.apple.security.device.camera 16 | 17 | com.apple.security.personal-information.addressbook 18 | 19 | com.apple.security.personal-information.calendars 20 | 21 | com.apple.security.personal-information.location 22 | 23 | com.apple.security.personal-information.photos-library 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /build/mac/hyper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Deeply inspired by https://github.com/Microsoft/vscode/blob/1.65.2/resources/darwin/bin/code.sh 3 | 4 | # TODO: bash is deprecated on macOS and will be removed. 5 | # Port this to /bin/sh or /bin/zsh 6 | 7 | function app_realpath() { 8 | SOURCE=$1 9 | while [ -h "$SOURCE" ]; do 10 | DIR=$(dirname "$SOURCE") 11 | SOURCE=$(readlink "$SOURCE") 12 | [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE 13 | done 14 | SOURCE_DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )" 15 | echo "${SOURCE_DIR%%"${SOURCE_DIR#*.app}"}" 16 | } 17 | 18 | APP_PATH="$(app_realpath "${BASH_SOURCE[0]}")" 19 | if [ -z "$APP_PATH" ]; then 20 | echo "Unable to determine app path from symlink : ${BASH_SOURCE[0]}" 21 | exit 1 22 | fi 23 | 24 | CONTENTS="$APP_PATH/Contents" 25 | ELECTRON="$CONTENTS/MacOS/Hyper" 26 | CLI="$CONTENTS/Resources/bin/cli.js" 27 | ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@" 28 | exit $? 29 | -------------------------------------------------------------------------------- /build/win/hyper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/win/bin/code.sh 3 | 4 | NAME="Hyper" 5 | HYPER_PATH="$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")" 6 | ELECTRON="$HYPER_PATH/$NAME.exe" 7 | if grep -q Microsoft /proc/version; then 8 | echo "Warning! Due to WSL limitations, you can't use CLI commands here. Please use Hyper CLI on cmd, PowerShell or GitBash/CygWin." 9 | echo "Please see: https://github.com/Microsoft/WSL/issues/1494" 10 | echo "" 11 | # If running under WSL don't pass cli.js to Electron, as environment vars 12 | # can't be transferred from WSL to Windows. 13 | # See: https://github.com/Microsoft/BashOnWindows/issues/1363 14 | # https://github.com/Microsoft/BashOnWindows/issues/1494 15 | "$ELECTRON" "$@" 16 | exit $? 17 | fi 18 | if [ "$(expr substr $(uname -s) 1 9)" == "CYGWIN_NT" ]; then 19 | CLI=$(cygpath -m "$HYPER_PATH/resources/bin/cli.js") 20 | else 21 | CLI="$HYPER_PATH/resources/bin/cli.js" 22 | fi 23 | ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@" 24 | exit $? 25 | 26 | -------------------------------------------------------------------------------- /build/win/hyper.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | set ELECTRON_RUN_AS_NODE=1 4 | call "%~dp0..\..\Hyper.exe" "%~dp0..\..\resources\bin\cli.js" %* 5 | endlocal -------------------------------------------------------------------------------- /build/win/installer.nsh: -------------------------------------------------------------------------------- 1 | !macro customInstall 2 | WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper" "" "Open &Hyper here" 3 | WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper" "Icon" `"$appExe"` 4 | WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper\command" "" `"$appExe" "%V"` 5 | 6 | WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper" "" "Open &Hyper here" 7 | WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper" "Icon" `"$appExe"` 8 | WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper\command" "" `"$appExe" "%V"` 9 | 10 | WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper" "" "Open &Hyper here" 11 | WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper" "Icon" `"$appExe"` 12 | WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper\command" "" `"$appExe" "%V"` 13 | !macroend 14 | 15 | !macro customUnInstall 16 | DeleteRegKey HKCU "Software\Classes\Directory\Background\shell\Hyper" 17 | DeleteRegKey HKCU "Software\Classes\Directory\shell\Hyper" 18 | DeleteRegKey HKCU "Software\Classes\Drive\shell\Hyper" 19 | !macroend 20 | 21 | !macro customInstallMode 22 | StrCpy $isForceCurrentInstall "1" 23 | !macroend 24 | 25 | !macro customInit 26 | IfFileExists $LOCALAPPDATA\Hyper\Update.exe 0 +2 27 | nsExec::Exec '"$LOCALAPPDATA\Hyper\Update.exe" --uninstall -s' 28 | !macroend 29 | -------------------------------------------------------------------------------- /cli/api.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line eslint-comments/disable-enable-pair 2 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 3 | import fs from 'fs'; 4 | import os from 'os'; 5 | import path from 'path'; 6 | 7 | import got from 'got'; 8 | import registryUrlModule from 'registry-url'; 9 | 10 | const registryUrl = registryUrlModule(); 11 | 12 | // If the user defines XDG_CONFIG_HOME they definitely want their config there, 13 | // otherwise use the home directory in linux/mac and userdata in windows 14 | const applicationDirectory = process.env.XDG_CONFIG_HOME 15 | ? path.join(process.env.XDG_CONFIG_HOME, 'Hyper') 16 | : process.platform === 'win32' 17 | ? path.join(process.env.APPDATA!, 'Hyper') 18 | : path.join(os.homedir(), '.config', 'Hyper'); 19 | 20 | const devConfigFileName = path.join(__dirname, `../hyper.json`); 21 | 22 | const fileName = 23 | process.env.NODE_ENV !== 'production' && fs.existsSync(devConfigFileName) 24 | ? devConfigFileName 25 | : path.join(applicationDirectory, 'hyper.json'); 26 | 27 | /** 28 | * We need to make sure the file reading and parsing is lazy so that failure to 29 | * statically analyze the hyper configuration isn't fatal for all kinds of 30 | * subcommands. We can use memoization to make reading and parsing lazy. 31 | */ 32 | function memoize any>(fn: T): T { 33 | let hasResult = false; 34 | let result: any; 35 | return ((...args: Parameters) => { 36 | if (!hasResult) { 37 | result = fn(...args); 38 | hasResult = true; 39 | } 40 | return result; 41 | }) as T; 42 | } 43 | 44 | const getFileContents = memoize(() => { 45 | return fs.readFileSync(fileName, 'utf8'); 46 | }); 47 | 48 | const getParsedFile = memoize(() => JSON.parse(getFileContents())); 49 | 50 | const getPluginsByKey = (key: string): any[] => getParsedFile()[key] || []; 51 | 52 | const getPlugins = memoize(() => { 53 | return getPluginsByKey('plugins'); 54 | }); 55 | 56 | const getLocalPlugins = memoize(() => { 57 | return getPluginsByKey('localPlugins'); 58 | }); 59 | 60 | function exists() { 61 | return getFileContents() !== undefined; 62 | } 63 | 64 | function isInstalled(plugin: string, locally?: boolean) { 65 | const array = locally ? getLocalPlugins() : getPlugins(); 66 | if (array && Array.isArray(array)) { 67 | return array.includes(plugin); 68 | } 69 | return false; 70 | } 71 | 72 | function save(config: any) { 73 | return fs.writeFileSync(fileName, JSON.stringify(config, null, 2), 'utf8'); 74 | } 75 | 76 | function getPackageName(plugin: string) { 77 | const isScoped = plugin[0] === '@'; 78 | const nameWithoutVersion = plugin.split('#')[0]; 79 | 80 | if (isScoped) { 81 | return `@${nameWithoutVersion.split('@')[1].replace('/', '%2f')}`; 82 | } 83 | 84 | return nameWithoutVersion.split('@')[0]; 85 | } 86 | 87 | function existsOnNpm(plugin: string) { 88 | const name = getPackageName(plugin); 89 | return got 90 | .get(registryUrl + name.toLowerCase(), {timeout: {request: 10000}, responseType: 'json'}) 91 | .then((res) => { 92 | if (!res.body.versions) { 93 | return Promise.reject(res); 94 | } else { 95 | return res; 96 | } 97 | }); 98 | } 99 | 100 | function install(plugin: string, locally?: boolean) { 101 | const array = locally ? getLocalPlugins() : getPlugins(); 102 | return existsOnNpm(plugin) 103 | .catch((err: any) => { 104 | const {statusCode} = err; 105 | if (statusCode && (statusCode === 404 || statusCode === 200)) { 106 | return Promise.reject(`${plugin} not found on npm`); 107 | } 108 | return Promise.reject(`${err.message}\nPlugin check failed. Check your internet connection or retry later.`); 109 | }) 110 | .then(() => { 111 | if (isInstalled(plugin, locally)) { 112 | return Promise.reject(`${plugin} is already installed`); 113 | } 114 | 115 | const config = getParsedFile(); 116 | config[locally ? 'localPlugins' : 'plugins'] = [...array, plugin]; 117 | save(config); 118 | }); 119 | } 120 | 121 | async function uninstall(plugin: string) { 122 | if (!isInstalled(plugin)) { 123 | return Promise.reject(`${plugin} is not installed`); 124 | } 125 | 126 | const config = getParsedFile(); 127 | config.plugins = getPlugins().filter((p) => p !== plugin); 128 | save(config); 129 | } 130 | 131 | function list() { 132 | if (getPlugins().length > 0) { 133 | return getPlugins().join('\n'); 134 | } 135 | return false; 136 | } 137 | 138 | export const configPath = fileName; 139 | export {exists, existsOnNpm, isInstalled, install, uninstall, list}; 140 | -------------------------------------------------------------------------------- /electron-builder-linux-ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/electron-builder", 3 | "extends": "electron-builder.json", 4 | "afterSign": null, 5 | "npmRebuild": false 6 | } 7 | -------------------------------------------------------------------------------- /electron-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/electron-builder", 3 | "appId": "co.zeit.hyper", 4 | "afterSign": "./bin/notarize.js", 5 | "afterPack": "./bin/cp-snapshot.js", 6 | "directories": { 7 | "app": "target" 8 | }, 9 | "extraResources": [ 10 | "./bin/yarn-standalone.js", 11 | "./bin/cli.js", 12 | { 13 | "from": "./build/${os}/", 14 | "to": "./bin/", 15 | "filter": [ 16 | "hyper*" 17 | ] 18 | } 19 | ], 20 | "artifactName": "${productName}-${version}-${arch}.${ext}", 21 | "linux": { 22 | "category": "TerminalEmulator", 23 | "target": [ 24 | "deb", 25 | "AppImage", 26 | "rpm", 27 | "snap", 28 | "pacman" 29 | ] 30 | }, 31 | "win": { 32 | "target": { 33 | "target": "nsis", 34 | "arch": [ 35 | "x64", 36 | "arm64" 37 | ] 38 | }, 39 | "rfc3161TimeStampServer": "http://timestamp.comodoca.com" 40 | }, 41 | "nsis": { 42 | "include": "build/win/installer.nsh", 43 | "oneClick": false, 44 | "perMachine": false, 45 | "allowToChangeInstallationDirectory": true 46 | }, 47 | "mac": { 48 | "target": { 49 | "target": "default", 50 | "arch": [ 51 | "x64", 52 | "arm64" 53 | ] 54 | }, 55 | "artifactName": "${productName}-${version}-${os}-${arch}.${ext}", 56 | "category": "public.app-category.developer-tools", 57 | "entitlements": "./build/mac/entitlements.plist", 58 | "entitlementsInherit": "./build/mac/entitlements.plist", 59 | "extendInfo": { 60 | "CFBundleDocumentTypes": [ 61 | { 62 | "CFBundleTypeName": "Folders", 63 | "CFBundleTypeRole": "Viewer", 64 | "LSHandlerRank": "Alternate", 65 | "LSItemContentTypes": [ 66 | "public.folder", 67 | "com.apple.bundle", 68 | "com.apple.package", 69 | "com.apple.resolvable" 70 | ] 71 | }, 72 | { 73 | "CFBundleTypeName": "UnixExecutables", 74 | "CFBundleTypeRole": "Shell", 75 | "LSHandlerRank": "Alternate", 76 | "LSItemContentTypes": [ 77 | "public.unix-executable" 78 | ] 79 | } 80 | ], 81 | "NSAppleEventsUsageDescription": "An application in Hyper wants to use AppleScript.", 82 | "NSCalendarsUsageDescription": "An application in Hyper wants to access Calendar data.", 83 | "NSCameraUsageDescription": "An application in Hyper wants to use the Camera.", 84 | "NSContactsUsageDescription": "An application in Hyper wants to access your Contacts.", 85 | "NSDesktopFolderUsageDescription": "An application in Hyper wants to access the Desktop folder.", 86 | "NSDocumentsFolderUsageDescription": "An application in Hyper wants to access the Documents folder.", 87 | "NSDownloadsFolderUsageDescription": "An application in Hyper wants to access the Downloads folder.", 88 | "NSFileProviderDomainUsageDescription": "An application in Hyper wants to access files managed by a file provider.", 89 | "NSFileProviderPresenceUsageDescription": "An application in Hyper wants to be informed when other apps access files that it manages.", 90 | "NSLocationUsageDescription": "An application in Hyper wants to access your location information.", 91 | "NSMicrophoneUsageDescription": "An application in Hyper wants to use your microphone.", 92 | "NSMotionUsageDescription": "An application in Hyper wants to use the device’s accelerometer.", 93 | "NSNetworkVolumesUsageDescription": "An application in Hyper wants to access files on a network volume.", 94 | "NSPhotoLibraryUsageDescription": "An application in Hyper wants to access the photo library.", 95 | "NSRemindersUsageDescription": "An application in Hyper wants to access your reminders.", 96 | "NSRemovableVolumesUsageDescription": "An application in Hyper wants to access files on a removable volume.", 97 | "NSSpeechRecognitionUsageDescription": "An application in Hyper wants to send user data to Apple’s speech recognition servers.", 98 | "NSSystemAdministrationUsageDescription": "The operation being performed by an application in Hyper requires elevated permission." 99 | }, 100 | "darkModeSupport": true 101 | }, 102 | "deb": { 103 | "compression": "bzip2", 104 | "afterInstall": "./build/linux/after-install.tpl" 105 | }, 106 | "rpm": { 107 | "afterInstall": "./build/linux/after-install.tpl", 108 | "fpm": [ 109 | "--rpm-rpmbuild-define", 110 | "_build_id_links none" 111 | ] 112 | }, 113 | "snap": { 114 | "confinement": "classic", 115 | "publish": "github" 116 | }, 117 | "protocols": { 118 | "name": "ssh URL", 119 | "schemes": [ 120 | "ssh" 121 | ] 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/actions/config.ts: -------------------------------------------------------------------------------- 1 | import type {configOptions} from '../../typings/config'; 2 | import {CONFIG_LOAD, CONFIG_RELOAD} from '../../typings/constants/config'; 3 | import type {HyperActions} from '../../typings/hyper'; 4 | 5 | export function loadConfig(config: configOptions): HyperActions { 6 | return { 7 | type: CONFIG_LOAD, 8 | config 9 | }; 10 | } 11 | 12 | export function reloadConfig(config: configOptions): HyperActions { 13 | const now = Date.now(); 14 | return { 15 | type: CONFIG_RELOAD, 16 | config, 17 | now 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /lib/actions/header.ts: -------------------------------------------------------------------------------- 1 | import {CLOSE_TAB, CHANGE_TAB} from '../../typings/constants/tabs'; 2 | import { 3 | UI_WINDOW_MAXIMIZE, 4 | UI_WINDOW_UNMAXIMIZE, 5 | UI_OPEN_HAMBURGER_MENU, 6 | UI_WINDOW_MINIMIZE, 7 | UI_WINDOW_CLOSE 8 | } from '../../typings/constants/ui'; 9 | import type {HyperDispatch} from '../../typings/hyper'; 10 | import rpc from '../rpc'; 11 | 12 | import {userExitTermGroup, setActiveGroup} from './term-groups'; 13 | 14 | export function closeTab(uid: string) { 15 | return (dispatch: HyperDispatch) => { 16 | dispatch({ 17 | type: CLOSE_TAB, 18 | uid, 19 | effect() { 20 | dispatch(userExitTermGroup(uid)); 21 | } 22 | }); 23 | }; 24 | } 25 | 26 | export function changeTab(uid: string) { 27 | return (dispatch: HyperDispatch) => { 28 | dispatch({ 29 | type: CHANGE_TAB, 30 | uid, 31 | effect() { 32 | dispatch(setActiveGroup(uid)); 33 | } 34 | }); 35 | }; 36 | } 37 | 38 | export function maximize() { 39 | return (dispatch: HyperDispatch) => { 40 | dispatch({ 41 | type: UI_WINDOW_MAXIMIZE, 42 | effect() { 43 | rpc.emit('maximize'); 44 | } 45 | }); 46 | }; 47 | } 48 | 49 | export function unmaximize() { 50 | return (dispatch: HyperDispatch) => { 51 | dispatch({ 52 | type: UI_WINDOW_UNMAXIMIZE, 53 | effect() { 54 | rpc.emit('unmaximize'); 55 | } 56 | }); 57 | }; 58 | } 59 | 60 | export function openHamburgerMenu(coordinates: {x: number; y: number}) { 61 | return (dispatch: HyperDispatch) => { 62 | dispatch({ 63 | type: UI_OPEN_HAMBURGER_MENU, 64 | effect() { 65 | rpc.emit('open hamburger menu', coordinates); 66 | } 67 | }); 68 | }; 69 | } 70 | 71 | export function minimize() { 72 | return (dispatch: HyperDispatch) => { 73 | dispatch({ 74 | type: UI_WINDOW_MINIMIZE, 75 | effect() { 76 | rpc.emit('minimize'); 77 | } 78 | }); 79 | }; 80 | } 81 | 82 | export function close() { 83 | return (dispatch: HyperDispatch) => { 84 | dispatch({ 85 | type: UI_WINDOW_CLOSE, 86 | effect() { 87 | rpc.emit('close'); 88 | } 89 | }); 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /lib/actions/index.ts: -------------------------------------------------------------------------------- 1 | import {INIT} from '../../typings/constants'; 2 | import type {HyperDispatch} from '../../typings/hyper'; 3 | import rpc from '../rpc'; 4 | 5 | export default function init() { 6 | return (dispatch: HyperDispatch) => { 7 | dispatch({ 8 | type: INIT, 9 | effect: () => { 10 | rpc.emit('init', null); 11 | } 12 | }); 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /lib/actions/notifications.ts: -------------------------------------------------------------------------------- 1 | import {NOTIFICATION_MESSAGE, NOTIFICATION_DISMISS} from '../../typings/constants/notifications'; 2 | import type {HyperActions} from '../../typings/hyper'; 3 | 4 | export function dismissNotification(id: string): HyperActions { 5 | return { 6 | type: NOTIFICATION_DISMISS, 7 | id 8 | }; 9 | } 10 | 11 | export function addNotificationMessage(text: string, url: string | null = null, dismissable = true): HyperActions { 12 | return { 13 | type: NOTIFICATION_MESSAGE, 14 | text, 15 | url, 16 | dismissable 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /lib/actions/sessions.ts: -------------------------------------------------------------------------------- 1 | import type {Session} from '../../typings/common'; 2 | import { 3 | SESSION_ADD, 4 | SESSION_RESIZE, 5 | SESSION_REQUEST, 6 | SESSION_ADD_DATA, 7 | SESSION_PTY_DATA, 8 | SESSION_PTY_EXIT, 9 | SESSION_USER_EXIT, 10 | SESSION_SET_ACTIVE, 11 | SESSION_CLEAR_ACTIVE, 12 | SESSION_USER_DATA, 13 | SESSION_SET_XTERM_TITLE, 14 | SESSION_SEARCH 15 | } from '../../typings/constants/sessions'; 16 | import type {HyperState, HyperDispatch, HyperActions} from '../../typings/hyper'; 17 | import rpc from '../rpc'; 18 | import {keys} from '../utils/object'; 19 | import findBySession from '../utils/term-groups'; 20 | 21 | export function addSession({uid, shell, pid, cols = null, rows = null, splitDirection, activeUid, profile}: Session) { 22 | return (dispatch: HyperDispatch, getState: () => HyperState) => { 23 | const {sessions} = getState(); 24 | const now = Date.now(); 25 | dispatch({ 26 | type: SESSION_ADD, 27 | uid, 28 | shell, 29 | pid, 30 | cols, 31 | rows, 32 | splitDirection, 33 | activeUid: activeUid ? activeUid : sessions.activeUid, 34 | now, 35 | profile 36 | }); 37 | }; 38 | } 39 | 40 | export function requestSession(profile: string | undefined) { 41 | return (dispatch: HyperDispatch, getState: () => HyperState) => { 42 | dispatch({ 43 | type: SESSION_REQUEST, 44 | effect: () => { 45 | const {ui} = getState(); 46 | const {cwd} = ui; 47 | rpc.emit('new', {cwd, profile}); 48 | } 49 | }); 50 | }; 51 | } 52 | 53 | export function addSessionData(uid: string, data: string) { 54 | return (dispatch: HyperDispatch) => { 55 | dispatch({ 56 | type: SESSION_ADD_DATA, 57 | data, 58 | effect() { 59 | const now = Date.now(); 60 | dispatch({ 61 | type: SESSION_PTY_DATA, 62 | uid, 63 | data, 64 | now 65 | }); 66 | } 67 | }); 68 | }; 69 | } 70 | 71 | function createExitAction(type: typeof SESSION_USER_EXIT | typeof SESSION_PTY_EXIT) { 72 | return (uid: string) => (dispatch: HyperDispatch, getState: () => HyperState) => { 73 | return dispatch({ 74 | type, 75 | uid, 76 | effect() { 77 | if (type === SESSION_USER_EXIT) { 78 | rpc.emit('exit', {uid}); 79 | } 80 | 81 | const sessions = keys(getState().sessions.sessions); 82 | if (sessions.length === 0) { 83 | window.close(); 84 | } 85 | } 86 | } as HyperActions); 87 | }; 88 | } 89 | 90 | // we want to distinguish an exit 91 | // that's UI initiated vs pty initiated 92 | export const userExitSession = createExitAction(SESSION_USER_EXIT); 93 | export const ptyExitSession = createExitAction(SESSION_PTY_EXIT); 94 | 95 | export function setActiveSession(uid: string) { 96 | return (dispatch: HyperDispatch) => { 97 | dispatch({ 98 | type: SESSION_SET_ACTIVE, 99 | uid 100 | }); 101 | }; 102 | } 103 | 104 | export function clearActiveSession(): HyperActions { 105 | return { 106 | type: SESSION_CLEAR_ACTIVE 107 | }; 108 | } 109 | 110 | export function setSessionXtermTitle(uid: string, title: string): HyperActions { 111 | return { 112 | type: SESSION_SET_XTERM_TITLE, 113 | uid, 114 | title 115 | }; 116 | } 117 | 118 | export function resizeSession(uid: string, cols: number, rows: number) { 119 | return (dispatch: HyperDispatch, getState: () => HyperState) => { 120 | const {termGroups} = getState(); 121 | const group = findBySession(termGroups, uid)!; 122 | const isStandaloneTerm = !group.parentUid && !group.children.length; 123 | const now = Date.now(); 124 | dispatch({ 125 | type: SESSION_RESIZE, 126 | uid, 127 | cols, 128 | rows, 129 | isStandaloneTerm, 130 | now, 131 | effect() { 132 | rpc.emit('resize', {uid, cols, rows}); 133 | } 134 | }); 135 | }; 136 | } 137 | 138 | export function openSearch(uid?: string) { 139 | return (dispatch: HyperDispatch, getState: () => HyperState) => { 140 | const targetUid = uid || getState().sessions.activeUid!; 141 | dispatch({ 142 | type: SESSION_SEARCH, 143 | uid: targetUid, 144 | value: true 145 | }); 146 | }; 147 | } 148 | 149 | export function closeSearch(uid?: string, keyEvent?: any) { 150 | return (dispatch: HyperDispatch, getState: () => HyperState) => { 151 | const targetUid = uid || getState().sessions.activeUid!; 152 | if (getState().sessions.sessions[targetUid]?.search) { 153 | dispatch({ 154 | type: SESSION_SEARCH, 155 | uid: targetUid, 156 | value: false 157 | }); 158 | } else { 159 | if (keyEvent) { 160 | keyEvent.catched = false; 161 | } 162 | } 163 | }; 164 | } 165 | 166 | export function sendSessionData(uid: string | null, data: string, escaped?: boolean) { 167 | return (dispatch: HyperDispatch, getState: () => HyperState) => { 168 | dispatch({ 169 | type: SESSION_USER_DATA, 170 | data, 171 | effect() { 172 | // If no uid is passed, data is sent to the active session. 173 | const targetUid = uid || getState().sessions.activeUid; 174 | 175 | rpc.emit('data', {uid: targetUid, data, escaped}); 176 | } 177 | }); 178 | }; 179 | } 180 | -------------------------------------------------------------------------------- /lib/actions/updater.ts: -------------------------------------------------------------------------------- 1 | import {UPDATE_INSTALL, UPDATE_AVAILABLE} from '../../typings/constants/updater'; 2 | import type {HyperActions} from '../../typings/hyper'; 3 | import rpc from '../rpc'; 4 | 5 | export function installUpdate(): HyperActions { 6 | return { 7 | type: UPDATE_INSTALL, 8 | effect: () => { 9 | rpc.emit('quit and install'); 10 | } 11 | }; 12 | } 13 | 14 | export function updateAvailable(version: string, notes: string, releaseUrl: string, canInstall: boolean): HyperActions { 15 | return { 16 | type: UPDATE_AVAILABLE, 17 | version, 18 | notes, 19 | releaseUrl, 20 | canInstall 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /lib/command-registry.ts: -------------------------------------------------------------------------------- 1 | import type {HyperDispatch} from '../typings/hyper'; 2 | 3 | import {closeSearch} from './actions/sessions'; 4 | import {ipcRenderer} from './utils/ipc'; 5 | 6 | let commands: Record void> = { 7 | 'editor:search-close': (e, dispatch) => { 8 | dispatch(closeSearch(undefined, e)); 9 | window.focusActiveTerm(); 10 | } 11 | }; 12 | 13 | export const getRegisteredKeys = async () => { 14 | const keymaps = await ipcRenderer.invoke('getDecoratedKeymaps'); 15 | 16 | return Object.keys(keymaps).reduce((result: Record, actionName) => { 17 | const commandKeys = keymaps[actionName]; 18 | commandKeys.forEach((shortcut) => { 19 | result[shortcut] = actionName; 20 | }); 21 | return result; 22 | }, {}); 23 | }; 24 | 25 | export const registerCommandHandlers = (cmds: typeof commands) => { 26 | if (!cmds) { 27 | return; 28 | } 29 | 30 | commands = Object.assign(commands, cmds); 31 | }; 32 | 33 | export const getCommandHandler = (command: string) => { 34 | return commands[command]; 35 | }; 36 | 37 | // Some commands are directly excuted by Electron menuItem role. 38 | // They should not be prevented to reach Electron. 39 | const roleCommands = [ 40 | 'window:close', 41 | 'editor:undo', 42 | 'editor:redo', 43 | 'editor:cut', 44 | 'editor:copy', 45 | 'editor:paste', 46 | 'editor:selectAll', 47 | 'window:minimize', 48 | 'window:zoom', 49 | 'window:toggleFullScreen' 50 | ]; 51 | 52 | export const shouldPreventDefault = (command: string) => !roleCommands.includes(command); 53 | -------------------------------------------------------------------------------- /lib/components/new-tab.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef, useState} from 'react'; 2 | 3 | import {VscChevronDown} from '@react-icons/all-files/vsc/VscChevronDown'; 4 | import useClickAway from 'react-use/lib/useClickAway'; 5 | 6 | import type {configOptions} from '../../typings/config'; 7 | 8 | interface Props { 9 | defaultProfile: string; 10 | profiles: configOptions['profiles']; 11 | openNewTab: (name: string) => void; 12 | backgroundColor: string; 13 | borderColor: string; 14 | tabsVisible: boolean; 15 | } 16 | const isMac = /Mac/.test(navigator.userAgent); 17 | 18 | const DropdownButton = ({defaultProfile, profiles, openNewTab, backgroundColor, borderColor, tabsVisible}: Props) => { 19 | const [dropdownOpen, setDropdownOpen] = useState(false); 20 | const ref = useRef(null); 21 | 22 | const toggleDropdown = () => { 23 | setDropdownOpen(!dropdownOpen); 24 | }; 25 | 26 | useClickAway(ref, () => { 27 | setDropdownOpen(false); 28 | }); 29 | 30 | return ( 31 |
e.stopPropagation()} 37 | onBlur={() => setDropdownOpen(false)} 38 | > 39 | 40 | 41 | {dropdownOpen && ( 42 |
    50 | {profiles.map((profile) => ( 51 |
  • { 54 | openNewTab(profile.name); 55 | setDropdownOpen(false); 56 | }} 57 | className={`profile_dropdown_item ${ 58 | profile.name === defaultProfile && profiles.length > 1 ? 'profile_dropdown_item_default' : '' 59 | }`} 60 | > 61 | {profile.name} 62 |
  • 63 | ))} 64 |
65 | )} 66 | 67 | 145 |
146 | ); 147 | }; 148 | 149 | export default DropdownButton; 150 | -------------------------------------------------------------------------------- /lib/components/notification.tsx: -------------------------------------------------------------------------------- 1 | import React, {forwardRef, useEffect, useRef, useState} from 'react'; 2 | 3 | import type {NotificationProps} from '../../typings/hyper'; 4 | 5 | const Notification = forwardRef>((props, ref) => { 6 | const dismissTimer = useRef(undefined); 7 | const [dismissing, setDismissing] = useState(false); 8 | 9 | useEffect(() => { 10 | setDismissTimer(); 11 | }, []); 12 | 13 | useEffect(() => { 14 | // if we have a timer going and the notification text 15 | // changed we reset the timer 16 | resetDismissTimer(); 17 | setDismissing(false); 18 | }, [props.text]); 19 | 20 | const handleDismiss = () => { 21 | setDismissing(true); 22 | }; 23 | 24 | const onElement = (el: HTMLDivElement | null) => { 25 | if (el) { 26 | el.addEventListener('webkitTransitionEnd', () => { 27 | if (dismissing) { 28 | props.onDismiss(); 29 | } 30 | }); 31 | const {backgroundColor} = props; 32 | if (backgroundColor) { 33 | el.style.setProperty('background-color', backgroundColor, 'important'); 34 | } 35 | 36 | if (ref) { 37 | if (typeof ref === 'function') ref(el); 38 | else ref.current = el; 39 | } 40 | } 41 | }; 42 | 43 | const setDismissTimer = () => { 44 | if (typeof props.dismissAfter === 'number') { 45 | dismissTimer.current = setTimeout(() => { 46 | handleDismiss(); 47 | }, props.dismissAfter); 48 | } 49 | }; 50 | 51 | const resetDismissTimer = () => { 52 | clearTimeout(dismissTimer.current); 53 | setDismissTimer(); 54 | }; 55 | 56 | useEffect(() => { 57 | return () => { 58 | clearTimeout(dismissTimer.current); 59 | }; 60 | }, []); 61 | 62 | const {backgroundColor, color} = props; 63 | const opacity = dismissing ? 0 : 1; 64 | return ( 65 |
66 | {props.customChildrenBefore} 67 | {props.children || props.text} 68 | {props.userDismissable ? ( 69 | 70 | [x] 71 | 72 | ) : null} 73 | {props.customChildren} 74 | 75 | 104 |
105 | ); 106 | }); 107 | 108 | Notification.displayName = 'Notification'; 109 | 110 | export default Notification; 111 | -------------------------------------------------------------------------------- /lib/components/notifications.tsx: -------------------------------------------------------------------------------- 1 | import React, {forwardRef} from 'react'; 2 | 3 | import type {NotificationsProps} from '../../typings/hyper'; 4 | import {decorate} from '../utils/plugins'; 5 | 6 | import Notification_ from './notification'; 7 | 8 | const Notification = decorate(Notification_, 'Notification'); 9 | 10 | const Notifications = forwardRef((props, ref) => { 11 | return ( 12 |
13 | {props.customChildrenBefore} 14 | {props.fontShowing && ( 15 | 23 | )} 24 | 25 | {props.resizeShowing && ( 26 | 34 | )} 35 | 36 | {props.messageShowing && ( 37 | 45 | {props.messageURL ? ( 46 | <> 47 | {props.messageText} ( 48 | { 51 | void window.require('electron').shell.openExternal(ev.currentTarget.href); 52 | ev.preventDefault(); 53 | }} 54 | href={props.messageURL} 55 | > 56 | more 57 | 58 | ) 59 | 60 | ) : null} 61 | 62 | )} 63 | 64 | {props.updateShowing && ( 65 | 73 | Version {props.updateVersion} ready. 74 | {props.updateNote && ` ${props.updateNote.trim().replace(/\.$/, '')}`} ( 75 | { 78 | void window.require('electron').shell.openExternal(ev.currentTarget.href); 79 | ev.preventDefault(); 80 | }} 81 | href={`https://github.com/vercel/hyper/releases/tag/${props.updateVersion}`} 82 | > 83 | notes 84 | 85 | ).{' '} 86 | {props.updateCanInstall ? ( 87 | 95 | Restart 96 | 97 | ) : ( 98 | { 106 | void window.require('electron').shell.openExternal(ev.currentTarget.href); 107 | ev.preventDefault(); 108 | }} 109 | href={props.updateReleaseUrl!} 110 | > 111 | Download 112 | 113 | )} 114 | .{' '} 115 | 116 | )} 117 | {props.customChildren} 118 | 119 | 126 |
127 | ); 128 | }); 129 | 130 | Notifications.displayName = 'Notifications'; 131 | 132 | export default Notifications; 133 | -------------------------------------------------------------------------------- /lib/components/style-sheet.tsx: -------------------------------------------------------------------------------- 1 | import React, {forwardRef} from 'react'; 2 | 3 | import type {StyleSheetProps} from '../../typings/hyper'; 4 | 5 | const StyleSheet = forwardRef((props, ref) => { 6 | const {borderColor} = props; 7 | 8 | return ( 9 | 22 | ); 23 | }); 24 | 25 | StyleSheet.displayName = 'StyleSheet'; 26 | 27 | export default StyleSheet; 28 | -------------------------------------------------------------------------------- /lib/components/tab.tsx: -------------------------------------------------------------------------------- 1 | import React, {forwardRef} from 'react'; 2 | 3 | import type {TabProps} from '../../typings/hyper'; 4 | 5 | const Tab = forwardRef((props, ref) => { 6 | const handleClick = (event: React.MouseEvent) => { 7 | const isLeftClick = event.nativeEvent.which === 1; 8 | 9 | if (isLeftClick && !props.isActive) { 10 | props.onSelect(); 11 | } 12 | }; 13 | 14 | const handleMouseUp = (event: React.MouseEvent) => { 15 | const isMiddleClick = event.nativeEvent.which === 2; 16 | 17 | if (isMiddleClick) { 18 | props.onClose(); 19 | } 20 | }; 21 | 22 | const {isActive, isFirst, isLast, borderColor, hasActivity} = props; 23 | 24 | return ( 25 | <> 26 |
  • 34 | {props.customChildrenBefore} 35 | 40 | 41 | {props.text} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {props.customChildren} 50 |
  • 51 | 52 | 162 | 163 | ); 164 | }); 165 | 166 | Tab.displayName = 'Tab'; 167 | 168 | export default Tab; 169 | -------------------------------------------------------------------------------- /lib/components/tabs.tsx: -------------------------------------------------------------------------------- 1 | import React, {forwardRef} from 'react'; 2 | 3 | import type {TabsProps} from '../../typings/hyper'; 4 | import {decorate, getTabProps} from '../utils/plugins'; 5 | 6 | import DropdownButton from './new-tab'; 7 | import Tab_ from './tab'; 8 | 9 | const Tab = decorate(Tab_, 'Tab'); 10 | const isMac = /Mac/.test(navigator.userAgent); 11 | 12 | const Tabs = forwardRef((props, ref) => { 13 | const {tabs = [], borderColor, onChange, onClose, fullScreen} = props; 14 | 15 | const hide = !isMac && tabs.length === 1; 16 | 17 | return ( 18 | 108 | ); 109 | }); 110 | 111 | Tabs.displayName = 'Tabs'; 112 | 113 | export default Tabs; 114 | -------------------------------------------------------------------------------- /lib/containers/header.ts: -------------------------------------------------------------------------------- 1 | import {createSelector} from 'reselect'; 2 | 3 | import type {HyperState, HyperDispatch, ITab} from '../../typings/hyper'; 4 | import {closeTab, changeTab, maximize, openHamburgerMenu, unmaximize, minimize, close} from '../actions/header'; 5 | import {requestTermGroup} from '../actions/term-groups'; 6 | import Header from '../components/header'; 7 | import {getRootGroups} from '../selectors'; 8 | import {connect} from '../utils/plugins'; 9 | 10 | const isMac = /Mac/.test(navigator.userAgent); 11 | 12 | const getSessions = ({sessions}: HyperState) => sessions.sessions; 13 | const getActiveRootGroup = ({termGroups}: HyperState) => termGroups.activeRootGroup; 14 | const getActiveSessions = ({termGroups}: HyperState) => termGroups.activeSessions; 15 | const getActivityMarkers = ({ui}: HyperState) => ui.activityMarkers; 16 | const getTabs = createSelector( 17 | [getSessions, getRootGroups, getActiveSessions, getActiveRootGroup, getActivityMarkers], 18 | (sessions, rootGroups, activeSessions, activeRootGroup, activityMarkers) => 19 | rootGroups.map((t): ITab => { 20 | const activeSessionUid = activeSessions[t.uid]; 21 | const session = sessions[activeSessionUid]; 22 | return { 23 | uid: t.uid, 24 | title: session.title, 25 | isActive: t.uid === activeRootGroup, 26 | hasActivity: activityMarkers[session.uid] 27 | }; 28 | }) 29 | ); 30 | 31 | const mapStateToProps = (state: HyperState) => { 32 | return { 33 | // active is an index 34 | isMac, 35 | tabs: getTabs(state), 36 | activeMarkers: state.ui.activityMarkers, 37 | borderColor: state.ui.borderColor, 38 | backgroundColor: state.ui.backgroundColor, 39 | maximized: state.ui.maximized, 40 | fullScreen: state.ui.fullScreen, 41 | showHamburgerMenu: state.ui.showHamburgerMenu, 42 | showWindowControls: state.ui.showWindowControls, 43 | defaultProfile: state.ui.defaultProfile, 44 | profiles: state.ui.profiles 45 | }; 46 | }; 47 | 48 | const mapDispatchToProps = (dispatch: HyperDispatch) => { 49 | return { 50 | onCloseTab: (i: string) => { 51 | dispatch(closeTab(i)); 52 | }, 53 | 54 | onChangeTab: (i: string) => { 55 | dispatch(changeTab(i)); 56 | }, 57 | 58 | maximize: () => { 59 | dispatch(maximize()); 60 | }, 61 | 62 | unmaximize: () => { 63 | dispatch(unmaximize()); 64 | }, 65 | 66 | openHamburgerMenu: (coordinates: {x: number; y: number}) => { 67 | dispatch(openHamburgerMenu(coordinates)); 68 | }, 69 | 70 | minimize: () => { 71 | dispatch(minimize()); 72 | }, 73 | 74 | close: () => { 75 | dispatch(close()); 76 | }, 77 | 78 | openNewTab: (profile: string) => { 79 | dispatch(requestTermGroup(undefined, profile)); 80 | } 81 | }; 82 | }; 83 | 84 | export const HeaderContainer = connect(mapStateToProps, mapDispatchToProps, null)(Header, 'Header'); 85 | 86 | export type HeaderConnectedProps = ReturnType & ReturnType; 87 | -------------------------------------------------------------------------------- /lib/containers/hyper.tsx: -------------------------------------------------------------------------------- 1 | import React, {forwardRef, useEffect, useRef} from 'react'; 2 | 3 | import Mousetrap from 'mousetrap'; 4 | import type {MousetrapInstance} from 'mousetrap'; 5 | import stylis from 'stylis'; 6 | 7 | import type {HyperState, HyperProps, HyperDispatch} from '../../typings/hyper'; 8 | import * as uiActions from '../actions/ui'; 9 | import {getRegisteredKeys, getCommandHandler, shouldPreventDefault} from '../command-registry'; 10 | import type Terms from '../components/terms'; 11 | import {connect} from '../utils/plugins'; 12 | 13 | import {HeaderContainer} from './header'; 14 | import NotificationsContainer from './notifications'; 15 | import TermsContainer from './terms'; 16 | 17 | const isMac = /Mac/.test(navigator.userAgent); 18 | 19 | const Hyper = forwardRef((props, ref) => { 20 | const mousetrap = useRef(null); 21 | const terms = useRef(null); 22 | 23 | useEffect(() => { 24 | void attachKeyListeners(); 25 | }, [props.lastConfigUpdate]); 26 | useEffect(() => { 27 | handleFocusActive(props.activeSession); 28 | }, [props.activeSession]); 29 | 30 | const handleFocusActive = (uid?: string | null) => { 31 | const term = uid && terms.current?.getTermByUid(uid); 32 | if (term) { 33 | term.focus(); 34 | } 35 | }; 36 | 37 | const handleSelectAll = () => { 38 | const term = terms.current?.getActiveTerm(); 39 | if (term) { 40 | term.selectAll(); 41 | } 42 | }; 43 | 44 | const attachKeyListeners = async () => { 45 | if (!mousetrap.current) { 46 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 47 | mousetrap.current = new (Mousetrap as any)(window, true); 48 | mousetrap.current!.stopCallback = () => { 49 | // All events should be intercepted even if focus is in an input/textarea 50 | return false; 51 | }; 52 | } else { 53 | mousetrap.current.reset(); 54 | } 55 | 56 | const keys = await getRegisteredKeys(); 57 | Object.keys(keys).forEach((commandKeys) => { 58 | mousetrap.current?.bind( 59 | commandKeys, 60 | (e) => { 61 | const command = keys[commandKeys]; 62 | // We should tell xterm to ignore this event. 63 | (e as any).catched = true; 64 | props.execCommand(command, getCommandHandler(command), e); 65 | shouldPreventDefault(command) && e.preventDefault(); 66 | }, 67 | 'keydown' 68 | ); 69 | }); 70 | }; 71 | 72 | useEffect(() => { 73 | void attachKeyListeners(); 74 | window.rpc.on('term selectAll', handleSelectAll); 75 | }, []); 76 | 77 | const onTermsRef = (_terms: Terms | null) => { 78 | terms.current = _terms; 79 | window.focusActiveTerm = (uid?: string) => { 80 | if (uid) { 81 | handleFocusActive(uid); 82 | } else { 83 | terms.current?.getActiveTerm()?.focus(); 84 | } 85 | }; 86 | }; 87 | 88 | useEffect(() => { 89 | return () => { 90 | mousetrap.current?.reset(); 91 | }; 92 | }, []); 93 | 94 | const {isMac: isMac_, customCSS, uiFontFamily, borderColor, maximized, fullScreen} = props; 95 | const borderWidth = isMac_ ? '' : `${maximized ? '0' : '1'}px`; 96 | stylis.set({prefix: false}); 97 | return ( 98 |
    99 |
    103 | 104 | 105 | {props.customInnerChildren} 106 |
    107 | 108 | 109 | 110 | {props.customChildren} 111 | 112 | 129 | 130 | {/* 131 | Add custom CSS to Hyper. 132 | We add a scope to the customCSS so that it can get around the weighting applied by styled-jsx 133 | */} 134 |