Compare commits

..

No commits in common. "canary" and "v4.0.0-canary.2" have entirely different histories.

137 changed files with 6968 additions and 157648 deletions

View file

@ -3,9 +3,7 @@
"react", "react",
"prettier", "prettier",
"@typescript-eslint", "@typescript-eslint",
"eslint-comments", "eslint-comments"
"lodash",
"import"
], ],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
@ -34,11 +32,7 @@
"settings": { "settings": {
"react": { "react": {
"version": "detect" "version": "detect"
}, }
"import/resolver": {
"typescript": {}
},
"import/internal-regex": "^(electron|react)$"
}, },
"rules": { "rules": {
"func-names": [ "func-names": [
@ -65,16 +59,7 @@
"bracketSameLine": false "bracketSameLine": false
} }
], ],
"eslint-comments/no-unused-disable": "error", "eslint-comments/no-unused-disable": "error"
"react/no-unknown-property":[
"error",
{
"ignore": [
"jsx",
"global"
]
}
]
}, },
"overrides": [ "overrides": [
{ {
@ -98,33 +83,7 @@
"@typescript-eslint/no-shadow": ["error"], "@typescript-eslint/no-shadow": ["error"],
"@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/restrict-template-expressions": "off", "@typescript-eslint/restrict-template-expressions": "off"
"@typescript-eslint/consistent-type-imports": [ "error", { "disallowTypeAnnotations": false } ],
"lodash/prop-shorthand": [ "error", "always" ],
"lodash/import-scope": [ "error", "method" ],
"lodash/collection-return": "error",
"lodash/collection-method-value": "error",
"import/no-extraneous-dependencies": "error",
"import/no-anonymous-default-export": "error",
"import/order": [
"error",
{
"groups": [
"builtin",
"internal",
"external",
"parent",
"sibling",
"index"
],
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"orderImportKind": "desc",
"caseInsensitive": true
}
}
]
} }
}, },
{ {

View file

@ -1,137 +0,0 @@
name: 'Build Linux ARM'
description: 'Cross-compiles Hyper app for ARMv7l and ARM64 using arm-runner'
inputs:
node-version:
description: 'Node.js version to use'
required: true
matrix-name:
description: 'Matrix name (arch)'
required: true
matrix-cpu:
description: 'CPU architecture'
required: true
matrix-image:
description: 'Base OS image for ARM emulation'
required: true
upload-artifact:
description: 'Whether to upload artifacts'
required: false
default: 'true'
runs:
using: 'composite'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Fix node-gyp and Python
shell: bash
run: |
python3 -m pip install packaging setuptools
- name: Get yarn cache directory path
shell: bash
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache/restore@v4
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock', 'app/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Delete old electron headers
# These can sometimes be present on a freshly-provisioned github runner, so remove them
shell: bash
run: |
rm -rf ~/.electron-gyp || true
- name: Install dependencies and tools
shell: bash
run: |
yarn install
sudo apt update
sudo apt install libarchive-tools
- name: Rebuild node-pty and all native modules for ARM
uses: pguyot/arm-runner-action@v2.6.5
with:
image_additional_mb: 2000
base_image: ${{ inputs.matrix-image }}
cpu: ${{ inputs.matrix-cpu }}
shell: bash
copy_artifact_path: target
copy_artifact_dest: target
commands: |
# Install Python distutils in the ARM container
sudo apt-get update
sudo apt-get install -y python3-pip python3-setuptools python3-dev
pip3 install setuptools --break-system-packages || pip3 install setuptools
cd target
# TODO upgrade node to 20.11.0 to match NODE_VERSION
wget https://nodejs.org/dist/v18.16.0/node-v18.16.0-linux-${{ inputs.matrix-name }}.tar.xz
tar -xJf node-v18.16.0-linux-${{ inputs.matrix-name }}.tar.xz
sudo cp node-v18.16.0-linux-${{ inputs.matrix-name }}/* /usr/local/ -R
rm node-v18.16.0-linux-${{ inputs.matrix-name }}.tar.xz
cd ..
npm run rebuild-node-pty
cd target
npx electron-rebuild
- name: Compile
shell: bash
run: yarn run build
- name: Chown rebuilt node_modules
shell: bash
run: |
sudo chown -R $USER:$USER target/node_modules
- name: Prepare v8 snapshot (only armv7l)
if: ${{ inputs.matrix-name == 'armv7l' }}
shell: bash
run: |
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install -y libglib2.0-0:i386 libexpat1:i386 libgcc-s1:i386
npm_config_arch=armv7l yarn run v8-snapshot:arch
- name: Build final Electron App for ARM
shell: bash
run: |
yarn run electron-builder -p never -l deb rpm AppImage pacman --${{ inputs.matrix-name }} -c electron-builder-linux-ci.json
env:
GH_TOKEN: ${{ env.GH_TOKEN }}
- name: Archive Build Artifacts
if: ${{ inputs.upload-artifact == 'true' }}
uses: actions/upload-artifact@v4
with:
name: hyper-linux-${{ inputs.matrix-name }}
path: |
dist/*.snap
dist/*.AppImage
dist/*.deb
dist/*.rpm
dist/*.pacman
# - name: Run E2E Tests on Linux
# if: runner.os == 'Linux'
# uses: GabrielBB/xvfb-action@v1.7
# with:
# run: |
# yarn run test
#

View file

@ -1,172 +0,0 @@
name: 'Build'
description: 'Builds, tests, and packages the app for release'
inputs:
node-version:
description: 'Node.js version to use'
required: true
matrix-name:
description: 'Matrix Name'
required: true
matrix-os:
description: 'Matrix OS'
required: true
upload-artifact:
description: 'Whether to upload artifacts'
required: false
default: 'true'
runs:
using: 'composite'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Fix node-gyp and Python
shell: bash
run: |
if [[ "$RUNNER_OS" == "macOS" ]]; then
brew install python-setuptools python-packaging
else
python3 -m pip install $EXTRA_ARGS packaging setuptools
fi
- name: Get yarn cache directory path
shell: bash
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache/restore@v4
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock', 'app/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Delete old electron headers
# These can sometimes be present on a freshly-provisioned github runner, so remove them
shell: bash
run: |
rm -rf ~/.electron-gyp || true
- name: Install
shell: bash
run: yarn install
env:
npm_config_node_gyp: ${{ github.workspace }}${{ runner.os == 'Windows' && '\node_modules\node-gyp\bin\node-gyp.js' || '/node_modules/node-gyp/bin/node-gyp.js' }}
- name: Install libarchive-tools
shell: bash
if: runner.os == 'Linux'
run: |
sudo apt update
sudo apt install libarchive-tools
- name: Rebuild native modules for Electron
shell: bash
run: npx electron-rebuild
- name: Lint and Run Unit Tests
shell: bash
run: yarn run test
- name: Getting Build Icon
shell: bash
if: github.ref == 'refs/heads/canary' || github.base_ref == 'canary'
run: |
cp build/canary.ico build/icon.ico
cp build/canary.icns build/icon.icns
- name: Build
shell: bash
run: |
if [ -z "$CSC_LINK" ] ; then unset CSC_LINK ; fi
if [ -z "$CSC_KEY_PASSWORD" ] ; then unset CSC_KEY_PASSWORD ; fi
if [ -z "$WIN_CSC_LINK" ] ; then unset WIN_CSC_LINK ; fi
if [ -z "$WIN_CSC_KEY_PASSWORD" ] ; then unset WIN_CSC_KEY_PASSWORD ; fi
if [ -z "$APPLE_ID" ] ; then unset APPLE_ID ; fi
if [ -z "$APPLE_APP_SPECIFIC_PASSWORD" ] ; then unset APPLE_APP_SPECIFIC_PASSWORD ; fi
yarn run dist
env:
GH_TOKEN: ${{ env.GH_TOKEN }}
CSC_LINK: ${{ env.MAC_CERT_P12_BASE64 }}
CSC_KEY_PASSWORD: ${{ env.MAC_CERT_P12_PASSWORD }}
WIN_CSC_LINK: ${{ env.WIN_CERT_P12_BASE64 }}
WIN_CSC_KEY_PASSWORD: ${{ env.WIN_CERT_P12_PASSWORD }}
APPLE_ID: ${{ env.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ env.APPLE_PASSWORD }}
- name: Archive Build Artifacts
uses: actions/upload-artifact@v4
with:
name: hyper-${{ runner.os }}-${{ inputs.matrix-name }}
path: |
dist/*.dmg
dist/*.snap
dist/*.AppImage
dist/*.deb
dist/*.pacman
dist/*.exe
- name: Run E2E Tests
shell: bash
if: runner.os != 'Linux'
run: yarn run test:e2e --verbose
- name: Run E2E Tests on Linux
if: runner.os == 'Linux'
uses: GabrielBB/xvfb-action@v1.7
with:
run: |
yarn run test:e2e
env:
SHELL: /bin/bash
DEBUG: "pw:browser*"
- name: Archive E2E test screenshot
uses: actions/upload-artifact@v4
with:
name: e2e-${{ inputs.matrix-os }}-${{ strategy.job-index }}
path: dist/tmp/*.png
- name: Save the pr number in an artifact
shell: bash
if: github.event_name == 'pull_request'
env:
PR_NUM: ${{ github.event.number }}
run: echo $PR_NUM > pr_num.txt
- name: Upload the pr num
uses: actions/upload-artifact@v4
if: github.event_name == 'pull_request'
with:
name: pr_num
path: ./pr_num.txt
- uses: actions/cache/save@v4
if: github.event_name == 'push'
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock', 'app/yarn.lock') }}
# - name: Run E2E Tests (non-Linux)
# if: runner.os != 'Linux'
# shell: bash
# run: yarn run test:e2e --verbose
#
# - name: Run E2E Tests on Linux
# if: runner.os == 'Linux'
# uses: GabrielBB/xvfb-action@v1.7
# with:
# run: |
# yarn run test
#

View file

@ -5,31 +5,17 @@ updates:
schedule: schedule:
interval: weekly interval: weekly
time: '11:00' time: '11:00'
open-pull-requests-limit: 30
target-branch: canary target-branch: canary
versioning-strategy: increase versioning-strategy: increase
commit-message:
prefix: "chore(deps-dev):"
groups:
minorAndPatch:
update-types:
- "minor"
- "patch"
open-pull-requests-limit: 100
- package-ecosystem: npm - package-ecosystem: npm
directory: "/app" directory: "/app"
schedule: schedule:
interval: weekly interval: weekly
time: '11:00' time: '11:00'
open-pull-requests-limit: 30
target-branch: canary target-branch: canary
versioning-strategy: increase versioning-strategy: increase
commit-message:
prefix: "chore(deps):"
groups:
minorAndPatch:
update-types:
- "minor"
- "patch"
open-pull-requests-limit: 100
- package-ecosystem: github-actions - package-ecosystem: github-actions
directory: "/" directory: "/"
schedule: schedule:

View file

@ -1 +1,8 @@
<!-- Please check `Allow edits from maintainers`. Thanks! --> <!-- Hi there! Thanks for submitting a PR! We're excited to see what you've got for us.
- To help whoever reviews your PR, it'd be extremely helpful for you to list whether your PR is ready to be merged,
If there's anything left to do and if there are any related PRs
- It'd also be extremely helpful to enable us to update your PR incase we need to rebase or what-not by checking `Allow edits from maintainers`
- If your PR changes some API, please make a PR for hyper website too: https://github.com/vercel/hyper-site.
Thanks, again! -->

View file

@ -1,154 +0,0 @@
name: Node CI
on:
push:
branches:
workflow_dispatch:
defaults:
run:
shell: bash
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: 20.11.0
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# CSC_LINK: ${{ secrets.MAC_CERT_P12_BASE64 }}
# CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_P12_PASSWORD }}
# WIN_CSC_LINK: ${{ secrets.WIN_CERT_P12_BASE64 }}
# WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CERT_P12_PASSWORD }}
# APPLE_ID: ${{ secrets.APPLE_ID }}
# APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
jobs:
build-ubuntu:
name: Build Ubuntu
runs-on: ubuntu-latest
concurrency:
group: build-ubuntu-${{ github.ref }}
cancel-in-progress: false
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build
uses: ./.github/actions/build
env:
NODE_VERSION: ${{ env.NODE_VERSION }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
node-version: ${{ env.NODE_VERSION }}
matrix-os: ubuntu-latest
matrix-name: ubuntu
upload-artifact: false
build-macos:
name: Build macOS
runs-on: macos-latest
concurrency:
group: build-macos-${{ github.ref }}
cancel-in-progress: false
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build
uses: ./.github/actions/build
env:
NODE_VERSION: ${{ env.NODE_VERSION }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
node-version: ${{ env.NODE_VERSION }}
matrix-os: macos-latest
matrix-name: macos
upload-artifact: false
build-windows:
name: Build Windows
runs-on: windows-latest
concurrency:
group: build-win-${{ github.ref }}
cancel-in-progress: false
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build
uses: ./.github/actions/build
env:
NODE_VERSION: ${{ env.NODE_VERSION }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
node-version: ${{ env.NODE_VERSION }}
matrix-os: windows-latest
matrix-name: win
upload-artifact: false
# ARM Linux:
build-linux-armv7l:
name: Build Linux ARMv7l
runs-on: ubuntu-latest
concurrency:
group: build-linux-armv7l-${{ github.ref }}
cancel-in-progress: false
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up QEMU and binfmt
run: |
# Install qemu-user-static
sudo apt-get update
sudo apt-get install -y qemu-user-static binfmt-support python3-setuptools
# Install distutils for node-gyp
sudo apt-get install -y python3-distutils || sudo apt-get install -y python3-dev python-is-python3
# Download and install binfmt configurations
wget https://github.com/qemu/qemu/raw/master/scripts/qemu-binfmt-conf.sh
chmod +x qemu-binfmt-conf.sh
sudo ./qemu-binfmt-conf.sh --qemu-path /usr/bin --qemu-suffix -static --debian
# Import the binfmt configurations
sudo update-binfmts --import qemu-arm
sudo update-binfmts --import qemu-aarch64
# Verify they exist
ls -la /proc/sys/fs/binfmt_misc/qemu-arm* || true
- name: Build ARMv7l
uses: ./.github/actions/build-linux-arm
env:
NODE_VERSION: ${{ env.NODE_VERSION }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
node-version: ${{ env.NODE_VERSION }}
matrix-name: armv7l
matrix-cpu: cortex-a8
matrix-image: raspios_lite:latest
upload-artifact: false
build-linux-arm64:
name: Build Linux ARM64
runs-on: ubuntu-latest
concurrency:
group: build-linux-arm64-${{ github.ref }}
cancel-in-progress: false
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build ARM64
uses: ./.github/actions/build-linux-arm
env:
NODE_VERSION: ${{ env.NODE_VERSION }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
node-version: ${{ env.NODE_VERSION }}
matrix-name: arm64
matrix-cpu: cortex-a53
matrix-image: raspios_lite_arm64:latest
upload-artifact: false

View file

@ -35,11 +35,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -50,7 +50,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v3 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@ -64,4 +64,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v2

View file

@ -14,14 +14,14 @@ jobs:
WORKFLOW_RUN_INFO: ${{ toJSON(github.event.workflow_run) }} WORKFLOW_RUN_INFO: ${{ toJSON(github.event.workflow_run) }}
run: echo "$WORKFLOW_RUN_INFO" run: echo "$WORKFLOW_RUN_INFO"
- name: Download Artifacts - name: Download Artifacts
uses: dawidd6/action-download-artifact@v6 uses: dawidd6/action-download-artifact@v2.24.2
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: nodejs.yml workflow: nodejs.yml
run_id: ${{ github.event.workflow_run.id }} run_id: ${{ github.event.workflow_run.id }}
name: e2e name: e2e
- name: Get PR number - name: Get PR number
uses: dawidd6/action-download-artifact@v6 uses: dawidd6/action-download-artifact@v2.24.2
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: nodejs.yml workflow: nodejs.yml
@ -29,7 +29,7 @@ jobs:
name: pr_num name: pr_num
- name: Read the pr_num file - name: Read the pr_num file
id: pr_num_reader id: pr_num_reader
uses: juliangruber/read-file-action@v1.1.7 uses: juliangruber/read-file-action@v1.1.6
with: with:
path: ./pr_num.txt path: ./pr_num.txt
- name: List images - name: List images

81
.github/workflows/nodejs.yml vendored Normal file
View file

@ -0,0 +1,81 @@
name: Node CI
on:
push:
branches:
- master
- canary
pull_request:
defaults:
run:
shell: bash
jobs:
build:
runs-on: ${{matrix.os}}
strategy:
matrix:
node-version: [16.x]
os:
- macos-12
- ubuntu-18.04
- windows-latest
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: yarn
cache-dependency-path: |
yarn.lock
app/yarn.lock
- name: Install
run: yarn install
- name: Lint and Run Unit Tests
run: yarn run test
- name: Getting Build Icon
if: github.ref == 'refs/heads/canary' || github.base_ref == 'canary'
run: |
cp build/canary.ico build/icon.ico
cp build/canary.icns build/icon.icns
- name: Build
run: if [ ! -z "$CSC_LINK" ] ; then yarn run dist ; else unset CSC_LINK && unset WIN_CSC_LINK && yarn run dist --publish=never ; fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.MAC_CERT_P12_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_P12_PASSWORD }}
WIN_CSC_LINK: ${{ secrets.WIN_CERT_P12_BASE64 }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CERT_P12_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
- name: Archive Build Artifacts
uses: LabhanshAgrawal/upload-artifact@v3
with:
path: |
dist/*.dmg
dist/*.snap
dist/*.AppImage
dist/*.deb
dist/*.rpm
dist/*.exe
- name: Run E2E Tests
if: runner.os != 'Linux'
run: yarn run test:e2e
- name: Archive E2E test screenshot
if: runner.os != 'Linux'
uses: actions/upload-artifact@v3
with:
name: e2e
path: dist/tmp/*.png
- name: Save the pr number in an artifact
if: github.event_name == 'pull_request'
env:
PR_NUM: ${{ github.event.number }}
run: echo $PR_NUM > pr_num.txt
- name: Upload the pr num
uses: actions/upload-artifact@v3
if: github.event_name == 'pull_request'
with:
name: pr_num
path: ./pr_num.txt

View file

@ -1,194 +0,0 @@
name: Create Release
on:
push:
tags:
- "v*"
workflow_dispatch:
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: 20.11.0
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.MAC_CERT_P12_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_P12_PASSWORD }}
WIN_CSC_LINK: ${{ secrets.WIN_CERT_P12_BASE64 }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CERT_P12_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
jobs:
build-ubuntu:
name: Build Ubuntu
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build
uses: ./.github/actions/build
env:
NODE_VERSION: ${{ env.NODE_VERSION }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
node-version: ${{ env.NODE_VERSION }}
matrix-os: ubuntu-latest
matrix-name: ubuntu
upload-artifact: true
build-macos:
name: Build macOS
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build
uses: ./.github/actions/build
env:
NODE_VERSION: ${{ env.NODE_VERSION }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
node-version: ${{ env.NODE_VERSION }}
matrix-os: macos-latest
matrix-name: macos
upload-artifact: true
build-windows:
name: Build Windows
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build
uses: ./.github/actions/build
env:
NODE_VERSION: ${{ env.NODE_VERSION }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
node-version: ${{ env.NODE_VERSION }}
matrix-os: windows-latest
matrix-name: win
upload-artifact: true
# ARM Linux:
build-linux-armv7l:
name: Build Linux ARMv7l
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up QEMU and binfmt
run: |
# Install qemu-user-static
sudo apt-get update
sudo apt-get install -y qemu-user-static binfmt-support python3-setuptools
# Install distutils for node-gyp
sudo apt-get install -y python3-distutils || sudo apt-get install -y python3-dev python-is-python3
# Download and install binfmt configurations
wget https://github.com/qemu/qemu/raw/master/scripts/qemu-binfmt-conf.sh
chmod +x qemu-binfmt-conf.sh
sudo ./qemu-binfmt-conf.sh --qemu-path /usr/bin --qemu-suffix -static --debian
# Import the binfmt configurations
sudo update-binfmts --import qemu-arm
sudo update-binfmts --import qemu-aarch64
# Verify they exist
ls -la /proc/sys/fs/binfmt_misc/qemu-arm* || true
- name: Build ARMv7l
uses: ./.github/actions/build-linux-arm
env:
NODE_VERSION: ${{ env.NODE_VERSION }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
node-version: ${{ env.NODE_VERSION }}
matrix-name: armv7l
matrix-cpu: cortex-a8
matrix-image: raspios_lite:latest
upload-artifact: true
build-linux-arm64:
name: Build Linux ARM64
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build ARM64
uses: ./.github/actions/build-linux-arm
env:
NODE_VERSION: ${{ env.NODE_VERSION }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
node-version: ${{ env.NODE_VERSION }}
matrix-name: arm64
matrix-cpu: cortex-a53
matrix-image: raspios_lite_arm64:latest
upload-artifact: true
###
upload-release:
name: Upload and Create Release
needs:
- build-ubuntu
- build-macos
- build-windows
- build-linux-armv7l
- build-linux-arm64
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: ./artifacts
- name: List downloaded artifacts
run: ls -R ./artifacts
- name: Ensure non-empty artifacts
run: |
echo "Checking for zero-byte files in artifacts/..."
find artifacts/ -type f -empty -print -delete
echo "Cleaned up any zero-byte files."
echo "Remaining files:"
ls -R artifacts/
- name: Dump release assets for debugging
if: always()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GitHub CLI needs this
run: |
echo "Fetching release assets for tag: ${{ github.ref_name }}"
gh release view "${{ github.ref_name }}" --repo "${{ github.repository }}" --json assets --jq '.assets[] | "\(.name) - \(.url)"' || true
- name: Create GitHub Release
# Pinned to work around https://github.com/softprops/action-gh-release/issues/445
uses: softprops/action-gh-release@v2.0.5
env:
NODE_VERSION: ${{ env.NODE_VERSION }}
ACTIONS_STEP_DEBUG: true
with:
preserve_order: true
overwrite: true
name: Release ${{ github.ref_name }}
tag_name: ${{ github.ref }}
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
files: |
artifacts/**/*.dmg
artifacts/**/*.snap
artifacts/**/*.AppImage
artifacts/**/*.deb
artifacts/**/*.pacman
artifacts/**/*.exe

3
.husky/pre-push Normal file → Executable file
View file

@ -1 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn test yarn test

View file

@ -1 +0,0 @@
20.11.0

1
.nvmrc
View file

@ -1 +0,0 @@
20.11.0

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
yarnPath: .yarn/releases/yarn-classic.cjs

View file

@ -1,24 +1,19 @@
![](https://assets.vercel.com/image/upload/v1549723846/repositories/hyper/hyper-3-repo-banner.png)
<p align="center"> <p align="center">
<img alt="hyper - modern web-based terminal" height=150 src="https://github.com/user-attachments/assets/3096f20a-8116-45ce-8c5e-0f1106107484"> <a aria-label="Vercel logo" href="https://vercel.com">
</p> <img src="https://img.shields.io/badge/MADE%20BY%20Vercel-000000.svg?style=for-the-badge&logo=vercel&labelColor=000000&logoWidth=20">
</a>
<p> </p>
<a aria-label="Vercel logo" href="https://vercel.com"><img
src="https://img.shields.io/badge/MADE%20BY%20Vercel-000000.svg?style=for-the-badge&logo=vercel&labelColor=000000&logoWidth=20"
/></a> <a aria-label="Quine logo" href="https://quineglobal.com"><img
width="143" height="28" alt="forked-by-quine" src="https://github.com/user-attachments/assets/57decaa2-7d8c-4d13-ada7-ff6b964346f7"
/></a>
</p>
[![Node CI](https://github.com/quine-global/hyper/actions/workflows/ci.yml/badge.svg?branch=canary)](https://github.com/quine-global/hyper/actions/workflows/ci.yml)
[![Node CI](https://github.com/vercel/hyper/workflows/Node%20CI/badge.svg?event=push)](https://github.com/vercel/hyper/actions?query=workflow%3A%22Node+CI%22+branch%3Acanary+event%3Apush)
[![Changelog #213](https://img.shields.io/badge/changelog-%23213-lightgrey.svg)](https://changelog.com/213) [![Changelog #213](https://img.shields.io/badge/changelog-%23213-lightgrey.svg)](https://changelog.com/213)
For more details, head to: https://hyper.is For more details, head to: https://hyper.is
## Project goals ## Project goals
The goal of the project is to create a beautiful and customizable experience for command-line interface users, built on open web standards. We have picked up where Vercel left off, and intend to first offer stability, followed by a more tailored experience. We will still support customizability, but stability of theming APIs is not a goal. The goal of the project is to create a beautiful and extensible experience for command-line interface users, built on open web standards. In the beginning, our focus will be primarily around speed, stability and the development of the correct API for extension authors.
In the future, we anticipate the community will come up with innovative additions to enhance what could be the simplest, most powerful and well-tested interface for productivity. 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.

View file

@ -1,6 +1,5 @@
import {EventEmitter} from 'events';
import fetch from 'electron-fetch'; import fetch from 'electron-fetch';
import {EventEmitter} from 'events';
class AutoUpdater extends EventEmitter implements Electron.AutoUpdater { class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
updateURL!: string; updateURL!: string;
@ -27,12 +26,13 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
this.emit('update-not-available'); this.emit('update-not-available');
return; return;
} }
return res.json().then(({name, notes, pub_date}: {name: string; notes: string; pub_date: string}) => { return res.json().then(({name, notes, pub_date}) => {
// Only name is mandatory, needed to construct release URL. // Only name is mandatory, needed to construct release URL.
if (!name) { if (!name) {
throw new Error('Malformed server response: release name is missing.'); throw new Error('Malformed server response: release name is missing.');
} }
const date = pub_date ? new Date(pub_date) : new Date(); // If `null` is passed to Date constructor, current time will be used. This doesn't work with `undefined`
const date = new Date(pub_date || null);
this.emit('update-available', {}, notes, name, date); this.emit('update-available', {}, notes, name, date);
}); });
}) })
@ -47,6 +47,4 @@ class AutoUpdater extends EventEmitter implements Electron.AutoUpdater {
} }
} }
const autoUpdaterLinux = new AutoUpdater(); export default new AutoUpdater();
export default autoUpdaterLinux;

View file

@ -1,6 +1,4 @@
import {app, Menu} from 'electron'; import {app, Menu, BrowserWindow} from 'electron';
import type {BrowserWindow} from 'electron';
import {openConfig, getConfig} from './config'; import {openConfig, getConfig} from './config';
import {updatePlugins} from './plugins'; import {updatePlugins} from './plugins';
import {installCLI} from './utils/cli-install'; import {installCLI} from './utils/cli-install';
@ -146,22 +144,6 @@ const commands: Record<string, (focusedWindow?: BrowserWindow) => void> = {
}; };
}); });
//Profile specific commands
getConfig().profiles.forEach((profile) => {
commands[`window:new:${profile.name}`] = () => {
setTimeout(() => app.createWindow(undefined, undefined, profile.name), 0);
};
commands[`tab:new:${profile.name}`] = (focusedWindow) => {
focusedWindow?.rpc.emit('termgroup add req', {profile: profile.name});
};
commands[`pane:splitRight:${profile.name}`] = (focusedWindow) => {
focusedWindow?.rpc.emit('split request vertical', {profile: profile.name});
};
commands[`pane:splitDown:${profile.name}`] = (focusedWindow) => {
focusedWindow?.rpc.emit('split request horizontal', {profile: profile.name});
};
});
export const execCommand = (command: string, focusedWindow?: BrowserWindow) => { export const execCommand = (command: string, focusedWindow?: BrowserWindow) => {
const fn = commands[command]; const fn = commands[command];
if (fn) { if (fn) {

View file

@ -1,14 +1,11 @@
import {app} from 'electron';
import chokidar from 'chokidar'; import chokidar from 'chokidar';
import notify from './notify';
import type {parsedConfig, configOptions} from '../typings/config';
import {_import, getDefaultConfig} from './config/import'; import {_import, getDefaultConfig} from './config/import';
import _openConfig from './config/open'; import _openConfig from './config/open';
import {cfgPath, cfgDir} from './config/paths'; import {cfgPath, cfgDir} from './config/paths';
import notify from './notify';
import {getColorMap} from './utils/colors'; import {getColorMap} from './utils/colors';
import {parsedConfig, configOptions} from '../lib/config';
import {app} from 'electron';
const watchers: Function[] = []; const watchers: Function[] = [];
let cfg: parsedConfig = {} as any; let cfg: parsedConfig = {} as any;
@ -81,30 +78,8 @@ export const getConfigDir = () => {
return cfgDir; return cfgDir;
}; };
export const getDefaultProfile = () => {
return cfg.config.defaultProfile || cfg.config.profiles[0]?.name || 'default';
};
// get config for the default profile, keeping it for backward compatibility
export const getConfig = () => { export const getConfig = () => {
return getProfileConfig(getDefaultProfile()); return cfg.config;
};
export const getProfiles = () => {
return cfg.config.profiles;
};
export const getProfileConfig = (profileName: string): configOptions => {
const {profiles, defaultProfile, ...baseConfig} = cfg.config;
const profileConfig = profiles.find((p) => p.name === profileName)?.config || {};
for (const key in profileConfig) {
if (typeof baseConfig[key] === 'object' && !Array.isArray(baseConfig[key])) {
baseConfig[key] = {...baseConfig[key], ...profileConfig[key]};
} else {
baseConfig[key] = profileConfig[key];
}
}
return {...baseConfig, defaultProfile, profiles};
}; };
export const openConfig = () => { export const openConfig = () => {

View file

@ -61,15 +61,7 @@
"disableAutoUpdates": false, "disableAutoUpdates": false,
"autoUpdatePlugins": true, "autoUpdatePlugins": true,
"preserveCWD": true, "preserveCWD": true,
"screenReaderMode": false, "screenReaderMode": false
"imageSupport": true,
"defaultProfile": "default",
"profiles": [
{
"name": "default",
"config": {}
}
]
}, },
"plugins": [], "plugins": [],
"localPlugins": [], "localPlugins": [],

View file

@ -1,11 +1,10 @@
import {readFileSync, mkdirpSync} from 'fs-extra'; import {readFileSync} from 'fs-extra';
import {sync as mkdirpSync} from 'mkdirp';
import type {rawConfig} from '../../typings/config';
import notify from '../notify';
import {_init} from './init';
import {migrateHyper3Config} from './migrate';
import {defaultCfg, cfgPath, plugs, defaultPlatformKeyPath} from './paths'; import {defaultCfg, cfgPath, plugs, defaultPlatformKeyPath} from './paths';
import {_init} from './init';
import notify from '../notify';
import {rawConfig} from '../../lib/config';
import {migrateHyper3Config} from './migrate';
let defaultConfig: rawConfig; let defaultConfig: rawConfig;

View file

@ -1,14 +1,12 @@
import vm from 'vm'; import vm from 'vm';
import merge from 'lodash/merge';
import type {parsedConfig, rawConfig, configOptions} from '../../typings/config';
import notify from '../notify'; import notify from '../notify';
import mapKeys from '../utils/map-keys'; import mapKeys from '../utils/map-keys';
import {parsedConfig, rawConfig, configOptions} from '../../lib/config';
import _ from 'lodash';
const _extract = (script?: vm.Script): Record<string, any> => { const _extract = (script?: vm.Script): Record<string, any> => {
const module: Record<string, any> = {}; const module: Record<string, any> = {};
script?.runInNewContext({module}, {displayErrors: true}); script?.runInNewContext({module});
if (!module.exports) { if (!module.exports) {
throw new Error('Error reading configuration: `module.exports` not set'); throw new Error('Error reading configuration: `module.exports` not set');
} }
@ -18,10 +16,10 @@ const _extract = (script?: vm.Script): Record<string, any> => {
const _syntaxValidation = (cfg: string) => { const _syntaxValidation = (cfg: string) => {
try { try {
return new vm.Script(cfg, {filename: '.hyper.js'}); return new vm.Script(cfg, {filename: '.hyper.js', displayErrors: true});
} catch (_err) { } catch (_err) {
const err = _err as {name: string}; const err = _err as {name: string};
notify(`Error loading config: ${err.name}`, JSON.stringify(err), {error: err}); notify(`Error loading config: ${err.name}`, `${err}`, {error: err});
} }
}; };
@ -34,19 +32,7 @@ const _init = (userCfg: rawConfig, defaultCfg: rawConfig): parsedConfig => {
return { return {
config: (() => { config: (() => {
if (userCfg?.config) { if (userCfg?.config) {
const conf = userCfg.config; return _.merge({}, defaultCfg.config, userCfg.config);
conf.defaultProfile = conf.defaultProfile || 'default';
conf.profiles = conf.profiles || [];
conf.profiles = conf.profiles.length > 0 ? conf.profiles : [{name: 'default', config: {}}];
conf.profiles = conf.profiles.map((p, i) => ({
...p,
name: p.name || `profile-${i + 1}`,
config: p.config || {}
}));
if (!conf.profiles.map((p) => p.name).includes(conf.defaultProfile)) {
conf.defaultProfile = conf.profiles[0].name;
}
return merge({}, defaultCfg.config, conf);
} else { } else {
notify('Error reading configuration: `config` key is missing'); notify('Error reading configuration: `config` key is missing');
return defaultCfg.config || ({} as configOptions); return defaultCfg.config || ({} as configOptions);
@ -55,8 +41,8 @@ const _init = (userCfg: rawConfig, defaultCfg: rawConfig): parsedConfig => {
// Merging platform specific keymaps with user defined keymaps // Merging platform specific keymaps with user defined keymaps
keymaps: mapKeys({...defaultCfg.keymaps, ...userCfg?.keymaps}), keymaps: mapKeys({...defaultCfg.keymaps, ...userCfg?.keymaps}),
// Ignore undefined values in plugin and localPlugins array Issue #1862 // Ignore undefined values in plugin and localPlugins array Issue #1862
plugins: userCfg?.plugins?.filter(Boolean) || [], plugins: (userCfg?.plugins && userCfg.plugins.filter(Boolean)) || [],
localPlugins: userCfg?.localPlugins?.filter(Boolean) || [] localPlugins: (userCfg?.localPlugins && userCfg.localPlugins.filter(Boolean)) || []
}; };
}; };

View file

@ -1,14 +1,11 @@
import {dirname, resolve} from 'path';
import {builders, namedTypes} from 'ast-types';
import type {ExpressionKind} from 'ast-types/lib/gen/kinds';
import {copy, copySync, existsSync, readFileSync, writeFileSync} from 'fs-extra';
import merge from 'lodash/merge';
import {parse, prettyPrint} from 'recast'; import {parse, prettyPrint} from 'recast';
import {builders, namedTypes} from 'ast-types';
import * as babelParser from 'recast/parsers/babel'; import * as babelParser from 'recast/parsers/babel';
import {copy, copySync, existsSync, readFileSync, writeFileSync} from 'fs-extra';
import {dirname, resolve} from 'path';
import _ from 'lodash';
import notify from '../notify'; import notify from '../notify';
import {_extractDefault} from './init'; import {_extractDefault} from './init';
import {cfgDir, cfgPath, defaultCfg, legacyCfgPath, plugs, schemaFile, schemaPath} from './paths'; import {cfgDir, cfgPath, defaultCfg, legacyCfgPath, plugs, schemaFile, schemaPath} from './paths';
@ -66,7 +63,7 @@ export function configToPlugin(code: string): string {
}); });
const statements = ast.program.body; const statements = ast.program.body;
let moduleExportsNode: namedTypes.AssignmentExpression | null = null; let moduleExportsNode: namedTypes.AssignmentExpression | null = null;
let configNode: ExpressionKind | null = null; let configNode: any = null;
for (const statement of statements) { for (const statement of statements) {
if (namedTypes.ExpressionStatement.check(statement)) { if (namedTypes.ExpressionStatement.check(statement)) {
@ -89,7 +86,7 @@ export function configToPlugin(code: string): string {
namedTypes.Identifier.check(property.key) && namedTypes.Identifier.check(property.key) &&
property.key.name === 'config' property.key.name === 'config'
) { ) {
configNode = property.value as ExpressionKind; configNode = property.value;
if (namedTypes.ObjectExpression.check(property.value)) { if (namedTypes.ObjectExpression.check(property.value)) {
configNode = removeProperties(property.value); configNode = removeProperties(property.value);
} }
@ -168,7 +165,7 @@ export const migrateHyper3Config = () => {
try { try {
const legacyCfgRaw = readFileSync(legacyCfgPath, 'utf8'); const legacyCfgRaw = readFileSync(legacyCfgPath, 'utf8');
const legacyCfgData = _extractDefault(legacyCfgRaw); const legacyCfgData = _extractDefault(legacyCfgRaw);
newCfgData = merge({}, defaultCfgData, legacyCfgData); newCfgData = _.merge({}, defaultCfgData, legacyCfgData);
const pluginCode = configToPlugin(legacyCfgRaw); const pluginCode = configToPlugin(legacyCfgRaw);
if (pluginCode) { if (pluginCode) {

View file

@ -1,10 +1,7 @@
import {exec} from 'child_process';
import {shell} from 'electron'; import {shell} from 'electron';
import * as Registry from 'native-reg';
import {cfgPath} from './paths'; import {cfgPath} from './paths';
import * as Registry from 'native-reg';
import {exec} from 'child_process';
const getUserChoiceKey = () => { const getUserChoiceKey = () => {
try { try {
@ -60,7 +57,7 @@ const openNotepad = (file: string) =>
}); });
}); });
const openConfig = () => { export default () => {
// Windows opens .js files with WScript.exe by default // Windows opens .js files with WScript.exe by default
// If the user hasn't set up an editor for .js files, we fallback to notepad. // If the user hasn't set up an editor for .js files, we fallback to notepad.
if (process.platform === 'win32') { if (process.platform === 'win32') {
@ -76,5 +73,3 @@ const openConfig = () => {
} }
return shell.openPath(cfgPath).then((error) => error === ''); return shell.openPath(cfgPath).then((error) => error === '');
}; };
export default openConfig;

View file

@ -1,10 +1,8 @@
// This module exports paths, names, and other metadata that is referenced // This module exports paths, names, and other metadata that is referenced
import {statSync} from 'fs';
import {homedir} from 'os'; import {homedir} from 'os';
import {resolve, join} from 'path';
import {app} from 'electron'; import {app} from 'electron';
import {statSync} from 'fs';
import {resolve, join} from 'path';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
const cfgFile = 'hyper.json'; const cfgFile = 'hyper.json';
@ -17,15 +15,15 @@ const homeDirectory = homedir();
let cfgDir = process.env.XDG_CONFIG_HOME let cfgDir = process.env.XDG_CONFIG_HOME
? join(process.env.XDG_CONFIG_HOME, 'Hyper') ? join(process.env.XDG_CONFIG_HOME, 'Hyper')
: process.platform === 'win32' : process.platform === 'win32'
? app.getPath('userData') ? app.getPath('userData')
: join(homeDirectory, '.config', 'Hyper'); : join(homeDirectory, '.config', 'Hyper');
const legacyCfgPath = join( const legacyCfgPath = join(
process.env.XDG_CONFIG_HOME !== undefined process.env.XDG_CONFIG_HOME !== undefined
? join(process.env.XDG_CONFIG_HOME, 'hyper') ? join(process.env.XDG_CONFIG_HOME, 'hyper')
: process.platform == 'win32' : process.platform == 'win32'
? app.getPath('userData') ? app.getPath('userData')
: homedir(), : homedir(),
'.hyper.js' '.hyper.js'
); );

View file

@ -24,19 +24,25 @@
} }
], ],
"description": "A string or number representing text font weight." "description": "A string or number representing text font weight."
}, }
"Partial<profileConfigOptions>": { },
"properties": {
"config": {
"properties": { "properties": {
"autoUpdatePlugins": {
"description": "if `true` (default), Hyper will update plugins every 5 hours\nyou can also set it to a custom time e.g. `1d` or `2h`",
"type": [
"string",
"boolean"
]
},
"backgroundColor": { "backgroundColor": {
"description": "terminal background color\n\nopacity is only supported on macOS", "description": "terminal background color\n\nopacity is only supported on macOS",
"type": "string" "type": "string"
}, },
"bell": { "bell": {
"description": "Supported Options:\n1. 'SOUND' -> Enables the bell as a sound\n2. false: turns off the bell", "description": "Supported Options:\n1. 'SOUND' -> Enables the bell as a sound\n2. false: turns off the bell",
"enum": [ "type": "string"
"SOUND",
false
]
}, },
"bellSound": { "bellSound": {
"description": "base64 encoded string of the sound file to use for the bell\nif null, the default bell will be used", "description": "base64 encoded string of the sound file to use for the bell\nif null, the default bell will be used",
@ -157,6 +163,14 @@
], ],
"type": "string" "type": "string"
}, },
"defaultSSHApp": {
"description": "if `true` hyper will be set as the default protocol client for SSH",
"type": "boolean"
},
"disableAutoUpdates": {
"description": "if `true` hyper will not check for updates",
"type": "boolean"
},
"disableLigatures": { "disableLigatures": {
"description": "if `false` Hyper will use ligatures provided by some fonts", "description": "if `false` Hyper will use ligatures provided by some fonts",
"type": "boolean" "type": "boolean"
@ -188,10 +202,6 @@
"description": "color of the text", "description": "color of the text",
"type": "string" "type": "string"
}, },
"imageSupport": {
"description": "Whether to enable Sixel and iTerm2 inline image protocol support or not.",
"type": "boolean"
},
"letterSpacing": { "letterSpacing": {
"description": "letter spacing as a relative unit", "description": "letter spacing as a relative unit",
"type": "number" "type": "number"
@ -243,11 +253,11 @@
"type": "string" "type": "string"
}, },
"shell": { "shell": {
"description": "the shell to run when spawning a new session (e.g. /usr/local/bin/fish)\nif left empty, your system's login shell will be used by default\n\nWindows\n- Make sure to use a full path if the binary name doesn't work\n- Remove `--login` in shellArgs\n\nWindows Subsystem for Linux (WSL) - previously Bash on Windows\n- Example: `C:\\\\Windows\\\\System32\\\\wsl.exe`\n\nGit-bash on Windows\n- Example: `C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe`\n\nPowerShell on Windows\n- Example: `C:\\\\WINDOWS\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe`\n\nCygwin\n- Example: `C:\\\\cygwin64\\\\bin\\\\bash.exe`\n\nGit Bash\n- Example: `C:\\\\Program Files\\\\Git\\\\git-cmd.exe`\nThen Add `--command=usr/bin/bash.exe` to shellArgs", "description": "the shell to run when spawning a new session (i.e. /usr/local/bin/fish)\nif left empty, your system's login shell will be used by default\n\nWindows\n- Make sure to use a full path if the binary name doesn't work\n- Remove `--login` in shellArgs\n\nWindows Subsystem for Linux (WSL) - previously Bash on Windows\n- Example: `C:\\\\Windows\\\\System32\\\\wsl.exe`\n\nGit-bash on Windows\n- Example: `C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe`\n\nPowerShell on Windows\n- Example: `C:\\\\WINDOWS\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe`\n\nCygwin\n- Example: `C:\\\\cygwin64\\\\bin\\\\bash.exe`\n\nGit Bash\n- Example: `C:\\\\Program Files\\\\Git\\\\git-cmd.exe`\nThen Add `--command=usr/bin/bash.exe` to shellArgs",
"type": "string" "type": "string"
}, },
"shellArgs": { "shellArgs": {
"description": "for setting shell arguments (e.g. for using interactive shellArgs: `['-i']`)\nby default `['--login']` will be used", "description": "for setting shell arguments (i.e. for using interactive shellArgs: `['-i']`)\nby default `['--login']` will be used",
"items": { "items": {
"type": "string" "type": "string"
}, },
@ -277,6 +287,17 @@
"uiFontFamily": { "uiFontFamily": {
"type": "string" "type": "string"
}, },
"updateChannel": {
"description": "choose either `'stable'` for receiving highly polished, or `'canary'` for less polished but more frequent updates",
"enum": [
"canary",
"stable"
],
"type": "string"
},
"useConpty": {
"type": "boolean"
},
"webGLRenderer": { "webGLRenderer": {
"description": "Whether to use the WebGL renderer. Set it to false to use canvas-based\nrendering (slower, but supports transparent backgrounds)", "description": "Whether to use the WebGL renderer. Set it to false to use canvas-based\nrendering (slower, but supports transparent backgrounds)",
"type": "boolean" "type": "boolean"
@ -311,414 +332,50 @@
"type": "string" "type": "string"
} }
}, },
"required": [
"autoUpdatePlugins",
"backgroundColor",
"bell",
"bellSound",
"bellSoundURL",
"borderColor",
"colors",
"copyOnSelect",
"css",
"cursorAccentColor",
"cursorBlink",
"cursorColor",
"cursorShape",
"defaultSSHApp",
"disableAutoUpdates",
"disableLigatures",
"env",
"fontFamily",
"fontSize",
"fontWeight",
"fontWeightBold",
"foregroundColor",
"letterSpacing",
"lineHeight",
"macOptionSelectionMode",
"padding",
"preserveCWD",
"quickEdit",
"screenReaderMode",
"scrollback",
"selectionColor",
"shell",
"shellArgs",
"showHamburgerMenu",
"showWindowControls",
"termCSS",
"updateChannel",
"webGLRenderer",
"webLinksActivationKey",
"workingDirectory"
],
"type": "object" "type": "object"
}, },
"configOptions": {
"allOf": [
{
"properties": {
"autoUpdatePlugins": {
"description": "if `true` (default), Hyper will update plugins every 5 hours\nyou can also set it to a custom time e.g. `1d` or `2h`",
"type": [
"string",
"boolean"
]
},
"defaultSSHApp": {
"description": "if `true` hyper will be set as the default protocol client for SSH",
"type": "boolean"
},
"disableAutoUpdates": {
"description": "if `true` hyper will not check for updates",
"type": "boolean"
},
"updateChannel": {
"description": "choose either `'stable'` for receiving highly polished, or `'canary'` for less polished but more frequent updates",
"enum": [
"canary",
"stable"
],
"type": "string"
},
"useConpty": {
"type": "boolean"
}
},
"required": [
"autoUpdatePlugins",
"defaultSSHApp",
"disableAutoUpdates",
"updateChannel"
],
"type": "object"
},
{
"properties": {
"backgroundColor": {
"description": "terminal background color\n\nopacity is only supported on macOS",
"type": "string"
},
"bell": {
"description": "Supported Options:\n1. 'SOUND' -> Enables the bell as a sound\n2. false: turns off the bell",
"enum": [
"SOUND",
false
]
},
"bellSound": {
"description": "base64 encoded string of the sound file to use for the bell\nif null, the default bell will be used",
"type": [
"string",
"null"
]
},
"bellSoundURL": {
"description": "An absolute file path to a sound file on the machine.",
"type": [
"string",
"null"
]
},
"borderColor": {
"description": "border color (window, tabs)",
"type": "string"
},
"colors": {
"description": "the full list. if you're going to provide the full color palette,\nincluding the 6 x 6 color cubes and the grayscale map, just provide\nan array here instead of a color map object",
"properties": {
"black": {
"type": "string"
},
"blue": {
"type": "string"
},
"cyan": {
"type": "string"
},
"green": {
"type": "string"
},
"lightBlack": {
"type": "string"
},
"lightBlue": {
"type": "string"
},
"lightCyan": {
"type": "string"
},
"lightGreen": {
"type": "string"
},
"lightMagenta": {
"type": "string"
},
"lightRed": {
"type": "string"
},
"lightWhite": {
"type": "string"
},
"lightYellow": {
"type": "string"
},
"magenta": {
"type": "string"
},
"red": {
"type": "string"
},
"white": {
"type": "string"
},
"yellow": {
"type": "string"
}
},
"required": [
"black",
"blue",
"cyan",
"green",
"lightBlack",
"lightBlue",
"lightCyan",
"lightGreen",
"lightMagenta",
"lightRed",
"lightWhite",
"lightYellow",
"magenta",
"red",
"white",
"yellow"
],
"type": "object"
},
"copyOnSelect": {
"description": "if `true` selected text will automatically be copied to the clipboard",
"type": "boolean"
},
"css": {
"description": "custom CSS to embed in the main window",
"type": "string"
},
"cursorAccentColor": {
"description": "terminal text color under BLOCK cursor",
"type": "string"
},
"cursorBlink": {
"description": "set to `true` for blinking cursor",
"type": "boolean"
},
"cursorColor": {
"description": "terminal cursor background color and opacity (hex, rgb, hsl, hsv, hwb or cmyk)",
"type": "string"
},
"cursorShape": {
"description": "`'BEAM'` for |, `'UNDERLINE'` for _, `'BLOCK'` for █",
"enum": [
"BEAM",
"BLOCK",
"UNDERLINE"
],
"type": "string"
},
"disableLigatures": {
"description": "if `false` Hyper will use ligatures provided by some fonts",
"type": "boolean"
},
"env": {
"additionalProperties": {
"type": "string"
},
"description": "for environment variables",
"type": "object"
},
"fontFamily": {
"description": "font family with optional fallbacks",
"type": "string"
},
"fontSize": {
"description": "default font size in pixels for all tabs",
"type": "number"
},
"fontWeight": {
"$ref": "#/definitions/FontWeight",
"description": "default font weight eg:'normal', '400', 'bold'"
},
"fontWeightBold": {
"$ref": "#/definitions/FontWeight",
"description": "font weight for bold characters eg:'normal', '600', 'bold'"
},
"foregroundColor": {
"description": "color of the text",
"type": "string"
},
"imageSupport": {
"description": "Whether to enable Sixel and iTerm2 inline image protocol support or not.",
"type": "boolean"
},
"letterSpacing": {
"description": "letter spacing as a relative unit",
"type": "number"
},
"lineHeight": {
"description": "line height as a relative unit",
"type": "number"
},
"macOptionSelectionMode": {
"description": "choose either `'vertical'`, if you want the column mode when Option key is hold during selection (Default)\nor `'force'`, if you want to force selection regardless of whether the terminal is in mouse events mode\n(inside tmux or vim with mouse mode enabled for example).",
"type": "string"
},
"modifierKeys": {
"properties": {
"altIsMeta": {
"type": "boolean"
},
"cmdIsMeta": {
"type": "boolean"
}
},
"required": [
"altIsMeta",
"cmdIsMeta"
],
"type": "object"
},
"padding": {
"description": "custom padding (CSS format, i.e.: `top right bottom left` or `top horizontal bottom` or `vertical horizontal` or `all`)",
"type": "string"
},
"preserveCWD": {
"description": "set to true to preserve working directory when creating splits or tabs",
"type": "boolean"
},
"quickEdit": {
"description": "if `true` on right click selected text will be copied or pasted if no\nselection is present (`true` by default on Windows and disables the context menu feature)",
"type": "boolean"
},
"screenReaderMode": {
"description": "set to true to enable screen reading apps (like NVDA) to read the contents of the terminal",
"type": "boolean"
},
"scrollback": {
"type": "number"
},
"selectionColor": {
"description": "terminal selection color",
"type": "string"
},
"shell": {
"description": "the shell to run when spawning a new session (e.g. /usr/local/bin/fish)\nif left empty, your system's login shell will be used by default\n\nWindows\n- Make sure to use a full path if the binary name doesn't work\n- Remove `--login` in shellArgs\n\nWindows Subsystem for Linux (WSL) - previously Bash on Windows\n- Example: `C:\\\\Windows\\\\System32\\\\wsl.exe`\n\nGit-bash on Windows\n- Example: `C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe`\n\nPowerShell on Windows\n- Example: `C:\\\\WINDOWS\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe`\n\nCygwin\n- Example: `C:\\\\cygwin64\\\\bin\\\\bash.exe`\n\nGit Bash\n- Example: `C:\\\\Program Files\\\\Git\\\\git-cmd.exe`\nThen Add `--command=usr/bin/bash.exe` to shellArgs",
"type": "string"
},
"shellArgs": {
"description": "for setting shell arguments (e.g. for using interactive shellArgs: `['-i']`)\nby default `['--login']` will be used",
"items": {
"type": "string"
},
"type": "array"
},
"showHamburgerMenu": {
"description": "if you're using a Linux setup which show native menus, set to false\n\ndefault: `true` on Linux, `true` on Windows, ignored on macOS",
"enum": [
"",
false,
true
]
},
"showWindowControls": {
"description": "set to `false` if you want to hide the minimize, maximize and close buttons\n\nadditionally, set to `'left'` if you want them on the left, like in Ubuntu\n\ndefault: `true` on Windows and Linux, ignored on macOS",
"enum": [
"",
false,
"left",
true
]
},
"termCSS": {
"description": "custom CSS to embed in the terminal window",
"type": "string"
},
"uiFontFamily": {
"type": "string"
},
"webGLRenderer": {
"description": "Whether to use the WebGL renderer. Set it to false to use canvas-based\nrendering (slower, but supports transparent backgrounds)",
"type": "boolean"
},
"webLinksActivationKey": {
"description": "keypress required for weblink activation: [ctrl | alt | meta | shift]",
"enum": [
"",
"alt",
"ctrl",
"meta",
"shift"
],
"type": "string"
},
"windowSize": {
"description": "Initial window size in pixels",
"items": [
{
"type": "number"
},
{
"type": "number"
}
],
"maxItems": 2,
"minItems": 2,
"type": "array"
},
"workingDirectory": {
"description": "set custom startup directory (must be an absolute path)",
"type": "string"
}
},
"required": [
"backgroundColor",
"bell",
"bellSound",
"bellSoundURL",
"borderColor",
"colors",
"copyOnSelect",
"css",
"cursorAccentColor",
"cursorBlink",
"cursorColor",
"cursorShape",
"disableLigatures",
"env",
"fontFamily",
"fontSize",
"fontWeight",
"fontWeightBold",
"foregroundColor",
"imageSupport",
"letterSpacing",
"lineHeight",
"macOptionSelectionMode",
"padding",
"preserveCWD",
"quickEdit",
"screenReaderMode",
"scrollback",
"selectionColor",
"shell",
"shellArgs",
"showHamburgerMenu",
"showWindowControls",
"termCSS",
"webGLRenderer",
"webLinksActivationKey",
"workingDirectory"
],
"type": "object"
},
{
"properties": {
"defaultProfile": {
"description": "The default profile name to use when launching a new session",
"type": "string"
},
"profiles": {
"description": "A list of profiles to use",
"items": {
"properties": {
"config": {
"$ref": "#/definitions/Partial<profileConfigOptions>",
"description": "Specify all the options you want to override for each profile.\nOptions set here override the defaults set in the root."
},
"name": {
"type": "string"
}
},
"required": [
"config",
"name"
],
"type": "object"
},
"type": "array"
}
},
"required": [
"defaultProfile",
"profiles"
],
"type": "object"
}
]
}
},
"properties": {
"config": {
"$ref": "#/definitions/configOptions"
},
"keymaps": { "keymaps": {
"additionalProperties": { "additionalProperties": {
"anyOf": [ "anyOf": [

View file

@ -1,6 +1,5 @@
import type {BrowserWindow} from 'electron';
import Config from 'electron-store'; import Config from 'electron-store';
import {BrowserWindow} from 'electron';
export const defaults = { export const defaults = {
windowPosition: [50, 50] as [number, number], windowPosition: [50, 50] as [number, number],

8
app/ext-modules.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
declare module 'git-describe' {
export function gitDescribe(...args: any[]): void;
}
declare module 'default-shell' {
const val: string;
export default val;
}

View file

@ -1,17 +1,16 @@
import type {Server} from '../app/rpc'; import type {Server} from './rpc';
declare global { declare global {
namespace Electron { namespace Electron {
interface App { interface App {
config: typeof import('../app/config'); config: typeof import('./config');
plugins: typeof import('../app/plugins'); plugins: typeof import('./plugins');
getWindows: () => Set<BrowserWindow>; getWindows: () => Set<BrowserWindow>;
getLastFocusedWindow: () => BrowserWindow | null; getLastFocusedWindow: () => BrowserWindow | null;
windowCallback?: (win: BrowserWindow) => void; windowCallback?: (win: BrowserWindow) => void;
createWindow: ( createWindow: (
fn?: (win: BrowserWindow) => void, fn?: (win: BrowserWindow) => void,
options?: {size?: [number, number]; position?: [number, number]}, options?: {size?: [number, number]; position?: [number, number]}
profileName?: string
) => BrowserWindow; ) => BrowserWindow;
setVersion: (version: string) => void; setVersion: (version: string) => void;
} }
@ -23,7 +22,6 @@ declare global {
focusTime: number; focusTime: number;
clean: () => void; clean: () => void;
rpc: Server; rpc: Server;
profileName: string;
} }
} }
} }

1
app/index.d.ts vendored Normal file
View file

@ -0,0 +1 @@
// Dummy file, required by tsc

View file

@ -1,4 +1,3 @@
// eslint-disable-next-line import/order
import {cfgPath} from './config/paths'; import {cfgPath} from './config/paths';
// Print diagnostic information for a few arguments instead of running Hyper. // Print diagnostic information for a few arguments instead of running Hyper.
@ -12,29 +11,25 @@ if (['--help', '-v', '--version'].includes(process.argv[1])) {
} }
// Enable remote module // Enable remote module
// eslint-disable-next-line import/order
import {initialize as remoteInitialize} from '@electron/remote/main'; import {initialize as remoteInitialize} from '@electron/remote/main';
remoteInitialize(); remoteInitialize();
// set up config
// eslint-disable-next-line import/order
import * as config from './config';
config.setup();
// Native // Native
import {resolve} from 'path'; import {resolve} from 'path';
// Packages // Packages
import {app, BrowserWindow, Menu, screen} from 'electron'; import {app, BrowserWindow, Menu, screen} from 'electron';
import isDev from 'electron-is-dev';
import {gitDescribe} from 'git-describe'; import {gitDescribe} from 'git-describe';
import parseUrl from 'parse-url'; import isDev from 'electron-is-dev';
import * as config from './config';
// set up config
config.setup();
import * as AppMenu from './menus/menu';
import * as plugins from './plugins'; import * as plugins from './plugins';
import {newWindow} from './ui/window';
import {installCLI} from './utils/cli-install'; import {installCLI} from './utils/cli-install';
import * as AppMenu from './menus/menu';
import {newWindow} from './ui/window';
import * as windowUtils from './utils/window-utils'; import * as windowUtils from './utils/window-utils';
const windowSet = new Set<BrowserWindow>([]); const windowSet = new Set<BrowserWindow>([]);
@ -62,7 +57,7 @@ if (isDev) {
console.log('running in dev mode'); console.log('running in dev mode');
// Override default appVersion which is set from package.json // Override default appVersion which is set from package.json
gitDescribe({customArguments: ['--tags']}, (error: any, gitInfo: {raw: string}) => { gitDescribe({customArguments: ['--tags']}, (error: any, gitInfo: any) => {
if (!error) { if (!error) {
app.setVersion(gitInfo.raw); app.setVersion(gitInfo.raw);
} }
@ -78,13 +73,15 @@ async function installDevExtensions(isDev_: boolean) {
if (!isDev_) { if (!isDev_) {
return []; return [];
} }
const {default: installer, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS} = await import('electron-devtools-installer'); const installer = await import('electron-devtools-installer');
const extensions = [REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS]; const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'] as const;
const forceDownload = Boolean(process.env.UPGRADE_EXTENSIONS); const forceDownload = Boolean(process.env.UPGRADE_EXTENSIONS);
return Promise.all( return Promise.all(
extensions.map((extension) => installer(extension, {forceDownload, loadExtensionOptions: {allowFileAccess: true}})) extensions.map((name) =>
installer.default(installer[name], {forceDownload, loadExtensionOptions: {allowFileAccess: true}})
)
); );
} }
@ -94,10 +91,9 @@ app.on('ready', () =>
.then(() => { .then(() => {
function createWindow( function createWindow(
fn?: (win: BrowserWindow) => void, fn?: (win: BrowserWindow) => void,
options: {size?: [number, number]; position?: [number, number]} = {}, options: {size?: [number, number]; position?: [number, number]} = {}
profileName: string = config.getDefaultProfile()
) { ) {
const cfg = plugins.getDecoratedConfig(profileName); const cfg = plugins.getDecoratedConfig();
const winSet = config.getWin(); const winSet = config.getWin();
let [startX, startY] = winSet.position; let [startX, startY] = winSet.position;
@ -139,14 +135,10 @@ app.on('ready', () =>
[startX, startY] = config.windowDefaults.windowPosition; [startX, startY] = config.windowDefaults.windowPosition;
} }
const hwin = newWindow({width, height, x: startX, y: startY}, cfg, fn, profileName); const hwin = newWindow({width, height, x: startX, y: startY}, cfg, fn);
windowSet.add(hwin); windowSet.add(hwin);
void hwin.loadURL(url); void hwin.loadURL(url);
hwin.once('ready-to-show', () => {
hwin.show();
});
// the window can be closed by the browser process itself // the window can be closed by the browser process itself
hwin.on('close', () => { hwin.on('close', () => {
hwin.clean(); hwin.clean();
@ -190,7 +182,7 @@ app.on('ready', () =>
} }
} }
]); ]);
app.dock?.setMenu(dockMenu); app.dock.setMenu(dockMenu);
} }
Menu.setApplicationMenu(AppMenu.buildMenu(menu)); Menu.setApplicationMenu(AppMenu.buildMenu(menu));
@ -242,6 +234,6 @@ app.on('open-file', (_event, path) => {
app.on('open-url', (_event, sshUrl) => { app.on('open-url', (_event, sshUrl) => {
GetWindow((win: BrowserWindow) => { GetWindow((win: BrowserWindow) => {
win.rpc.emit('open ssh', parseUrl(sshUrl)); win.rpc.emit('open ssh', sshUrl);
}); });
}); });

View file

@ -1,21 +1,19 @@
// Packages // Packages
import {app, dialog, Menu} from 'electron'; import {app, dialog, Menu, BrowserWindow} from 'electron';
import type {BrowserWindow} from 'electron';
// Utilities // Utilities
import {execCommand} from '../commands';
import {getConfig} from '../config'; import {getConfig} from '../config';
import {icon} from '../config/paths'; import {icon} from '../config/paths';
import {getDecoratedKeymaps} from '../plugins';
import {getRendererTypes} from '../utils/renderer-utils';
import darwinMenu from './menus/darwin';
import editMenu from './menus/edit';
import helpMenu from './menus/help';
import shellMenu from './menus/shell';
import toolsMenu from './menus/tools';
import viewMenu from './menus/view'; import viewMenu from './menus/view';
import shellMenu from './menus/shell';
import editMenu from './menus/edit';
import toolsMenu from './menus/tools';
import windowMenu from './menus/window'; import windowMenu from './menus/window';
import helpMenu from './menus/help';
import darwinMenu from './menus/darwin';
import {getDecoratedKeymaps} from '../plugins';
import {execCommand} from '../commands';
import {getRendererTypes} from '../utils/renderer-utils';
const appName = app.name; const appName = app.name;
const appVersion = app.getVersion(); const appVersion = app.getVersion();
@ -56,30 +54,14 @@ export const createMenu = (
void dialog.showMessageBox({ void dialog.showMessageBox({
title: `About ${appName}`, title: `About ${appName}`,
message: `${appName} ${appVersion} (${updateChannel})`, message: `${appName} ${appVersion} (${updateChannel})`,
detail: ` detail: `Renderers: ${renderers}\nPlugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright © 2022 Vercel, Inc.`,
Renderers: ${renderers}
Plugins: ${pluginList}
Maintained by QUINE Global
Copyright © 2025
Created by Guillermo Rauch
Copyright © 2022 Vercel, Inc.
`
.split('\n')
.map((z) => z.trim())
.join('\n'),
buttons: [], buttons: [],
icon: icon as any icon: icon as any
}); });
}; };
const menu = [ const menu = [
...(process.platform === 'darwin' ? [darwinMenu(commandKeys, execCommand, showAbout)] : []), ...(process.platform === 'darwin' ? [darwinMenu(commandKeys, execCommand, showAbout)] : []),
shellMenu( shellMenu(commandKeys, execCommand),
commandKeys,
(command, focusedWindow) => execCommand(command, focusedWindow as BrowserWindow | undefined),
getConfig().profiles.map((p) => p.name)
),
editMenu(commandKeys, execCommand), editMenu(commandKeys, execCommand),
viewMenu(commandKeys, execCommand), viewMenu(commandKeys, execCommand),
toolsMenu(commandKeys, execCommand), toolsMenu(commandKeys, execCommand),

View file

@ -1,9 +1,8 @@
// This menu label is overrided by OSX to be the appName // This menu label is overrided by OSX to be the appName
// The label is set to appName here so it matches actual behavior // The label is set to appName here so it matches actual behavior
import {app} from 'electron'; import {app, BrowserWindow, MenuItemConstructorOptions} from 'electron';
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron';
const darwinMenu = ( export default (
commandKeys: Record<string, string>, commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void, execCommand: (command: string, focusedWindow?: BrowserWindow) => void,
showAbout: () => void showAbout: () => void
@ -55,5 +54,3 @@ const darwinMenu = (
] ]
}; };
}; };
export default darwinMenu;

View file

@ -1,6 +1,6 @@
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
const editMenu = ( export default (
commandKeys: Record<string, string>, commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void execCommand: (command: string, focusedWindow?: BrowserWindow) => void
) => { ) => {
@ -38,7 +38,7 @@ const editMenu = (
label: 'Select All', label: 'Select All',
accelerator: commandKeys['editor:selectAll'], accelerator: commandKeys['editor:selectAll'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('editor:selectAll', focusedWindow as BrowserWindow | undefined); execCommand('editor:selectAll', focusedWindow);
} }
}, },
{ {
@ -51,28 +51,28 @@ const editMenu = (
label: 'Previous word', label: 'Previous word',
accelerator: commandKeys['editor:movePreviousWord'], accelerator: commandKeys['editor:movePreviousWord'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('editor:movePreviousWord', focusedWindow as BrowserWindow | undefined); execCommand('editor:movePreviousWord', focusedWindow);
} }
}, },
{ {
label: 'Next word', label: 'Next word',
accelerator: commandKeys['editor:moveNextWord'], accelerator: commandKeys['editor:moveNextWord'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('editor:moveNextWord', focusedWindow as BrowserWindow | undefined); execCommand('editor:moveNextWord', focusedWindow);
} }
}, },
{ {
label: 'Line beginning', label: 'Line beginning',
accelerator: commandKeys['editor:moveBeginningLine'], accelerator: commandKeys['editor:moveBeginningLine'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('editor:moveBeginningLine', focusedWindow as BrowserWindow | undefined); execCommand('editor:moveBeginningLine', focusedWindow);
} }
}, },
{ {
label: 'Line end', label: 'Line end',
accelerator: commandKeys['editor:moveEndLine'], accelerator: commandKeys['editor:moveEndLine'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('editor:moveEndLine', focusedWindow as BrowserWindow | undefined); execCommand('editor:moveEndLine', focusedWindow);
} }
} }
] ]
@ -84,28 +84,28 @@ const editMenu = (
label: 'Previous word', label: 'Previous word',
accelerator: commandKeys['editor:deletePreviousWord'], accelerator: commandKeys['editor:deletePreviousWord'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('editor:deletePreviousWord', focusedWindow as BrowserWindow | undefined); execCommand('editor:deletePreviousWord', focusedWindow);
} }
}, },
{ {
label: 'Next word', label: 'Next word',
accelerator: commandKeys['editor:deleteNextWord'], accelerator: commandKeys['editor:deleteNextWord'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('editor:deleteNextWord', focusedWindow as BrowserWindow | undefined); execCommand('editor:deleteNextWord', focusedWindow);
} }
}, },
{ {
label: 'Line beginning', label: 'Line beginning',
accelerator: commandKeys['editor:deleteBeginningLine'], accelerator: commandKeys['editor:deleteBeginningLine'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('editor:deleteBeginningLine', focusedWindow as BrowserWindow | undefined); execCommand('editor:deleteBeginningLine', focusedWindow);
} }
}, },
{ {
label: 'Line end', label: 'Line end',
accelerator: commandKeys['editor:deleteEndLine'], accelerator: commandKeys['editor:deleteEndLine'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('editor:deleteEndLine', focusedWindow as BrowserWindow | undefined); execCommand('editor:deleteEndLine', focusedWindow);
} }
} }
] ]
@ -117,14 +117,14 @@ const editMenu = (
label: 'Clear Buffer', label: 'Clear Buffer',
accelerator: commandKeys['editor:clearBuffer'], accelerator: commandKeys['editor:clearBuffer'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('editor:clearBuffer', focusedWindow as BrowserWindow | undefined); execCommand('editor:clearBuffer', focusedWindow);
} }
}, },
{ {
label: 'Search', label: 'Search',
accelerator: commandKeys['editor:search'], accelerator: commandKeys['editor:search'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('editor:search', focusedWindow as BrowserWindow | undefined); execCommand('editor:search', focusedWindow);
} }
} }
]; ];
@ -147,5 +147,3 @@ const editMenu = (
submenu submenu
}; };
}; };
export default editMenu;

View file

@ -1,14 +1,10 @@
import {release} from 'os'; import {release} from 'os';
import {app, shell, MenuItemConstructorOptions, dialog, clipboard} from 'electron';
import {app, shell, dialog, clipboard} from 'electron';
import type {MenuItemConstructorOptions} from 'electron';
import {getConfig, getPlugins} from '../../config'; import {getConfig, getPlugins} from '../../config';
const {arch, env, platform, versions} = process;
import {version} from '../../package.json'; import {version} from '../../package.json';
const {arch, env, platform, versions} = process; export default (commands: Record<string, string>, showAbout: () => void): MenuItemConstructorOptions => {
const helpMenu = (commands: Record<string, string>, showAbout: () => void): MenuItemConstructorOptions => {
const submenu: MenuItemConstructorOptions[] = [ const submenu: MenuItemConstructorOptions[] = [
{ {
label: `${app.name} Website`, label: `${app.name} Website`,
@ -61,11 +57,11 @@ ${JSON.stringify(getPlugins(), null, 2)}
\`\`\` \`\`\`
</details>`; </details>`;
const issueURL = `https://github.com/quine-global/hyper/issues/new?body=${encodeURIComponent(body)}`; const issueURL = `https://github.com/vercel/hyper/issues/new?body=${encodeURIComponent(body)}`;
const copyAndSend = () => { const copyAndSend = () => {
clipboard.writeText(body); clipboard.writeText(body);
void shell.openExternal( void shell.openExternal(
`https://github.com/quine-global/hyper/issues/new?body=${encodeURIComponent( `https://github.com/vercel/hyper/issues/new?body=${encodeURIComponent(
'<!-- We have written the needed data into your clipboard because it was too large to send. ' + '<!-- We have written the needed data into your clipboard because it was too large to send. ' +
'Please paste. -->\n' 'Please paste. -->\n'
)}` )}`
@ -110,5 +106,3 @@ ${JSON.stringify(getPlugins(), null, 2)}
submenu submenu
}; };
}; };
export default helpMenu;

View file

@ -1,9 +1,8 @@
import type {BaseWindow, MenuItemConstructorOptions} from 'electron'; import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
const shellMenu = ( export default (
commandKeys: Record<string, string>, commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BaseWindow) => void, execCommand: (command: string, focusedWindow?: BrowserWindow) => void
profiles: string[]
): MenuItemConstructorOptions => { ): MenuItemConstructorOptions => {
const isMac = process.platform === 'darwin'; const isMac = process.platform === 'darwin';
@ -44,47 +43,6 @@ const shellMenu = (
{ {
type: 'separator' type: 'separator'
}, },
...profiles.map(
(profile): MenuItemConstructorOptions => ({
label: profile,
submenu: [
{
label: 'New Tab',
accelerator: commandKeys[`tab:new:${profile}`],
click(item, focusedWindow) {
execCommand(`tab:new:${profile}`, focusedWindow);
}
},
{
label: 'New Window',
accelerator: commandKeys[`window:new:${profile}`],
click(item, focusedWindow) {
execCommand(`window:new:${profile}`, focusedWindow);
}
},
{
type: 'separator'
},
{
label: 'Split Down',
accelerator: commandKeys[`pane:splitDown:${profile}`],
click(item, focusedWindow) {
execCommand(`pane:splitDown:${profile}`, focusedWindow);
}
},
{
label: 'Split Right',
accelerator: commandKeys[`pane:splitRight:${profile}`],
click(item, focusedWindow) {
execCommand(`pane:splitRight:${profile}`, focusedWindow);
}
}
]
})
),
{
type: 'separator'
},
{ {
label: 'Close', label: 'Close',
accelerator: commandKeys['pane:close'], accelerator: commandKeys['pane:close'],
@ -100,5 +58,3 @@ const shellMenu = (
] ]
}; };
}; };
export default shellMenu;

View file

@ -1,6 +1,6 @@
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
const toolsMenu = ( export default (
commands: Record<string, string>, commands: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => { ): MenuItemConstructorOptions => {
@ -45,5 +45,3 @@ const toolsMenu = (
] ]
}; };
}; };
export default toolsMenu;

View file

@ -1,6 +1,6 @@
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
const viewMenu = ( export default (
commandKeys: Record<string, string>, commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => { ): MenuItemConstructorOptions => {
@ -11,21 +11,21 @@ const viewMenu = (
label: 'Reload', label: 'Reload',
accelerator: commandKeys['window:reload'], accelerator: commandKeys['window:reload'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('window:reload', focusedWindow as BrowserWindow); execCommand('window:reload', focusedWindow);
} }
}, },
{ {
label: 'Full Reload', label: 'Full Reload',
accelerator: commandKeys['window:reloadFull'], accelerator: commandKeys['window:reloadFull'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('window:reloadFull', focusedWindow as BrowserWindow); execCommand('window:reloadFull', focusedWindow);
} }
}, },
{ {
label: 'Developer Tools', label: 'Developer Tools',
accelerator: commandKeys['window:devtools'], accelerator: commandKeys['window:devtools'],
click: (item, focusedWindow) => { click: (item, focusedWindow) => {
execCommand('window:devtools', focusedWindow as BrowserWindow); execCommand('window:devtools', focusedWindow);
} }
}, },
{ {
@ -35,25 +35,23 @@ const viewMenu = (
label: 'Reset Zoom Level', label: 'Reset Zoom Level',
accelerator: commandKeys['zoom:reset'], accelerator: commandKeys['zoom:reset'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('zoom:reset', focusedWindow as BrowserWindow); execCommand('zoom:reset', focusedWindow);
} }
}, },
{ {
label: 'Zoom In', label: 'Zoom In',
accelerator: commandKeys['zoom:in'], accelerator: commandKeys['zoom:in'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('zoom:in', focusedWindow as BrowserWindow); execCommand('zoom:in', focusedWindow);
} }
}, },
{ {
label: 'Zoom Out', label: 'Zoom Out',
accelerator: commandKeys['zoom:out'], accelerator: commandKeys['zoom:out'],
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('zoom:out', focusedWindow as BrowserWindow); execCommand('zoom:out', focusedWindow);
} }
} }
] ]
}; };
}; };
export default viewMenu;

View file

@ -1,6 +1,6 @@
import type {BrowserWindow, MenuItemConstructorOptions} from 'electron'; import {BrowserWindow, MenuItemConstructorOptions} from 'electron';
const windowMenu = ( export default (
commandKeys: Record<string, string>, commandKeys: Record<string, string>,
execCommand: (command: string, focusedWindow?: BrowserWindow) => void execCommand: (command: string, focusedWindow?: BrowserWindow) => void
): MenuItemConstructorOptions => { ): MenuItemConstructorOptions => {
@ -37,14 +37,14 @@ const windowMenu = (
label: 'Previous', label: 'Previous',
accelerator: commandKeys['tab:prev'], accelerator: commandKeys['tab:prev'],
click: (item, focusedWindow) => { click: (item, focusedWindow) => {
execCommand('tab:prev', focusedWindow as BrowserWindow); execCommand('tab:prev', focusedWindow);
} }
}, },
{ {
label: 'Next', label: 'Next',
accelerator: commandKeys['tab:next'], accelerator: commandKeys['tab:next'],
click: (item, focusedWindow) => { click: (item, focusedWindow) => {
execCommand('tab:next', focusedWindow as BrowserWindow); execCommand('tab:next', focusedWindow);
} }
}, },
{ {
@ -63,14 +63,14 @@ const windowMenu = (
label: 'Previous', label: 'Previous',
accelerator: commandKeys['pane:prev'], accelerator: commandKeys['pane:prev'],
click: (item, focusedWindow) => { click: (item, focusedWindow) => {
execCommand('pane:prev', focusedWindow as BrowserWindow); execCommand('pane:prev', focusedWindow);
} }
}, },
{ {
label: 'Next', label: 'Next',
accelerator: commandKeys['pane:next'], accelerator: commandKeys['pane:next'],
click: (item, focusedWindow) => { click: (item, focusedWindow) => {
execCommand('pane:next', focusedWindow as BrowserWindow); execCommand('pane:next', focusedWindow);
} }
} }
] ]
@ -84,7 +84,7 @@ const windowMenu = (
{ {
label: 'Toggle Always on Top', label: 'Toggle Always on Top',
click: (item, focusedWindow) => { click: (item, focusedWindow) => {
execCommand('window:toggleKeepOnTop', focusedWindow as BrowserWindow); execCommand('window:toggleKeepOnTop', focusedWindow);
} }
}, },
{ {
@ -94,5 +94,3 @@ const windowMenu = (
] ]
}; };
}; };
export default windowMenu;

View file

@ -1,9 +1,7 @@
import type {BrowserWindow} from 'electron';
import fetch from 'electron-fetch';
import ms from 'ms'; import ms from 'ms';
import fetch from 'electron-fetch';
import {version} from './package.json'; import {version} from './package.json';
import {BrowserWindow} from 'electron';
const NEWS_URL = 'https://hyper-news.now.sh'; const NEWS_URL = 'https://hyper-news.now.sh';
@ -24,7 +22,7 @@ export default function fetchNotifications(win: BrowserWindow) {
}) })
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
const message: {text: string; url: string; dismissable: boolean} | '' = data.message || ''; const {message} = data || {};
if (typeof message !== 'object' && message !== '') { if (typeof message !== 'object' && message !== '') {
throw new Error('Bad response'); throw new Error('Bad response');
} }

View file

@ -1,5 +1,4 @@
import {app, Notification} from 'electron'; import {app, Notification} from 'electron';
import {icon} from './config/paths'; import {icon} from './config/paths';
export default function notify(title: string, body = '', details: {error?: any} = {}) { export default function notify(title: string, body = '', details: {error?: any} = {}) {

View file

@ -2,50 +2,43 @@
"name": "hyper", "name": "hyper",
"productName": "Hyper", "productName": "Hyper",
"description": "A terminal built on web technologies", "description": "A terminal built on web technologies",
"version": "4.0.0-q-canary.8", "version": "4.0.0-canary.2",
"license": "MIT", "license": "MIT",
"author": { "author": {
"name": "ZEIT, Inc.", "name": "ZEIT, Inc.",
"email": "team@zeit.co" "email": "team@zeit.co"
}, },
"repository": "quine-global/hyper", "repository": "zeit/hyper",
"scripts": {
"postinstall": "npx patch-package"
},
"dependencies": { "dependencies": {
"@babel/parser": "7.27.0", "@babel/parser": "7.20.7",
"@electron/remote": "2.1.2", "@electron/remote": "2.0.9",
"ast-types": "^0.16.1",
"async-retry": "1.3.3", "async-retry": "1.3.3",
"chokidar": "^3.6.0", "chokidar": "^3.5.3",
"color": "4.2.3", "color": "4.2.3",
"default-shell": "1.0.1", "default-shell": "1.0.1",
"electron-devtools-installer": "3.2.1",
"electron-fetch": "1.9.1", "electron-fetch": "1.9.1",
"electron-is-dev": "2.0.0", "electron-is-dev": "2.0.0",
"electron-store": "8.2.0", "electron-store": "8.1.0",
"fs-extra": "11.3.0", "fs-extra": "11.1.0",
"git-describe": "4.1.1", "git-describe": "4.1.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"mkdirp": "1.0.4",
"ms": "2.1.3", "ms": "2.1.3",
"native-process-working-directory": "^1.0.2", "native-process-working-directory": "^1.0.2",
"node-pty": "1.1.0-beta33", "node-pty": "0.11.0-beta27",
"os-locale": "5.0.0", "os-locale": "5.0.0",
"parse-url": "9.2.0", "parse-url": "8.1.0",
"pify": "5.0.0",
"queue": "6.0.2", "queue": "6.0.2",
"quine-electron-drag-click": "2.0.0", "react": "17.0.2",
"react": "18.3.1", "react-dom": "17.0.2",
"react-dom": "18.3.1", "recast": "0.22.0",
"recast": "0.23.11", "semver": "7.3.8",
"semver": "7.7.1",
"shell-env": "3.0.1", "shell-env": "3.0.1",
"sudo-prompt": "^9.2.1", "sudo-prompt": "^9.2.1",
"uuid": "10.0.0" "uuid": "9.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"native-reg": "1.1.1" "native-reg": "1.1.1"
},
"devDependencies": {
"node-gyp": "^10.2.0"
} }
} }

View file

@ -1,28 +1,22 @@
/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
import {exec, execFile} from 'child_process'; import {app, dialog, BrowserWindow, App, ipcMain} from 'electron';
import {writeFileSync} from 'fs';
import {resolve, basename} from 'path'; import {resolve, basename} from 'path';
import {promisify} from 'util'; import {writeFileSync} from 'fs';
import {app, dialog, ipcMain as _ipcMain} from 'electron';
import type {BrowserWindow, App, MenuItemConstructorOptions} from 'electron';
import React from 'react';
import Config from 'electron-store'; import Config from 'electron-store';
import ms from 'ms'; import ms from 'ms';
import React from 'react';
import ReactDom from 'react-dom'; import ReactDom from 'react-dom';
import type {IpcMainWithCommands} from '../typings/common';
import type {configOptions} from '../typings/config';
import * as config from './config'; import * as config from './config';
import {plugs} from './config/paths';
import notify from './notify'; import notify from './notify';
import {availableExtensions} from './plugins/extensions'; import {availableExtensions} from './plugins/extensions';
import {install} from './plugins/install'; import {install} from './plugins/install';
import {plugs} from './config/paths';
import mapKeys from './utils/map-keys'; import mapKeys from './utils/map-keys';
import {configOptions} from '../lib/config';
import {promisify} from 'util';
import {exec, execFile} from 'child_process';
// local storage // local storage
const cache = new Config(); const cache = new Config();
@ -211,7 +205,7 @@ function syncPackageJSON() {
description: 'Auto-generated from `hyper.json`!', description: 'Auto-generated from `hyper.json`!',
private: true, private: true,
version: '0.0.1', version: '0.0.1',
repository: 'quine-global/hyper', repository: 'vercel/hyper',
license: 'MIT', license: 'MIT',
homepage: 'https://hyper.is', homepage: 'https://hyper.is',
dependencies dependencies
@ -282,7 +276,7 @@ function requirePlugins(): any[] {
const {plugins: plugins_, localPlugins} = paths; const {plugins: plugins_, localPlugins} = paths;
const load = (path_: string) => { const load = (path_: string) => {
let mod: Record<string, any>; let mod: any;
try { try {
mod = require(path_); mod = require(path_);
const exposed = mod && Object.keys(mod).some((key) => availableExtensions.has(key)); const exposed = mod && Object.keys(mod).some((key) => availableExtensions.has(key));
@ -318,7 +312,7 @@ function requirePlugins(): any[] {
...localPlugins.filter((p) => basename(p) !== 'migrated-hyper3-config') ...localPlugins.filter((p) => basename(p) !== 'migrated-hyper3-config')
] ]
.map(load) .map(load)
.filter((v): v is Record<string, any> => Boolean(v)); .filter((v) => Boolean(v));
} }
export const onApp = (app_: App) => { export const onApp = (app_: App) => {
@ -421,7 +415,7 @@ export const getDeprecatedConfig = () => {
return deprecated; return deprecated;
}; };
export const decorateMenu = (tpl: MenuItemConstructorOptions[]) => { export const decorateMenu = (tpl: any) => {
return decorateObject(tpl, 'decorateMenu'); return decorateObject(tpl, 'decorateMenu');
}; };
@ -429,8 +423,8 @@ export const getDecoratedEnv = (baseEnv: Record<string, string>) => {
return decorateObject(baseEnv, 'decorateEnv'); return decorateObject(baseEnv, 'decorateEnv');
}; };
export const getDecoratedConfig = (profile: string) => { export const getDecoratedConfig = () => {
const baseConfig = config.getProfileConfig(profile); const baseConfig = config.getConfig();
const decoratedConfig = decorateObject(baseConfig, 'decorateConfig'); const decoratedConfig = decorateObject(baseConfig, 'decorateConfig');
const fixedConfig = config.fixConfigDefaults(decoratedConfig); const fixedConfig = config.fixConfigDefaults(decoratedConfig);
const translatedConfig = config.htermConfigTranslate(fixedConfig); const translatedConfig = config.htermConfigTranslate(fixedConfig);
@ -462,19 +456,12 @@ export const decorateSessionClass = <T>(Session: T): T => {
export {toDependencies as _toDependencies}; export {toDependencies as _toDependencies};
const ipcMain = _ipcMain as IpcMainWithCommands; ipcMain.handle('child_process.exec', (event, args) => {
const {command, options} = args;
ipcMain.handle('child_process.exec', (event, command, options) => {
return promisify(exec)(command, options); return promisify(exec)(command, options);
}); });
ipcMain.handle('child_process.execFile', (event, file, args, options) => { ipcMain.handle('child_process.execFile', (event, _args) => {
const {file, args, options} = _args;
return promisify(execFile)(file, args, options); return promisify(execFile)(file, args, options);
}); });
ipcMain.handle('getLoadedPluginVersions', () => getLoadedPluginVersions());
ipcMain.handle('getPaths', () => getPaths());
ipcMain.handle('getBasePaths', () => getBasePaths());
ipcMain.handle('getDeprecatedConfig', () => getDeprecatedConfig());
ipcMain.handle('getDecoratedConfig', (e, profile) => getDecoratedConfig(profile));
ipcMain.handle('getDecoratedKeymaps', () => getDecoratedKeymaps());

View file

@ -1,8 +1,6 @@
import cp from 'child_process'; import cp from 'child_process';
import ms from 'ms';
import queue from 'queue'; import queue from 'queue';
import ms from 'ms';
import {yarn, plugs} from '../config/paths'; import {yarn, plugs} from '../config/paths';
export const install = (fn: (err: string | null) => void) => { export const install = (fn: (err: string | null) => void) => {

View file

@ -1,22 +1,15 @@
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
import {ipcMain, BrowserWindow} from 'electron';
import {ipcMain} from 'electron';
import type {BrowserWindow, IpcMainEvent} from 'electron';
import {v4 as uuidv4} from 'uuid'; import {v4 as uuidv4} from 'uuid';
import type {TypedEmitter, MainEvents, RendererEvents, FilterNever} from '../typings/common'; export class Server extends EventEmitter {
export class Server {
emitter: TypedEmitter<MainEvents>;
destroyed = false; destroyed = false;
win: BrowserWindow; win: BrowserWindow;
id!: string; id!: string;
constructor(win: BrowserWindow) { constructor(win: BrowserWindow) {
this.emitter = new EventEmitter(); super();
this.win = win; this.win = win;
this.emit = this.emit.bind(this); this.ipcListener = this.ipcListener.bind(this);
if (this.destroyed) { if (this.destroyed) {
return; return;
@ -25,13 +18,14 @@ export class Server {
const uid = uuidv4(); const uid = uuidv4();
this.id = uid; this.id = uid;
// eslint-disable-next-line @typescript-eslint/unbound-method
ipcMain.on(uid, this.ipcListener); ipcMain.on(uid, this.ipcListener);
// we intentionally subscribe to `on` instead of `once` // we intentionally subscribe to `on` instead of `once`
// to support reloading the window and re-initializing // to support reloading the window and re-initializing
// the channel // the channel
this.wc.on('did-finish-load', () => { this.wc.on('did-finish-load', () => {
this.wc.send('init', uid, win.profileName); this.wc.send('init', uid);
}); });
} }
@ -39,35 +33,23 @@ export class Server {
return this.win.webContents; return this.win.webContents;
} }
ipcListener = <U extends keyof MainEvents>(event: IpcMainEvent, {ev, data}: {ev: U; data: MainEvents[U]}) => ipcListener(event: any, {ev, data}: {ev: string; data: any}) {
this.emitter.emit(ev, data); super.emit(ev, data);
}
on = <U extends keyof MainEvents>(ev: U, fn: (arg0: MainEvents[U]) => void) => { emit(ch: string, data: any = {}): any {
this.emitter.on(ev, fn);
return this;
};
once = <U extends keyof MainEvents>(ev: U, fn: (arg0: MainEvents[U]) => void) => {
this.emitter.once(ev, fn);
return this;
};
emit<U extends Exclude<keyof RendererEvents, FilterNever<RendererEvents>>>(ch: U): boolean;
emit<U extends FilterNever<RendererEvents>>(ch: U, data: RendererEvents[U]): boolean;
emit<U extends keyof RendererEvents>(ch: U, data?: RendererEvents[U]) {
// This check is needed because data-batching can cause extra data to be // This check is needed because data-batching can cause extra data to be
// emitted after the window has already closed // emitted after the window has already closed
if (!this.win.isDestroyed()) { if (!this.win.isDestroyed()) {
this.wc.send(this.id, {ch, data}); this.wc.send(this.id, {ch, data});
return true;
} }
return false;
} }
destroy() { destroy() {
this.emitter.removeAllListeners(); this.removeAllListeners();
this.wc.removeAllListeners(); this.wc.removeAllListeners();
if (this.id) { if (this.id) {
// eslint-disable-next-line @typescript-eslint/unbound-method
ipcMain.removeListener(this.id, this.ipcListener); ipcMain.removeListener(this.id, this.ipcListener);
} else { } else {
// mark for `genUid` in constructor // mark for `genUid` in constructor
@ -76,8 +58,6 @@ export class Server {
} }
} }
const createRPC = (win: BrowserWindow) => { export default (win: BrowserWindow) => {
return new Server(win); return new Server(win);
}; };
export default createRPC;

View file

@ -1,17 +1,14 @@
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
import {dirname} from 'path';
import {StringDecoder} from 'string_decoder'; import {StringDecoder} from 'string_decoder';
import defaultShell from 'default-shell'; import defaultShell from 'default-shell';
import type {IPty, IWindowsPtyForkOptions, spawn as npSpawn} from 'node-pty';
import osLocale from 'os-locale';
import shellEnv from 'shell-env';
import * as config from './config';
import {cliScriptPath} from './config/paths';
import {productName, version} from './package.json';
import {getDecoratedEnv} from './plugins'; import {getDecoratedEnv} from './plugins';
import {getFallBackShellConfig} from './utils/shell-fallback'; import {productName, version} from './package.json';
import * as config from './config';
import {IPty, IWindowsPtyForkOptions, spawn as npSpawn} from 'node-pty';
import {cliScriptPath} from './config/paths';
import {dirname} from 'path';
import shellEnv from 'shell-env';
import osLocale from 'os-locale';
const createNodePtyError = () => const createNodePtyError = () =>
new Error( new Error(
@ -26,6 +23,7 @@ try {
throw createNodePtyError(); throw createNodePtyError();
} }
const envFromConfig = config.getConfig().env || {};
const useConpty = config.getConfig().useConpty; const useConpty = config.getConfig().useConpty;
// Max duration to batch session data before sending it to the renderer process. // Max duration to batch session data before sending it to the renderer process.
@ -58,7 +56,7 @@ class DataBatcher extends EventEmitter {
this.timeout = null; this.timeout = null;
} }
write(chunk: Buffer | string) { write(chunk: Buffer) {
if (this.data.length + chunk.length >= BATCH_MAX_SIZE) { if (this.data.length + chunk.length >= BATCH_MAX_SIZE) {
// We've reached the max batch size. Flush it and start another one // We've reached the max batch size. Flush it and start another one
if (this.timeout) { if (this.timeout) {
@ -68,7 +66,7 @@ class DataBatcher extends EventEmitter {
this.flush(); this.flush();
} }
this.data += typeof chunk === 'string' ? chunk : this.decoder.write(chunk); this.data += this.decoder.write(chunk);
if (!this.timeout) { if (!this.timeout) {
this.timeout = setTimeout(() => this.flush(), BATCH_DURATION_MS); this.timeout = setTimeout(() => this.flush(), BATCH_DURATION_MS);
@ -86,12 +84,11 @@ class DataBatcher extends EventEmitter {
interface SessionOptions { interface SessionOptions {
uid: string; uid: string;
rows?: number; rows: number;
cols?: number; cols: number;
cwd?: string; cwd: string;
shell?: string; shell: string;
shellArgs?: string[]; shellArgs: string[];
profile: string;
} }
export default class Session extends EventEmitter { export default class Session extends EventEmitter {
pty: IPty | null; pty: IPty | null;
@ -99,7 +96,6 @@ export default class Session extends EventEmitter {
shell: string | null; shell: string | null;
ended: boolean; ended: boolean;
initTimestamp: number; initTimestamp: number;
profile!: string;
constructor(options: SessionOptions) { constructor(options: SessionOptions) {
super(); super();
this.pty = null; this.pty = null;
@ -110,25 +106,22 @@ export default class Session extends EventEmitter {
this.init(options); this.init(options);
} }
init({uid, rows, cols, cwd, shell: _shell, shellArgs: _shellArgs, profile}: SessionOptions) { init({uid, rows, cols: columns, cwd, shell: _shell, shellArgs: _shellArgs}: SessionOptions) {
this.profile = profile;
const envFromConfig = config.getProfileConfig(profile).env || {};
const defaultShellArgs = ['--login'];
const shell = _shell || defaultShell;
const shellArgs = _shellArgs || defaultShellArgs;
const cleanEnv = const cleanEnv =
process.env['APPIMAGE'] && process.env['APPDIR'] ? shellEnv.sync(_shell || defaultShell) : process.env; process.env['APPIMAGE'] && process.env['APPDIR'] ? shellEnv.sync(_shell || defaultShell) : process.env;
const baseEnv: Record<string, string> = { const baseEnv = Object.assign(
...cleanEnv, {},
LANG: `${osLocale.sync().replace(/-/, '_')}.UTF-8`, cleanEnv,
TERM: 'xterm-256color', {
COLORTERM: 'truecolor', LANG: `${osLocale.sync().replace(/-/, '_')}.UTF-8`,
TERM_PROGRAM: productName, TERM: 'xterm-256color',
TERM_PROGRAM_VERSION: version, COLORTERM: 'truecolor',
...envFromConfig TERM_PROGRAM: productName,
}; TERM_PROGRAM_VERSION: version
},
envFromConfig
);
// path to AppImage mount point is added to PATH environment variable automatically // path to AppImage mount point is added to PATH environment variable automatically
// which conflicts with the cli // which conflicts with the cli
if (baseEnv['APPIMAGE'] && baseEnv['APPDIR']) { if (baseEnv['APPIMAGE'] && baseEnv['APPDIR']) {
@ -144,8 +137,10 @@ export default class Session extends EventEmitter {
delete baseEnv.GOOGLE_API_KEY; delete baseEnv.GOOGLE_API_KEY;
} }
const defaultShellArgs = ['--login'];
const options: IWindowsPtyForkOptions = { const options: IWindowsPtyForkOptions = {
cols, cols: columns,
rows, rows,
cwd, cwd,
env: getDecoratedEnv(baseEnv) env: getDecoratedEnv(baseEnv)
@ -156,6 +151,9 @@ export default class Session extends EventEmitter {
options.useConpty = useConpty; options.useConpty = useConpty;
} }
const shell = _shell || defaultShell;
const shellArgs = _shellArgs || defaultShellArgs;
try { try {
this.pty = spawn(shell, shellArgs, options); this.pty = spawn(shell, shellArgs, options);
} catch (_err) { } catch (_err) {
@ -172,7 +170,7 @@ export default class Session extends EventEmitter {
if (this.ended) { if (this.ended) {
return; return;
} }
this.batcher?.write(chunk); this.batcher?.write(chunk as any);
}); });
this.batcher.on('flush', (data: string) => { this.batcher.on('flush', (data: string) => {
@ -185,32 +183,15 @@ export default class Session extends EventEmitter {
// this will inform users in case there are errors in the config instead of instant exit // this will inform users in case there are errors in the config instead of instant exit
const runDuration = new Date().getTime() - this.initTimestamp; const runDuration = new Date().getTime() - this.initTimestamp;
if (e.exitCode > 0 && runDuration < 1000) { if (e.exitCode > 0 && runDuration < 1000) {
const fallBackShellConfig = getFallBackShellConfig(shell, shellArgs, defaultShell, defaultShellArgs); const defaultShellConfig = {shell: defaultShell, shellArgs: defaultShellArgs};
if (fallBackShellConfig) { const msg = `
const msg = `
shell exited in ${runDuration} ms with exit code ${e.exitCode} shell exited in ${runDuration} ms with exit code ${e.exitCode}
please check the shell config: ${JSON.stringify({shell, shellArgs}, undefined, 2)} please check the shell config: ${JSON.stringify({shell, shellArgs}, undefined, 2)}
using fallback shell config: ${JSON.stringify(fallBackShellConfig, undefined, 2)} fallback to default shell config: ${JSON.stringify(defaultShellConfig, undefined, 2)}
`; `;
console.warn(msg); console.warn(msg);
this.batcher?.write(msg.replace(/\n/g, '\r\n')); this.batcher?.write(msg.replace(/\n/g, '\r\n') as any);
this.init({ this.init({uid, rows, cols: columns, cwd, ...defaultShellConfig});
uid,
rows,
cols,
cwd,
shell: fallBackShellConfig.shell,
shellArgs: fallBackShellConfig.shellArgs,
profile
});
} else {
const msg = `
shell exited in ${runDuration} ms with exit code ${e.exitCode}
No fallback available, please check the shell config.
`;
console.warn(msg);
this.batcher?.write(msg.replace(/\n/g, '\r\n'));
}
} else { } else {
this.ended = true; this.ended = true;
this.emit('exit'); this.emit('exit');

View file

@ -1,20 +1,12 @@
{ {
"extends": "../tsconfig.base.json", "extends": "../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"declaration": true,
"declarationDir": "../dist/tmp/appdts/", "declarationDir": "../dist/tmp/appdts/",
"outDir": "../target/", "outDir": "../target/",
"composite": true,
"noImplicitAny": false "noImplicitAny": false
}, },
"include": [ "include": [
"./**/*", "./**/*",
"./package.json", "./package.json"
"../typings/extend-electron.d.ts",
"../typings/ext-modules.d.ts"
],
"exclude": [
"../dist/**/*",
"../target/**/*"
] ]
} }

View file

@ -1,11 +1,8 @@
import type {MenuItemConstructorOptions, BrowserWindow} from 'electron';
import {execCommand} from '../commands';
import {getProfiles} from '../config';
import editMenu from '../menus/menus/edit'; import editMenu from '../menus/menus/edit';
import shellMenu from '../menus/menus/shell'; import shellMenu from '../menus/menus/shell';
import {execCommand} from '../commands';
import {getDecoratedKeymaps} from '../plugins'; import {getDecoratedKeymaps} from '../plugins';
import {MenuItemConstructorOptions, BrowserWindow} from 'electron';
const separator: MenuItemConstructorOptions = {type: 'separator'}; const separator: MenuItemConstructorOptions = {type: 'separator'};
const getCommandKeys = (keymaps: Record<string, string[]>): Record<string, string> => const getCommandKeys = (keymaps: Record<string, string[]>): Record<string, string> =>
@ -23,20 +20,14 @@ const filterCutCopy = (selection: string, menuItem: MenuItemConstructorOptions)
return menuItem; return menuItem;
}; };
const contextMenuTemplate = ( export default (
createWindow: (fn?: (win: BrowserWindow) => void, options?: Record<string, any>) => BrowserWindow, createWindow: (fn?: (win: BrowserWindow) => void, options?: Record<string, any>) => BrowserWindow,
selection: string selection: string
) => { ) => {
const commandKeys = getCommandKeys(getDecoratedKeymaps()); const commandKeys = getCommandKeys(getDecoratedKeymaps());
const _shell = shellMenu( const _shell = shellMenu(commandKeys, execCommand).submenu as MenuItemConstructorOptions[];
commandKeys,
(command, focusedWindow) => execCommand(command, focusedWindow as BrowserWindow | undefined),
getProfiles().map((p) => p.name)
).submenu as MenuItemConstructorOptions[];
const _edit = editMenu(commandKeys, execCommand).submenu.filter(filterCutCopy.bind(null, selection)); const _edit = editMenu(commandKeys, execCommand).submenu.filter(filterCutCopy.bind(null, selection));
return _edit return _edit
.concat(separator, _shell) .concat(separator, _shell)
.filter((menuItem) => !Object.prototype.hasOwnProperty.call(menuItem, 'enabled') || menuItem.enabled); .filter((menuItem) => !Object.prototype.hasOwnProperty.call(menuItem, 'enabled') || menuItem.enabled);
}; };
export default contextMenuTemplate;

View file

@ -1,41 +1,28 @@
import {existsSync} from 'fs'; import {app, BrowserWindow, shell, Menu, BrowserWindowConstructorOptions, Event} from 'electron';
import {isAbsolute, normalize, sep} from 'path'; import {isAbsolute, normalize, sep} from 'path';
import {URL, fileURLToPath} from 'url'; import {URL, fileURLToPath} from 'url';
import {app, BrowserWindow, shell, Menu} from 'electron';
import type {BrowserWindowConstructorOptions} from 'electron';
import {enable as remoteEnable} from '@electron/remote/main';
import isDev from 'electron-is-dev';
import {getWorkingDirectoryFromPID} from 'native-process-working-directory';
import electronDragClick from 'quine-electron-drag-click';
import {v4 as uuidv4} from 'uuid'; import {v4 as uuidv4} from 'uuid';
import isDev from 'electron-is-dev';
import type {sessionExtraOptions} from '../../typings/common';
import type {configOptions} from '../../typings/config';
import {execCommand} from '../commands';
import {getDefaultProfile} from '../config';
import {icon, homeDirectory} from '../config/paths';
import fetchNotifications from '../notifications';
import notify from '../notify';
import {decorateSessionOptions, decorateSessionClass} from '../plugins';
import createRPC from '../rpc';
import Session from '../session';
import updater from '../updater'; import updater from '../updater';
import {setRendererType, unsetRendererType} from '../utils/renderer-utils';
import toElectronBackgroundColor from '../utils/to-electron-background-color'; import toElectronBackgroundColor from '../utils/to-electron-background-color';
import {icon, homeDirectory} from '../config/paths';
import createRPC from '../rpc';
import notify from '../notify';
import fetchNotifications from '../notifications';
import Session from '../session';
import contextMenuTemplate from './contextmenu'; import contextMenuTemplate from './contextmenu';
import {execCommand} from '../commands';
if (process.platform === 'darwin') { import {setRendererType, unsetRendererType} from '../utils/renderer-utils';
electronDragClick(); import {decorateSessionOptions, decorateSessionClass} from '../plugins';
} import {enable as remoteEnable} from '@electron/remote/main';
import {configOptions} from '../../lib/config';
import {getWorkingDirectoryFromPID} from 'native-process-working-directory';
import {existsSync} from 'fs';
export function newWindow( export function newWindow(
options_: BrowserWindowConstructorOptions, options_: BrowserWindowConstructorOptions,
cfg: configOptions, cfg: configOptions,
fn?: (win: BrowserWindow) => void, fn?: (win: BrowserWindow) => void
profileName: string = getDefaultProfile()
): BrowserWindow { ): BrowserWindow {
const classOpts = Object.assign({uid: uuidv4()}); const classOpts = Object.assign({uid: uuidv4()});
app.plugins.decorateWindowClass(classOpts); app.plugins.decorateWindowClass(classOpts);
@ -61,8 +48,6 @@ export function newWindow(
}; };
const window = new BrowserWindow(app.plugins.getDecoratedBrowserOptions(winOpts)); const window = new BrowserWindow(app.plugins.getDecoratedBrowserOptions(winOpts));
window.profileName = profileName;
// Enable remote module on this window // Enable remote module on this window
remoteEnable(window.webContents); remoteEnable(window.webContents);
@ -75,13 +60,28 @@ export function newWindow(
const sessions = new Map<string, Session>(); const sessions = new Map<string, Session>();
const updateBackgroundColor = () => { const updateBackgroundColor = () => {
const cfg_ = app.plugins.getDecoratedConfig(profileName); const cfg_ = app.plugins.getDecoratedConfig();
window.setBackgroundColor(toElectronBackgroundColor(cfg_.backgroundColor || '#000')); window.setBackgroundColor(toElectronBackgroundColor(cfg_.backgroundColor || '#000'));
}; };
// set working directory
let argPath = process.argv[1];
if (argPath && process.platform === 'win32') {
if (/[a-zA-Z]:"/.test(argPath)) {
argPath = argPath.replace('"', sep);
}
argPath = normalize(argPath + sep);
}
let workingDirectory = homeDirectory;
if (argPath && isAbsolute(argPath)) {
workingDirectory = argPath;
} else if (cfg.workingDirectory && isAbsolute(cfg.workingDirectory)) {
workingDirectory = cfg.workingDirectory;
}
// config changes // config changes
const cfgUnsubscribe = app.config.subscribe(() => { const cfgUnsubscribe = app.config.subscribe(() => {
const cfg_ = app.plugins.getDecoratedConfig(profileName); const cfg_ = app.plugins.getDecoratedConfig();
// notify renderer // notify renderer
window.webContents.send('config change'); window.webContents.send('config change');
@ -124,58 +124,34 @@ export function newWindow(
} }
}); });
function createSession(extraOptions: sessionExtraOptions = {}) { function createSession(extraOptions: any = {}) {
const uid = uuidv4(); const uid = uuidv4();
const extraOptionsFiltered: sessionExtraOptions = {}; const extraOptionsFiltered: any = {};
Object.keys(extraOptions).forEach((key) => { Object.keys(extraOptions).forEach((key) => {
if (extraOptions[key] !== undefined) extraOptionsFiltered[key] = extraOptions[key]; if (extraOptions[key] !== undefined) extraOptionsFiltered[key] = extraOptions[key];
}); });
const profile = extraOptionsFiltered.profile || profileName;
const activeSession = extraOptionsFiltered.activeUid ? sessions.get(extraOptionsFiltered.activeUid) : undefined;
let cwd = ''; let cwd = '';
if (cfg.preserveCWD !== false && activeSession && activeSession.profile === profile) { if (cfg.preserveCWD === undefined || cfg.preserveCWD) {
const activePID = activeSession.pty?.pid; const activePID = extraOptionsFiltered.activeUid && sessions.get(extraOptionsFiltered.activeUid)?.pty?.pid;
if (activePID !== undefined) { try {
try { cwd = activePID && getWorkingDirectoryFromPID(activePID);
cwd = getWorkingDirectoryFromPID(activePID) || ''; } catch (error) {
} catch (error) { console.error(error);
console.error(error);
}
} }
cwd = cwd && isAbsolute(cwd) && existsSync(cwd) ? cwd : ''; cwd = cwd && isAbsolute(cwd) && existsSync(cwd) ? cwd : '';
} }
const profileCfg = app.plugins.getDecoratedConfig(profile);
// set working directory
let argPath = process.argv[1];
if (argPath && process.platform === 'win32') {
if (/[a-zA-Z]:"/.test(argPath)) {
argPath = argPath.replace('"', sep);
}
argPath = normalize(argPath + sep);
}
let workingDirectory = homeDirectory;
if (argPath && isAbsolute(argPath)) {
workingDirectory = argPath;
} else if (profileCfg.workingDirectory && isAbsolute(profileCfg.workingDirectory)) {
workingDirectory = profileCfg.workingDirectory;
}
// remove the rows and cols, the wrong value of them will break layout when init create // remove the rows and cols, the wrong value of them will break layout when init create
const defaultOptions = Object.assign( const defaultOptions = Object.assign(
{ {
cwd: cwd || workingDirectory, cwd: cwd || workingDirectory,
splitDirection: undefined, splitDirection: undefined,
shell: profileCfg.shell, shell: cfg.shell,
shellArgs: profileCfg.shellArgs && Array.from(profileCfg.shellArgs) shellArgs: cfg.shellArgs && Array.from(cfg.shellArgs)
}, },
extraOptionsFiltered, extraOptionsFiltered,
{ {uid}
profile: extraOptionsFiltered.profile || profileName,
uid
}
); );
const options = decorateSessionOptions(defaultOptions); const options = decorateSessionOptions(defaultOptions);
const DecoratedSession = decorateSessionClass(Session); const DecoratedSession = decorateSessionClass(Session);
@ -195,8 +171,7 @@ export function newWindow(
splitDirection: options.splitDirection, splitDirection: options.splitDirection,
shell: session.shell, shell: session.shell,
pid: session.pty ? session.pty.pid : null, pid: session.pty ? session.pty.pid : null,
activeUid: options.activeUid ?? undefined, activeUid: options.activeUid
profile: options.profile
}); });
session.on('data', (data: string) => { session.on('data', (data: string) => {
@ -231,8 +206,8 @@ export function newWindow(
session.resize({cols, rows}); session.resize({cols, rows});
} }
}); });
rpc.on('data', ({uid, data, escaped}) => { rpc.on('data', ({uid, data, escaped}: {uid: string; data: string; escaped: boolean}) => {
const session = uid && sessions.get(uid); const session = sessions.get(uid);
if (session) { if (session) {
if (escaped) { if (escaped) {
const escapedData = session.shell?.endsWith('cmd.exe') const escapedData = session.shell?.endsWith('cmd.exe')
@ -262,12 +237,11 @@ export function newWindow(
// Same deal as above, grabbing the window titlebar when the window // Same deal as above, grabbing the window titlebar when the window
// is maximized on Windows results in unmaximize, without hitting any // is maximized on Windows results in unmaximize, without hitting any
// app buttons // app buttons
const onGeometryChange = () => rpc.emit('windowGeometry change', {isMaximized: window.isMaximized()}); for (const ev of ['maximize', 'unmaximize', 'minimize', 'restore'] as any) {
window.on('maximize', onGeometryChange); window.on(ev, () => {
window.on('unmaximize', onGeometryChange); rpc.emit('windowGeometry change', {isMaximized: window.isMaximized()});
window.on('minimize', onGeometryChange); });
window.on('restore', onGeometryChange); }
window.on('move', () => { window.on('move', () => {
const position = window.getPosition(); const position = window.getPosition();
rpc.emit('move', {bounds: {x: position[0], y: position[1]}}); rpc.emit('move', {bounds: {x: position[0], y: position[1]}});
@ -281,10 +255,10 @@ export function newWindow(
}); });
// pass on the full screen events from the window to react // pass on the full screen events from the window to react
rpc.win.on('enter-full-screen', () => { rpc.win.on('enter-full-screen', () => {
rpc.emit('enter full screen'); rpc.emit('enter full screen', {});
}); });
rpc.win.on('leave-full-screen', () => { rpc.win.on('leave-full-screen', () => {
rpc.emit('leave full screen'); rpc.emit('leave full screen', {});
}); });
const deleteSessions = () => { const deleteSessions = () => {
sessions.forEach((session, key) => { sessions.forEach((session, key) => {
@ -302,33 +276,22 @@ export function newWindow(
} }
}); });
const handleDroppedURL = (url: string) => { const handleDrop = (event: Event, url: string) => {
const protocol = typeof url === 'string' && new URL(url).protocol; const protocol = typeof url === 'string' && new URL(url).protocol;
if (protocol === 'file:') { if (protocol === 'file:') {
event.preventDefault();
const path = fileURLToPath(url); const path = fileURLToPath(url);
return {uid: null, data: path, escaped: true}; rpc.emit('session data send', {data: path, escaped: true});
} else if (protocol === 'http:' || protocol === 'https:') { } else if (protocol === 'http:' || protocol === 'https:') {
return {uid: null, data: url}; event.preventDefault();
rpc.emit('session data send', {data: url});
} }
}; };
// If file is dropped onto the terminal window, navigate and new-window events are prevented // If file is dropped onto the terminal window, navigate and new-window events are prevented
// and it's path is added to active session. // and his path is added to active session.
window.webContents.on('will-navigate', (event, url) => { window.webContents.on('will-navigate', handleDrop);
const data = handleDroppedURL(url); window.webContents.on('new-window', handleDrop);
if (data) {
event.preventDefault();
rpc.emit('session data send', data);
}
});
window.webContents.setWindowOpenHandler(({url}) => {
const data = handleDroppedURL(url);
if (data) {
rpc.emit('session data send', data);
return {action: 'deny'};
}
return {action: 'allow'};
});
// expose internals to extension authors // expose internals to extension authors
window.rpc = rpc; window.rpc = rpc;

View file

@ -1,29 +1,12 @@
// Packages // Packages
import electron, {app} from 'electron'; import electron, {app, BrowserWindow, AutoUpdater} from 'electron';
import type {BrowserWindow, AutoUpdater as OriginalAutoUpdater} from 'electron';
import retry from 'async-retry';
import ms from 'ms'; import ms from 'ms';
import retry from 'async-retry';
// Utilities // Utilities
import autoUpdaterLinux from './auto-updater-linux';
import {getDefaultProfile} from './config';
import {version} from './package.json'; import {version} from './package.json';
import {getDecoratedConfig} from './plugins'; import {getDecoratedConfig} from './plugins';
import autoUpdaterLinux from './auto-updater-linux';
// Necessary due to typescript not handling overloads well
type AutoUpdaterEvent =
| 'error'
| 'checking-for-update'
| 'before-quit-for-update'
| 'update-downloaded'
| 'update-available'
| 'update-not-available';
interface AutoUpdater extends OriginalAutoUpdater {
on(event: AutoUpdaterEvent, listener: Function): this;
removeListener(event: AutoUpdaterEvent, listener: Function): this;
}
const {platform} = process; const {platform} = process;
const isLinux = platform === 'linux'; const isLinux = platform === 'linux';
@ -32,7 +15,7 @@ const autoUpdater: AutoUpdater = isLinux ? autoUpdaterLinux : electron.autoUpdat
const getDecoratedConfigWithRetry = async () => { const getDecoratedConfigWithRetry = async () => {
return await retry(() => { return await retry(() => {
const content = getDecoratedConfig(getDefaultProfile()); const content = getDecoratedConfig();
if (!content) { if (!content) {
throw new Error('No config content loaded'); throw new Error('No config content loaded');
} }
@ -86,23 +69,28 @@ async function init() {
isInit = true; isInit = true;
} }
const updater = (win: BrowserWindow) => { export default (win: BrowserWindow) => {
if (!isInit) { if (!isInit) {
void init(); void init();
} }
const {rpc} = win; const {rpc} = win;
const onupdate = (ev: Event, releaseNotes: string, releaseName: string, date: Date, updateUrl: string) => { const onupdate = (
const releaseUrl = updateUrl || `https://github.com/quine-global/hyper/releases/tag/${releaseName}`; ev: Event,
rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !isLinux}); releaseNotes: string,
releaseName: string,
date: Date,
updateUrl: string,
onQuitAndInstall: any
) => {
const releaseUrl = updateUrl || `https://github.com/vercel/hyper/releases/tag/${releaseName}`;
rpc.emit('update available', {releaseNotes, releaseName, releaseUrl, canInstall: !!onQuitAndInstall});
}; };
if (isLinux) { const eventName: any = isLinux ? 'update-available' : 'update-downloaded';
autoUpdater.on('update-available', onupdate);
} else { autoUpdater.on(eventName, onupdate);
autoUpdater.on('update-downloaded', onupdate);
}
rpc.once('quit and install', () => { rpc.once('quit and install', () => {
autoUpdater.quitAndInstall(); autoUpdater.quitAndInstall();
@ -123,12 +111,6 @@ const updater = (win: BrowserWindow) => {
}); });
win.on('close', () => { win.on('close', () => {
if (isLinux) { autoUpdater.removeListener(eventName, onupdate);
autoUpdater.removeListener('update-available', onupdate);
} else {
autoUpdater.removeListener('update-downloaded', onupdate);
}
}); });
}; };
export default updater;

View file

@ -1,23 +1,20 @@
import {existsSync, readlink, symlink} from 'fs'; import pify from 'pify';
import fs from 'fs';
import path from 'path'; import path from 'path';
import {promisify} from 'util'; import notify from '../notify';
import {cliScriptPath, cliLinkPath} from '../config/paths';
import {clipboard, dialog} from 'electron';
import {mkdirpSync} from 'fs-extra';
import * as Registry from 'native-reg'; import * as Registry from 'native-reg';
import type {ValueType} from 'native-reg'; import type {ValueType} from 'native-reg';
import sudoPrompt from 'sudo-prompt'; import sudoPrompt from 'sudo-prompt';
import {clipboard, dialog} from 'electron';
import {mkdirpSync} from 'fs-extra';
import {cliScriptPath, cliLinkPath} from '../config/paths'; const readlink = pify(fs.readlink);
import notify from '../notify'; const symlink = pify(fs.symlink);
const sudoExec = pify(sudoPrompt.exec, {multiArgs: true});
const readLink = promisify(readlink);
const symLink = promisify(symlink);
const sudoExec = promisify(sudoPrompt.exec);
const checkInstall = () => { const checkInstall = () => {
return readLink(cliLinkPath) return readlink(cliLinkPath)
.then((link) => link === cliScriptPath) .then((link) => link === cliScriptPath)
.catch((err) => { .catch((err) => {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {
@ -35,14 +32,14 @@ const addSymlink = async (silent: boolean) => {
return; return;
} }
console.log('Linking HyperCLI'); console.log('Linking HyperCLI');
if (!existsSync(path.dirname(cliLinkPath))) { if (!fs.existsSync(path.dirname(cliLinkPath))) {
try { try {
mkdirpSync(path.dirname(cliLinkPath)); mkdirpSync(path.dirname(cliLinkPath));
} catch (err) { } catch (err) {
throw `Failed to create directory ${path.dirname(cliLinkPath)} - ${err}`; throw `Failed to create directory ${path.dirname(cliLinkPath)} - ${err}`;
} }
} }
await symLink(cliScriptPath, cliLinkPath); await symlink(cliScriptPath, cliLinkPath);
} catch (_err) { } catch (_err) {
const err = _err as {code: string}; const err = _err as {code: string};
// 'EINVAL' is returned by readlink, // 'EINVAL' is returned by readlink,

View file

@ -11,7 +11,7 @@ const generatePrefixedCommand = (command: string, shortcuts: string[]) => {
return result; return result;
}; };
const mapKeys = (config: Record<string, string[] | string>) => { export default (config: Record<string, string[] | string>) => {
return Object.keys(config).reduce((keymap: Record<string, string[]>, command: string) => { return Object.keys(config).reduce((keymap: Record<string, string[]>, command: string) => {
if (!command) { if (!command) {
return keymap; return keymap;
@ -39,5 +39,3 @@ const mapKeys = (config: Record<string, string[] | string>) => {
return keymap; return keymap;
}, {}); }, {});
}; };
export default mapKeys;

View file

@ -1,25 +0,0 @@
export const getFallBackShellConfig = (
shell: string,
shellArgs: string[],
defaultShell: string,
defaultShellArgs: string[]
): {
shell: string;
shellArgs: string[];
} | null => {
if (shellArgs.length > 0) {
return {
shell,
shellArgs: []
};
}
if (shell != defaultShell) {
return {
shell: defaultShell,
shellArgs: defaultShellArgs
};
}
return null;
};

View file

@ -9,7 +9,7 @@ const regKeys = [
]; ];
const regParts = [ const regParts = [
{key: 'command', name: '', value: `${appPath} "%V"`}, {key: 'command', name: '', value: `${appPath} "%V"`},
{name: '', value: 'Open &Hyper here'}, {name: '', value: 'Open Hyper here'},
{name: 'Icon', value: `${appPath}`} {name: 'Icon', value: `${appPath}`}
]; ];

View file

@ -4,7 +4,7 @@ import Color from 'color';
// returns a background color that's in hex // returns a background color that's in hex
// format including the alpha channel (e.g.: `#00000050`) // format including the alpha channel (e.g.: `#00000050`)
// input can be any css value (rgb, hsl, string…) // input can be any css value (rgb, hsl, string…)
const toElectronBackgroundColor = (bgColor: string) => { export default (bgColor: string) => {
const color = Color(bgColor); const color = Color(bgColor);
if (color.alpha() === 1) { if (color.alpha() === 1) {
@ -15,5 +15,3 @@ const toElectronBackgroundColor = (bgColor: string) => {
const alphaHex = Math.round(color.alpha() * 255).toString(16); const alphaHex = Math.round(color.alpha() * 255).toString(16);
return `#${alphaHex}${color.hex().toString().slice(1)}`; return `#${alphaHex}${color.hex().toString().slice(1)}`;
}; };
export default toElectronBackgroundColor;

File diff suppressed because it is too large Load diff

View file

@ -2,8 +2,5 @@ module.exports = {
files: ['test/*'], files: ['test/*'],
extensions: ['ts'], extensions: ['ts'],
require: ['ts-node/register/transpile-only'], require: ['ts-node/register/transpile-only'],
timeout: '2m', timeout: '30s'
verbose: true,
// Due to permissions issues, Windows needs cache turned off
cache: false
}; };

View file

@ -1,8 +1,5 @@
module.exports = { module.exports = {
files: ['test/unit/*'], files: ['test/unit/*'],
extensions: ['ts'], extensions: ['ts'],
require: ['ts-node/register/transpile-only'], require: ['ts-node/register/transpile-only']
verbose: true,
// Due to permissions issues, Windows needs cache turned off
cache: false
}; };

51
bin/cp-snapshot.js vendored
View file

@ -1,7 +1,5 @@
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const fsPromises = require('fs/promises');
const {Arch} = require('electron-builder'); const {Arch} = require('electron-builder');
function copySnapshot(pathToElectron, archToCopy) { function copySnapshot(pathToElectron, archToCopy) {
@ -11,32 +9,30 @@ function copySnapshot(pathToElectron, archToCopy) {
const pathToBlobV8 = path.resolve(__dirname, '..', 'cache', archToCopy, v8ContextFileName); const pathToBlobV8 = path.resolve(__dirname, '..', 'cache', archToCopy, v8ContextFileName);
console.log('Copying v8 snapshots from', pathToBlob, 'to', pathToElectron); console.log('Copying v8 snapshots from', pathToBlob, 'to', pathToElectron);
fs.mkdirSync(pathToElectron, { recursive: true });
fs.copyFileSync(pathToBlob, path.join(pathToElectron, snapshotFileName)); fs.copyFileSync(pathToBlob, path.join(pathToElectron, snapshotFileName));
fs.copyFileSync(pathToBlobV8, path.join(pathToElectron, v8ContextFileName)); fs.copyFileSync(pathToBlobV8, path.join(pathToElectron, v8ContextFileName));
} }
function getPathToElectron() { function getPathToElectron() {
const electronPath = require.resolve('electron');
switch (process.platform) { switch (process.platform) {
case 'darwin': case 'darwin':
return path.resolve( return path.resolve(
electronPath, __dirname,
'..', '..',
'..', 'node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources'
'..',
'dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources'
); );
case 'win32': case 'win32':
case 'linux': case 'linux':
return path.resolve(electronPath, '..', '..', '..', 'dist'); return path.resolve(__dirname, '..', 'node_modules', 'electron', 'dist');
} }
} }
function getV8ContextFileName(archToCopy) { function getV8ContextFileName(archToCopy) {
return `snapshot_blob.bin`; if (process.platform === 'darwin') {
return `v8_context_snapshot${archToCopy === 'arm64' ? '.arm64' : '.x86_64'}.bin`;
} else {
return `v8_context_snapshot.bin`;
}
} }
exports.default = async (context) => { exports.default = async (context) => {
@ -46,7 +42,6 @@ exports.default = async (context) => {
? `${context.appOutDir}/Hyper.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources` ? `${context.appOutDir}/Hyper.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources`
: context.appOutDir; : context.appOutDir;
copySnapshot(pathToElectron, archToCopy); copySnapshot(pathToElectron, archToCopy);
useLoaderScriptFix(context);
}; };
if (require.main === module) { if (require.main === module) {
@ -56,33 +51,3 @@ if (require.main === module) {
copySnapshot(pathToElectron, archToCopy); copySnapshot(pathToElectron, archToCopy);
} }
} }
// copied and modified from https://github.com/gergof/electron-builder-sandbox-fix/blob/master/lib/index.js
// copied and modified from https://github.com/Adamant-im/adamant-im/blob/7b20272a717833ffb0b49b034ab9974118fc59ec/scripts/electron/sandboxFix.js
const useLoaderScriptFix = async (params) => {
if (params.electronPlatformName !== 'linux') {
// this fix is only required on linux
return
}
const executable = path.join(params.appOutDir, params.packager.executableName)
const loaderScript = `#!/usr/bin/env bash
set -u
SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd )"
exec "$SCRIPT_DIR/${params.packager.executableName}.bin" "--no-sandbox" "$@"
`
try {
await fsPromises.rename(executable, executable + '.bin')
await fsPromises.writeFile(executable, loaderScript)
await fsPromises.chmod(executable, 0o755)
} catch (e) {
console.error('failed to create loader for sandbox fix: ' + e.message)
throw new Error('Failed to create loader for sandbox fix')
}
console.log('sandbox fix successfully applied')
}

43
bin/mk-snapshot.js vendored
View file

@ -9,18 +9,7 @@ const excludedModules = {};
const crossArchDirs = ['clang_x86_v8_arm', 'clang_x64_v8_arm64', 'win_clang_x64']; const crossArchDirs = ['clang_x86_v8_arm', 'clang_x64_v8_arm64', 'win_clang_x64'];
const archMap = {
x64: 'x86_64',
arm64: 'arm64'
};
async function main() { async function main() {
const npmConfigArch = process.env.npm_config_arch;
if (!npmConfigArch) {
throw new Error('env var npm_config_arch is not specified')
}
const baseDirPath = path.resolve(__dirname, '..'); const baseDirPath = path.resolve(__dirname, '..');
console.log('Creating a linked script..'); console.log('Creating a linked script..');
@ -38,25 +27,11 @@ async function main() {
// Verify if we will be able to use this in `mksnapshot` // Verify if we will be able to use this in `mksnapshot`
vm.runInNewContext(result.snapshotScript, undefined, {filename: snapshotScriptPath, displayErrors: true}); vm.runInNewContext(result.snapshotScript, undefined, {filename: snapshotScriptPath, displayErrors: true});
const outputBlobPath = `${baseDirPath}/cache/${npmConfigArch}`; const outputBlobPath = `${baseDirPath}/cache/${process.env.npm_config_arch}`;
await mkdirp(outputBlobPath); await mkdirp(outputBlobPath);
let mksnapshotBinPath
if (process.platform === 'win32') {
mksnapshotBinPath =
require.resolve(
path.join("electron-mksnapshot", "bin", "mksnapshot.exe")
);
} else {
mksnapshotBinPath =
require.resolve(
path.join("electron-mksnapshot", "bin", "mksnapshot")
);
}
mksnapshotBinPath = path.dirname(mksnapshotBinPath);
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
const mksnapshotBinPath = `${baseDirPath}/node_modules/electron-mksnapshot/bin`;
const matchingDirs = crossArchDirs.map((dir) => `${mksnapshotBinPath}/${dir}`).filter((dir) => fs.existsSync(dir)); const matchingDirs = crossArchDirs.map((dir) => `${mksnapshotBinPath}/${dir}`).filter((dir) => fs.existsSync(dir));
for (const dir of matchingDirs) { for (const dir of matchingDirs) {
if (fs.existsSync(`${mksnapshotBinPath}/gen/v8/embedded.S`)) { if (fs.existsSync(`${mksnapshotBinPath}/gen/v8/embedded.S`)) {
@ -66,19 +41,11 @@ async function main() {
} }
} }
const startupBlobPath = path.join(outputBlobPath, 'snapshot_blob.bin');
console.log(`Generating startup blob in "${outputBlobPath}"`); console.log(`Generating startup blob in "${outputBlobPath}"`);
const res = childProcess.execFileSync( childProcess.execFileSync(
require.resolve(`electron-mksnapshot/bin/mksnapshot${process.platform === 'win32' ? '.exe' : ''}`), path.resolve(__dirname, '..', 'node_modules', '.bin', 'mksnapshot' + (process.platform === 'win32' ? '.cmd' : '')),
[ [snapshotScriptPath, '--output_dir', outputBlobPath]
'--startup-src=' + snapshotScriptPath,
'--startup-blob=' + startupBlobPath,
`--target-arch=${archMap[process.env.npm_config_arch]}`,
//'--v8-context-snapshot=' + v8SnapshotPath
]
); );
console.log('result:', res.toString())
} }
main().catch((err) => console.error(err)); main().catch((err) => console.error(err));

7
bin/notarize.js vendored
View file

@ -1,15 +1,14 @@
exports.default = async function notarizing(context) { const { notarize } = require("electron-notarize");
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context; const { electronPlatformName, appOutDir } = context;
if (electronPlatformName !== "darwin" || !process.env.APPLE_ID || !process.env.APPLE_PASSWORD) { if (electronPlatformName !== "darwin" || !process.env.APPLE_ID || !process.env.APPLE_PASSWORD) {
return; return;
} }
const { notarize } = await import('@electron/notarize');
const appName = context.packager.appInfo.productFilename; const appName = context.packager.appInfo.productFilename;
return await notarize({ return await notarize({
appBundleId: "com.quineglobal.hyper", appBundleId: "co.zeit.hyper",
appPath: `${appOutDir}/${appName}.app`, appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLE_ID, appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_PASSWORD appleIdPassword: process.env.APPLE_PASSWORD

22
bin/snapshot-libs.js vendored
View file

@ -7,25 +7,25 @@ require('normalize-url');
require('parse-url'); require('parse-url');
require('php-escape-shell'); require('php-escape-shell');
require('plist'); require('plist');
require('react-deep-force-update');
require('react-dom');
require('react-redux');
require('react');
require('redux-thunk'); require('redux-thunk');
require('redux'); require('redux');
require('reselect'); require('reselect');
require('seamless-immutable'); require('seamless-immutable');
require('stylis'); require('stylis');
require('@xterm/addon-unicode11'); require('xterm-addon-unicode11');
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
if (false) { if (false) {
require('args'); require('args');
require('mousetrap'); require('mousetrap');
require('open'); require('open');
require('react-dom'); require('xterm-addon-fit');
require('react-redux'); require('xterm-addon-ligatures');
require('react'); require('xterm-addon-search');
require('@xterm/addon-fit'); require('xterm-addon-web-links');
require('@xterm/addon-image'); require('xterm-addon-webgl');
require('@xterm/addon-search'); require('xterm');
require('@xterm/addon-web-links');
require('@xterm/addon-webgl');
require('@xterm/addon-canvas');
require('@xterm/xterm');
} }

View file

@ -1,15 +1,15 @@
!macro customInstall !macro customInstall
WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper" "" "Open &Hyper here" WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper" "" "Open Hyper here"
WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper" "Icon" `"$appExe"` WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper" "Icon" "$appExe"
WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper\command" "" `"$appExe" "%V"` WriteRegStr HKCU "Software\Classes\Directory\Background\shell\Hyper\command" "" `$appExe "%V"`
WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper" "" "Open &Hyper here" WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper" "" "Open Hyper here"
WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper" "Icon" `"$appExe"` WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper" "Icon" "$appExe"
WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper\command" "" `"$appExe" "%V"` WriteRegStr HKCU "Software\Classes\Directory\shell\Hyper\command" "" `$appExe "%V"`
WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper" "" "Open &Hyper here" WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper" "" "Open Hyper here"
WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper" "Icon" `"$appExe"` WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper" "Icon" "$appExe"
WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper\command" "" `"$appExe" "%V"` WriteRegStr HKCU "Software\Classes\Drive\shell\Hyper\command" "" `$appExe "%V"`
!macroend !macroend
!macro customUnInstall !macro customUnInstall

View file

@ -2,20 +2,18 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-return */
import fs from 'fs'; import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path';
import got from 'got'; import got from 'got';
import registryUrlModule from 'registry-url'; import registryUrlModule from 'registry-url';
const registryUrl = registryUrlModule(); const registryUrl = registryUrlModule();
import path from 'path';
// If the user defines XDG_CONFIG_HOME they definitely want their config there, // If the user defines XDG_CONFIG_HOME they definitely want their config there,
// otherwise use the home directory in linux/mac and userdata in windows // otherwise use the home directory in linux/mac and userdata in windows
const applicationDirectory = process.env.XDG_CONFIG_HOME const applicationDirectory = process.env.XDG_CONFIG_HOME
? path.join(process.env.XDG_CONFIG_HOME, 'Hyper') ? path.join(process.env.XDG_CONFIG_HOME, 'Hyper')
: process.platform === 'win32' : process.platform === 'win32'
? path.join(process.env.APPDATA!, 'Hyper') ? path.join(process.env.APPDATA!, 'Hyper')
: path.join(os.homedir(), '.config', 'Hyper'); : path.join(os.homedir(), '.config', 'Hyper');
const devConfigFileName = path.join(__dirname, `../hyper.json`); const devConfigFileName = path.join(__dirname, `../hyper.json`);
@ -32,7 +30,7 @@ const fileName =
function memoize<T extends (...args: any[]) => any>(fn: T): T { function memoize<T extends (...args: any[]) => any>(fn: T): T {
let hasResult = false; let hasResult = false;
let result: any; let result: any;
return ((...args: Parameters<T>) => { return ((...args: any[]) => {
if (!hasResult) { if (!hasResult) {
result = fn(...args); result = fn(...args);
hasResult = true; hasResult = true;

View file

@ -1,20 +1,16 @@
// This is a CLI tool, using console is OK // This is a CLI tool, using console is OK
/* eslint no-console: 0 */ /* eslint no-console: 0 */
import {spawn, exec} from 'child_process'; import {spawn, exec, SpawnOptions} from 'child_process';
import type {SpawnOptions} from 'child_process';
import {existsSync} from 'fs';
import {isAbsolute, resolve} from 'path'; import {isAbsolute, resolve} from 'path';
import {promisify} from 'util'; import {existsSync} from 'fs';
import {version} from '../app/package.json';
import pify from 'pify';
import args from 'args'; import args from 'args';
import chalk from 'chalk'; import chalk from 'chalk';
import open from 'open';
import _columnify from 'columnify'; import _columnify from 'columnify';
import got from 'got'; import got from 'got';
import open from 'open';
import ora from 'ora'; import ora from 'ora';
import {version} from '../app/package.json';
import * as api from './api'; import * as api from './api';
let commandPromise: Promise<void> | undefined; let commandPromise: Promise<void> | undefined;
@ -195,10 +191,8 @@ const main = (argv: string[]) => {
version: false, version: false,
mri: { mri: {
boolean: ['v', 'verbose'] boolean: ['v', 'verbose']
}, }
mainColor: 'yellow', } as any);
subColor: 'dim'
});
if (commandPromise) { if (commandPromise) {
return commandPromise; return commandPromise;
@ -234,11 +228,11 @@ const main = (argv: string[]) => {
options['stdio'] = 'ignore'; options['stdio'] = 'ignore';
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
//Use `open` to prevent multiple Hyper process //Use `open` to prevent multiple Hyper process
const cmd = `open -b com.quineglobal.hyper ${args_}`; const cmd = `open -b co.zeit.hyper ${args_}`;
const opts = { const opts = {
env env
}; };
return promisify(exec)(cmd, opts); return pify(exec)(cmd, opts);
} }
} }

View file

@ -1,6 +0,0 @@
{
"$schema": "http://json.schemastore.org/electron-builder",
"extends": "electron-builder.json",
"afterSign": null,
"npmRebuild": false
}

View file

@ -1,5 +1,6 @@
{ {
"appId": "com.quineglobal.hyper", "$schema": "http://json.schemastore.org/electron-builder",
"appId": "co.zeit.hyper",
"afterSign": "./bin/notarize.js", "afterSign": "./bin/notarize.js",
"afterPack": "./bin/cp-snapshot.js", "afterPack": "./bin/cp-snapshot.js",
"directories": { "directories": {
@ -16,27 +17,43 @@
] ]
} }
], ],
"artifactName": "${productName}-${version}-${arch}.${ext}",
"linux": { "linux": {
"category": "TerminalEmulator", "category": "TerminalEmulator",
"target": [ "target": [
"deb", {
"AppImage", "target": "deb",
"snap", "arch": [
"pacman" "x64",
"arm64"
]
},
{
"target": "AppImage",
"arch": [
"x64",
"arm64"
]
},
{
"target": "rpm",
"arch": [
"x64",
"arm64"
]
},
{
"target": "snap",
"arch": [
"x64"
]
}
] ]
}, },
"win": { "win": {
"target": { "target": [
"target": "nsis", "nsis"
"arch": [ ],
"x64", "rfc3161TimeStampServer": "http://timestamp.comodoca.com"
"arm64"
]
},
"signtoolOptions": {
"timeStampServer": "http://timestamp.comodoca.com"
}
}, },
"nsis": { "nsis": {
"include": "build/win/installer.nsh", "include": "build/win/installer.nsh",
@ -103,6 +120,13 @@
"compression": "bzip2", "compression": "bzip2",
"afterInstall": "./build/linux/after-install.tpl" "afterInstall": "./build/linux/after-install.tpl"
}, },
"rpm": {
"afterInstall": "./build/linux/after-install.tpl",
"fpm": [
"--rpm-rpmbuild-define",
"_build_id_links none"
]
},
"snap": { "snap": {
"confinement": "classic", "confinement": "classic",
"publish": "github" "publish": "github"

View file

@ -1,6 +1,6 @@
import type {configOptions} from '../../typings/config'; import {CONFIG_LOAD, CONFIG_RELOAD} from '../constants/config';
import {CONFIG_LOAD, CONFIG_RELOAD} from '../../typings/constants/config'; import {HyperActions} from '../hyper';
import type {HyperActions} from '../../typings/hyper'; import {configOptions} from '../config';
export function loadConfig(config: configOptions): HyperActions { export function loadConfig(config: configOptions): HyperActions {
return { return {

View file

@ -1,15 +1,14 @@
import {CLOSE_TAB, CHANGE_TAB} from '../../typings/constants/tabs'; import {CLOSE_TAB, CHANGE_TAB} from '../constants/tabs';
import { import {
UI_WINDOW_MAXIMIZE, UI_WINDOW_MAXIMIZE,
UI_WINDOW_UNMAXIMIZE, UI_WINDOW_UNMAXIMIZE,
UI_OPEN_HAMBURGER_MENU, UI_OPEN_HAMBURGER_MENU,
UI_WINDOW_MINIMIZE, UI_WINDOW_MINIMIZE,
UI_WINDOW_CLOSE UI_WINDOW_CLOSE
} from '../../typings/constants/ui'; } from '../constants/ui';
import type {HyperDispatch} from '../../typings/hyper';
import rpc from '../rpc'; import rpc from '../rpc';
import {userExitTermGroup, setActiveGroup} from './term-groups'; import {userExitTermGroup, setActiveGroup} from './term-groups';
import {HyperDispatch} from '../hyper';
export function closeTab(uid: string) { export function closeTab(uid: string) {
return (dispatch: HyperDispatch) => { return (dispatch: HyperDispatch) => {
@ -40,7 +39,7 @@ export function maximize() {
dispatch({ dispatch({
type: UI_WINDOW_MAXIMIZE, type: UI_WINDOW_MAXIMIZE,
effect() { effect() {
rpc.emit('maximize'); rpc.emit('maximize', null);
} }
}); });
}; };
@ -51,7 +50,7 @@ export function unmaximize() {
dispatch({ dispatch({
type: UI_WINDOW_UNMAXIMIZE, type: UI_WINDOW_UNMAXIMIZE,
effect() { effect() {
rpc.emit('unmaximize'); rpc.emit('unmaximize', null);
} }
}); });
}; };
@ -73,7 +72,7 @@ export function minimize() {
dispatch({ dispatch({
type: UI_WINDOW_MINIMIZE, type: UI_WINDOW_MINIMIZE,
effect() { effect() {
rpc.emit('minimize'); rpc.emit('minimize', null);
} }
}); });
}; };
@ -84,7 +83,7 @@ export function close() {
dispatch({ dispatch({
type: UI_WINDOW_CLOSE, type: UI_WINDOW_CLOSE,
effect() { effect() {
rpc.emit('close'); rpc.emit('close', null);
} }
}); });
}; };

View file

@ -1,6 +1,6 @@
import {INIT} from '../../typings/constants';
import type {HyperDispatch} from '../../typings/hyper';
import rpc from '../rpc'; import rpc from '../rpc';
import {INIT} from '../constants';
import {HyperDispatch} from '../hyper';
export default function init() { export default function init() {
return (dispatch: HyperDispatch) => { return (dispatch: HyperDispatch) => {

View file

@ -1,5 +1,5 @@
import {NOTIFICATION_MESSAGE, NOTIFICATION_DISMISS} from '../../typings/constants/notifications'; import {NOTIFICATION_MESSAGE, NOTIFICATION_DISMISS} from '../constants/notifications';
import type {HyperActions} from '../../typings/hyper'; import {HyperActions} from '../hyper';
export function dismissNotification(id: string): HyperActions { export function dismissNotification(id: string): HyperActions {
return { return {

View file

@ -1,4 +1,6 @@
import type {Session} from '../../typings/common'; import rpc from '../rpc';
import {keys} from '../utils/object';
import findBySession from '../utils/term-groups';
import { import {
SESSION_ADD, SESSION_ADD,
SESSION_RESIZE, SESSION_RESIZE,
@ -12,13 +14,10 @@ import {
SESSION_USER_DATA, SESSION_USER_DATA,
SESSION_SET_XTERM_TITLE, SESSION_SET_XTERM_TITLE,
SESSION_SEARCH SESSION_SEARCH
} from '../../typings/constants/sessions'; } from '../constants/sessions';
import type {HyperState, HyperDispatch, HyperActions} from '../../typings/hyper'; import {HyperState, session, HyperDispatch, HyperActions} from '../hyper';
import rpc from '../rpc';
import {keys} from '../utils/object';
import findBySession from '../utils/term-groups';
export function addSession({uid, shell, pid, cols = null, rows = null, splitDirection, activeUid, profile}: Session) { export function addSession({uid, shell, pid, cols, rows, splitDirection, activeUid}: session) {
return (dispatch: HyperDispatch, getState: () => HyperState) => { return (dispatch: HyperDispatch, getState: () => HyperState) => {
const {sessions} = getState(); const {sessions} = getState();
const now = Date.now(); const now = Date.now();
@ -31,20 +30,20 @@ export function addSession({uid, shell, pid, cols = null, rows = null, splitDire
rows, rows,
splitDirection, splitDirection,
activeUid: activeUid ? activeUid : sessions.activeUid, activeUid: activeUid ? activeUid : sessions.activeUid,
now, now
profile
}); });
}; };
} }
export function requestSession(profile: string | undefined) { export function requestSession() {
return (dispatch: HyperDispatch, getState: () => HyperState) => { return (dispatch: HyperDispatch, getState: () => HyperState) => {
dispatch({ dispatch({
type: SESSION_REQUEST, type: SESSION_REQUEST,
effect: () => { effect: () => {
const {ui} = getState(); const {ui} = getState();
const {cwd} = ui; // the cols and rows from preview session maybe not accurate. so remove.
rpc.emit('new', {cwd, profile}); const {/*cols, rows,*/ cwd} = ui;
rpc.emit('new', {cwd});
} }
}); });
}; };
@ -141,7 +140,7 @@ export function openSearch(uid?: string) {
dispatch({ dispatch({
type: SESSION_SEARCH, type: SESSION_SEARCH,
uid: targetUid, uid: targetUid,
value: new Date() value: true
}); });
}; };
} }
@ -153,7 +152,7 @@ export function closeSearch(uid?: string, keyEvent?: any) {
dispatch({ dispatch({
type: SESSION_SEARCH, type: SESSION_SEARCH,
uid: targetUid, uid: targetUid,
value: null value: false
}); });
} else { } else {
if (keyEvent) { if (keyEvent) {
@ -163,7 +162,7 @@ export function closeSearch(uid?: string, keyEvent?: any) {
}; };
} }
export function sendSessionData(uid: string | null, data: string, escaped?: boolean) { export function sendSessionData(uid: string | null, data: any, escaped?: boolean | null) {
return (dispatch: HyperDispatch, getState: () => HyperState) => { return (dispatch: HyperDispatch, getState: () => HyperState) => {
dispatch({ dispatch({
type: SESSION_USER_DATA, type: SESSION_USER_DATA,

View file

@ -1,32 +1,28 @@
import {SESSION_REQUEST} from '../../typings/constants/sessions'; import rpc from '../rpc';
import { import {
DIRECTION, DIRECTION,
TERM_GROUP_RESIZE, TERM_GROUP_RESIZE,
TERM_GROUP_REQUEST, TERM_GROUP_REQUEST,
TERM_GROUP_EXIT, TERM_GROUP_EXIT,
TERM_GROUP_EXIT_ACTIVE TERM_GROUP_EXIT_ACTIVE
} from '../../typings/constants/term-groups'; } from '../constants/term-groups';
import type {ITermState, ITermGroup, HyperState, HyperDispatch, HyperActions} from '../../typings/hyper'; import {SESSION_REQUEST} from '../constants/sessions';
import rpc from '../rpc';
import {getRootGroups} from '../selectors';
import findBySession from '../utils/term-groups'; import findBySession from '../utils/term-groups';
import {getRootGroups} from '../selectors';
import {setActiveSession, ptyExitSession, userExitSession} from './sessions'; import {setActiveSession, ptyExitSession, userExitSession} from './sessions';
import {ITermState, ITermGroup, HyperState, HyperDispatch, HyperActions} from '../hyper';
function requestSplit(direction: 'VERTICAL' | 'HORIZONTAL') { function requestSplit(direction: 'VERTICAL' | 'HORIZONTAL') {
return (_activeUid: string | undefined, _profile: string | undefined) => return (activeUid: string) =>
(dispatch: HyperDispatch, getState: () => HyperState): void => { (dispatch: HyperDispatch, getState: () => HyperState): void => {
dispatch({ dispatch({
type: SESSION_REQUEST, type: SESSION_REQUEST,
effect: () => { effect: () => {
const {ui, sessions} = getState(); const {ui, sessions} = getState();
const activeUid = _activeUid ? _activeUid : sessions.activeUid;
const profile = _profile ? _profile : activeUid ? sessions.sessions[activeUid].profile : window.profileName;
rpc.emit('new', { rpc.emit('new', {
splitDirection: direction, splitDirection: direction,
cwd: ui.cwd, cwd: ui.cwd,
activeUid, activeUid: activeUid ? activeUid : sessions.activeUid
profile
}); });
} }
}); });
@ -44,20 +40,17 @@ export function resizeTermGroup(uid: string, sizes: number[]): HyperActions {
}; };
} }
export function requestTermGroup(_activeUid: string | undefined, _profile: string | undefined) { export function requestTermGroup(activeUid: string) {
return (dispatch: HyperDispatch, getState: () => HyperState) => { return (dispatch: HyperDispatch, getState: () => HyperState) => {
dispatch({ dispatch({
type: TERM_GROUP_REQUEST, type: TERM_GROUP_REQUEST,
effect: () => { effect: () => {
const {ui, sessions} = getState(); const {ui, sessions} = getState();
const {cwd} = ui; const {cwd} = ui;
const activeUid = _activeUid ? _activeUid : sessions.activeUid;
const profile = _profile ? _profile : activeUid ? sessions.sessions[activeUid].profile : window.profileName;
rpc.emit('new', { rpc.emit('new', {
isNewGroup: true, isNewGroup: true,
cwd, cwd,
activeUid, activeUid: activeUid ? activeUid : sessions.activeUid
profile
}); });
} }
}); });

View file

@ -1,9 +1,10 @@
import {stat} from 'fs';
import type {Stats} from 'fs';
import type parseUrl from 'parse-url';
import {php_escapeshellcmd as escapeShellCmd} from 'php-escape-shell'; import {php_escapeshellcmd as escapeShellCmd} from 'php-escape-shell';
import {isExecutable} from '../utils/file';
import {getRootGroups} from '../selectors';
import findBySession from '../utils/term-groups';
import notify from '../utils/notify';
import rpc from '../rpc';
import {requestSession, sendSessionData, setActiveSession} from './sessions';
import { import {
UI_FONT_SIZE_SET, UI_FONT_SIZE_SET,
UI_FONT_SIZE_INCR, UI_FONT_SIZE_INCR,
@ -23,18 +24,14 @@ import {
UI_OPEN_SSH_URL, UI_OPEN_SSH_URL,
UI_CONTEXTMENU_OPEN, UI_CONTEXTMENU_OPEN,
UI_COMMAND_EXEC UI_COMMAND_EXEC
} from '../../typings/constants/ui'; } from '../constants/ui';
import type {HyperState, HyperDispatch, HyperActions, ITermGroups} from '../../typings/hyper';
import rpc from '../rpc';
import {getRootGroups} from '../selectors';
import {isExecutable} from '../utils/file';
import notify from '../utils/notify';
import findBySession from '../utils/term-groups';
import {requestSession, sendSessionData, setActiveSession} from './sessions';
import {setActiveGroup} from './term-groups'; import {setActiveGroup} from './term-groups';
import parseUrl from 'parse-url';
import {HyperState, HyperDispatch, HyperActions, ITermGroups} from '../hyper';
import {stat, Stats} from 'fs';
export function openContextMenu(uid: string, selection: string) { export function openContextMenu(uid: string, selection: any) {
return (dispatch: HyperDispatch, getState: () => HyperState) => { return (dispatch: HyperDispatch, getState: () => HyperState) => {
dispatch({ dispatch({
type: UI_CONTEXTMENU_OPEN, type: UI_CONTEXTMENU_OPEN,
@ -272,11 +269,11 @@ export function openFile(path: string) {
} }
rpc.once('session add', ({uid}) => { rpc.once('session add', ({uid}) => {
rpc.once('session data', () => { rpc.once('session data', () => {
dispatch(sendSessionData(uid, command)); dispatch(sendSessionData(uid, command, null));
}); });
}); });
} }
dispatch(requestSession(undefined)); dispatch(requestSession());
}); });
} }
}); });
@ -295,11 +292,12 @@ export function leaveFullScreen(): HyperActions {
}; };
} }
export function openSSH(parsedUrl: ReturnType<typeof parseUrl>) { export function openSSH(url: string) {
return (dispatch: HyperDispatch) => { return (dispatch: HyperDispatch) => {
dispatch({ dispatch({
type: UI_OPEN_SSH_URL, type: UI_OPEN_SSH_URL,
effect() { effect() {
const parsedUrl = parseUrl(url, true);
let command = `${parsedUrl.protocol} ${parsedUrl.user ? `${parsedUrl.user}@` : ''}${parsedUrl.resource}`; let command = `${parsedUrl.protocol} ${parsedUrl.user ? `${parsedUrl.user}@` : ''}${parsedUrl.resource}`;
if (parsedUrl.port) command += ` -p ${parsedUrl.port}`; if (parsedUrl.port) command += ` -p ${parsedUrl.port}`;
@ -308,11 +306,11 @@ export function openSSH(parsedUrl: ReturnType<typeof parseUrl>) {
rpc.once('session add', ({uid}) => { rpc.once('session add', ({uid}) => {
rpc.once('session data', () => { rpc.once('session data', () => {
dispatch(sendSessionData(uid, command)); dispatch(sendSessionData(uid, command, null));
}); });
}); });
dispatch(requestSession(undefined)); dispatch(requestSession());
} }
}); });
}; };

View file

@ -1,12 +1,12 @@
import {UPDATE_INSTALL, UPDATE_AVAILABLE} from '../../typings/constants/updater'; import {UPDATE_INSTALL, UPDATE_AVAILABLE} from '../constants/updater';
import type {HyperActions} from '../../typings/hyper';
import rpc from '../rpc'; import rpc from '../rpc';
import {HyperActions} from '../hyper';
export function installUpdate(): HyperActions { export function installUpdate(): HyperActions {
return { return {
type: UPDATE_INSTALL, type: UPDATE_INSTALL,
effect: () => { effect: () => {
rpc.emit('quit and install'); rpc.emit('quit and install', null);
} }
}; };
} }

View file

@ -1,7 +1,9 @@
import type {HyperDispatch} from '../typings/hyper'; import {require as remoteRequire} from '@electron/remote';
import {HyperDispatch} from './hyper';
import {closeSearch} from './actions/sessions'; import {closeSearch} from './actions/sessions';
import {ipcRenderer} from './utils/ipc'; // TODO: Should be updates to new async API https://medium.com/@nornagon/electrons-remote-module-considered-harmful-70d69500f31
const {getDecoratedKeymaps} = remoteRequire('./plugins') as typeof import('../app/plugins');
let commands: Record<string, (event: any, dispatch: HyperDispatch) => void> = { let commands: Record<string, (event: any, dispatch: HyperDispatch) => void> = {
'editor:search-close': (e, dispatch) => { 'editor:search-close': (e, dispatch) => {
@ -10,8 +12,8 @@ let commands: Record<string, (event: any, dispatch: HyperDispatch) => void> = {
} }
}; };
export const getRegisteredKeys = async () => { export const getRegisteredKeys = () => {
const keymaps = await ipcRenderer.invoke('getDecoratedKeymaps'); const keymaps = getDecoratedKeymaps();
return Object.keys(keymaps).reduce((result: Record<string, string>, actionName) => { return Object.keys(keymaps).reduce((result: Record<string, string>, actionName) => {
const commandKeys = keymaps[actionName]; const commandKeys = keymaps[actionName];

View file

@ -1,27 +1,27 @@
import React, {forwardRef, useState} from 'react'; import React from 'react';
import type {HeaderProps} from '../../typings/hyper';
import {decorate, getTabsProps} from '../utils/plugins'; import {decorate, getTabsProps} from '../utils/plugins';
import Tabs_ from './tabs'; import Tabs_ from './tabs';
import {HeaderProps} from '../hyper';
const Tabs = decorate(Tabs_, 'Tabs'); const Tabs = decorate(Tabs_, 'Tabs');
const Header = forwardRef<HTMLElement, HeaderProps>((props, ref) => { export default class Header extends React.PureComponent<HeaderProps> {
const [headerMouseDownWindowX, setHeaderMouseDownWindowX] = useState<number>(0); headerMouseDownWindowX!: number;
const [headerMouseDownWindowY, setHeaderMouseDownWindowY] = useState<number>(0); headerMouseDownWindowY!: number;
const onChangeIntent = (active: string) => { onChangeIntent = (active: string) => {
// we ignore clicks if they're a byproduct of a drag // we ignore clicks if they're a byproduct of a drag
// motion to move the window // motion to move the window
if (window.screenX !== headerMouseDownWindowX || window.screenY !== headerMouseDownWindowY) { if (window.screenX !== this.headerMouseDownWindowX || window.screenY !== this.headerMouseDownWindowY) {
return; return;
} }
props.onChangeTab(active); this.props.onChangeTab(active);
}; };
const handleHeaderMouseDown = () => { handleHeaderMouseDown = () => {
// the hack of all hacks, this prevents the term // the hack of all hacks, this prevents the term
// iframe from losing focus, for example, when // iframe from losing focus, for example, when
// the user drags the nav around // the user drags the nav around
@ -30,43 +30,43 @@ const Header = forwardRef<HTMLElement, HeaderProps>((props, ref) => {
// persist start positions of a potential drag motion // persist start positions of a potential drag motion
// to differentiate dragging from clicking // to differentiate dragging from clicking
setHeaderMouseDownWindowX(window.screenX); this.headerMouseDownWindowX = window.screenX;
setHeaderMouseDownWindowY(window.screenY); this.headerMouseDownWindowY = window.screenY;
}; };
const handleHamburgerMenuClick = (event: React.MouseEvent) => { handleHamburgerMenuClick = (event: React.MouseEvent) => {
let {right: x, bottom: y} = event.currentTarget.getBoundingClientRect(); let {right: x, bottom: y} = event.currentTarget.getBoundingClientRect();
x -= 15; // to compensate padding x -= 15; // to compensate padding
y -= 12; // ^ same y -= 12; // ^ same
props.openHamburgerMenu({x, y}); this.props.openHamburgerMenu({x, y});
}; };
const handleMaximizeClick = () => { handleMaximizeClick = () => {
if (props.maximized) { if (this.props.maximized) {
props.unmaximize(); this.props.unmaximize();
} else { } else {
props.maximize(); this.props.maximize();
} }
}; };
const handleMinimizeClick = () => { handleMinimizeClick = () => {
props.minimize(); this.props.minimize();
}; };
const handleCloseClick = () => { handleCloseClick = () => {
props.close(); this.props.close();
}; };
const getWindowHeaderConfig = () => { getWindowHeaderConfig() {
const {showHamburgerMenu, showWindowControls} = props; const {showHamburgerMenu, showWindowControls} = this.props;
const defaults = { const defaults = {
hambMenu: !props.isMac, // show by default on windows and linux hambMenu: !this.props.isMac, // show by default on windows and linux
winCtrls: !props.isMac // show by default on Windows and Linux winCtrls: !this.props.isMac // show by default on Windows and Linux
}; };
// don't allow the user to change defaults on macOS // don't allow the user to change defaults on macOS
if (props.isMac) { if (this.props.isMac) {
return defaults; return defaults;
} }
@ -74,187 +74,182 @@ const Header = forwardRef<HTMLElement, HeaderProps>((props, ref) => {
hambMenu: showHamburgerMenu === '' ? defaults.hambMenu : showHamburgerMenu, hambMenu: showHamburgerMenu === '' ? defaults.hambMenu : showHamburgerMenu,
winCtrls: showWindowControls === '' ? defaults.winCtrls : showWindowControls winCtrls: showWindowControls === '' ? defaults.winCtrls : showWindowControls
}; };
};
const {isMac} = props;
const {borderColor} = props;
let title = 'Hyper';
if (props.tabs.length === 1 && props.tabs[0].title) {
// if there's only one tab we use its title as the window title
title = props.tabs[0].title;
} }
const {hambMenu, winCtrls} = getWindowHeaderConfig();
const left = winCtrls === 'left';
const maxButtonHref = props.maximized
? './renderer/assets/icons.svg#restore-window'
: './renderer/assets/icons.svg#maximize-window';
return ( render() {
<header const {isMac} = this.props;
className={`header_header ${isMac && 'header_headerRounded'}`} const props = getTabsProps(this.props, {
onMouseDown={handleHeaderMouseDown} tabs: this.props.tabs,
onMouseUp={() => window.focusActiveTerm()} borderColor: this.props.borderColor,
onDoubleClick={handleMaximizeClick} onClose: this.props.onCloseTab,
ref={ref} onChange: this.onChangeIntent,
> fullScreen: this.props.fullScreen
{!isMac && ( });
<div const {borderColor} = props;
className={`header_windowHeader ${props.tabs.length > 1 ? 'header_windowHeaderWithBorder' : ''}`} let title = 'Hyper';
style={{borderColor}} if (props.tabs.length === 1 && props.tabs[0].title) {
> // if there's only one tab we use its title as the window title
{hambMenu && ( title = props.tabs[0].title;
<svg }
className={`header_shape ${left ? 'header_hamburgerMenuRight' : 'header_hamburgerMenuLeft'}`} const {hambMenu, winCtrls} = this.getWindowHeaderConfig();
onClick={handleHamburgerMenuClick} const left = winCtrls === 'left';
> const maxButtonHref = this.props.maximized
<use xlinkHref="./renderer/assets/icons.svg#hamburger-menu" /> ? './renderer/assets/icons.svg#restore-window'
</svg> : './renderer/assets/icons.svg#maximize-window';
)}
<span className="header_appTitle">{title}</span> return (
{winCtrls && ( <header
<div className={`header_windowControls ${left ? 'header_windowControlsLeft' : ''}`}> className={`header_header ${isMac && 'header_headerRounded'}`}
<div className={`${left ? 'header_minimizeWindowLeft' : ''}`} onClick={handleMinimizeClick}> onMouseDown={this.handleHeaderMouseDown}
<svg className="header_shape"> onMouseUp={() => window.focusActiveTerm()}
<use xlinkHref="./renderer/assets/icons.svg#minimize-window" /> onDoubleClick={this.handleMaximizeClick}
</svg> >
{!isMac && (
<div
className={`header_windowHeader ${props.tabs.length > 1 ? 'header_windowHeaderWithBorder' : ''}`}
style={{borderColor}}
>
{hambMenu && (
<svg
className={`header_shape ${left ? 'header_hamburgerMenuRight' : 'header_hamburgerMenuLeft'}`}
onClick={this.handleHamburgerMenuClick}
>
<use xlinkHref="./renderer/assets/icons.svg#hamburger-menu" />
</svg>
)}
<span className="header_appTitle">{title}</span>
{winCtrls && (
<div className={`header_windowControls ${left ? 'header_windowControlsLeft' : ''}`}>
<div className={`${left ? 'header_minimizeWindowLeft' : ''}`} onClick={this.handleMinimizeClick}>
<svg className="header_shape">
<use xlinkHref="./renderer/assets/icons.svg#minimize-window" />
</svg>
</div>
<div className={`${left ? 'header_maximizeWindowLeft' : ''}`} onClick={this.handleMaximizeClick}>
<svg className="header_shape">
<use xlinkHref={maxButtonHref} />
</svg>
</div>
<div
className={`header_closeWindow ${left ? 'header_closeWindowLeft' : ''}`}
onClick={this.handleCloseClick}
>
<svg className="header_shape">
<use xlinkHref="./renderer/assets/icons.svg#close-window" />
</svg>
</div>
</div> </div>
<div className={`${left ? 'header_maximizeWindowLeft' : ''}`} onClick={handleMaximizeClick}> )}
<svg className="header_shape"> </div>
<use xlinkHref={maxButtonHref} /> )}
</svg> {this.props.customChildrenBefore}
</div> <Tabs {...props} />
<div className={`header_closeWindow ${left ? 'header_closeWindowLeft' : ''}`} onClick={handleCloseClick}> {this.props.customChildren}
<svg className="header_shape">
<use xlinkHref="./renderer/assets/icons.svg#close-window" />
</svg>
</div>
</div>
)}
</div>
)}
{props.customChildrenBefore}
<Tabs
{...getTabsProps(props, {
tabs: props.tabs,
borderColor: props.borderColor,
backgroundColor: props.backgroundColor,
onClose: props.onCloseTab,
onChange: onChangeIntent,
fullScreen: props.fullScreen,
defaultProfile: props.defaultProfile,
profiles: props.profiles.asMutable({deep: true}),
openNewTab: props.openNewTab
})}
/>
{props.customChildren}
<style jsx>{` <style jsx>{`
.header_header { .header_header {
position: fixed; position: fixed;
top: 1px; top: 1px;
left: 1px; left: 1px;
right: 1px; right: 1px;
z-index: 100; z-index: 100;
} }
.header_headerRounded { .header_headerRounded {
border-top-left-radius: 4px; border-top-left-radius: 4px;
border-top-right-radius: 4px; border-top-right-radius: 4px;
} }
.header_windowHeader { .header_windowHeader {
height: 34px; height: 34px;
width: 100%; width: 100%;
position: fixed; position: fixed;
top: 1px; top: 1px;
left: 1px; left: 1px;
right: 1px; right: 1px;
-webkit-app-region: drag; -webkit-app-region: drag;
-webkit-user-select: none; -webkit-user-select: none;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.header_windowHeaderWithBorder { .header_windowHeaderWithBorder {
border-color: #ccc; border-color: #ccc;
border-bottom-style: solid; border-bottom-style: solid;
border-bottom-width: 1px; border-bottom-width: 1px;
} }
.header_appTitle { .header_appTitle {
font-size: 12px; font-size: 12px;
} }
.header_shape, .header_shape,
.header_shape > svg { .header_shape > svg {
width: 40px; width: 40px;
height: 34px; height: 34px;
padding: 12px 15px 12px 15px; padding: 12px 15px 12px 15px;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
color: #fff; color: #fff;
opacity: 0.5; opacity: 0.5;
shape-rendering: crispEdges; shape-rendering: crispEdges;
} }
.header_shape:hover { .header_shape:hover {
opacity: 1; opacity: 1;
} }
.header_shape:active { .header_shape:active {
opacity: 0.3; opacity: 0.3;
} }
.header_hamburgerMenuLeft { .header_hamburgerMenuLeft {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
} }
.header_hamburgerMenuRight { .header_hamburgerMenuRight {
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
} }
.header_windowControls { .header_windowControls {
display: flex; display: flex;
width: 120px; width: 120px;
height: 34px; height: 34px;
justify-content: space-between; justify-content: space-between;
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
} }
.header_windowControlsLeft { .header_windowControlsLeft {
left: 0px; left: 0px;
} }
.header_closeWindowLeft { .header_closeWindowLeft {
order: 1; order: 1;
} }
.header_minimizeWindowLeft { .header_minimizeWindowLeft {
order: 2; order: 2;
} }
.header_maximizeWindowLeft { .header_maximizeWindowLeft {
order: 3; order: 3;
} }
.header_closeWindow:hover { .header_closeWindow:hover {
color: #fe354e; color: #fe354e;
} }
.header_closeWindow:active { .header_closeWindow:active {
color: #fe354e; color: #fe354e;
} }
`}</style> `}</style>
</header> </header>
); );
}); }
}
Header.displayName = 'Header';
export default Header;

View file

@ -1,149 +0,0 @@
import React, {useRef, useState} from 'react';
import {VscChevronDown} from '@react-icons/all-files/vsc/VscChevronDown';
import useClickAway from 'react-use/lib/useClickAway';
import type {configOptions} from '../../typings/config';
interface Props {
defaultProfile: string;
profiles: configOptions['profiles'];
openNewTab: (name: string) => void;
backgroundColor: string;
borderColor: string;
tabsVisible: boolean;
}
const isMac = /Mac/.test(navigator.userAgent);
const DropdownButton = ({defaultProfile, profiles, openNewTab, backgroundColor, borderColor, tabsVisible}: Props) => {
const [dropdownOpen, setDropdownOpen] = useState(false);
const ref = useRef(null);
const toggleDropdown = () => {
setDropdownOpen(!dropdownOpen);
};
useClickAway(ref, () => {
setDropdownOpen(false);
});
return (
<div
ref={ref}
title="New Tab"
className={`new_tab ${tabsVisible ? 'tabs_visible' : 'tabs_hidden'}`}
onClick={toggleDropdown}
onDoubleClick={(e) => e.stopPropagation()}
onBlur={() => setDropdownOpen(false)}
>
<VscChevronDown style={{verticalAlign: 'middle'}} />
{dropdownOpen && (
<ul
key="dropdown"
className="profile_dropdown"
style={{
borderColor,
backgroundColor
}}
>
{profiles.map((profile) => (
<li
key={profile.name}
onClick={() => {
openNewTab(profile.name);
setDropdownOpen(false);
}}
className={`profile_dropdown_item ${
profile.name === defaultProfile && profiles.length > 1 ? 'profile_dropdown_item_default' : ''
}`}
>
{profile.name}
</li>
))}
</ul>
)}
<style jsx>{`
.profile_dropdown {
border-width: 1px;
border-style: solid;
border-bottom-width: 0px;
border-right-width: 0px;
position: absolute;
top: 33px;
right: 0px;
z-index: 1000;
padding: 0px;
margin: 0px;
list-style-type: none;
white-space: nowrap;
min-width: 120px;
}
.profile_dropdown_item {
padding: 0px 20px;
height: 34px;
line-height: 34px;
cursor: pointer;
font-size: 12px;
color: #fff;
background-color: transparent;
border-width: 0px;
border-style: solid;
border-color: transparent;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: ${borderColor};
text-align: start;
text-transform: capitalize;
}
.profile_dropdown_item:hover {
background-color: ${borderColor};
}
.profile_dropdown_item_default {
font-weight: bold;
}
.new_tab {
background: transparent;
color: #fff;
border-left: 1px;
border-bottom: 1px;
border-left-style: solid;
border-bottom-style: solid;
border-left-width: 1px;
border-bottom-width: 1px;
cursor: pointer;
font-size: 12px;
height: 34px;
line-height: 34px;
padding: 0 16px;
position: relative;
text-align: center;
-webkit-user-select: none;
${isMac ? '-webkit-app-region: drag;' : ''}
top: '0px';
}
.tabs_visible {
border-color: ${borderColor};
}
.tabs_hidden {
border-color: transparent;
position: absolute;
right: 0px;
}
.tabs_hidden:hover {
border-color: ${borderColor};
}
`}</style>
</div>
);
};
export default DropdownButton;

View file

@ -1,110 +1,115 @@
import React, {forwardRef, useEffect, useRef, useState} from 'react'; import React from 'react';
import {NotificationProps, NotificationState} from '../hyper';
import type {NotificationProps} from '../../typings/hyper'; export default class Notification extends React.PureComponent<NotificationProps, NotificationState> {
dismissTimer!: NodeJS.Timeout;
constructor(props: NotificationProps) {
super(props);
this.state = {
dismissing: false
};
}
const Notification = forwardRef<HTMLDivElement, React.PropsWithChildren<NotificationProps>>((props, ref) => { componentDidMount() {
const dismissTimer = useRef<NodeJS.Timeout | undefined>(undefined); if (this.props.dismissAfter) {
const [dismissing, setDismissing] = useState(false); this.setDismissTimer();
}
}
useEffect(() => { componentDidUpdate(prevProps: NotificationProps, prevState: NotificationState) {
setDismissTimer();
}, []);
useEffect(() => {
// if we have a timer going and the notification text // if we have a timer going and the notification text
// changed we reset the timer // changed we reset the timer
resetDismissTimer(); if (this.props.text !== prevProps.text) {
setDismissing(false); if (prevProps.dismissAfter) {
}, [props.text]); this.resetDismissTimer();
}
if (prevState.dismissing) {
this.setState({dismissing: false});
}
}
}
const handleDismiss = () => { handleDismiss = () => {
setDismissing(true); this.setState({dismissing: true});
}; };
const onElement = (el: HTMLDivElement | null) => { onElement = (el: HTMLDivElement | null) => {
if (el) { if (el) {
el.addEventListener('webkitTransitionEnd', () => { el.addEventListener('webkitTransitionEnd', () => {
if (dismissing) { if (this.state.dismissing) {
props.onDismiss(); this.props.onDismiss();
} }
}); });
const {backgroundColor} = props; const {backgroundColor} = this.props;
if (backgroundColor) { if (backgroundColor) {
el.style.setProperty('background-color', backgroundColor, 'important'); el.style.setProperty('background-color', backgroundColor, 'important');
} }
if (ref) {
if (typeof ref === 'function') ref(el);
else ref.current = el;
}
} }
}; };
const setDismissTimer = () => { setDismissTimer() {
if (typeof props.dismissAfter === 'number') { this.dismissTimer = setTimeout(() => {
dismissTimer.current = setTimeout(() => { this.handleDismiss();
handleDismiss(); }, this.props.dismissAfter);
}, props.dismissAfter); }
}
};
const resetDismissTimer = () => { resetDismissTimer() {
clearTimeout(dismissTimer.current); clearTimeout(this.dismissTimer);
setDismissTimer(); this.setDismissTimer();
}; }
useEffect(() => { componentWillUnmount() {
return () => { clearTimeout(this.dismissTimer);
clearTimeout(dismissTimer.current); }
};
}, []);
const {backgroundColor, color} = props; render() {
const opacity = dismissing ? 0 : 1; const {backgroundColor, color} = this.props;
return ( const opacity = this.state.dismissing ? 0 : 1;
<div ref={onElement} style={{opacity, backgroundColor, color}} className="notification_indicator"> return (
{props.customChildrenBefore} <div ref={this.onElement} style={{opacity, backgroundColor, color}} className="notification_indicator">
{props.children || props.text} {this.props.customChildrenBefore}
{props.userDismissable ? ( {this.props.children || this.props.text}
<a className="notification_dismissLink" onClick={handleDismiss} style={{color: props.userDismissColor}}> {this.props.userDismissable ? (
[x] <a
</a> className="notification_dismissLink"
) : null} onClick={this.handleDismiss}
{props.customChildren} style={{color: this.props.userDismissColor}}
>
[x]
</a>
) : null}
{this.props.customChildren}
<style jsx>{` <style jsx>{`
.notification_indicator { .notification_indicator {
display: inline-block; display: inline-block;
cursor: default; cursor: default;
-webkit-user-select: none; -webkit-user-select: none;
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
padding: 8px 14px 9px; padding: 8px 14px 9px;
margin-left: 10px; margin-left: 10px;
transition: 150ms opacity ease; transition: 150ms opacity ease;
color: #fff; color: #fff;
font-size: 12px; font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
} }
.notification_dismissLink { .notification_dismissLink {
position: relative; position: relative;
left: 4px; left: 4px;
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
color: currentColor; color: currentColor;
transition: font-weight 0.1s ease-in-out; transition: font-weight 0.1s ease-in-out;
} }
.notification_dismissLink:hover, .notification_dismissLink:hover,
.notification_dismissLink:focus { .notification_dismissLink:focus {
font-weight: 900; font-weight: 900;
} }
`}</style> `}</style>
</div> </div>
); );
}); }
}
Notification.displayName = 'Notification';
export default Notification;

View file

@ -1,132 +1,132 @@
import React, {forwardRef} from 'react'; import React from 'react';
import type {NotificationsProps} from '../../typings/hyper';
import {decorate} from '../utils/plugins'; import {decorate} from '../utils/plugins';
import Notification_ from './notification'; import Notification_ from './notification';
import {NotificationsProps} from '../hyper';
const Notification = decorate(Notification_, 'Notification'); const Notification = decorate(Notification_, 'Notification');
const Notifications = forwardRef<HTMLDivElement, NotificationsProps>((props, ref) => { export default class Notifications extends React.PureComponent<NotificationsProps> {
return ( render() {
<div className="notifications_view" ref={ref}> return (
{props.customChildrenBefore} <div className="notifications_view">
{props.fontShowing && ( {this.props.customChildrenBefore}
<Notification {this.props.fontShowing && (
key="font" <Notification
backgroundColor="rgba(255, 255, 255, .2)" key="font"
text={`${props.fontSize}px`} backgroundColor="rgba(255, 255, 255, .2)"
userDismissable={false} text={`${this.props.fontSize}px`}
onDismiss={props.onDismissFont} userDismissable={false}
dismissAfter={1000} onDismiss={this.props.onDismissFont}
/> dismissAfter={1000}
)} />
)}
{props.resizeShowing && ( {this.props.resizeShowing && (
<Notification <Notification
key="resize" key="resize"
backgroundColor="rgba(255, 255, 255, .2)" backgroundColor="rgba(255, 255, 255, .2)"
text={`${props.cols}x${props.rows}`} text={`${this.props.cols}x${this.props.rows}`}
userDismissable={false} userDismissable={false}
onDismiss={props.onDismissResize} onDismiss={this.props.onDismissResize}
dismissAfter={1000} dismissAfter={1000}
/> />
)} )}
{props.messageShowing && ( {this.props.messageShowing && (
<Notification <Notification
key="message" key="message"
backgroundColor="#FE354E" backgroundColor="#FE354E"
color="#fff" color="#fff"
text={props.messageText} text={this.props.messageText}
onDismiss={props.onDismissMessage} onDismiss={this.props.onDismissMessage}
userDismissable={props.messageDismissable} userDismissable={this.props.messageDismissable}
>
{props.messageURL ? (
<>
{props.messageText} (
<a
style={{color: '#fff'}}
onClick={(ev) => {
void window.require('electron').shell.openExternal(ev.currentTarget.href);
ev.preventDefault();
}}
href={props.messageURL}
>
more
</a>
)
</>
) : null}
</Notification>
)}
{props.updateShowing && (
<Notification
key="update"
backgroundColor="#18E179"
color="#000"
text={`Version ${props.updateVersion} ready`}
onDismiss={props.onDismissUpdate}
userDismissable
>
Version <b>{props.updateVersion}</b> ready.
{props.updateNote && ` ${props.updateNote.trim().replace(/\.$/, '')}`} (
<a
style={{color: '#000'}}
onClick={(ev) => {
void window.require('electron').shell.openExternal(ev.currentTarget.href);
ev.preventDefault();
}}
href={`https://github.com/quine-global/hyper/releases/tag/${props.updateVersion}`}
> >
notes {this.props.messageURL
</a> ? [
).{' '} this.props.messageText,
{props.updateCanInstall ? ( ' (',
<a
key="link"
style={{color: '#fff'}}
onClick={(ev) => {
void window.require('electron').shell.openExternal(ev.currentTarget.href);
ev.preventDefault();
}}
href={this.props.messageURL}
>
more
</a>,
') '
]
: null}
</Notification>
)}
{this.props.updateShowing && (
<Notification
key="update"
backgroundColor="#18E179"
color="#000"
text={`Version ${this.props.updateVersion} ready`}
onDismiss={this.props.onDismissUpdate}
userDismissable
>
Version <b>{this.props.updateVersion}</b> ready.
{this.props.updateNote && ` ${this.props.updateNote.trim().replace(/\.$/, '')}`} (
<a <a
style={{ style={{color: '#000'}}
cursor: 'pointer',
textDecoration: 'underline',
fontWeight: 'bold'
}}
onClick={props.onUpdateInstall}
>
Restart
</a>
) : (
<a
style={{
color: '#000',
cursor: 'pointer',
textDecoration: 'underline',
fontWeight: 'bold'
}}
onClick={(ev) => { onClick={(ev) => {
void window.require('electron').shell.openExternal(ev.currentTarget.href); void window.require('electron').shell.openExternal(ev.currentTarget.href);
ev.preventDefault(); ev.preventDefault();
}} }}
href={props.updateReleaseUrl!} href={`https://github.com/vercel/hyper/releases/tag/${this.props.updateVersion}`}
> >
Download notes
</a> </a>
)} ).{' '}
.{' '} {this.props.updateCanInstall ? (
</Notification> <a
)} style={{
{props.customChildren} cursor: 'pointer',
textDecoration: 'underline',
fontWeight: 'bold'
}}
onClick={this.props.onUpdateInstall}
>
Restart
</a>
) : (
<a
style={{
color: '#000',
cursor: 'pointer',
textDecoration: 'underline',
fontWeight: 'bold'
}}
onClick={(ev) => {
void window.require('electron').shell.openExternal(ev.currentTarget.href);
ev.preventDefault();
}}
href={this.props.updateReleaseUrl!}
>
Download
</a>
)}
.{' '}
</Notification>
)}
{this.props.customChildren}
<style jsx>{` <style jsx>{`
.notifications_view { .notifications_view {
position: fixed; position: fixed;
bottom: 20px; bottom: 20px;
right: 20px; right: 20px;
} }
`}</style> `}</style>
</div> </div>
); );
}); }
}
Notifications.displayName = 'Notifications';
export default Notifications;

View file

@ -1,15 +1,13 @@
import React, {useCallback, useRef, useEffect, forwardRef} from 'react'; import React, {useCallback} from 'react';
import {SearchBoxProps} from '../hyper';
import {VscArrowDown} from '@react-icons/all-files/vsc/VscArrowDown';
import {VscArrowUp} from '@react-icons/all-files/vsc/VscArrowUp'; import {VscArrowUp} from '@react-icons/all-files/vsc/VscArrowUp';
import {VscCaseSensitive} from '@react-icons/all-files/vsc/VscCaseSensitive'; import {VscArrowDown} from '@react-icons/all-files/vsc/VscArrowDown';
import {VscClose} from '@react-icons/all-files/vsc/VscClose'; import {VscClose} from '@react-icons/all-files/vsc/VscClose';
import {VscCaseSensitive} from '@react-icons/all-files/vsc/VscCaseSensitive';
import {VscRegex} from '@react-icons/all-files/vsc/VscRegex'; import {VscRegex} from '@react-icons/all-files/vsc/VscRegex';
import {VscWholeWord} from '@react-icons/all-files/vsc/VscWholeWord'; import {VscWholeWord} from '@react-icons/all-files/vsc/VscWholeWord';
import clsx from 'clsx'; import clsx from 'clsx';
import type {SearchBoxProps} from '../../typings/hyper';
type SearchButtonColors = { type SearchButtonColors = {
foregroundColor: string; foregroundColor: string;
selectionColor: string; selectionColor: string;
@ -84,163 +82,179 @@ const SearchButton = ({
); );
}; };
const SearchBox = forwardRef<HTMLDivElement, SearchBoxProps>((props, ref) => { class SearchBox extends React.PureComponent<SearchBoxProps> {
const { searchTerm: string;
caseSensitive, input: HTMLInputElement | null = null;
dateFocused, searchButtonColors: SearchButtonColors;
wholeWord,
regex,
results,
toggleCaseSensitive,
toggleWholeWord,
toggleRegex,
next,
prev,
close,
backgroundColor,
foregroundColor,
borderColor,
selectionColor,
font
} = props;
const searchTermRef = useRef<string>(''); constructor(props: SearchBoxProps) {
const inputRef = useRef<HTMLInputElement | null>(null); super(props);
this.searchTerm = '';
this.searchButtonColors = {
backgroundColor: this.props.borderColor,
selectionColor: this.props.selectionColor,
foregroundColor: this.props.foregroundColor
};
}
const handleChange = useCallback( handleChange = (event: React.KeyboardEvent<HTMLInputElement>) => {
(event: React.KeyboardEvent<HTMLInputElement>) => { this.searchTerm = event.currentTarget.value;
searchTermRef.current = event.currentTarget.value; if (event.shiftKey && event.key === 'Enter') {
if (event.shiftKey && event.key === 'Enter') { this.props.prev(this.searchTerm);
prev(searchTermRef.current); } else if (event.key === 'Enter') {
} else if (event.key === 'Enter') { this.props.next(this.searchTerm);
next(searchTermRef.current);
}
},
[prev, next]
);
useEffect(() => {
inputRef.current?.focus();
}, [inputRef.current]);
useEffect(() => {
if (!dateFocused) {
return;
} }
inputRef.current?.focus();
inputRef.current?.select();
}, [dateFocused]);
const searchButtonColors: SearchButtonColors = {
backgroundColor: borderColor,
selectionColor,
foregroundColor
}; };
return ( componentDidMount(): void {
<div className="flex-row search-container" ref={ref}> this.input?.focus();
<div className="flex-row search-box"> }
<input className="search-input" type="text" onKeyDown={handleChange} ref={inputRef} placeholder="Search" />
<SearchButton onClick={toggleCaseSensitive} active={caseSensitive} title="Match Case" {...searchButtonColors}> render() {
<VscCaseSensitive size="14px" /> const {
</SearchButton> caseSensitive,
wholeWord,
regex,
results,
toggleCaseSensitive,
toggleWholeWord,
toggleRegex,
next,
prev,
close,
backgroundColor,
foregroundColor,
borderColor,
selectionColor,
font
} = this.props;
<SearchButton onClick={toggleWholeWord} active={wholeWord} title="Match Whole Word" {...searchButtonColors}> return (
<VscWholeWord size="14px" /> <div className="flex-row search-container">
</SearchButton> <div className="flex-row search-box">
<input
className="search-input"
type="text"
onKeyDown={this.handleChange}
ref={(input) => {
this.input = input;
}}
placeholder="Search"
></input>
<SearchButton onClick={toggleRegex} active={regex} title="Use Regular Expression" {...searchButtonColors}> <SearchButton
<VscRegex size="14px" /> onClick={toggleCaseSensitive}
</SearchButton> active={caseSensitive}
</div> title="Match Case"
{...this.searchButtonColors}
>
<VscCaseSensitive size="14px" />
</SearchButton>
<span style={{minWidth: '60px', marginLeft: '4px'}}> <SearchButton
{results === undefined onClick={toggleWholeWord}
? '' active={wholeWord}
: results.resultCount === 0 title="Match Whole Word"
{...this.searchButtonColors}
>
<VscWholeWord size="14px" />
</SearchButton>
<SearchButton
onClick={toggleRegex}
active={regex}
title="Use Regular Expression"
{...this.searchButtonColors}
>
<VscRegex size="14px" />
</SearchButton>
</div>
<span style={{minWidth: '60px', marginLeft: '4px'}}>
{results === undefined
? ''
: results.resultCount === 0
? 'No results' ? 'No results'
: `${results.resultIndex + 1} of ${results.resultCount}`} : `${results.resultIndex + 1} of ${results.resultCount}`}
</span> </span>
<div className="flex-row"> <div className="flex-row">
<SearchButton <SearchButton
onClick={() => prev(searchTermRef.current)} onClick={() => prev(this.searchTerm)}
active={false} active={false}
title="Previous Match" title="Previous Match"
{...searchButtonColors} {...this.searchButtonColors}
> >
<VscArrowUp size="14px" /> <VscArrowUp size="14px" />
</SearchButton> </SearchButton>
<SearchButton <SearchButton
onClick={() => next(searchTermRef.current)} onClick={() => next(this.searchTerm)}
active={false} active={false}
title="Next Match" title="Next Match"
{...searchButtonColors} {...this.searchButtonColors}
> >
<VscArrowDown size="14px" /> <VscArrowDown size="14px" />
</SearchButton> </SearchButton>
<SearchButton onClick={close} active={false} title="Close" {...searchButtonColors}> <SearchButton onClick={() => close()} active={false} title="Close" {...this.searchButtonColors}>
<VscClose size="14px" /> <VscClose size="14px" />
</SearchButton> </SearchButton>
</div>
<style jsx>
{`
.search-container {
background-color: ${backgroundColor};
border: 1px solid ${borderColor};
border-radius: 2px;
position: absolute;
right: 13px;
top: 4px;
z-index: 10;
padding: 4px;
font-family: ${font};
font-size: 12px;
}
.search-input {
outline: none;
background-color: transparent;
border: none;
color: ${foregroundColor};
align-self: stretch;
width: 100px;
}
.flex-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 4px;
}
.search-box {
border: none;
border-radius: 2px;
outline: ${borderColor} solid 1px;
background-color: ${backgroundColor};
color: ${foregroundColor};
padding: 0px 4px;
}
.search-input::placeholder {
color: ${foregroundColor};
}
.search-box:focus-within {
outline: ${selectionColor} solid 2px;
}
`}
</style>
</div> </div>
);
<style jsx> }
{` }
.search-container {
background-color: ${backgroundColor};
border: 1px solid ${borderColor};
border-radius: 2px;
position: absolute;
right: 13px;
top: 4px;
z-index: 10;
padding: 4px;
font-family: ${font};
font-size: 12px;
}
.search-input {
outline: none;
background-color: transparent;
border: none;
color: ${foregroundColor};
align-self: stretch;
width: 100px;
}
.flex-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 4px;
}
.search-box {
border: none;
border-radius: 2px;
outline: ${borderColor} solid 1px;
background-color: ${backgroundColor};
color: ${foregroundColor};
padding: 0px 4px;
}
.search-input::placeholder {
color: ${foregroundColor};
}
.search-box:focus-within {
outline: ${selectionColor} solid 2px;
}
`}
</style>
</div>
);
});
SearchBox.displayName = 'SearchBox';
export default SearchBox; export default SearchBox;

View file

@ -1,191 +1,219 @@
import React, {useState, useEffect, useRef, forwardRef} from 'react'; import React from 'react';
import _ from 'lodash';
import {SplitPaneProps} from '../hyper';
import sum from 'lodash/sum'; export default class SplitPane extends React.PureComponent<SplitPaneProps, {dragging: boolean}> {
dragPanePosition!: number;
dragTarget!: Element;
panes!: Element[];
paneIndex!: number;
d1!: 'height' | 'width';
d2!: 'top' | 'left';
d3!: 'clientX' | 'clientY';
panesSize!: number;
dragging!: boolean;
constructor(props: SplitPaneProps) {
super(props);
this.state = {dragging: false};
}
import type {SplitPaneProps} from '../../typings/hyper'; componentDidUpdate(prevProps: SplitPaneProps) {
if (this.state.dragging && prevProps.sizes !== this.props.sizes) {
// recompute positions for ongoing dragging
this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2];
}
}
const SplitPane = forwardRef<HTMLDivElement, SplitPaneProps>((props, ref) => { setupPanes(ev: any) {
const dragPanePosition = useRef<number>(0); this.panes = Array.from(ev.target.parentNode.childNodes);
const dragTarget = useRef<HTMLDivElement | null>(null); this.paneIndex = this.panes.indexOf(ev.target);
const paneIndex = useRef<number>(0); this.paneIndex -= Math.ceil(this.paneIndex / 2);
const d1 = props.direction === 'horizontal' ? 'height' : 'width'; }
const d2 = props.direction === 'horizontal' ? 'top' : 'left';
const d3 = props.direction === 'horizontal' ? 'clientY' : 'clientX';
const panesSize = useRef<number | null>(null);
const [dragging, setDragging] = useState(false);
const handleAutoResize = (ev: React.MouseEvent<HTMLDivElement>, index: number) => { handleAutoResize = (ev: React.MouseEvent) => {
ev.preventDefault(); ev.preventDefault();
paneIndex.current = index; this.setupPanes(ev);
const sizes_ = getSizes(); const sizes_ = this.getSizes();
sizes_[paneIndex.current] = 0; sizes_[this.paneIndex] = 0;
sizes_[paneIndex.current + 1] = 0; sizes_[this.paneIndex + 1] = 0;
const availableWidth = 1 - sum(sizes_); const availableWidth = 1 - _.sum(sizes_);
sizes_[paneIndex.current] = availableWidth / 2; sizes_[this.paneIndex] = availableWidth / 2;
sizes_[paneIndex.current + 1] = availableWidth / 2; sizes_[this.paneIndex + 1] = availableWidth / 2;
props.onResize(sizes_); this.props.onResize(sizes_);
}; };
const handleDragStart = (ev: React.MouseEvent<HTMLDivElement>, index: number) => { handleDragStart = (ev: any) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
ev.preventDefault(); ev.preventDefault();
setDragging(true); this.setState({dragging: true});
window.addEventListener('mousemove', onDrag); window.addEventListener('mousemove', this.onDrag);
window.addEventListener('mouseup', onDragEnd); window.addEventListener('mouseup', this.onDragEnd);
const target = ev.target as HTMLDivElement; // dimensions to consider
dragTarget.current = target; if (this.props.direction === 'horizontal') {
dragPanePosition.current = dragTarget.current.getBoundingClientRect()[d2]; this.d1 = 'height';
panesSize.current = target.parentElement!.getBoundingClientRect()[d1]; this.d2 = 'top';
paneIndex.current = index; this.d3 = 'clientY';
} else {
this.d1 = 'width';
this.d2 = 'left';
this.d3 = 'clientX';
}
this.dragTarget = ev.target;
this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2];
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
this.panesSize = ev.target.parentNode.getBoundingClientRect()[this.d1];
this.setupPanes(ev);
}; };
const getSizes = () => { getSizes() {
const {sizes} = props; const {sizes} = this.props;
let sizes_: number[]; let sizes_: number[];
if (sizes) { if (sizes) {
sizes_ = [...sizes.asMutable()]; sizes_ = [...sizes.asMutable()];
} else { } else {
const total = props.children.length; const total = (this.props.children as React.ReactNodeArray).length;
const count = new Array<number>(total).fill(1 / total); const count = new Array<number>(total).fill(1 / total);
sizes_ = count; sizes_ = count;
} }
return sizes_; return sizes_;
}; }
const onDrag = (ev: MouseEvent) => { onDrag = (ev: MouseEvent) => {
const sizes_ = getSizes(); const sizes_ = this.getSizes();
const i = paneIndex.current; const i = this.paneIndex;
const pos = ev[d3]; const pos = ev[this.d3];
const d = Math.abs(dragPanePosition.current - pos) / panesSize.current!; const d = Math.abs(this.dragPanePosition - pos) / this.panesSize;
if (pos > dragPanePosition.current) { if (pos > this.dragPanePosition) {
sizes_[i] += d; sizes_[i] += d;
sizes_[i + 1] -= d; sizes_[i + 1] -= d;
} else { } else {
sizes_[i] -= d; sizes_[i] -= d;
sizes_[i + 1] += d; sizes_[i + 1] += d;
} }
props.onResize(sizes_); this.props.onResize(sizes_);
}; };
const onDragEnd = () => { onDragEnd = () => {
window.removeEventListener('mousemove', onDrag); if (this.state.dragging) {
window.removeEventListener('mouseup', onDragEnd); window.removeEventListener('mousemove', this.onDrag);
setDragging(false); window.removeEventListener('mouseup', this.onDragEnd);
this.setState({dragging: false});
}
}; };
useEffect(() => { render() {
return () => { const children = this.props.children as React.ReactNodeArray;
onDragEnd(); const {direction, borderColor} = this.props;
}; const sizeProperty = direction === 'horizontal' ? 'height' : 'width';
}, []); // workaround for the fact that if we don't specify
// sizes, sometimes flex fails to calculate the
const {children, direction, borderColor} = props; // right height for the horizontal panes
const sizeProperty = direction === 'horizontal' ? 'height' : 'width'; const sizes = this.props.sizes || new Array<number>(children.length).fill(1 / children.length);
// workaround for the fact that if we don't specify return (
// sizes, sometimes flex fails to calculate the <div className={`splitpane_panes splitpane_panes_${direction}`}>
// right height for the horizontal panes {React.Children.map(children, (child, i) => {
const sizes = props.sizes || new Array<number>(children.length).fill(1 / children.length); const style = {
return ( // flexBasis doesn't work for the first horizontal pane, height need to be specified
<div className={`splitpane_panes splitpane_panes_${direction}`} ref={ref}> [sizeProperty]: `${sizes[i] * 100}%`,
{children.map((child, i) => { flexBasis: `${sizes[i] * 100}%`,
const style = { flexGrow: 0
// flexBasis doesn't work for the first horizontal pane, height need to be specified };
[sizeProperty]: `${sizes[i] * 100}%`, return [
flexBasis: `${sizes[i] * 100}%`, <div key="pane" className="splitpane_pane" style={style}>
flexGrow: 0
};
return (
<React.Fragment key={i}>
<div className="splitpane_pane" style={style}>
{child} {child}
</div> </div>,
{i < children.length - 1 ? ( i < children.length - 1 ? (
<div <div
onMouseDown={(e) => handleDragStart(e, i)} key="divider"
onDoubleClick={(e) => handleAutoResize(e, i)} onMouseDown={this.handleDragStart}
onDoubleClick={this.handleAutoResize}
style={{backgroundColor: borderColor}} style={{backgroundColor: borderColor}}
className={`splitpane_divider splitpane_divider_${direction}`} className={`splitpane_divider splitpane_divider_${direction}`}
/> />
) : null} ) : null
</React.Fragment> ];
); })}
})} <div style={{display: this.state.dragging ? 'block' : 'none'}} className="splitpane_shim" />
<div style={{display: dragging ? 'block' : 'none'}} className="splitpane_shim" />
<style jsx>{` <style jsx>{`
.splitpane_panes { .splitpane_panes {
display: flex; display: flex;
flex: 1; flex: 1;
outline: none; outline: none;
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.splitpane_panes_vertical { .splitpane_panes_vertical {
flex-direction: row; flex-direction: row;
} }
.splitpane_panes_horizontal { .splitpane_panes_horizontal {
flex-direction: column; flex-direction: column;
} }
.splitpane_pane { .splitpane_pane {
flex: 1; flex: 1;
outline: none; outline: none;
position: relative; position: relative;
} }
.splitpane_divider { .splitpane_divider {
box-sizing: border-box; box-sizing: border-box;
z-index: 1; z-index: 1;
background-clip: padding-box; background-clip: padding-box;
flex-shrink: 0; flex-shrink: 0;
} }
.splitpane_divider_vertical { .splitpane_divider_vertical {
border-left: 5px solid rgba(255, 255, 255, 0); border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0); border-right: 5px solid rgba(255, 255, 255, 0);
width: 11px; width: 11px;
margin: 0 -5px; margin: 0 -5px;
cursor: col-resize; cursor: col-resize;
} }
.splitpane_divider_horizontal { .splitpane_divider_horizontal {
height: 11px; height: 11px;
margin: -5px 0; margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0); border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0); border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize; cursor: row-resize;
width: 100%; width: 100%;
} }
/* /*
this shim is used to make sure mousemove events this shim is used to make sure mousemove events
trigger in all the draggable area of the screen trigger in all the draggable area of the screen
this is not the case due to hterm's <iframe> this is not the case due to hterm's <iframe>
*/ */
.splitpane_shim { .splitpane_shim {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: transparent; background: transparent;
} }
`}</style> `}</style>
</div> </div>
); );
}); }
SplitPane.displayName = 'SplitPane'; componentWillUnmount() {
// ensure drag end
export default SplitPane; if (this.dragging) {
this.onDragEnd();
}
}
}

View file

@ -1,31 +1,24 @@
import React, {forwardRef} from 'react'; import React from 'react';
import {StyleSheetProps} from '../hyper';
import {useDevicePixelRatio} from 'use-device-pixel-ratio'; export default class StyleSheet extends React.PureComponent<StyleSheetProps> {
render() {
const {borderColor} = this.props;
import type {StyleSheetProps} from '../../typings/hyper'; return (
<style jsx global>{`
const StyleSheet = forwardRef<HTMLStyleElement, StyleSheetProps>((props, ref) => { ::-webkit-scrollbar {
const {borderColor} = props; width: 5px;
}
const dpr = useDevicePixelRatio(); ::-webkit-scrollbar-thumb {
-webkit-border-radius: 10px;
return ( border-radius: 10px;
<style jsx global ref={ref}>{` background: ${borderColor};
::-webkit-scrollbar { }
width: ${5 * dpr}px; ::-webkit-scrollbar-thumb:window-inactive {
} background: ${borderColor};
::-webkit-scrollbar-thumb { }
-webkit-border-radius: 10px; `}</style>
border-radius: 10px; );
background: ${borderColor}; }
} }
::-webkit-scrollbar-thumb:window-inactive {
background: ${borderColor};
}
`}</style>
);
});
StyleSheet.displayName = 'StyleSheet';
export default StyleSheet;

View file

@ -1,179 +1,164 @@
import React, {useEffect, useRef} from 'react'; import React from 'react';
import {TabProps} from '../hyper';
import type {TabProps} from '../../typings/hyper'; export default class Tab extends React.PureComponent<TabProps> {
constructor(props: TabProps) {
super(props);
}
const Tab = (props: TabProps) => { handleClick = (event: React.MouseEvent) => {
const handleClick = (event: React.MouseEvent) => {
const isLeftClick = event.nativeEvent.which === 1; const isLeftClick = event.nativeEvent.which === 1;
if (isLeftClick && !props.isActive) { if (isLeftClick && !this.props.isActive) {
props.onSelect(); this.props.onSelect();
} }
}; };
const handleMouseUp = (event: React.MouseEvent) => { handleMouseUp = (event: React.MouseEvent) => {
const isMiddleClick = event.nativeEvent.which === 2; const isMiddleClick = event.nativeEvent.which === 2;
if (isMiddleClick) { if (isMiddleClick) {
props.onClose(); this.props.onClose();
} }
}; };
const ref = useRef<HTMLLIElement>(null); render() {
const {isActive, isFirst, isLast, borderColor, hasActivity} = this.props;
useEffect(() => { return (
if (props.lastFocused) { <React.Fragment>
ref?.current?.scrollIntoView({ <li
behavior: 'smooth' onClick={this.props.onClick}
}); style={{borderColor}}
} className={`tab_tab ${isFirst ? 'tab_first' : ''} ${isActive ? 'tab_active' : ''} ${
}, [props.lastFocused]); isFirst && isActive ? 'tab_firstActive' : ''
} ${hasActivity ? 'tab_hasActivity' : ''}`}
const {isActive, isFirst, isLast, borderColor, hasActivity} = props;
return (
<>
<li
onClick={props.onClick}
style={{borderColor}}
className={`tab_tab ${isFirst ? 'tab_first' : ''} ${isActive ? 'tab_active' : ''} ${
isFirst && isActive ? 'tab_firstActive' : ''
} ${hasActivity ? 'tab_hasActivity' : ''}`}
ref={ref}
>
{props.customChildrenBefore}
<span
className={`tab_text ${isLast ? 'tab_textLast' : ''} ${isActive ? 'tab_textActive' : ''}`}
onClick={handleClick}
onMouseUp={handleMouseUp}
> >
<span title={props.text} className="tab_textInner"> {this.props.customChildrenBefore}
{props.text} <span
className={`tab_text ${isLast ? 'tab_textLast' : ''} ${isActive ? 'tab_textActive' : ''}`}
onClick={this.handleClick}
onMouseUp={this.handleMouseUp}
>
<span title={this.props.text} className="tab_textInner">
{this.props.text}
</span>
</span> </span>
</span> <i className="tab_icon" onClick={this.props.onClose}>
<i className="tab_icon" onClick={props.onClose}> <svg className="tab_shape">
<svg className="tab_shape"> <use xlinkHref="./renderer/assets/icons.svg#close-tab" />
<use xlinkHref="./renderer/assets/icons.svg#close-tab" /> </svg>
</svg> </i>
</i> {this.props.customChildren}
{props.customChildren} </li>
</li>
<style jsx>{` <style jsx>{`
.tab_tab { .tab_tab {
color: #ccc; color: #ccc;
border-color: #ccc; border-color: #ccc;
border-bottom-width: 1px; border-bottom-width: 1px;
border-bottom-style: solid; border-bottom-style: solid;
border-left-width: 1px; border-left-width: 1px;
border-left-style: solid; border-left-style: solid;
list-style-type: none; list-style-type: none;
flex-grow: 1; flex-grow: 1;
position: relative; position: relative;
min-width: 10em; }
}
.tab_tab:hover { .tab_tab:hover {
color: #ccc; color: #ccc;
} }
.tab_first { .tab_first {
border-left-width: 0; border-left-width: 0;
padding-left: 1px; padding-left: 1px;
} }
.tab_firstActive { .tab_firstActive {
border-left-width: 1px; border-left-width: 1px;
padding-left: 0; padding-left: 0;
} }
.tab_active { .tab_active {
color: #fff; color: #fff;
border-bottom-width: 0; border-bottom-width: 0;
} }
.tab_active:hover { .tab_active:hover {
color: #fff; color: #fff;
} }
.tab_hasActivity { .tab_hasActivity {
color: #50e3c2; color: #50e3c2;
} }
.tab_hasActivity:hover { .tab_hasActivity:hover {
color: #50e3c2; color: #50e3c2;
} }
.tab_text { .tab_text {
transition: color 0.2s ease; transition: color 0.2s ease;
height: 34px; height: 34px;
display: block; display: block;
width: 100%; width: 100%;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.tab_textInner { .tab_textInner {
position: absolute; position: absolute;
left: 24px; left: 24px;
right: 24px; right: 24px;
top: 0; top: 0;
bottom: 0; bottom: 0;
text-align: center; text-align: center;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
.tab_icon { .tab_icon {
transition: transition: opacity 0.2s ease, color 0.2s ease, transform 0.25s ease, background-color 0.1s ease;
opacity 0.2s ease, pointer-events: none;
color 0.2s ease, position: absolute;
transform 0.25s ease, right: 7px;
background-color 0.1s ease; top: 10px;
pointer-events: none; display: inline-block;
position: absolute; width: 14px;
right: 7px; height: 14px;
top: 10px; border-radius: 100%;
display: inline-block; color: #e9e9e9;
width: 14px; opacity: 0;
height: 14px; transform: scale(0.95);
border-radius: 100%; }
color: #e9e9e9;
opacity: 0;
transform: scale(0.95);
}
.tab_icon:hover { .tab_icon:hover {
background-color: rgba(255, 255, 255, 0.13); background-color: rgba(255, 255, 255, 0.13);
color: #fff; color: #fff;
} }
.tab_icon:active { .tab_icon:active {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
color: #909090; color: #909090;
} }
.tab_tab:hover .tab_icon { .tab_tab:hover .tab_icon {
opacity: 1; opacity: 1;
transform: none; transform: none;
pointer-events: all; pointer-events: all;
} }
.tab_shape { .tab_shape {
position: absolute; position: absolute;
left: 4px; left: 4px;
top: 4px; top: 4px;
width: 6px; width: 6px;
height: 6px; height: 6px;
vertical-align: middle; vertical-align: middle;
fill: currentColor; fill: currentColor;
shape-rendering: crispEdges; shape-rendering: crispEdges;
} }
`}</style> `}</style>
</> </React.Fragment>
); );
}; }
}
Tab.displayName = 'Tab';
export default Tab;

View file

@ -1,142 +1,105 @@
import React, {forwardRef, useEffect, useState} from 'react'; import React from 'react';
import debounce from 'lodash/debounce';
import type {ITab, TabsProps} from '../../typings/hyper';
import {decorate, getTabProps} from '../utils/plugins'; import {decorate, getTabProps} from '../utils/plugins';
import DropdownButton from './new-tab';
import Tab_ from './tab'; import Tab_ from './tab';
import {TabsProps} from '../hyper';
const Tab = decorate(Tab_, 'Tab'); const Tab = decorate(Tab_, 'Tab');
const isMac = /Mac/.test(navigator.userAgent); const isMac = /Mac/.test(navigator.userAgent);
const Tabs = forwardRef<HTMLElement, TabsProps>((props, ref) => { export default class Tabs extends React.PureComponent<TabsProps> {
const {tabs = [], borderColor, onChange, onClose, fullScreen} = props; render() {
const {tabs = [], borderColor, onChange, onClose, fullScreen} = this.props;
const [shouldFocusCounter, setShouldFocusCounter] = useState({ const hide = !isMac && tabs.length === 1;
index: 0,
when: undefined as Date | undefined
});
const scrollToActiveTab = debounce((currTabs: ITab[]) => { return (
const activeTab = currTabs.findIndex((t) => t.isActive); <nav className={`tabs_nav ${hide ? 'tabs_hiddenNav' : ''}`}>
setShouldFocusCounter({ {this.props.customChildrenBefore}
index: activeTab, {tabs.length === 1 && isMac ? <div className="tabs_title">{tabs[0].title}</div> : null}
when: new Date() {tabs.length > 1
}); ? [
}, 100); <ul key="list" className={`tabs_list ${fullScreen && isMac ? 'tabs_fullScreen' : ''}`}>
{tabs.map((tab, i) => {
const {uid, title, isActive, hasActivity} = tab;
const props = getTabProps(tab, this.props, {
text: title === '' ? 'Shell' : title,
isFirst: i === 0,
isLast: tabs.length - 1 === i,
borderColor,
isActive,
hasActivity,
onSelect: onChange.bind(null, uid),
onClose: onClose.bind(null, uid)
});
return <Tab key={`tab-${uid}`} {...props} />;
})}
</ul>,
isMac && (
<div
key="shim"
style={{borderColor}}
className={`tabs_borderShim ${fullScreen ? 'tabs_borderShimUndo' : ''}`}
/>
)
]
: null}
{this.props.customChildren}
useEffect(() => { <style jsx>{`
scrollToActiveTab(tabs); .tabs_nav {
}, [tabs, tabs.length]); font-size: 12px;
height: 34px;
line-height: 34px;
vertical-align: middle;
color: #9b9b9b;
cursor: default;
position: relative;
-webkit-user-select: none;
-webkit-app-region: ${isMac ? 'drag' : ''};
top: ${isMac ? '0px' : '34px'};
}
const hide = !isMac && tabs.length === 1; .tabs_hiddenNav {
display: none;
}
return ( .tabs_title {
<nav className={`tabs_nav ${hide ? 'tabs_hiddenNav' : ''}`} ref={ref}> text-align: center;
{props.customChildrenBefore} color: #fff;
{tabs.length === 1 && isMac ? <div className="tabs_title">{tabs[0].title}</div> : null} overflow: hidden;
{tabs.length > 1 ? ( text-overflow: ellipsis;
<> white-space: nowrap;
<ul key="list" className={`tabs_list ${fullScreen && isMac ? 'tabs_fullScreen' : ''}`}> padding-left: 76px;
{tabs.map((tab, i) => { padding-right: 76px;
const {uid, title, isActive, hasActivity} = tab; }
const tabProps = getTabProps(tab, props, {
text: title === '' ? 'Shell' : title,
isFirst: i === 0,
isLast: tabs.length - 1 === i,
borderColor,
isActive,
hasActivity,
onSelect: onChange.bind(null, uid),
onClose: onClose.bind(null, uid),
lastFocused: undefined as Date | undefined
});
if (shouldFocusCounter.index === i) {
tabProps.lastFocused = shouldFocusCounter.when;
}
return <Tab key={`tab-${uid}`} {...tabProps} />;
})}
</ul>
{isMac && (
<div
key="shim"
style={{borderColor}}
className={`tabs_borderShim ${fullScreen ? 'tabs_borderShimUndo' : ''}`}
/>
)}
</>
) : null}
<DropdownButton {...props} tabsVisible={tabs.length > 1} />
{props.customChildren}
<style jsx>{` .tabs_list {
.tabs_nav { max-height: 34px;
font-size: 12px; display: flex;
height: 34px; flex-flow: row;
line-height: 34px; margin-left: ${isMac ? '76px' : '0'};
vertical-align: middle; }
color: #9b9b9b;
cursor: default;
position: relative;
-webkit-user-select: none;
-webkit-app-region: ${isMac ? 'drag' : ''};
top: ${isMac ? '0px' : '34px'};
display: flex;
flex-flow: row;
}
.tabs_hiddenNav { .tabs_fullScreen {
display: none; margin-left: -1px;
} }
.tabs_title { .tabs_borderShim {
text-align: center; position: absolute;
color: #fff; width: 76px;
overflow: hidden; bottom: 0;
text-overflow: ellipsis; border-color: #ccc;
white-space: nowrap; border-bottom-style: solid;
padding-left: 76px; border-bottom-width: 1px;
padding-right: 76px; }
flex-grow: 1;
}
.tabs_list { .tabs_borderShimUndo {
max-height: 34px; border-bottom-width: 0px;
display: flex; }
flex-flow: row; `}</style>
margin-left: ${isMac ? '76px' : '0'}; </nav>
flex-grow: 1; );
overflow-x: auto; }
} }
.tabs_list::-webkit-scrollbar,
.tabs_list::-webkit-scrollbar-button {
display: none;
}
.tabs_fullScreen {
margin-left: -1px;
}
.tabs_borderShim {
position: absolute;
width: 76px;
bottom: 0;
border-color: #ccc;
border-bottom-style: solid;
border-bottom-width: 1px;
}
.tabs_borderShimUndo {
border-bottom-width: 0px;
}
`}</style>
</nav>
);
});
Tabs.displayName = 'Tabs';
export default Tabs;

View file

@ -1,13 +1,10 @@
import React from 'react'; import React from 'react';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import type {HyperState, HyperDispatch, TermGroupProps, TermGroupOwnProps} from '../../typings/hyper';
import {resizeTermGroup} from '../actions/term-groups';
import {decorate, getTermProps, getTermGroupProps} from '../utils/plugins'; import {decorate, getTermProps, getTermGroupProps} from '../utils/plugins';
import {resizeTermGroup} from '../actions/term-groups';
import SplitPane_ from './split-pane';
import Term_ from './term'; import Term_ from './term';
import SplitPane_ from './split-pane';
import {HyperState, HyperDispatch, TermGroupProps, TermGroupOwnProps} from '../hyper';
const Term = decorate(Term_, 'Term'); const Term = decorate(Term_, 'Term');
const SplitPane = decorate(SplitPane_, 'SplitPane'); const SplitPane = decorate(SplitPane_, 'SplitPane');
@ -85,6 +82,7 @@ class TermGroup_ extends React.PureComponent<TermGroupProps> {
letterSpacing: this.props.letterSpacing, letterSpacing: this.props.letterSpacing,
modifierKeys: this.props.modifierKeys, modifierKeys: this.props.modifierKeys,
padding: this.props.padding, padding: this.props.padding,
url: session.url,
cleared: session.cleared, cleared: session.cleared,
search: session.search, search: session.search,
cols: session.cols, cols: session.cols,
@ -108,8 +106,6 @@ class TermGroup_ extends React.PureComponent<TermGroupProps> {
macOptionSelectionMode: this.props.macOptionSelectionMode, macOptionSelectionMode: this.props.macOptionSelectionMode,
disableLigatures: this.props.disableLigatures, disableLigatures: this.props.disableLigatures,
screenReaderMode: this.props.screenReaderMode, screenReaderMode: this.props.screenReaderMode,
windowsPty: this.props.windowsPty,
imageSupport: this.props.imageSupport,
uid uid
}); });

View file

@ -1,33 +1,24 @@
import {clipboard, shell} from 'electron';
import React from 'react'; import React from 'react';
import {Terminal, ITerminalOptions, IDisposable} from 'xterm';
import {CanvasAddon} from '@xterm/addon-canvas'; import {FitAddon} from 'xterm-addon-fit';
import {FitAddon} from '@xterm/addon-fit'; import {WebLinksAddon} from 'xterm-addon-web-links';
import {ImageAddon} from '@xterm/addon-image'; import {SearchAddon, ISearchDecorationOptions} from 'xterm-addon-search';
import {LigaturesAddon} from '@xterm/addon-ligatures'; import {WebglAddon} from 'xterm-addon-webgl';
import {SearchAddon} from '@xterm/addon-search'; import {LigaturesAddon} from 'xterm-addon-ligatures';
import type {ISearchDecorationOptions} from '@xterm/addon-search'; import {Unicode11Addon} from 'xterm-addon-unicode11';
import {Unicode11Addon} from '@xterm/addon-unicode11'; import {clipboard, shell} from 'electron';
import {WebLinksAddon} from '@xterm/addon-web-links';
import {WebglAddon} from '@xterm/addon-webgl';
import {Terminal} from '@xterm/xterm';
import type {ITerminalOptions, IDisposable} from '@xterm/xterm';
import Color from 'color'; import Color from 'color';
import isEqual from 'lodash/isEqual';
import pickBy from 'lodash/pickBy';
import type {TermProps} from '../../typings/hyper';
import terms from '../terms'; import terms from '../terms';
import processClipboard from '../utils/paste'; import processClipboard from '../utils/paste';
import {decorate} from '../utils/plugins';
import _SearchBox from './searchBox'; import _SearchBox from './searchBox';
import {TermProps} from '../hyper';
import '@xterm/xterm/css/xterm.css'; import {ObjectTypedKeys} from '../utils/object';
import {decorate} from '../utils/plugins';
import 'xterm/css/xterm.css';
const SearchBox = decorate(_SearchBox, 'SearchBox'); const SearchBox = decorate(_SearchBox, 'SearchBox');
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(navigator.platform) || process.platform === 'win32'; const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(navigator.platform);
// map old hterm constants to xterm.js // map old hterm constants to xterm.js
const CURSOR_STYLES = { const CURSOR_STYLES = {
@ -51,7 +42,7 @@ const isWebgl2Supported = (() => {
const getTermOptions = (props: TermProps): ITerminalOptions => { const getTermOptions = (props: TermProps): ITerminalOptions => {
// Set a background color only if it is opaque // Set a background color only if it is opaque
const needTransparency = Color(props.backgroundColor).alpha() < 1; const needTransparency = Color(props.backgroundColor).alpha() < 1;
const backgroundColor = needTransparency ? 'rgba(0,0,0,0)' : props.backgroundColor; const backgroundColor = needTransparency ? 'transparent' : props.backgroundColor;
return { return {
macOptionIsMeta: props.modifierKeys.altIsMeta, macOptionIsMeta: props.modifierKeys.altIsMeta,
@ -66,14 +57,14 @@ const getTermOptions = (props: TermProps): ITerminalOptions => {
letterSpacing: props.letterSpacing, letterSpacing: props.letterSpacing,
allowTransparency: needTransparency, allowTransparency: needTransparency,
macOptionClickForcesSelection: props.macOptionSelectionMode === 'force', macOptionClickForcesSelection: props.macOptionSelectionMode === 'force',
bellStyle: props.bell === 'SOUND' ? 'sound' : 'none',
windowsMode: isWindows, windowsMode: isWindows,
...(isWindows && props.windowsPty && {windowsPty: props.windowsPty}),
theme: { theme: {
foreground: props.foregroundColor, foreground: props.foregroundColor,
background: backgroundColor, background: backgroundColor,
cursor: props.cursorColor, cursor: props.cursorColor,
cursorAccent: props.cursorAccentColor, cursorAccent: props.cursorAccentColor,
selectionBackground: props.selectionColor, selection: props.selectionColor,
black: props.colors.black, black: props.colors.black,
red: props.colors.red, red: props.colors.red,
green: props.colors.green, green: props.colors.green,
@ -92,8 +83,7 @@ const getTermOptions = (props: TermProps): ITerminalOptions => {
brightWhite: props.colors.lightWhite brightWhite: props.colors.lightWhite
}, },
screenReaderMode: props.screenReaderMode, screenReaderMode: props.screenReaderMode,
overviewRulerWidth: 20, overviewRulerWidth: 20
allowProposedApi: true
}; };
}; };
@ -117,8 +107,7 @@ export default class Term extends React.PureComponent<
termWrapperRef: HTMLElement | null; termWrapperRef: HTMLElement | null;
termOptions: ITerminalOptions; termOptions: ITerminalOptions;
disposableListeners: IDisposable[]; disposableListeners: IDisposable[];
defaultBellSound: HTMLAudioElement | null; termDefaultBellSound: string | null;
bellSound: HTMLAudioElement | null;
fitAddon: FitAddon; fitAddon: FitAddon;
searchAddon: SearchAddon; searchAddon: SearchAddon;
static rendererTypes: Record<string, string>; static rendererTypes: Record<string, string>;
@ -142,8 +131,7 @@ export default class Term extends React.PureComponent<
this.termWrapperRef = null; this.termWrapperRef = null;
this.termOptions = {}; this.termOptions = {};
this.disposableListeners = []; this.disposableListeners = [];
this.defaultBellSound = null; this.termDefaultBellSound = null;
this.bellSound = null;
this.fitAddon = new FitAddon(); this.fitAddon = new FitAddon();
this.searchAddon = new SearchAddon(); this.searchAddon = new SearchAddon();
this.searchDecorations = { this.searchDecorations = {
@ -170,14 +158,7 @@ export default class Term extends React.PureComponent<
this.termOptions = getTermOptions(props); this.termOptions = getTermOptions(props);
this.term = props.term || new Terminal(this.termOptions); this.term = props.term || new Terminal(this.termOptions);
this.defaultBellSound = new Audio( this.termDefaultBellSound = this.term.getOption('bellSound');
// Source: https://freesound.org/people/altemark/sounds/45759/
// This sound is released under the Creative Commons Attribution 3.0 Unported
// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been
// made, apart from the conversion to base64.
'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjMyLjEwNAAAAAAAAAAAAAAA//tQxAADB8AhSmxhIIEVCSiJrDCQBTcu3UrAIwUdkRgQbFAZC1CQEwTJ9mjRvBA4UOLD8nKVOWfh+UlK3z/177OXrfOdKl7pyn3Xf//WreyTRUoAWgBgkOAGbZHBgG1OF6zM82DWbZaUmMBptgQhGjsyYqc9ae9XFz280948NMBWInljyzsNRFLPWdnZGWrddDsjK1unuSrVN9jJsK8KuQtQCtMBjCEtImISdNKJOopIpBFpNSMbIHCSRpRR5iakjTiyzLhchUUBwCgyKiweBv/7UsQbg8isVNoMPMjAAAA0gAAABEVFGmgqK////9bP/6XCykxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'
);
this.setBellSound(props.bell, props.bellSound);
// The parent element for the terminal is attached and removed manually so // The parent element for the terminal is attached and removed manually so
// that we can preserve it across mounts and unmounts of the component // that we can preserve it across mounts and unmounts of the component
@ -205,9 +186,9 @@ export default class Term extends React.PureComponent<
} }
Term.reportRenderer(props.uid, useWebGL ? 'WebGL' : 'Canvas'); Term.reportRenderer(props.uid, useWebGL ? 'WebGL' : 'Canvas');
const shallActivateWebLink = (event: MouseEvent): boolean => { const shallActivateWebLink = (event: Record<string, any> | undefined): boolean => {
if (!event) return false; // eslint-disable-next-line @typescript-eslint/no-unsafe-return
return props.webLinksActivationKey ? event[`${props.webLinksActivationKey}Key`] : true; return event && (!props.webLinksActivationKey || event[`${props.webLinksActivationKey}Key`]);
}; };
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
@ -215,34 +196,29 @@ export default class Term extends React.PureComponent<
this.term.loadAddon(this.fitAddon); this.term.loadAddon(this.fitAddon);
this.term.loadAddon(this.searchAddon); this.term.loadAddon(this.searchAddon);
this.term.loadAddon( this.term.loadAddon(
new WebLinksAddon((event, uri) => { new WebLinksAddon(
if (shallActivateWebLink(event)) void shell.openExternal(uri); (event: MouseEvent | undefined, uri: string) => {
}) if (shallActivateWebLink(event)) void shell.openExternal(uri);
},
{
// prevent default electron link handling to allow selection, e.g. via double-click
willLinkActivate: (event: MouseEvent | undefined) => {
event?.preventDefault();
return shallActivateWebLink(event);
},
priority: Date.now()
}
)
); );
this.term.open(this.termRef); this.term.open(this.termRef);
if (useWebGL) { if (useWebGL) {
const webglAddon = new WebglAddon(); this.term.loadAddon(new WebglAddon());
this.term.loadAddon(webglAddon);
webglAddon.onContextLoss(() => {
console.warn('WebGL context lost. Falling back to canvas-based rendering.');
webglAddon.dispose();
this.term.loadAddon(new CanvasAddon());
});
} else {
this.term.loadAddon(new CanvasAddon());
} }
if (props.disableLigatures !== true && !useWebGL) { if (props.disableLigatures !== true && !useWebGL) {
this.term.loadAddon(new LigaturesAddon()); this.term.loadAddon(new LigaturesAddon());
} }
this.term.loadAddon(new Unicode11Addon()); this.term.loadAddon(new Unicode11Addon());
this.term.unicode.activeVersion = '11'; this.term.unicode.activeVersion = '11';
if (props.imageSupport) {
this.term.loadAddon(new ImageAddon());
}
} else { } else {
// get the cached plugins // get the cached plugins
this.fitAddon = props.fitAddon!; this.fitAddon = props.fitAddon!;
@ -276,10 +252,6 @@ export default class Term extends React.PureComponent<
this.disposableListeners.push(this.term.onData(props.onData)); this.disposableListeners.push(this.term.onData(props.onData));
} }
this.term.onBell(() => {
this.ringBell();
});
if (props.onResize) { if (props.onResize) {
this.disposableListeners.push( this.disposableListeners.push(
this.term.onResize(({cols, rows}) => { this.term.onResize(({cols, rows}) => {
@ -421,18 +393,6 @@ export default class Term extends React.PureComponent<
return !e.catched; return !e.catched;
} }
setBellSound(bell: 'SOUND' | false, sound: string | null) {
if (bell && bell.toUpperCase() === 'SOUND') {
this.bellSound = sound ? new Audio(sound) : this.defaultBellSound;
} else {
this.bellSound = null;
}
}
ringBell() {
void this.bellSound?.play();
}
componentDidUpdate(prevProps: TermProps) { componentDidUpdate(prevProps: TermProps) {
if (!prevProps.cleared && this.props.cleared) { if (!prevProps.cleared && this.props.cleared) {
this.clear(); this.clear();
@ -440,19 +400,40 @@ export default class Term extends React.PureComponent<
const nextTermOptions = getTermOptions(this.props); const nextTermOptions = getTermOptions(this.props);
if (prevProps.bell !== this.props.bell || prevProps.bellSound !== this.props.bellSound) { // Use bellSound in nextProps if it exists
this.setBellSound(this.props.bell, this.props.bellSound); // otherwise use the default sound found in xterm.
} nextTermOptions.bellSound = this.props.bellSound || this.termDefaultBellSound!;
if (prevProps.search && !this.props.search) { if (prevProps.search && !this.props.search) {
this.closeSearchBox(); this.closeSearchBox();
} }
// Update only options that have changed. // Update only options that have changed.
this.term.options = pickBy( ObjectTypedKeys(nextTermOptions)
nextTermOptions, .filter((option) => option !== 'theme' && nextTermOptions[option] !== this.termOptions[option])
(value, key) => !isEqual(this.termOptions[key as keyof ITerminalOptions], value) .forEach((option) => {
); try {
this.term.setOption(option, nextTermOptions[option]);
} catch (_e) {
const e = _e as {message: string};
if (/The webgl renderer only works with the webgl char atlas/i.test(e.message)) {
// Ignore this because the char atlas will also be changed
} else {
throw e;
}
}
});
// Do we need to update theme?
const shouldUpdateTheme =
!this.termOptions.theme ||
nextTermOptions.rendererType !== this.termOptions.rendererType ||
ObjectTypedKeys(nextTermOptions.theme!).some(
(option) => nextTermOptions.theme![option] !== this.termOptions.theme![option]
);
if (shouldUpdateTheme) {
this.term.setOption('theme', nextTermOptions.theme);
}
this.termOptions = nextTermOptions; this.termOptions = nextTermOptions;
@ -518,7 +499,6 @@ export default class Term extends React.PureComponent<
{this.props.customChildren} {this.props.customChildren}
{this.props.search ? ( {this.props.search ? (
<SearchBox <SearchBox
dateFocused={this.props.search}
next={this.searchNext} next={this.searchNext}
prev={this.searchPrevious} prev={this.searchPrevious}
close={this.closeSearchBox} close={this.closeSearchBox}

View file

@ -1,20 +1,18 @@
import React from 'react'; import React from 'react';
import type {TermsProps, HyperDispatch} from '../../typings/hyper';
import {registerCommandHandlers} from '../command-registry';
import {ObjectTypedKeys} from '../utils/object';
import {decorate, getTermGroupProps} from '../utils/plugins'; import {decorate, getTermGroupProps} from '../utils/plugins';
import {registerCommandHandlers} from '../command-registry';
import StyleSheet_ from './style-sheet';
import type Term from './term';
import TermGroup_ from './term-group'; import TermGroup_ from './term-group';
import StyleSheet_ from './style-sheet';
import {TermsProps, HyperDispatch} from '../hyper';
import Term from './term';
import {ObjectTypedKeys} from '../utils/object';
const TermGroup = decorate(TermGroup_, 'TermGroup'); const TermGroup = decorate(TermGroup_, 'TermGroup');
const StyleSheet = decorate(StyleSheet_, 'StyleSheet'); const StyleSheet = decorate(StyleSheet_, 'StyleSheet');
const isMac = /Mac/.test(navigator.userAgent); const isMac = /Mac/.test(navigator.userAgent);
export default class Terms extends React.Component<React.PropsWithChildren<TermsProps>> { export default class Terms extends React.Component<TermsProps> {
terms: Record<string, Term>; terms: Record<string, Term>;
registerCommands: (cmds: Record<string, (e: any, dispatch: HyperDispatch) => void>) => void; registerCommands: (cmds: Record<string, (e: any, dispatch: HyperDispatch) => void>) => void;
constructor(props: TermsProps, context: any) { constructor(props: TermsProps, context: any) {
@ -121,8 +119,6 @@ export default class Terms extends React.Component<React.PropsWithChildren<Terms
macOptionSelectionMode: this.props.macOptionSelectionMode, macOptionSelectionMode: this.props.macOptionSelectionMode,
disableLigatures: this.props.disableLigatures, disableLigatures: this.props.disableLigatures,
screenReaderMode: this.props.screenReaderMode, screenReaderMode: this.props.screenReaderMode,
windowsPty: this.props.windowsPty,
imageSupport: this.props.imageSupport,
parentProps: this.props parentProps: this.props
}); });

View file

@ -1,4 +1,4 @@
import type {FontWeight} from '@xterm/xterm'; import {FontWeight} from 'xterm';
export type ColorMap = { export type ColorMap = {
black: string; black: string;
@ -19,22 +19,12 @@ export type ColorMap = {
yellow: string; yellow: string;
}; };
type rootConfigOptions = { export type configOptions = {
/** /**
* if `true` (default), Hyper will update plugins every 5 hours * if `true` (default), Hyper will update plugins every 5 hours
* you can also set it to a custom time e.g. `1d` or `2h` * you can also set it to a custom time e.g. `1d` or `2h`
*/ */
autoUpdatePlugins: boolean | string; autoUpdatePlugins: boolean | string;
/** if `true` hyper will be set as the default protocol client for SSH */
defaultSSHApp: boolean;
/** if `true` hyper will not check for updates */
disableAutoUpdates: boolean;
/** choose either `'stable'` for receiving highly polished, or `'canary'` for less polished but more frequent updates */
updateChannel: 'stable' | 'canary';
useConpty?: boolean;
};
type profileConfigOptions = {
/** /**
* terminal background color * terminal background color
* *
@ -46,7 +36,7 @@ type profileConfigOptions = {
* 1. 'SOUND' -> Enables the bell as a sound * 1. 'SOUND' -> Enables the bell as a sound
* 2. false: turns off the bell * 2. false: turns off the bell
*/ */
bell: 'SOUND' | false; bell: string;
/** /**
* base64 encoded string of the sound file to use for the bell * base64 encoded string of the sound file to use for the bell
* if null, the default bell will be used * if null, the default bell will be used
@ -78,6 +68,10 @@ type profileConfigOptions = {
cursorColor: string; cursorColor: string;
/** `'BEAM'` for |, `'UNDERLINE'` for _, `'BLOCK'` for █ */ /** `'BEAM'` for |, `'UNDERLINE'` for _, `'BLOCK'` for █ */
cursorShape: 'BEAM' | 'UNDERLINE' | 'BLOCK'; cursorShape: 'BEAM' | 'UNDERLINE' | 'BLOCK';
/** if `true` hyper will be set as the default protocol client for SSH */
defaultSSHApp: boolean;
/** if `true` hyper will not check for updates */
disableAutoUpdates: boolean;
/** if `false` Hyper will use ligatures provided by some fonts */ /** if `false` Hyper will use ligatures provided by some fonts */
disableLigatures: boolean; disableLigatures: boolean;
/** for environment variables */ /** for environment variables */
@ -92,10 +86,6 @@ type profileConfigOptions = {
fontWeightBold: FontWeight; fontWeightBold: FontWeight;
/** color of the text */ /** color of the text */
foregroundColor: string; foregroundColor: string;
/**
* Whether to enable Sixel and iTerm2 inline image protocol support or not.
*/
imageSupport: boolean;
/** letter spacing as a relative unit */ /** letter spacing as a relative unit */
letterSpacing: number; letterSpacing: number;
/** line height as a relative unit */ /** line height as a relative unit */
@ -129,7 +119,7 @@ type profileConfigOptions = {
/** terminal selection color */ /** terminal selection color */
selectionColor: string; selectionColor: string;
/** /**
* the shell to run when spawning a new session (e.g. /usr/local/bin/fish) * the shell to run when spawning a new session (i.e. /usr/local/bin/fish)
* if left empty, your system's login shell will be used by default * if left empty, your system's login shell will be used by default
* *
* Windows * Windows
@ -154,7 +144,7 @@ type profileConfigOptions = {
*/ */
shell: string; shell: string;
/** /**
* for setting shell arguments (e.g. for using interactive shellArgs: `['-i']`) * for setting shell arguments (i.e. for using interactive shellArgs: `['-i']`)
* by default `['--login']` will be used * by default `['--login']` will be used
*/ */
shellArgs: string[]; shellArgs: string[];
@ -175,6 +165,9 @@ type profileConfigOptions = {
/** custom CSS to embed in the terminal window */ /** custom CSS to embed in the terminal window */
termCSS: string; termCSS: string;
uiFontFamily?: string; uiFontFamily?: string;
/** choose either `'stable'` for receiving highly polished, or `'canary'` for less polished but more frequent updates */
updateChannel: 'stable' | 'canary';
useConpty?: boolean;
/** /**
* Whether to use the WebGL renderer. Set it to false to use canvas-based * Whether to use the WebGL renderer. Set it to false to use canvas-based
* rendering (slower, but supports transparent backgrounds) * rendering (slower, but supports transparent backgrounds)
@ -191,25 +184,6 @@ type profileConfigOptions = {
workingDirectory: string; workingDirectory: string;
}; };
export type configOptions = rootConfigOptions &
profileConfigOptions & {
/**
* The default profile name to use when launching a new session
*/
defaultProfile: string;
/**
* A list of profiles to use
*/
profiles: {
name: string;
/**
* Specify all the options you want to override for each profile.
* Options set here override the defaults set in the root.
*/
config: Partial<profileConfigOptions>;
}[];
};
export type rawConfig = { export type rawConfig = {
config?: configOptions; config?: configOptions;
/** /**

View file

@ -1,4 +1,4 @@
import type {configOptions} from '../config'; import {configOptions} from '../config';
export const CONFIG_LOAD = 'CONFIG_LOAD'; export const CONFIG_LOAD = 'CONFIG_LOAD';
export const CONFIG_RELOAD = 'CONFIG_RELOAD'; export const CONFIG_RELOAD = 'CONFIG_RELOAD';

View file

@ -8,6 +8,8 @@ export const SESSION_USER_EXIT = 'SESSION_USER_EXIT';
export const SESSION_SET_ACTIVE = 'SESSION_SET_ACTIVE'; export const SESSION_SET_ACTIVE = 'SESSION_SET_ACTIVE';
export const SESSION_CLEAR_ACTIVE = 'SESSION_CLEAR_ACTIVE'; export const SESSION_CLEAR_ACTIVE = 'SESSION_CLEAR_ACTIVE';
export const SESSION_USER_DATA = 'SESSION_USER_DATA'; export const SESSION_USER_DATA = 'SESSION_USER_DATA';
export const SESSION_URL_SET = 'SESSION_URL_SET';
export const SESSION_URL_UNSET = 'SESSION_URL_UNSET';
export const SESSION_SET_XTERM_TITLE = 'SESSION_SET_XTERM_TITLE'; export const SESSION_SET_XTERM_TITLE = 'SESSION_SET_XTERM_TITLE';
export const SESSION_SET_CWD = 'SESSION_SET_CWD'; export const SESSION_SET_CWD = 'SESSION_SET_CWD';
export const SESSION_SEARCH = 'SESSION_SEARCH'; export const SESSION_SEARCH = 'SESSION_SEARCH';
@ -22,7 +24,6 @@ export interface SessionAddAction {
splitDirection?: 'HORIZONTAL' | 'VERTICAL'; splitDirection?: 'HORIZONTAL' | 'VERTICAL';
activeUid: string | null; activeUid: string | null;
now: number; now: number;
profile: string;
} }
export interface SessionResizeAction { export interface SessionResizeAction {
type: typeof SESSION_RESIZE; type: typeof SESSION_RESIZE;
@ -40,7 +41,6 @@ export interface SessionAddDataAction {
} }
export interface SessionPtyDataAction { export interface SessionPtyDataAction {
type: typeof SESSION_PTY_DATA; type: typeof SESSION_PTY_DATA;
data: string;
uid: string; uid: string;
now: number; now: number;
} }
@ -62,6 +62,14 @@ export interface SessionClearActiveAction {
export interface SessionUserDataAction { export interface SessionUserDataAction {
type: typeof SESSION_USER_DATA; type: typeof SESSION_USER_DATA;
} }
export interface SessionUrlSetAction {
type: typeof SESSION_URL_SET;
uid: string;
}
export interface SessionUrlUnsetAction {
type: typeof SESSION_URL_UNSET;
uid: string;
}
export interface SessionSetXtermTitleAction { export interface SessionSetXtermTitleAction {
type: typeof SESSION_SET_XTERM_TITLE; type: typeof SESSION_SET_XTERM_TITLE;
uid: string; uid: string;
@ -74,7 +82,7 @@ export interface SessionSetCwdAction {
export interface SessionSearchAction { export interface SessionSearchAction {
type: typeof SESSION_SEARCH; type: typeof SESSION_SEARCH;
uid: string; uid: string;
value: Date | null; value: boolean;
} }
export type SessionActions = export type SessionActions =
@ -88,6 +96,8 @@ export type SessionActions =
| SessionSetActiveAction | SessionSetActiveAction
| SessionClearActiveAction | SessionClearActiveAction
| SessionUserDataAction | SessionUserDataAction
| SessionUrlSetAction
| SessionUrlUnsetAction
| SessionSetXtermTitleAction | SessionSetXtermTitleAction
| SessionSetCwdAction | SessionSetCwdAction
| SessionSearchAction; | SessionSearchAction;

View file

@ -2,10 +2,10 @@ export const TERM_GROUP_REQUEST = 'TERM_GROUP_REQUEST';
export const TERM_GROUP_EXIT = 'TERM_GROUP_EXIT'; export const TERM_GROUP_EXIT = 'TERM_GROUP_EXIT';
export const TERM_GROUP_RESIZE = 'TERM_GROUP_RESIZE'; export const TERM_GROUP_RESIZE = 'TERM_GROUP_RESIZE';
export const TERM_GROUP_EXIT_ACTIVE = 'TERM_GROUP_EXIT_ACTIVE'; export const TERM_GROUP_EXIT_ACTIVE = 'TERM_GROUP_EXIT_ACTIVE';
export enum DIRECTION { export const DIRECTION = {
HORIZONTAL = 'HORIZONTAL', HORIZONTAL: 'HORIZONTAL',
VERTICAL = 'VERTICAL' VERTICAL: 'VERTICAL'
} } as const;
export interface TermGroupRequestAction { export interface TermGroupRequestAction {
type: typeof TERM_GROUP_REQUEST; type: typeof TERM_GROUP_REQUEST;

View file

@ -1,11 +1,10 @@
import {createSelector} from 'reselect'; import {createSelector} from 'reselect';
import type {HyperState, HyperDispatch, ITab} from '../../typings/hyper';
import {closeTab, changeTab, maximize, openHamburgerMenu, unmaximize, minimize, close} from '../actions/header';
import {requestTermGroup} from '../actions/term-groups';
import Header from '../components/header'; import Header from '../components/header';
import {getRootGroups} from '../selectors'; import {closeTab, changeTab, maximize, openHamburgerMenu, unmaximize, minimize, close} from '../actions/header';
import {connect} from '../utils/plugins'; import {connect} from '../utils/plugins';
import {getRootGroups} from '../selectors';
import {HyperState, HyperDispatch, ITab} from '../hyper';
const isMac = /Mac/.test(navigator.userAgent); const isMac = /Mac/.test(navigator.userAgent);
@ -39,9 +38,7 @@ const mapStateToProps = (state: HyperState) => {
maximized: state.ui.maximized, maximized: state.ui.maximized,
fullScreen: state.ui.fullScreen, fullScreen: state.ui.fullScreen,
showHamburgerMenu: state.ui.showHamburgerMenu, showHamburgerMenu: state.ui.showHamburgerMenu,
showWindowControls: state.ui.showWindowControls, showWindowControls: state.ui.showWindowControls
defaultProfile: state.ui.defaultProfile,
profiles: state.ui.profiles
}; };
}; };
@ -73,10 +70,6 @@ const mapDispatchToProps = (dispatch: HyperDispatch) => {
close: () => { close: () => {
dispatch(close()); dispatch(close());
},
openNewTab: (profile: string) => {
dispatch(requestTermGroup(undefined, profile));
} }
}; };
}; };

View file

@ -1,142 +1,149 @@
import React, {forwardRef, useEffect, useRef} from 'react'; import React from 'react';
import Mousetrap, {MousetrapInstance} from 'mousetrap';
import Mousetrap from 'mousetrap'; import {connect} from '../utils/plugins';
import type {MousetrapInstance} from 'mousetrap';
import stylis from 'stylis';
import type {HyperState, HyperProps, HyperDispatch} from '../../typings/hyper';
import * as uiActions from '../actions/ui'; import * as uiActions from '../actions/ui';
import {getRegisteredKeys, getCommandHandler, shouldPreventDefault} from '../command-registry'; import {getRegisteredKeys, getCommandHandler, shouldPreventDefault} from '../command-registry';
import type Terms from '../components/terms'; import stylis from 'stylis';
import {connect} from '../utils/plugins';
import {HeaderContainer} from './header'; import {HeaderContainer} from './header';
import NotificationsContainer from './notifications';
import TermsContainer from './terms'; import TermsContainer from './terms';
import NotificationsContainer from './notifications';
import {HyperState, HyperProps, HyperDispatch} from '../hyper';
import Terms from '../components/terms';
const isMac = /Mac/.test(navigator.userAgent); const isMac = /Mac/.test(navigator.userAgent);
const Hyper = forwardRef<HTMLDivElement, HyperProps>((props, ref) => { class Hyper extends React.PureComponent<HyperProps> {
const mousetrap = useRef<MousetrapInstance | null>(null); mousetrap!: MousetrapInstance;
const terms = useRef<Terms | null>(null); terms!: Terms;
constructor(props: HyperProps) {
super(props);
}
useEffect(() => { componentDidUpdate(prev: HyperProps) {
void attachKeyListeners(); if (this.props.backgroundColor !== prev.backgroundColor) {
}, [props.lastConfigUpdate]); // this can be removed when `setBackgroundColor` in electron
useEffect(() => { // starts working again
handleFocusActive(props.activeSession); document.body.style.backgroundColor = this.props.backgroundColor;
}, [props.activeSession]); }
const {lastConfigUpdate} = this.props;
if (lastConfigUpdate && lastConfigUpdate !== prev.lastConfigUpdate) {
this.attachKeyListeners();
}
if (prev.activeSession !== this.props.activeSession) {
this.handleFocusActive(this.props.activeSession!);
}
}
const handleFocusActive = (uid?: string | null) => { handleFocusActive = (uid?: string) => {
const term = uid && terms.current?.getTermByUid(uid); const term = uid && this.terms.getTermByUid(uid);
if (term) { if (term) {
term.focus(); term.focus();
} }
}; };
const handleSelectAll = () => { handleSelectAll = () => {
const term = terms.current?.getActiveTerm(); const term = this.terms.getActiveTerm();
if (term) { if (term) {
term.selectAll(); term.selectAll();
} }
}; };
const attachKeyListeners = async () => { attachKeyListeners() {
if (!mousetrap.current) { if (!this.mousetrap) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-call
mousetrap.current = new (Mousetrap as any)(window, true); this.mousetrap = new (Mousetrap as any)(window, true);
mousetrap.current!.stopCallback = () => { this.mousetrap.stopCallback = () => {
// All events should be intercepted even if focus is in an input/textarea // All events should be intercepted even if focus is in an input/textarea
return false; return false;
}; };
} else { } else {
mousetrap.current.reset(); this.mousetrap.reset();
} }
const keys = await getRegisteredKeys(); const keys = getRegisteredKeys();
Object.keys(keys).forEach((commandKeys) => { Object.keys(keys).forEach((commandKeys) => {
mousetrap.current?.bind( this.mousetrap.bind(
commandKeys, commandKeys,
(e) => { (e) => {
const command = keys[commandKeys]; const command = keys[commandKeys];
// We should tell xterm to ignore this event. // We should tell to xterm that it should ignore this event.
(e as any).catched = true; (e as any).catched = true;
props.execCommand(command, getCommandHandler(command), e); this.props.execCommand(command, getCommandHandler(command), e);
shouldPreventDefault(command) && e.preventDefault(); shouldPreventDefault(command) && e.preventDefault();
}, },
'keydown' 'keydown'
); );
}); });
}; }
useEffect(() => { componentDidMount() {
void attachKeyListeners(); this.attachKeyListeners();
window.rpc.on('term selectAll', handleSelectAll); window.rpc.on('term selectAll', this.handleSelectAll);
}, []); }
const onTermsRef = (_terms: Terms | null) => { onTermsRef = (terms: Terms) => {
terms.current = _terms; this.terms = terms;
window.focusActiveTerm = (uid?: string) => { window.focusActiveTerm = (uid?: string) => {
if (uid) { if (uid) {
handleFocusActive(uid); this.handleFocusActive(uid);
} else { } else {
terms.current?.getActiveTerm()?.focus(); this.terms.getActiveTerm().focus();
} }
}; };
}; };
useEffect(() => { componentWillUnmount() {
return () => { document.body.style.backgroundColor = 'inherit';
mousetrap.current?.reset(); this.mousetrap?.reset();
}; }
}, []);
const {isMac: isMac_, customCSS, uiFontFamily, borderColor, maximized, fullScreen} = props; render() {
const borderWidth = isMac_ ? '' : `${maximized ? '0' : '1'}px`; const {isMac: isMac_, customCSS, uiFontFamily, borderColor, maximized, fullScreen} = this.props;
stylis.set({prefix: false}); const borderWidth = isMac_ ? '' : `${maximized ? '0' : '1'}px`;
return ( stylis.set({prefix: false});
<div id="hyper" ref={ref}> return (
<div <div id="hyper">
style={{fontFamily: uiFontFamily, borderColor, borderWidth}} <div
className={`hyper_main ${isMac_ && 'hyper_mainRounded'} ${fullScreen ? 'fullScreen' : ''}`} style={{fontFamily: uiFontFamily, borderColor, borderWidth}}
> className={`hyper_main ${isMac_ && 'hyper_mainRounded'} ${fullScreen ? 'fullScreen' : ''}`}
<HeaderContainer /> >
<TermsContainer ref_={onTermsRef} /> <HeaderContainer />
{props.customInnerChildren} <TermsContainer ref_={this.onTermsRef} />
{this.props.customInnerChildren}
</div>
<NotificationsContainer />
{this.props.customChildren}
<style jsx>
{`
.hyper_main {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px solid #333;
}
.hyper_mainRounded {
border-radius: 10px;
}
`}
</style>
{/*
Add custom CSS to Hyper.
We add a scope to the customCSS so that it can get around the weighting applied by styled-jsx
*/}
<style dangerouslySetInnerHTML={{__html: stylis('#hyper', customCSS)}} />
</div> </div>
);
<NotificationsContainer /> }
}
{props.customChildren}
<style jsx>
{`
.hyper_main {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px solid #333;
}
.hyper_mainRounded {
border-radius: 10.5px;
overflow: hidden;
}
`}
</style>
{/*
Add custom CSS to Hyper.
We add a scope to the customCSS so that it can get around the weighting applied by styled-jsx
*/}
<style dangerouslySetInnerHTML={{__html: stylis('#hyper', customCSS)}} />
</div>
);
});
Hyper.displayName = 'Hyper';
const mapStateToProps = (state: HyperState) => { const mapStateToProps = (state: HyperState) => {
return { return {

View file

@ -1,8 +1,8 @@
import type {HyperState, HyperDispatch} from '../../typings/hyper';
import {dismissNotification} from '../actions/notifications';
import {installUpdate} from '../actions/updater';
import Notifications from '../components/notifications'; import Notifications from '../components/notifications';
import {installUpdate} from '../actions/updater';
import {connect} from '../utils/plugins'; import {connect} from '../utils/plugins';
import {dismissNotification} from '../actions/notifications';
import {HyperState, HyperDispatch} from '../hyper';
const mapStateToProps = (state: HyperState) => { const mapStateToProps = (state: HyperState) => {
const {ui} = state; const {ui} = state;

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