├── .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 | 
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | [](https://github.com/vercel/hyper/actions?query=workflow%3A%22Node+CI%22+branch%3Acanary+event%3Apush)
10 | [](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 |
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 |
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 |
19 | {props.customChildrenBefore}
20 | {tabs.length === 1 && isMac ? {tabs[0].title}
: null}
21 | {tabs.length > 1 ? (
22 | <>
23 |
24 | {tabs.map((tab, i) => {
25 | const {uid, title, isActive, hasActivity} = tab;
26 | const tabProps = getTabProps(tab, props, {
27 | text: title === '' ? 'Shell' : title,
28 | isFirst: i === 0,
29 | isLast: tabs.length - 1 === i,
30 | borderColor,
31 | isActive,
32 | hasActivity,
33 | onSelect: onChange.bind(null, uid),
34 | onClose: onClose.bind(null, uid)
35 | });
36 | return ;
37 | })}
38 |
39 | {isMac && (
40 |
45 | )}
46 | >
47 | ) : null}
48 | 1} />
49 | {props.customChildren}
50 |
51 |
107 |
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 |
135 |
136 | );
137 | });
138 |
139 | Hyper.displayName = 'Hyper';
140 |
141 | const mapStateToProps = (state: HyperState) => {
142 | return {
143 | isMac,
144 | customCSS: state.ui.css,
145 | uiFontFamily: state.ui.uiFontFamily,
146 | borderColor: state.ui.borderColor,
147 | activeSession: state.sessions.activeUid,
148 | backgroundColor: state.ui.backgroundColor,
149 | maximized: state.ui.maximized,
150 | fullScreen: state.ui.fullScreen,
151 | lastConfigUpdate: state.ui._lastUpdate
152 | };
153 | };
154 |
155 | const mapDispatchToProps = (dispatch: HyperDispatch) => {
156 | return {
157 | execCommand: (command: string, fn: (e: any, dispatch: HyperDispatch) => void, e: any) => {
158 | dispatch(uiActions.execCommand(command, fn, e));
159 | }
160 | };
161 | };
162 |
163 | const HyperContainer = connect(mapStateToProps, mapDispatchToProps, null, {forwardRef: true})(Hyper, 'Hyper');
164 |
165 | export default HyperContainer;
166 |
167 | export type HyperConnectedProps = ReturnType & ReturnType;
168 |
--------------------------------------------------------------------------------
/lib/containers/notifications.ts:
--------------------------------------------------------------------------------
1 | import type {HyperState, HyperDispatch} from '../../typings/hyper';
2 | import {dismissNotification} from '../actions/notifications';
3 | import {installUpdate} from '../actions/updater';
4 | import Notifications from '../components/notifications';
5 | import {connect} from '../utils/plugins';
6 |
7 | const mapStateToProps = (state: HyperState) => {
8 | const {ui} = state;
9 | const {notifications} = ui;
10 | let state_: Partial<{
11 | fontShowing: boolean;
12 | fontSize: number;
13 | fontText: string;
14 | resizeShowing: boolean;
15 | cols: number | null;
16 | rows: number | null;
17 | updateShowing: boolean;
18 | updateVersion: string | null;
19 | updateNote: string | null;
20 | updateReleaseUrl: string | null;
21 | updateCanInstall: boolean | null;
22 | messageShowing: boolean;
23 | messageText: string | null;
24 | messageURL: string | null;
25 | messageDismissable: boolean | null;
26 | }> = {};
27 |
28 | if (notifications.font) {
29 | const fontSize = ui.fontSizeOverride || ui.fontSize;
30 |
31 | state_ = {
32 | ...state_,
33 | fontShowing: true,
34 | fontSize,
35 | fontText: `${fontSize}px`
36 | };
37 | }
38 |
39 | if (notifications.resize) {
40 | const cols = ui.cols;
41 | const rows = ui.rows;
42 |
43 | state_ = {
44 | ...state_,
45 | resizeShowing: true,
46 | cols,
47 | rows
48 | };
49 | }
50 |
51 | if (notifications.updates) {
52 | state_ = {
53 | ...state_,
54 | updateShowing: true,
55 | updateVersion: ui.updateVersion,
56 | updateNote: ui.updateNotes!.split('\n')[0],
57 | updateReleaseUrl: ui.updateReleaseUrl,
58 | updateCanInstall: ui.updateCanInstall
59 | };
60 | } else if (notifications.message) {
61 | state_ = {
62 | ...state_,
63 | messageShowing: true,
64 | messageText: ui.messageText,
65 | messageURL: ui.messageURL,
66 | messageDismissable: ui.messageDismissable
67 | };
68 | }
69 |
70 | return state_;
71 | };
72 |
73 | const mapDispatchToProps = (dispatch: HyperDispatch) => {
74 | return {
75 | onDismissFont: () => {
76 | dispatch(dismissNotification('font'));
77 | },
78 | onDismissResize: () => {
79 | dispatch(dismissNotification('resize'));
80 | },
81 | onDismissUpdate: () => {
82 | dispatch(dismissNotification('updates'));
83 | },
84 | onDismissMessage: () => {
85 | dispatch(dismissNotification('message'));
86 | },
87 | onUpdateInstall: () => {
88 | dispatch(installUpdate());
89 | }
90 | };
91 | };
92 |
93 | const NotificationsContainer = connect(mapStateToProps, mapDispatchToProps, null)(Notifications, 'Notifications');
94 |
95 | export default NotificationsContainer;
96 |
97 | export type NotificationsConnectedProps = ReturnType & ReturnType;
98 |
--------------------------------------------------------------------------------
/lib/containers/terms.ts:
--------------------------------------------------------------------------------
1 | import type {HyperState, HyperDispatch} from '../../typings/hyper';
2 | import {
3 | resizeSession,
4 | sendSessionData,
5 | setSessionXtermTitle,
6 | setActiveSession,
7 | openSearch,
8 | closeSearch
9 | } from '../actions/sessions';
10 | import {openContextMenu} from '../actions/ui';
11 | import Terms from '../components/terms';
12 | import {getRootGroups} from '../selectors';
13 | import {connect} from '../utils/plugins';
14 |
15 | const mapStateToProps = (state: HyperState) => {
16 | const {sessions} = state.sessions;
17 | return {
18 | sessions,
19 | cols: state.ui.cols,
20 | rows: state.ui.rows,
21 | scrollback: state.ui.scrollback,
22 | termGroups: getRootGroups(state),
23 | activeRootGroup: state.termGroups.activeRootGroup,
24 | activeSession: state.sessions.activeUid,
25 | customCSS: state.ui.termCSS,
26 | write: state.sessions.write,
27 | fontSize: state.ui.fontSizeOverride ? state.ui.fontSizeOverride : state.ui.fontSize,
28 | fontFamily: state.ui.fontFamily,
29 | fontWeight: state.ui.fontWeight,
30 | fontWeightBold: state.ui.fontWeightBold,
31 | lineHeight: state.ui.lineHeight,
32 | letterSpacing: state.ui.letterSpacing,
33 | uiFontFamily: state.ui.uiFontFamily,
34 | fontSmoothing: state.ui.fontSmoothingOverride,
35 | padding: state.ui.padding,
36 | cursorColor: state.ui.cursorColor,
37 | cursorAccentColor: state.ui.cursorAccentColor,
38 | cursorShape: state.ui.cursorShape,
39 | cursorBlink: state.ui.cursorBlink,
40 | borderColor: state.ui.borderColor,
41 | selectionColor: state.ui.selectionColor,
42 | colors: state.ui.colors,
43 | foregroundColor: state.ui.foregroundColor,
44 | backgroundColor: state.ui.backgroundColor,
45 | bell: state.ui.bell,
46 | bellSoundURL: state.ui.bellSoundURL,
47 | bellSound: state.ui.bellSound,
48 | copyOnSelect: state.ui.copyOnSelect,
49 | modifierKeys: state.ui.modifierKeys,
50 | quickEdit: state.ui.quickEdit,
51 | webGLRenderer: state.ui.webGLRenderer,
52 | webLinksActivationKey: state.ui.webLinksActivationKey,
53 | macOptionSelectionMode: state.ui.macOptionSelectionMode,
54 | disableLigatures: state.ui.disableLigatures,
55 | screenReaderMode: state.ui.screenReaderMode,
56 | windowsPty: state.ui.windowsPty,
57 | imageSupport: state.ui.imageSupport
58 | };
59 | };
60 |
61 | const mapDispatchToProps = (dispatch: HyperDispatch) => {
62 | return {
63 | onData(uid: string, data: string) {
64 | dispatch(sendSessionData(uid, data));
65 | },
66 |
67 | onTitle(uid: string, title: string) {
68 | dispatch(setSessionXtermTitle(uid, title));
69 | },
70 |
71 | onResize(uid: string, cols: number, rows: number) {
72 | dispatch(resizeSession(uid, cols, rows));
73 | },
74 |
75 | onActive(uid: string) {
76 | dispatch(setActiveSession(uid));
77 | },
78 |
79 | onOpenSearch(uid: string) {
80 | dispatch(openSearch(uid));
81 | },
82 |
83 | onCloseSearch(uid: string) {
84 | dispatch(closeSearch(uid));
85 | },
86 |
87 | onContextMenu(uid: string, selection: string) {
88 | dispatch(setActiveSession(uid));
89 | dispatch(openContextMenu(uid, selection));
90 | }
91 | };
92 | };
93 |
94 | const TermsContainer = connect(mapStateToProps, mapDispatchToProps, null, {forwardRef: true})(Terms, 'Terms');
95 |
96 | export default TermsContainer;
97 |
98 | export type TermsConnectedProps = ReturnType & ReturnType;
99 |
--------------------------------------------------------------------------------
/lib/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import {combineReducers} from 'redux';
2 | import type {Reducer} from 'redux';
3 |
4 | import type {HyperActions, HyperState} from '../../typings/hyper';
5 |
6 | import sessions from './sessions';
7 | import termGroups from './term-groups';
8 | import ui from './ui';
9 |
10 | export default combineReducers({
11 | ui,
12 | sessions,
13 | termGroups
14 | }) as Reducer;
15 |
--------------------------------------------------------------------------------
/lib/reducers/sessions.ts:
--------------------------------------------------------------------------------
1 | import Immutable from 'seamless-immutable';
2 |
3 | import {
4 | SESSION_ADD,
5 | SESSION_PTY_EXIT,
6 | SESSION_USER_EXIT,
7 | SESSION_PTY_DATA,
8 | SESSION_SET_ACTIVE,
9 | SESSION_CLEAR_ACTIVE,
10 | SESSION_RESIZE,
11 | SESSION_SET_XTERM_TITLE,
12 | SESSION_SET_CWD,
13 | SESSION_SEARCH
14 | } from '../../typings/constants/sessions';
15 | import type {sessionState, session, Mutable, ISessionReducer} from '../../typings/hyper';
16 | import {decorateSessionsReducer} from '../utils/plugins';
17 |
18 | const initialState: sessionState = Immutable>({
19 | sessions: {},
20 | activeUid: null
21 | });
22 |
23 | function Session(obj: Immutable.DeepPartial) {
24 | const x: session = {
25 | uid: '',
26 | title: '',
27 | cols: null,
28 | rows: null,
29 | cleared: false,
30 | search: false,
31 | shell: '',
32 | pid: null,
33 | profile: ''
34 | };
35 | return Immutable(x).merge(obj);
36 | }
37 |
38 | function deleteSession(state: sessionState, uid: string) {
39 | return state.updateIn(['sessions'], (sessions: (typeof state)['sessions']) => {
40 | const sessions_ = sessions.asMutable();
41 | delete sessions_[uid];
42 | return sessions_;
43 | });
44 | }
45 |
46 | const reducer: ISessionReducer = (state = initialState, action) => {
47 | switch (action.type) {
48 | case SESSION_ADD:
49 | return state.set('activeUid', action.uid).setIn(
50 | ['sessions', action.uid],
51 | Session({
52 | cols: action.cols,
53 | rows: action.rows,
54 | uid: action.uid,
55 | shell: action.shell ? action.shell.split('/').pop() : null,
56 | pid: action.pid,
57 | profile: action.profile
58 | })
59 | );
60 |
61 | case SESSION_SET_ACTIVE:
62 | return state.set('activeUid', action.uid);
63 |
64 | case SESSION_SEARCH:
65 | return state.setIn(['sessions', action.uid, 'search'], action.value);
66 |
67 | case SESSION_CLEAR_ACTIVE:
68 | return state.merge(
69 | {
70 | sessions: {
71 | [state.activeUid!]: {
72 | cleared: true
73 | }
74 | }
75 | },
76 | {deep: true}
77 | );
78 |
79 | case SESSION_PTY_DATA:
80 | // we avoid a direct merge for perf reasons
81 | // as this is the most common action
82 | if (state.sessions[action.uid]?.cleared) {
83 | return state.merge(
84 | {
85 | sessions: {
86 | [action.uid]: {
87 | cleared: false
88 | }
89 | }
90 | },
91 | {deep: true}
92 | );
93 | }
94 | return state;
95 |
96 | case SESSION_PTY_EXIT:
97 | if (state.sessions[action.uid]) {
98 | return deleteSession(state, action.uid);
99 | }
100 | console.log('ignore pty exit: session removed by user');
101 | return state;
102 |
103 | case SESSION_USER_EXIT:
104 | return deleteSession(state, action.uid);
105 |
106 | case SESSION_SET_XTERM_TITLE:
107 | return state.setIn(
108 | ['sessions', action.uid, 'title'],
109 | // we need to trim the title because `cmd.exe`
110 | // likes to report ' ' as the title
111 | action.title.trim()
112 | );
113 |
114 | case SESSION_RESIZE:
115 | return state.setIn(
116 | ['sessions', action.uid],
117 | state.sessions[action.uid].merge({
118 | rows: action.rows,
119 | cols: action.cols,
120 | resizeAt: action.now
121 | })
122 | );
123 |
124 | case SESSION_SET_CWD:
125 | if (state.activeUid) {
126 | return state.setIn(['sessions', state.activeUid, 'cwd'], action.cwd);
127 | }
128 | return state;
129 |
130 | default:
131 | return state;
132 | }
133 | };
134 |
135 | export default decorateSessionsReducer(reducer);
136 |
--------------------------------------------------------------------------------
/lib/rpc.ts:
--------------------------------------------------------------------------------
1 | import RPC from './utils/rpc';
2 |
3 | const rpc = new RPC();
4 |
5 | export default rpc;
6 |
--------------------------------------------------------------------------------
/lib/selectors.ts:
--------------------------------------------------------------------------------
1 | import {createSelector} from 'reselect';
2 |
3 | import type {HyperState} from '../typings/hyper';
4 |
5 | const getTermGroups = ({termGroups}: Pick) => termGroups.termGroups;
6 | export const getRootGroups = createSelector(getTermGroups, (termGroups) =>
7 | Object.keys(termGroups)
8 | .map((uid) => termGroups[uid])
9 | .filter(({parentUid}) => !parentUid)
10 | );
11 |
--------------------------------------------------------------------------------
/lib/store/configure-store.dev.ts:
--------------------------------------------------------------------------------
1 | import {composeWithDevTools} from '@redux-devtools/extension';
2 | import {createStore, applyMiddleware} from 'redux';
3 | import _thunk from 'redux-thunk';
4 | import type {ThunkMiddleware} from 'redux-thunk';
5 |
6 | import type {HyperState, HyperActions} from '../../typings/hyper';
7 | import rootReducer from '../reducers/index';
8 | import effects from '../utils/effects';
9 | import * as plugins from '../utils/plugins';
10 |
11 | import writeMiddleware from './write-middleware';
12 |
13 | const thunk: ThunkMiddleware = _thunk;
14 |
15 | const configureStoreForDevelopment = () => {
16 | const enhancer = composeWithDevTools(applyMiddleware(thunk, plugins.middleware, thunk, writeMiddleware, effects));
17 |
18 | return createStore(rootReducer, enhancer);
19 | };
20 |
21 | export default configureStoreForDevelopment;
22 |
--------------------------------------------------------------------------------
/lib/store/configure-store.prod.ts:
--------------------------------------------------------------------------------
1 | import {createStore, applyMiddleware} from 'redux';
2 | import _thunk from 'redux-thunk';
3 | import type {ThunkMiddleware} from 'redux-thunk';
4 |
5 | import type {HyperState, HyperActions} from '../../typings/hyper';
6 | import rootReducer from '../reducers/index';
7 | import effects from '../utils/effects';
8 | import * as plugins from '../utils/plugins';
9 |
10 | import writeMiddleware from './write-middleware';
11 |
12 | const thunk: ThunkMiddleware = _thunk;
13 |
14 | const configureStoreForProd = () =>
15 | createStore(rootReducer, applyMiddleware(thunk, plugins.middleware, thunk, writeMiddleware, effects));
16 |
17 | export default configureStoreForProd;
18 |
--------------------------------------------------------------------------------
/lib/store/configure-store.ts:
--------------------------------------------------------------------------------
1 | import configureStoreForDevelopment from './configure-store.dev';
2 | import configureStoreForProduction from './configure-store.prod';
3 |
4 | const configureStore = () => {
5 | if (process.env.NODE_ENV === 'production') {
6 | return configureStoreForProduction();
7 | }
8 |
9 | return configureStoreForDevelopment();
10 | };
11 | export default configureStore;
12 |
--------------------------------------------------------------------------------
/lib/store/write-middleware.ts:
--------------------------------------------------------------------------------
1 | import type {Dispatch, Middleware} from 'redux';
2 |
3 | import type {HyperActions, HyperState} from '../../typings/hyper';
4 | import terms from '../terms';
5 |
6 | // the only side effect we perform from middleware
7 | // is to write to the react term instance directly
8 | // to avoid a performance hit
9 | const writeMiddleware: Middleware<{}, HyperState, Dispatch> = () => (next) => (action: HyperActions) => {
10 | if (action.type === 'SESSION_PTY_DATA') {
11 | const term = terms[action.uid];
12 | if (term) {
13 | term.term.write(action.data);
14 | }
15 | }
16 | next(action);
17 | };
18 |
19 | export default writeMiddleware;
20 |
--------------------------------------------------------------------------------
/lib/terms.ts:
--------------------------------------------------------------------------------
1 | import type Term from './components/term';
2 |
3 | // react Term components add themselves
4 | // to this object upon mounting / unmounting
5 | // this is to allow imperative access to the
6 | // term API, which is a performance
7 | // optimization for the most common action
8 | // within the system
9 |
10 | const terms: Record = {};
11 | export default terms;
12 |
--------------------------------------------------------------------------------
/lib/utils/config.ts:
--------------------------------------------------------------------------------
1 | import {require as remoteRequire, getCurrentWindow} from '@electron/remote';
2 | // TODO: Should be updates to new async API https://medium.com/@nornagon/electrons-remote-module-considered-harmful-70d69500f31
3 |
4 | import {ipcRenderer} from './ipc';
5 |
6 | const plugins = remoteRequire('./plugins') as typeof import('../../app/plugins');
7 |
8 | Object.defineProperty(window, 'profileName', {
9 | get() {
10 | return getCurrentWindow().profileName;
11 | },
12 | set() {
13 | throw new Error('profileName is readonly');
14 | }
15 | });
16 |
17 | export function getConfig() {
18 | return plugins.getDecoratedConfig(window.profileName);
19 | }
20 |
21 | export function subscribe(fn: (event: Electron.IpcRendererEvent, ...args: any[]) => void) {
22 | ipcRenderer.on('config change', fn);
23 | ipcRenderer.on('plugins change', fn);
24 | return () => {
25 | ipcRenderer.removeListener('config change', fn);
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/lib/utils/effects.ts:
--------------------------------------------------------------------------------
1 | import type {Dispatch, Middleware} from 'redux';
2 |
3 | import type {HyperActions, HyperState} from '../../typings/hyper';
4 | /**
5 | * Simple redux middleware that executes
6 | * the `effect` field if provided in an action
7 | * since this is preceded by the `plugins`
8 | * middleware. It allows authors to interrupt,
9 | * defer or add to existing side effects at will
10 | * as the result of an action being triggered.
11 | */
12 | const effectsMiddleware: Middleware<{}, HyperState, Dispatch> = () => (next) => (action) => {
13 | const ret = next(action);
14 | if (action.effect) {
15 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call
16 | action.effect();
17 | delete action.effect;
18 | }
19 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return
20 | return ret;
21 | };
22 | export default effectsMiddleware;
23 |
--------------------------------------------------------------------------------
/lib/utils/file.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Based on https://github.com/kevva/executable
3 | * Since this module doesn't expose the function to check stat mode only,
4 | * his logic is pasted here.
5 | *
6 | * Opened an issue and a pull request about it,
7 | * to maybe switch to module in the future:
8 | *
9 | * Issue: https://github.com/kevva/executable/issues/9
10 | * PR: https://github.com/kevva/executable/pull/10
11 | */
12 |
13 | import fs from 'fs';
14 | import type {Stats} from 'fs';
15 |
16 | export function isExecutable(fileStat: Stats): boolean {
17 | if (process.platform === 'win32') {
18 | return true;
19 | }
20 |
21 | return Boolean(fileStat.mode & 0o0001 || fileStat.mode & 0o0010 || fileStat.mode & 0o0100);
22 | }
23 |
24 | export function getBase64FileData(filePath: string): Promise {
25 | return new Promise((resolve): void => {
26 | return fs.readFile(filePath, (err, data) => {
27 | if (err) {
28 | // Gracefully fail with a warning
29 | console.warn('There was an error reading the file at the local location:', err);
30 | return resolve(null);
31 | }
32 |
33 | const base64Data = Buffer.from(data).toString('base64');
34 | return resolve(base64Data);
35 | });
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/lib/utils/ipc-child-process.ts:
--------------------------------------------------------------------------------
1 | import type {ExecFileOptions, ExecOptions} from 'child_process';
2 |
3 | import {ipcRenderer} from './ipc';
4 |
5 | export function exec(command: string, options: ExecOptions, callback: (..._args: any) => void) {
6 | if (typeof options === 'function') {
7 | callback = options;
8 | options = {};
9 | }
10 | ipcRenderer.invoke('child_process.exec', command, options).then(
11 | ({stdout, stderr}) => callback?.(null, stdout, stderr),
12 | (error) => callback?.(error, '', '')
13 | );
14 | }
15 |
16 | export function execSync() {
17 | console.error('Calling execSync from renderer is disabled');
18 | }
19 |
20 | export function execFile(file: string, args: string[], options: ExecFileOptions, callback: (..._args: any) => void) {
21 | if (typeof options === 'function') {
22 | callback = options;
23 | options = {};
24 | }
25 | if (typeof args === 'function') {
26 | callback = args;
27 | args = [];
28 | options = {};
29 | }
30 | ipcRenderer.invoke('child_process.execFile', file, args, options).then(
31 | ({stdout, stderr}) => callback?.(null, stdout, stderr),
32 | (error) => callback?.(error, '', '')
33 | );
34 | }
35 |
36 | export function execFileSync() {
37 | console.error('Calling execFileSync from renderer is disabled');
38 | }
39 |
40 | export function spawn() {
41 | console.error('Calling spawn from renderer is disabled');
42 | }
43 |
44 | export function spawnSync() {
45 | console.error('Calling spawnSync from renderer is disabled');
46 | }
47 |
48 | export function fork() {
49 | console.error('Calling fork from renderer is disabled');
50 | }
51 |
52 | const IPCChildProcess = {
53 | exec,
54 | execSync,
55 | execFile,
56 | execFileSync,
57 | spawn,
58 | spawnSync,
59 | fork
60 | };
61 |
62 | export default IPCChildProcess;
63 |
--------------------------------------------------------------------------------
/lib/utils/ipc.ts:
--------------------------------------------------------------------------------
1 | import {ipcRenderer as _ipc} from 'electron';
2 |
3 | import type {IpcRendererWithCommands} from '../../typings/common';
4 |
5 | export const ipcRenderer = _ipc as IpcRendererWithCommands;
6 |
--------------------------------------------------------------------------------
/lib/utils/notify.ts:
--------------------------------------------------------------------------------
1 | /* eslint no-new:0 */
2 | export default function notify(title: string, body: string, details: {error?: any} = {}) {
3 | console.log(`[Notification] ${title}: ${body}`);
4 | if (details.error) {
5 | console.error(details.error);
6 | }
7 | new Notification(title, {body});
8 | }
9 |
--------------------------------------------------------------------------------
/lib/utils/object.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line eslint-comments/disable-enable-pair
2 | /* eslint-disable @typescript-eslint/no-unsafe-return */
3 | const valsCache = new WeakMap();
4 |
5 | export function values(imm: Record) {
6 | if (!valsCache.has(imm)) {
7 | valsCache.set(imm, Object.values(imm));
8 | }
9 | return valsCache.get(imm);
10 | }
11 |
12 | const keysCache = new WeakMap();
13 | export function keys(imm: Record) {
14 | if (!keysCache.has(imm)) {
15 | keysCache.set(imm, Object.keys(imm));
16 | }
17 | return keysCache.get(imm);
18 | }
19 |
20 | export const ObjectTypedKeys = (obj: T) => {
21 | return Object.keys(obj) as (keyof T)[];
22 | };
23 |
--------------------------------------------------------------------------------
/lib/utils/paste.ts:
--------------------------------------------------------------------------------
1 | import {clipboard} from 'electron';
2 |
3 | import plist from 'plist';
4 |
5 | const getPath = (platform: string) => {
6 | switch (platform) {
7 | case 'darwin': {
8 | if (clipboard.has('NSFilenamesPboardType')) {
9 | // Parse plist file containing the path list of copied files
10 | const list = plist.parse(clipboard.read('NSFilenamesPboardType')) as plist.PlistArray;
11 | return "'" + list.join("' '") + "'";
12 | } else {
13 | return null;
14 | }
15 | }
16 | case 'win32': {
17 | const filepath = clipboard.read('FileNameW');
18 | return filepath.replace(new RegExp(String.fromCharCode(0), 'g'), '');
19 | }
20 | // Linux already pastes full path
21 | default:
22 | return null;
23 | }
24 | };
25 |
26 | export default function processClipboard() {
27 | return getPath(process.platform);
28 | }
29 |
--------------------------------------------------------------------------------
/lib/utils/rpc.ts:
--------------------------------------------------------------------------------
1 | import {EventEmitter} from 'events';
2 |
3 | import type {IpcRendererEvent} from 'electron';
4 |
5 | import type {
6 | FilterNever,
7 | IpcRendererWithCommands,
8 | MainEvents,
9 | RendererEvents,
10 | TypedEmitter
11 | } from '../../typings/common';
12 |
13 | import {ipcRenderer} from './ipc';
14 |
15 | export default class Client {
16 | emitter: TypedEmitter;
17 | ipc: IpcRendererWithCommands;
18 | id!: string;
19 |
20 | constructor() {
21 | this.emitter = new EventEmitter();
22 | this.ipc = ipcRenderer;
23 | this.emit = this.emit.bind(this);
24 | if (window.__rpcId) {
25 | setTimeout(() => {
26 | this.id = window.__rpcId;
27 | this.ipc.on(this.id, this.ipcListener);
28 | this.emitter.emit('ready');
29 | }, 0);
30 | } else {
31 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
32 | this.ipc.on('init', (ev: IpcRendererEvent, uid: string, profileName: string) => {
33 | // we cache so that if the object
34 | // gets re-instantiated we don't
35 | // wait for a `init` event
36 | window.__rpcId = uid;
37 | // window.profileName = profileName;
38 | this.id = uid;
39 | this.ipc.on(uid, this.ipcListener);
40 | this.emitter.emit('ready');
41 | });
42 | }
43 | }
44 |
45 | ipcListener = (
46 | event: IpcRendererEvent,
47 | {ch, data}: {ch: U; data: RendererEvents[U]}
48 | ) => this.emitter.emit(ch, data);
49 |
50 | on = (ev: U, fn: (arg0: RendererEvents[U]) => void) => {
51 | this.emitter.on(ev, fn);
52 | return this;
53 | };
54 |
55 | once = (ev: U, fn: (arg0: RendererEvents[U]) => void) => {
56 | this.emitter.once(ev, fn);
57 | return this;
58 | };
59 |
60 | emit>>(ev: U): boolean;
61 | emit>(ev: U, data: MainEvents[U]): boolean;
62 | emit(ev: U, data?: MainEvents[U]) {
63 | if (!this.id) {
64 | throw new Error('Not ready');
65 | }
66 | this.ipc.send(this.id, {ev, data});
67 | return true;
68 | }
69 |
70 | removeListener = (ev: U, fn: (arg0: RendererEvents[U]) => void) => {
71 | this.emitter.removeListener(ev, fn);
72 | return this;
73 | };
74 |
75 | removeAllListeners = () => {
76 | this.emitter.removeAllListeners();
77 | return this;
78 | };
79 |
80 | destroy = () => {
81 | this.removeAllListeners();
82 | this.ipc.removeAllListeners(this.id);
83 | };
84 | }
85 |
--------------------------------------------------------------------------------
/lib/utils/term-groups.ts:
--------------------------------------------------------------------------------
1 | import type {ITermState} from '../../typings/hyper';
2 |
3 | export default function findBySession(termGroupState: ITermState, sessionUid: string) {
4 | const {termGroups} = termGroupState;
5 | return Object.keys(termGroups)
6 | .map((uid) => termGroups[uid])
7 | .find((group) => group.sessionUid === sessionUid);
8 | }
9 |
--------------------------------------------------------------------------------
/lib/v8-snapshot-util.ts:
--------------------------------------------------------------------------------
1 | if (typeof snapshotResult !== 'undefined') {
2 | const Module = __non_webpack_require__('module');
3 | const originalLoad: (module: string, ...args: any[]) => any = Module._load;
4 |
5 | Module._load = function _load(module: string, ...args: unknown[]): NodeModule {
6 | let cachedModule = snapshotResult.customRequire.cache[module];
7 |
8 | if (cachedModule) return cachedModule.exports;
9 |
10 | if (snapshotResult.customRequire.definitions[module]) {
11 | cachedModule = {exports: snapshotResult.customRequire(module)};
12 | } else {
13 | cachedModule = {exports: originalLoad(module, ...args)};
14 | }
15 |
16 | snapshotResult.customRequire.cache[module] = cachedModule;
17 | return cachedModule.exports;
18 | };
19 |
20 | snapshotResult.setGlobals(global, process, window, document, console, global.require);
21 | }
22 |
--------------------------------------------------------------------------------
/release.js:
--------------------------------------------------------------------------------
1 | // Packages
2 | const {prompt} = require('inquirer');
3 |
4 | module.exports = async (markdown) => {
5 | const answers = await prompt([
6 | {
7 | name: 'intro',
8 | message: 'One-Line Release Summary'
9 | }
10 | ]);
11 |
12 | const {intro} = answers;
13 |
14 | if (intro === '') {
15 | console.error('Please specify a release summary!');
16 |
17 | process.exit(1);
18 | }
19 |
20 | return `${intro}\n\n${markdown}`;
21 | };
22 |
--------------------------------------------------------------------------------
/test/index.ts:
--------------------------------------------------------------------------------
1 | // Native
2 | import path from 'path';
3 |
4 | // Packages
5 | import test from 'ava';
6 | import fs from 'fs-extra';
7 | import {_electron} from 'playwright';
8 | import type {ElectronApplication} from 'playwright';
9 |
10 | let app: ElectronApplication;
11 |
12 | test.before(async () => {
13 | let pathToBinary;
14 |
15 | switch (process.platform) {
16 | case 'linux':
17 | pathToBinary = path.join(__dirname, '../dist/linux-unpacked/hyper');
18 | break;
19 |
20 | case 'darwin':
21 | pathToBinary = path.join(__dirname, '../dist/mac/Hyper.app/Contents/MacOS/Hyper');
22 | break;
23 |
24 | case 'win32':
25 | pathToBinary = path.join(__dirname, '../dist/win-unpacked/Hyper.exe');
26 | break;
27 |
28 | default:
29 | throw new Error('Path to the built binary needs to be defined for this platform in test/index.js');
30 | }
31 |
32 | app = await _electron.launch({
33 | executablePath: pathToBinary
34 | });
35 | await app.firstWindow();
36 | await new Promise((resolve) => setTimeout(resolve, 5000));
37 | });
38 |
39 | test.after(async () => {
40 | await app
41 | .evaluate(({BrowserWindow}) =>
42 | BrowserWindow.getFocusedWindow()
43 | ?.capturePage()
44 | .then((img) => img.toPNG().toString('base64'))
45 | )
46 | .then((img) => Buffer.from(img || '', 'base64'))
47 | .then(async (imageBuffer) => {
48 | await fs.writeFile(`dist/tmp/${process.platform}_test.png`, imageBuffer);
49 | });
50 | await app.close();
51 | });
52 |
53 | test('see if dev tools are open', async (t) => {
54 | t.false(await app.evaluate(({webContents}) => !!webContents.getFocusedWebContents()?.isDevToolsOpened()));
55 | });
56 |
--------------------------------------------------------------------------------
/test/testUtils/is-hex-color.ts:
--------------------------------------------------------------------------------
1 | function isHexColor(color: string) {
2 | return /(^#[0-9A-F]{6,8}$)|(^#[0-9A-F]{3}$)/i.test(color); // https://regex101.com/
3 | }
4 |
5 | export {isHexColor};
6 |
--------------------------------------------------------------------------------
/test/unit/cli-api.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable eslint-comments/disable-enable-pair */
2 | /* eslint-disable @typescript-eslint/no-unsafe-return */
3 | /* eslint-disable @typescript-eslint/no-unsafe-call */
4 | import test from 'ava';
5 |
6 | // eslint-disable-next-line @typescript-eslint/no-var-requires
7 | const proxyquire = require('proxyquire').noCallThru();
8 |
9 | test('existsOnNpm() builds the url for non-scoped packages', (t) => {
10 | let getUrl: string;
11 | const {existsOnNpm} = proxyquire('../../cli/api', {
12 | got: {
13 | get(url: string) {
14 | getUrl = url;
15 | return Promise.resolve({
16 | body: {
17 | versions: []
18 | }
19 | });
20 | }
21 | },
22 | 'registry-url': () => 'https://registry.npmjs.org/'
23 | });
24 |
25 | return existsOnNpm('pkg').then(() => {
26 | t.is(getUrl, 'https://registry.npmjs.org/pkg');
27 | });
28 | });
29 |
30 | test('existsOnNpm() builds the url for scoped packages', (t) => {
31 | let getUrl: string;
32 | const {existsOnNpm} = proxyquire('../../cli/api', {
33 | got: {
34 | get(url: string) {
35 | getUrl = url;
36 | return Promise.resolve({
37 | body: {
38 | versions: []
39 | }
40 | });
41 | }
42 | },
43 | 'registry-url': () => 'https://registry.npmjs.org/'
44 | });
45 |
46 | return existsOnNpm('@scope/pkg').then(() => {
47 | t.is(getUrl, 'https://registry.npmjs.org/@scope%2fpkg');
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/test/unit/to-electron-background-color.test.ts:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 |
3 | import toElectronBackgroundColor from '../../app/utils/to-electron-background-color';
4 | import {isHexColor} from '../testUtils/is-hex-color';
5 |
6 | test('toElectronBackgroundColor', (t) => {
7 | t.false(false);
8 | });
9 |
10 | test(`returns a color that's in hex`, (t) => {
11 | const hexColor = '#BADA55';
12 | const rgbColor = 'rgb(0,0,0)';
13 | const rgbaColor = 'rgb(0,0,0, 55)';
14 | const hslColor = 'hsl(15, 100%, 50%)';
15 | const hslaColor = 'hsl(15, 100%, 50%, 1)';
16 | const colorKeyword = 'pink';
17 |
18 | t.true(isHexColor(toElectronBackgroundColor(hexColor)));
19 |
20 | t.true(isHexColor(toElectronBackgroundColor(rgbColor)));
21 |
22 | t.true(isHexColor(toElectronBackgroundColor(rgbaColor)));
23 |
24 | t.true(isHexColor(toElectronBackgroundColor(hslColor)));
25 |
26 | t.true(isHexColor(toElectronBackgroundColor(hslaColor)));
27 |
28 | t.true(isHexColor(toElectronBackgroundColor(colorKeyword)));
29 | });
30 |
--------------------------------------------------------------------------------
/test/unit/window-utils.test.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line eslint-comments/disable-enable-pair
2 | /* eslint-disable @typescript-eslint/no-unsafe-call */
3 | import test from 'ava';
4 |
5 | // eslint-disable-next-line @typescript-eslint/no-var-requires
6 | const proxyquire = require('proxyquire').noCallThru();
7 |
8 | test('positionIsValid() returns true when window is on only screen', (t) => {
9 | const position = [50, 50];
10 | const windowUtils = proxyquire('../../app/utils/window-utils', {
11 | electron: {
12 | screen: {
13 | getAllDisplays: () => {
14 | return [
15 | {
16 | workArea: {
17 | x: 0,
18 | y: 0,
19 | width: 500,
20 | height: 500
21 | }
22 | }
23 | ];
24 | }
25 | }
26 | }
27 | });
28 |
29 | const result = windowUtils.positionIsValid(position);
30 |
31 | t.true(result);
32 | });
33 |
34 | test('positionIsValid() returns true when window is on second screen', (t) => {
35 | const position = [750, 50];
36 | const windowUtils = proxyquire('../../app/utils/window-utils', {
37 | electron: {
38 | screen: {
39 | getAllDisplays: () => {
40 | return [
41 | {
42 | workArea: {
43 | x: 0,
44 | y: 0,
45 | width: 500,
46 | height: 500
47 | }
48 | },
49 | {
50 | workArea: {
51 | x: 500,
52 | y: 0,
53 | width: 500,
54 | height: 500
55 | }
56 | }
57 | ];
58 | }
59 | }
60 | }
61 | });
62 |
63 | const result = windowUtils.positionIsValid(position);
64 |
65 | t.true(result);
66 | });
67 |
68 | test('positionIsValid() returns false when position isnt valid', (t) => {
69 | const primaryDisplay = {
70 | workArea: {
71 | x: 0,
72 | y: 0,
73 | width: 500,
74 | height: 500
75 | }
76 | };
77 | const position = [600, 50];
78 | const windowUtils = proxyquire('../../app/utils/window-utils', {
79 | electron: {
80 | screen: {
81 | getAllDisplays: () => {
82 | return [primaryDisplay];
83 | },
84 | getPrimaryDisplay: () => primaryDisplay
85 | }
86 | }
87 | });
88 |
89 | const result = windowUtils.positionIsValid(position);
90 |
91 | t.false(result);
92 | });
93 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "checkJs": false,
5 | "composite": true,
6 | "esModuleInterop": true,
7 | "jsx": "react",
8 | "module": "commonjs",
9 | "moduleResolution": "node",
10 | "preserveConstEnums": true,
11 | "removeComments": false,
12 | "resolveJsonModule": true,
13 | "sourceMap": true,
14 | "strict": true,
15 | "target": "ES2022",
16 | "typeRoots": [
17 | "./node_modules/@types"
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "include": [
4 | "./app/",
5 | "./lib/",
6 | "./test/",
7 | "./cli/",
8 | "./"
9 | ],
10 | "files": [
11 | "./app/index.d.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "./dist/tmp/root/"
5 | },
6 | "include": [
7 | "./app/",
8 | "./lib/",
9 | "./test/",
10 | "./cli/",
11 | "./typings/"
12 | ],
13 | "references": [
14 | {
15 | "path": "./app/tsconfig.json"
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/typings/common.d.ts:
--------------------------------------------------------------------------------
1 | import type {ExecFileOptions, ExecOptions} from 'child_process';
2 |
3 | import type {IpcMain, IpcRenderer} from 'electron';
4 |
5 | import type parseUrl from 'parse-url';
6 |
7 | import type {configOptions} from './config';
8 |
9 | export type Session = {
10 | uid: string;
11 | rows?: number | null;
12 | cols?: number | null;
13 | splitDirection?: 'HORIZONTAL' | 'VERTICAL';
14 | shell: string | null;
15 | pid: number | null;
16 | activeUid?: string;
17 | profile: string;
18 | };
19 |
20 | export type sessionExtraOptions = {
21 | cwd?: string;
22 | splitDirection?: 'HORIZONTAL' | 'VERTICAL';
23 | activeUid?: string | null;
24 | isNewGroup?: boolean;
25 | rows?: number;
26 | cols?: number;
27 | shell?: string;
28 | shellArgs?: string[];
29 | profile?: string;
30 | };
31 |
32 | export type MainEvents = {
33 | close: never;
34 | command: string;
35 | data: {uid: string | null; data: string; escaped?: boolean};
36 | exit: {uid: string};
37 | 'info renderer': {uid: string; type: string};
38 | init: null;
39 | maximize: never;
40 | minimize: never;
41 | new: sessionExtraOptions;
42 | 'open context menu': string;
43 | 'open external': {url: string};
44 | 'open hamburger menu': {x: number; y: number};
45 | 'quit and install': never;
46 | resize: {uid: string; cols: number; rows: number};
47 | unmaximize: never;
48 | };
49 |
50 | export type RendererEvents = {
51 | ready: never;
52 | 'add notification': {text: string; url: string; dismissable: boolean};
53 | 'update available': {releaseNotes: string; releaseName: string; releaseUrl: string; canInstall: boolean};
54 | 'open ssh': ReturnType;
55 | 'open file': {path: string};
56 | 'move jump req': number | 'last';
57 | 'reset fontSize req': never;
58 | 'move left req': never;
59 | 'move right req': never;
60 | 'prev pane req': never;
61 | 'decrease fontSize req': never;
62 | 'increase fontSize req': never;
63 | 'next pane req': never;
64 | 'session break req': never;
65 | 'session quit req': never;
66 | 'session search close': never;
67 | 'session search': never;
68 | 'session stop req': never;
69 | 'session tmux req': never;
70 | 'session del line beginning req': never;
71 | 'session del line end req': never;
72 | 'session del word left req': never;
73 | 'session del word right req': never;
74 | 'session move line beginning req': never;
75 | 'session move line end req': never;
76 | 'session move word left req': never;
77 | 'session move word right req': never;
78 | 'term selectAll': never;
79 | reload: never;
80 | 'session clear req': never;
81 | 'split request horizontal': {activeUid?: string; profile?: string};
82 | 'split request vertical': {activeUid?: string; profile?: string};
83 | 'termgroup add req': {activeUid?: string; profile?: string};
84 | 'termgroup close req': never;
85 | 'session add': Session;
86 | 'session data': string;
87 | 'session exit': {uid: string};
88 | 'windowGeometry change': {isMaximized: boolean};
89 | move: {bounds: {x: number; y: number}};
90 | 'enter full screen': never;
91 | 'leave full screen': never;
92 | 'session data send': {uid: string | null; data: string; escaped?: boolean};
93 | };
94 |
95 | /**
96 | * Get keys of T where the value is not never
97 | */
98 | export type FilterNever = {[K in keyof T]: T[K] extends never ? never : K}[keyof T];
99 |
100 | export interface TypedEmitter {
101 | on(event: E, listener: (args: Events[E]) => void): this;
102 | once(event: E, listener: (args: Events[E]) => void): this;
103 | emit>>(event: E): boolean;
104 | emit>(event: E, data: Events[E]): boolean;
105 | emit(event: E, data?: Events[E]): boolean;
106 | removeListener(event: E, listener: (args: Events[E]) => void): this;
107 | removeAllListeners(event?: E): this;
108 | }
109 |
110 | type OptionalPromise = T | Promise;
111 |
112 | export type IpcCommands = {
113 | 'child_process.exec': (command: string, options: ExecOptions) => {stdout: string; stderr: string};
114 | 'child_process.execFile': (
115 | file: string,
116 | args: string[],
117 | options: ExecFileOptions
118 | ) => {
119 | stdout: string;
120 | stderr: string;
121 | };
122 | getLoadedPluginVersions: () => {name: string; version: string}[];
123 | getPaths: () => {plugins: string[]; localPlugins: string[]};
124 | getBasePaths: () => {path: string; localPath: string};
125 | getDeprecatedConfig: () => Record;
126 | getDecoratedConfig: (profile: string) => configOptions;
127 | getDecoratedKeymaps: () => Record;
128 | };
129 |
130 | export interface IpcMainWithCommands extends IpcMain {
131 | handle(
132 | channel: E,
133 | listener: (
134 | event: Electron.IpcMainInvokeEvent,
135 | ...args: Parameters
136 | ) => OptionalPromise>
137 | ): void;
138 | }
139 |
140 | export interface IpcRendererWithCommands extends IpcRenderer {
141 | invoke(
142 | channel: E,
143 | ...args: Parameters
144 | ): Promise>;
145 | }
146 |
--------------------------------------------------------------------------------
/typings/constants/config.d.ts:
--------------------------------------------------------------------------------
1 | import type {configOptions} from '../config';
2 |
3 | export const CONFIG_LOAD = 'CONFIG_LOAD';
4 | export const CONFIG_RELOAD = 'CONFIG_RELOAD';
5 |
6 | export interface ConfigLoadAction {
7 | type: typeof CONFIG_LOAD;
8 | config: configOptions;
9 | now?: number;
10 | }
11 |
12 | export interface ConfigReloadAction {
13 | type: typeof CONFIG_RELOAD;
14 | config: configOptions;
15 | now: number;
16 | }
17 |
18 | export type ConfigActions = ConfigLoadAction | ConfigReloadAction;
19 |
--------------------------------------------------------------------------------
/typings/constants/index.d.ts:
--------------------------------------------------------------------------------
1 | export const INIT = 'INIT';
2 |
3 | export interface InitAction {
4 | type: typeof INIT;
5 | }
6 |
7 | export type InitActions = InitAction;
8 |
--------------------------------------------------------------------------------
/typings/constants/notifications.d.ts:
--------------------------------------------------------------------------------
1 | export const NOTIFICATION_MESSAGE = 'NOTIFICATION_MESSAGE';
2 | export const NOTIFICATION_DISMISS = 'NOTIFICATION_DISMISS';
3 |
4 | export interface NotificationMessageAction {
5 | type: typeof NOTIFICATION_MESSAGE;
6 | text: string;
7 | url: string | null;
8 | dismissable: boolean;
9 | }
10 | export interface NotificationDismissAction {
11 | type: typeof NOTIFICATION_DISMISS;
12 | id: string;
13 | }
14 |
15 | export type NotificationActions = NotificationMessageAction | NotificationDismissAction;
16 |
--------------------------------------------------------------------------------
/typings/constants/sessions.d.ts:
--------------------------------------------------------------------------------
1 | export const SESSION_ADD = 'SESSION_ADD';
2 | export const SESSION_RESIZE = 'SESSION_RESIZE';
3 | export const SESSION_REQUEST = 'SESSION_REQUEST';
4 | export const SESSION_ADD_DATA = 'SESSION_ADD_DATA';
5 | export const SESSION_PTY_DATA = 'SESSION_PTY_DATA';
6 | export const SESSION_PTY_EXIT = 'SESSION_PTY_EXIT';
7 | export const SESSION_USER_EXIT = 'SESSION_USER_EXIT';
8 | export const SESSION_SET_ACTIVE = 'SESSION_SET_ACTIVE';
9 | export const SESSION_CLEAR_ACTIVE = 'SESSION_CLEAR_ACTIVE';
10 | export const SESSION_USER_DATA = 'SESSION_USER_DATA';
11 | export const SESSION_SET_XTERM_TITLE = 'SESSION_SET_XTERM_TITLE';
12 | export const SESSION_SET_CWD = 'SESSION_SET_CWD';
13 | export const SESSION_SEARCH = 'SESSION_SEARCH';
14 |
15 | export interface SessionAddAction {
16 | type: typeof SESSION_ADD;
17 | uid: string;
18 | shell: string | null;
19 | pid: number | null;
20 | cols: number | null;
21 | rows: number | null;
22 | splitDirection?: 'HORIZONTAL' | 'VERTICAL';
23 | activeUid: string | null;
24 | now: number;
25 | profile: string;
26 | }
27 | export interface SessionResizeAction {
28 | type: typeof SESSION_RESIZE;
29 | uid: string;
30 | cols: number;
31 | rows: number;
32 | isStandaloneTerm: boolean;
33 | now: number;
34 | }
35 | export interface SessionRequestAction {
36 | type: typeof SESSION_REQUEST;
37 | }
38 | export interface SessionAddDataAction {
39 | type: typeof SESSION_ADD_DATA;
40 | }
41 | export interface SessionPtyDataAction {
42 | type: typeof SESSION_PTY_DATA;
43 | data: string;
44 | uid: string;
45 | now: number;
46 | }
47 | export interface SessionPtyExitAction {
48 | type: typeof SESSION_PTY_EXIT;
49 | uid: string;
50 | }
51 | export interface SessionUserExitAction {
52 | type: typeof SESSION_USER_EXIT;
53 | uid: string;
54 | }
55 | export interface SessionSetActiveAction {
56 | type: typeof SESSION_SET_ACTIVE;
57 | uid: string;
58 | }
59 | export interface SessionClearActiveAction {
60 | type: typeof SESSION_CLEAR_ACTIVE;
61 | }
62 | export interface SessionUserDataAction {
63 | type: typeof SESSION_USER_DATA;
64 | }
65 | export interface SessionSetXtermTitleAction {
66 | type: typeof SESSION_SET_XTERM_TITLE;
67 | uid: string;
68 | title: string;
69 | }
70 | export interface SessionSetCwdAction {
71 | type: typeof SESSION_SET_CWD;
72 | cwd: string;
73 | }
74 | export interface SessionSearchAction {
75 | type: typeof SESSION_SEARCH;
76 | uid: string;
77 | value: boolean;
78 | }
79 |
80 | export type SessionActions =
81 | | SessionAddAction
82 | | SessionResizeAction
83 | | SessionRequestAction
84 | | SessionAddDataAction
85 | | SessionPtyDataAction
86 | | SessionPtyExitAction
87 | | SessionUserExitAction
88 | | SessionSetActiveAction
89 | | SessionClearActiveAction
90 | | SessionUserDataAction
91 | | SessionSetXtermTitleAction
92 | | SessionSetCwdAction
93 | | SessionSearchAction;
94 |
--------------------------------------------------------------------------------
/typings/constants/tabs.d.ts:
--------------------------------------------------------------------------------
1 | export const CLOSE_TAB = 'CLOSE_TAB';
2 | export const CHANGE_TAB = 'CHANGE_TAB';
3 |
4 | export interface CloseTabAction {
5 | type: typeof CLOSE_TAB;
6 | }
7 | export interface ChangeTabAction {
8 | type: typeof CHANGE_TAB;
9 | }
10 |
11 | export type TabActions = CloseTabAction | ChangeTabAction;
12 |
--------------------------------------------------------------------------------
/typings/constants/term-groups.d.ts:
--------------------------------------------------------------------------------
1 | export const TERM_GROUP_REQUEST = 'TERM_GROUP_REQUEST';
2 | export const TERM_GROUP_EXIT = 'TERM_GROUP_EXIT';
3 | export const TERM_GROUP_RESIZE = 'TERM_GROUP_RESIZE';
4 | export const TERM_GROUP_EXIT_ACTIVE = 'TERM_GROUP_EXIT_ACTIVE';
5 | export enum DIRECTION {
6 | HORIZONTAL = 'HORIZONTAL',
7 | VERTICAL = 'VERTICAL'
8 | }
9 |
10 | export interface TermGroupRequestAction {
11 | type: typeof TERM_GROUP_REQUEST;
12 | }
13 | export interface TermGroupExitAction {
14 | type: typeof TERM_GROUP_EXIT;
15 | uid: string;
16 | }
17 | export interface TermGroupResizeAction {
18 | type: typeof TERM_GROUP_RESIZE;
19 | uid: string;
20 | sizes: number[];
21 | }
22 | export interface TermGroupExitActiveAction {
23 | type: typeof TERM_GROUP_EXIT_ACTIVE;
24 | }
25 |
26 | export type TermGroupActions =
27 | | TermGroupRequestAction
28 | | TermGroupExitAction
29 | | TermGroupResizeAction
30 | | TermGroupExitActiveAction;
31 |
--------------------------------------------------------------------------------
/typings/constants/ui.d.ts:
--------------------------------------------------------------------------------
1 | export const UI_FONT_SIZE_SET = 'UI_FONT_SIZE_SET';
2 | export const UI_FONT_SIZE_INCR = 'UI_FONT_SIZE_INCR';
3 | export const UI_FONT_SIZE_DECR = 'UI_FONT_SIZE_DECR';
4 | export const UI_FONT_SIZE_RESET = 'UI_FONT_SIZE_RESET';
5 | export const UI_FONT_SMOOTHING_SET = 'UI_FONT_SMOOTHING_SET';
6 | export const UI_MOVE_LEFT = 'UI_MOVE_LEFT';
7 | export const UI_MOVE_RIGHT = 'UI_MOVE_RIGHT';
8 | export const UI_MOVE_TO = 'UI_MOVE_TO';
9 | export const UI_MOVE_NEXT_PANE = 'UI_MOVE_NEXT_PANE';
10 | export const UI_MOVE_PREV_PANE = 'UI_MOVE_PREV_PANE';
11 | export const UI_SHOW_PREFERENCES = 'UI_SHOW_PREFERENCES';
12 | export const UI_WINDOW_MOVE = 'UI_WINDOW_MOVE';
13 | export const UI_WINDOW_MAXIMIZE = 'UI_WINDOW_MAXIMIZE';
14 | export const UI_WINDOW_UNMAXIMIZE = 'UI_WINDOW_UNMAXIMIZE';
15 | export const UI_WINDOW_GEOMETRY_CHANGED = 'UI_WINDOW_GEOMETRY_CHANGED';
16 | export const UI_OPEN_FILE = 'UI_OPEN_FILE';
17 | export const UI_OPEN_SSH_URL = 'UI_OPEN_SSH_URL';
18 | export const UI_OPEN_HAMBURGER_MENU = 'UI_OPEN_HAMBURGER_MENU';
19 | export const UI_WINDOW_MINIMIZE = 'UI_WINDOW_MINIMIZE';
20 | export const UI_WINDOW_CLOSE = 'UI_WINDOW_CLOSE';
21 | export const UI_ENTER_FULLSCREEN = 'UI_ENTER_FULLSCREEN';
22 | export const UI_LEAVE_FULLSCREEN = 'UI_LEAVE_FULLSCREEN';
23 | export const UI_CONTEXTMENU_OPEN = 'UI_CONTEXTMENU_OPEN';
24 | export const UI_COMMAND_EXEC = 'UI_COMMAND_EXEC';
25 |
26 | export interface UIFontSizeSetAction {
27 | type: typeof UI_FONT_SIZE_SET;
28 | value: number;
29 | }
30 | export interface UIFontSizeIncrAction {
31 | type: typeof UI_FONT_SIZE_INCR;
32 | }
33 | export interface UIFontSizeDecrAction {
34 | type: typeof UI_FONT_SIZE_DECR;
35 | }
36 | export interface UIFontSizeResetAction {
37 | type: typeof UI_FONT_SIZE_RESET;
38 | }
39 | export interface UIFontSmoothingSetAction {
40 | type: typeof UI_FONT_SMOOTHING_SET;
41 | fontSmoothing: string;
42 | }
43 | export interface UIMoveLeftAction {
44 | type: typeof UI_MOVE_LEFT;
45 | }
46 | export interface UIMoveRightAction {
47 | type: typeof UI_MOVE_RIGHT;
48 | }
49 | export interface UIMoveToAction {
50 | type: typeof UI_MOVE_TO;
51 | }
52 | export interface UIMoveNextPaneAction {
53 | type: typeof UI_MOVE_NEXT_PANE;
54 | }
55 | export interface UIMovePrevPaneAction {
56 | type: typeof UI_MOVE_PREV_PANE;
57 | }
58 | export interface UIShowPreferencesAction {
59 | type: typeof UI_SHOW_PREFERENCES;
60 | }
61 | export interface UIWindowMoveAction {
62 | type: typeof UI_WINDOW_MOVE;
63 | }
64 | export interface UIWindowMaximizeAction {
65 | type: typeof UI_WINDOW_MAXIMIZE;
66 | }
67 | export interface UIWindowUnmaximizeAction {
68 | type: typeof UI_WINDOW_UNMAXIMIZE;
69 | }
70 | export interface UIWindowGeometryChangedAction {
71 | type: typeof UI_WINDOW_GEOMETRY_CHANGED;
72 | isMaximized: boolean;
73 | }
74 | export interface UIOpenFileAction {
75 | type: typeof UI_OPEN_FILE;
76 | }
77 | export interface UIOpenSshUrlAction {
78 | type: typeof UI_OPEN_SSH_URL;
79 | }
80 | export interface UIOpenHamburgerMenuAction {
81 | type: typeof UI_OPEN_HAMBURGER_MENU;
82 | }
83 | export interface UIWindowMinimizeAction {
84 | type: typeof UI_WINDOW_MINIMIZE;
85 | }
86 | export interface UIWindowCloseAction {
87 | type: typeof UI_WINDOW_CLOSE;
88 | }
89 | export interface UIEnterFullscreenAction {
90 | type: typeof UI_ENTER_FULLSCREEN;
91 | }
92 | export interface UILeaveFullscreenAction {
93 | type: typeof UI_LEAVE_FULLSCREEN;
94 | }
95 | export interface UIContextmenuOpenAction {
96 | type: typeof UI_CONTEXTMENU_OPEN;
97 | }
98 | export interface UICommandExecAction {
99 | type: typeof UI_COMMAND_EXEC;
100 | command: string;
101 | }
102 |
103 | export type UIActions =
104 | | UIFontSizeSetAction
105 | | UIFontSizeIncrAction
106 | | UIFontSizeDecrAction
107 | | UIFontSizeResetAction
108 | | UIFontSmoothingSetAction
109 | | UIMoveLeftAction
110 | | UIMoveRightAction
111 | | UIMoveToAction
112 | | UIMoveNextPaneAction
113 | | UIMovePrevPaneAction
114 | | UIShowPreferencesAction
115 | | UIWindowMoveAction
116 | | UIWindowMaximizeAction
117 | | UIWindowUnmaximizeAction
118 | | UIWindowGeometryChangedAction
119 | | UIOpenFileAction
120 | | UIOpenSshUrlAction
121 | | UIOpenHamburgerMenuAction
122 | | UIWindowMinimizeAction
123 | | UIWindowCloseAction
124 | | UIEnterFullscreenAction
125 | | UILeaveFullscreenAction
126 | | UIContextmenuOpenAction
127 | | UICommandExecAction;
128 |
--------------------------------------------------------------------------------
/typings/constants/updater.d.ts:
--------------------------------------------------------------------------------
1 | export const UPDATE_INSTALL = 'UPDATE_INSTALL';
2 | export const UPDATE_AVAILABLE = 'UPDATE_AVAILABLE';
3 |
4 | export interface UpdateInstallAction {
5 | type: typeof UPDATE_INSTALL;
6 | }
7 | export interface UpdateAvailableAction {
8 | type: typeof UPDATE_AVAILABLE;
9 | version: string;
10 | notes: string | null;
11 | releaseUrl: string;
12 | canInstall: boolean;
13 | }
14 |
15 | export type UpdateActions = UpdateInstallAction | UpdateAvailableAction;
16 |
--------------------------------------------------------------------------------
/typings/ext-modules.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'php-escape-shell' {
2 | export function php_escapeshellcmd(path: string): string;
3 | }
4 |
5 | declare module 'git-describe' {
6 | export function gitDescribe(...args: any[]): void;
7 | }
8 |
9 | declare module 'default-shell' {
10 | const val: string;
11 | export default val;
12 | }
13 |
14 | declare module 'sudo-prompt' {
15 | export function exec(
16 | cmd: string,
17 | options: {name?: string; icns?: string; env?: {[key: string]: string}},
18 | callback: (error?: Error, stdout?: string | Buffer, stderr?: string | Buffer) => void
19 | ): void;
20 | }
21 |
--------------------------------------------------------------------------------
/typings/extend-electron.d.ts:
--------------------------------------------------------------------------------
1 | import type {Server} from '../app/rpc';
2 |
3 | declare global {
4 | namespace Electron {
5 | interface App {
6 | config: typeof import('../app/config');
7 | plugins: typeof import('../app/plugins');
8 | getWindows: () => Set;
9 | getLastFocusedWindow: () => BrowserWindow | null;
10 | windowCallback?: (win: BrowserWindow) => void;
11 | createWindow: (
12 | fn?: (win: BrowserWindow) => void,
13 | options?: {size?: [number, number]; position?: [number, number]},
14 | profileName?: string
15 | ) => BrowserWindow;
16 | setVersion: (version: string) => void;
17 | }
18 |
19 | // type Server = import('./rpc').Server;
20 | interface BrowserWindow {
21 | uid: string;
22 | sessions: Map;
23 | focusTime: number;
24 | clean: () => void;
25 | rpc: Server;
26 | profileName: string;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------