Compare commits

..

1 Commits

Author SHA1 Message Date
Eugene Pankov
39732908a3 ivy test 2019-12-19 01:45:12 +01:00
226 changed files with 4846 additions and 8860 deletions

View File

@@ -225,115 +225,6 @@
"contributions": [ "contributions": [
"code" "code"
] ]
},
{
"login": "LeSeulArtichaut",
"name": "LeSeulArtichaut",
"avatar_url": "https://avatars1.githubusercontent.com/u/38361244?v=4",
"profile": "https://github.com/LeSeulArtichaut",
"contributions": [
"code"
]
},
{
"login": "CyrilTaylor",
"name": "Cyril Taylor",
"avatar_url": "https://avatars0.githubusercontent.com/u/12631466?v=4",
"profile": "https://github.com/CyrilTaylor",
"contributions": [
"code"
]
},
{
"login": "nstefanou",
"name": "nstefanou",
"avatar_url": "https://avatars3.githubusercontent.com/u/51129173?v=4",
"profile": "https://github.com/nstefanou",
"contributions": [
"code",
"plugin"
]
},
{
"login": "orin220444",
"name": "orin220444",
"avatar_url": "https://avatars3.githubusercontent.com/u/30747229?v=4",
"profile": "https://github.com/orin220444",
"contributions": [
"code"
]
},
{
"login": "Goobles",
"name": "Gobius Dolhain",
"avatar_url": "https://avatars3.githubusercontent.com/u/8776771?v=4",
"profile": "https://github.com/Goobles",
"contributions": [
"code"
]
},
{
"login": "3l0w",
"name": "Gwilherm Folliot",
"avatar_url": "https://avatars2.githubusercontent.com/u/37798980?v=4",
"profile": "https://github.com/3l0w",
"contributions": [
"code"
]
},
{
"login": "dimitory",
"name": "Dmitry Pronin",
"avatar_url": "https://avatars0.githubusercontent.com/u/475955?v=4",
"profile": "https://github.com/Dimitory",
"contributions": [
"code"
]
},
{
"login": "JonathanBeverley",
"name": "Jonathan Beverley",
"avatar_url": "https://avatars1.githubusercontent.com/u/20328966?v=4",
"profile": "https://github.com/JonathanBeverley",
"contributions": [
"code"
]
},
{
"login": "zend",
"name": "Zenghai Liang",
"avatar_url": "https://avatars1.githubusercontent.com/u/25160?v=4",
"profile": "https://github.com/zend",
"contributions": [
"code"
]
},
{
"login": "matishadow",
"name": "Mateusz Tracz",
"avatar_url": "https://avatars0.githubusercontent.com/u/9083085?v=4",
"profile": "https://about.me/matishadow",
"contributions": [
"code"
]
},
{
"login": "pinpins",
"name": "pinpin",
"avatar_url": "https://avatars3.githubusercontent.com/u/36234677?v=4",
"profile": "https://zergpool.com",
"contributions": [
"code"
]
},
{
"login": "TakuroOnoda",
"name": "Takuro Onoda",
"avatar_url": "https://avatars0.githubusercontent.com/u/1407926?v=4",
"profile": "https://github.com/TakuroOnoda",
"contributions": [
"code"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,
@@ -341,6 +232,5 @@
"projectOwner": "Eugeny", "projectOwner": "Eugeny",
"repoType": "github", "repoType": "github",
"repoHost": "https://github.com", "repoHost": "https://github.com",
"commitConvention": "none", "commitConvention": "none"
"skipCi": true
} }

View File

@@ -79,7 +79,6 @@ rules:
args: after-used args: after-used
argsIgnorePattern: ^_ argsIgnorePattern: ^_
no-undef: error no-undef: error
no-var: error
object-curly-spacing: object-curly-spacing:
- error - error
- always - always
@@ -99,8 +98,3 @@ rules:
'@typescript-eslint/restrict-template-expressions': off '@typescript-eslint/restrict-template-expressions': off
'@typescript-eslint/no-dynamic-delete': off '@typescript-eslint/no-dynamic-delete': off
'@typescript-eslint/prefer-nullish-coalescing': off '@typescript-eslint/prefer-nullish-coalescing': off
'@typescript-eslint/prefer-readonly-parameter-types': off
'@typescript-eslint/no-unsafe-member-access': off
'@typescript-eslint/no-unsafe-call': off
'@typescript-eslint/no-unsafe-return': off
'@typescript-eslint/no-base-to-string': off # broken in typescript-eslint

View File

@@ -11,7 +11,7 @@ jobs:
- name: Installing Node - name: Installing Node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 10 version: 10
- name: Build - name: Build
run: | run: |

View File

@@ -11,7 +11,7 @@ jobs:
- name: Installing Node - name: Installing Node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 10 version: 10
- name: Install deps - name: Install deps
run: | run: |

View File

@@ -11,7 +11,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 10 version: 10
- name: Install deps - name: Install deps
run: | run: |
@@ -25,6 +25,9 @@ jobs:
- name: Build native deps - name: Build native deps
run: scripts/build-native.js run: scripts/build-native.js
- name: Build typings
run: yarn run build:typings
- name: Webpack - name: Webpack
run: yarn run build run: yarn run build

View File

@@ -11,7 +11,7 @@ jobs:
- name: Installing Node - name: Installing Node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 10 version: 10
- name: Install deps - name: Install deps
run: | run: |
@@ -25,24 +25,23 @@ jobs:
- name: Build native deps - name: Build native deps
run: scripts/build-native.js run: scripts/build-native.js
- name: Build typings
run: yarn run build:typings
- name: Webpack - name: Webpack
run: yarn run build run: yarn run build
- name: Prepackage plugins - name: Prepackage plugins
run: scripts/prepackage-plugins.js run: scripts/prepackage-plugins.js
- run: sed -i '' 's/updateInfo = await/\/\/updateInfo = await/g' node_modules/app-builder-lib/out/targets/ArchiveTarget.js
- name: Build and sign packages - name: Build and sign packages
run: scripts/build-macos.js run: scripts/build-macos.js
if: github.repository == 'Eugeny/terminus' && github.event_name == 'push' if: github.repository == 'Eugeny/terminus' && github.event_name == 'push'
env: env:
#DEBUG: electron-builder,electron-builder:* DEBUG: electron-builder,electron-builder:*
GH_TOKEN: ${{ secrets.GH_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
CSC_LINK: ${{ secrets.CSC_LINK }} CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPSTORE_USERNAME: ${{ secrets.APPSTORE_USERNAME }}
APPSTORE_PASSWORD: ${{ secrets.APPSTORE_PASSWORD }}
- name: Build packages without signing - name: Build packages without signing
run: scripts/build-macos.js run: scripts/build-macos.js

View File

@@ -11,7 +11,7 @@ jobs:
- name: Installing Node - name: Installing Node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 10 version: 10
- name: Build - name: Build
shell: powershell shell: powershell
@@ -39,7 +39,7 @@ jobs:
mkdir artifact-setup mkdir artifact-setup
mv dist/*-setup.exe artifact-setup/ mv dist/*-setup.exe artifact-setup/
mkdir artifact-portable mkdir artifact-portable
mv dist/*-portable.zip artifact-portable/ mv dist/*-portable.exe artifact-portable/
- uses: actions/upload-artifact@master - uses: actions/upload-artifact@master
name: Upload installer name: Upload installer

2
.gitignore vendored
View File

@@ -28,5 +28,3 @@ docs/api
.electron-symbols .electron-symbols
sentry.properties sentry.properties
sentry-symbols.js sentry-symbols.js
terminus-ssh/util/pagent.exe

View File

@@ -1,5 +1,5 @@
language: node_js language: node_js
node_js: 10 node_js: 11
stages: stages:
- Build - Build

View File

@@ -13,13 +13,12 @@
**Terminus** is a highly configurable terminal emulator for Windows, macOS and Linux **Terminus** is a highly configurable terminal emulator for Windows, macOS and Linux
* Integrated SSH client and connection manager
* Theming and color schemes * Theming and color schemes
* Fully configurable shortcuts * Fully configurable shortcuts
* Split panes * Split panes
* Remembers your tabs * Remembers your tabs
* PowerShell (and PS Core), WSL, Git-Bash, Cygwin, Cmder and CMD support * PowerShell (and PS Core), WSL, Git-Bash, Cygwin, Cmder and CMD support
* Direct file transfer from/to SSH sessions via Zmodem * Integrated SSH client and connection manager
* Full Unicode support including double-width characters * Full Unicode support including double-width characters
* Doesn't choke on fast-flowing outputs * Doesn't choke on fast-flowing outputs
* Proper shell experience on Windows including tab completion (via Clink) * Proper shell experience on Windows including tab completion (via Clink)
@@ -35,15 +34,12 @@
--- ---
# Portable
For portable in windows, user can create folder `data` at the same directory as `Terminal.exe` to save the settings.
# Plugins # Plugins
Plugins and themes can be installed directly from the Settings view inside Terminus. Plugins and themes can be installed directly from the Settings view inside Terminus.
* [clickable-links](https://github.com/Eugeny/terminus-clickable-links) - makes paths and URLs in the terminal clickable * [clickable-links](https://github.com/Eugeny/terminus-clickable-links) - makes paths and URLs in the terminal clickable
* [shell-selector](https://github.com/Eugeny/terminus-shell-selector) - a quick shell selector pane
* [title-control](https://github.com/kbjr/terminus-title-control) - allows modifying the title of the terminal tabs by providing a prefix, suffix, and/or strings to be removed * [title-control](https://github.com/kbjr/terminus-title-control) - allows modifying the title of the terminal tabs by providing a prefix, suffix, and/or strings to be removed
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - quickly send commands to one or all terminal tabs * [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - quickly send commands to one or all terminal tabs
* [save-output](https://github.com/Eugeny/terminus-save-output) - record terminal output into a file * [save-output](https://github.com/Eugeny/terminus-save-output) - record terminal output into a file
@@ -70,61 +66,42 @@ See [HACKING.md](https://github.com/Eugeny/terminus/blob/master/HACKING.md) and
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start --> <!-- prettier-ignore -->
<!-- markdownlint-disable -->
<table> <table>
<tr> <tr>
<td align="center"><a href="http://www.russellmyers.com"><img src="https://avatars2.githubusercontent.com/u/184085?v=4" width="100px;" alt=""/><br /><sub><b>Russell Myers</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=mezner" title="Code">💻</a></td> <td align="center"><a href="http://www.russellmyers.com"><img src="https://avatars2.githubusercontent.com/u/184085?v=4" width="100px;" alt="Russell Myers"/><br /><sub><b>Russell Myers</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=mezner" title="Code">💻</a></td>
<td align="center"><a href="http://www.morwire.com"><img src="https://avatars1.githubusercontent.com/u/3991658?v=4" width="100px;" alt=""/><br /><sub><b>Austin Warren</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=ehwarren" title="Code">💻</a></td> <td align="center"><a href="http://www.morwire.com"><img src="https://avatars1.githubusercontent.com/u/3991658?v=4" width="100px;" alt="Austin Warren"/><br /><sub><b>Austin Warren</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=ehwarren" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Drachenkaetzchen"><img src="https://avatars1.githubusercontent.com/u/162974?v=4" width="100px;" alt=""/><br /><sub><b>Felicia Hummel</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=Drachenkaetzchen" title="Code">💻</a></td> <td align="center"><a href="https://github.com/Drachenkaetzchen"><img src="https://avatars1.githubusercontent.com/u/162974?v=4" width="100px;" alt="Felicia Hummel"/><br /><sub><b>Felicia Hummel</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=Drachenkaetzchen" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/mikemaccana"><img src="https://avatars2.githubusercontent.com/u/172594?v=4" width="100px;" alt=""/><br /><sub><b>Mike MacCana</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=mikemaccana" title="Tests">⚠️</a> <a href="#design-mikemaccana" title="Design">🎨</a></td> <td align="center"><a href="https://github.com/mikemaccana"><img src="https://avatars2.githubusercontent.com/u/172594?v=4" width="100px;" alt="Mike MacCana"/><br /><sub><b>Mike MacCana</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=mikemaccana" title="Tests">⚠️</a> <a href="#design-mikemaccana" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/yxuko"><img src="https://avatars1.githubusercontent.com/u/1786317?v=4" width="100px;" alt=""/><br /><sub><b>Yacine Kanzari</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=yxuko" title="Code">💻</a></td> <td align="center"><a href="https://github.com/yxuko"><img src="https://avatars1.githubusercontent.com/u/1786317?v=4" width="100px;" alt="Yacine Kanzari"/><br /><sub><b>Yacine Kanzari</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=yxuko" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/BBJip"><img src="https://avatars2.githubusercontent.com/u/32908927?v=4" width="100px;" alt=""/><br /><sub><b>BBJip</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=BBJip" title="Code">💻</a></td> <td align="center"><a href="https://github.com/BBJip"><img src="https://avatars2.githubusercontent.com/u/32908927?v=4" width="100px;" alt="BBJip"/><br /><sub><b>BBJip</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=BBJip" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Futagirl"><img src="https://avatars2.githubusercontent.com/u/33533958?v=4" width="100px;" alt=""/><br /><sub><b>Futagirl</b></sub></a><br /><a href="#design-Futagirl" title="Design">🎨</a></td> <td align="center"><a href="https://github.com/Futagirl"><img src="https://avatars2.githubusercontent.com/u/33533958?v=4" width="100px;" alt="Futagirl"/><br /><sub><b>Futagirl</b></sub></a><br /><a href="#design-Futagirl" title="Design">🎨</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://www.levrik.io"><img src="https://avatars3.githubusercontent.com/u/9491603?v=4" width="100px;" alt=""/><br /><sub><b>Levin Rickert</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=levrik" title="Code">💻</a></td> <td align="center"><a href="https://www.levrik.io"><img src="https://avatars3.githubusercontent.com/u/9491603?v=4" width="100px;" alt="Levin Rickert"/><br /><sub><b>Levin Rickert</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=levrik" title="Code">💻</a></td>
<td align="center"><a href="https://kwonoj.github.io"><img src="https://avatars2.githubusercontent.com/u/1210596?v=4" width="100px;" alt=""/><br /><sub><b>OJ Kwon</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=kwonoj" title="Code">💻</a></td> <td align="center"><a href="https://kwonoj.github.io"><img src="https://avatars2.githubusercontent.com/u/1210596?v=4" width="100px;" alt="OJ Kwon"/><br /><sub><b>OJ Kwon</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=kwonoj" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Domain"><img src="https://avatars2.githubusercontent.com/u/903197?v=4" width="100px;" alt=""/><br /><sub><b>domain</b></sub></a><br /><a href="#plugin-Domain" title="Plugin/utility libraries">🔌</a> <a href="https://github.com/Eugeny/terminus/commits?author=Domain" title="Code">💻</a></td> <td align="center"><a href="https://github.com/Domain"><img src="https://avatars2.githubusercontent.com/u/903197?v=4" width="100px;" alt="domain"/><br /><sub><b>domain</b></sub></a><br /><a href="#plugin-Domain" title="Plugin/utility libraries">🔌</a> <a href="https://github.com/Eugeny/terminus/commits?author=Domain" title="Code">💻</a></td>
<td align="center"><a href="http://www.jbrumond.me"><img src="https://avatars1.githubusercontent.com/u/195127?v=4" width="100px;" alt=""/><br /><sub><b>James Brumond</b></sub></a><br /><a href="#plugin-kbjr" title="Plugin/utility libraries">🔌</a></td> <td align="center"><a href="http://www.jbrumond.me"><img src="https://avatars1.githubusercontent.com/u/195127?v=4" width="100px;" alt="James Brumond"/><br /><sub><b>James Brumond</b></sub></a><br /><a href="#plugin-kbjr" title="Plugin/utility libraries">🔌</a></td>
<td align="center"><a href="http://www.growingwiththeweb.com"><img src="https://avatars0.githubusercontent.com/u/2193314?v=4" width="100px;" alt=""/><br /><sub><b>Daniel Imms</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=Tyriar" title="Code">💻</a> <a href="#plugin-Tyriar" title="Plugin/utility libraries">🔌</a> <a href="https://github.com/Eugeny/terminus/commits?author=Tyriar" title="Tests">⚠️</a></td> <td align="center"><a href="http://www.growingwiththeweb.com"><img src="https://avatars0.githubusercontent.com/u/2193314?v=4" width="100px;" alt="Daniel Imms"/><br /><sub><b>Daniel Imms</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=Tyriar" title="Code">💻</a> <a href="#plugin-Tyriar" title="Plugin/utility libraries">🔌</a> <a href="https://github.com/Eugeny/terminus/commits?author=Tyriar" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/baflo"><img src="https://avatars2.githubusercontent.com/u/834350?v=4" width="100px;" alt=""/><br /><sub><b>Florian Bachmann</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=baflo" title="Code">💻</a></td> <td align="center"><a href="https://github.com/baflo"><img src="https://avatars2.githubusercontent.com/u/834350?v=4" width="100px;" alt="Florian Bachmann"/><br /><sub><b>Florian Bachmann</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=baflo" title="Code">💻</a></td>
<td align="center"><a href="http://michael-kuehnel.de"><img src="https://avatars2.githubusercontent.com/u/441011?v=4" width="100px;" alt=""/><br /><sub><b>Michael Kühnel</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=mischah" title="Code">💻</a> <a href="#design-mischah" title="Design">🎨</a></td> <td align="center"><a href="http://michael-kuehnel.de"><img src="https://avatars2.githubusercontent.com/u/441011?v=4" width="100px;" alt="Michael Kühnel"/><br /><sub><b>Michael Kühnel</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=mischah" title="Code">💻</a> <a href="#design-mischah" title="Design">🎨</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/NieLeben"><img src="https://avatars3.githubusercontent.com/u/47182955?v=4" width="100px;" alt=""/><br /><sub><b>Tilmann Meyer</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=NieLeben" title="Code">💻</a></td> <td align="center"><a href="https://github.com/NieLeben"><img src="https://avatars3.githubusercontent.com/u/47182955?v=4" width="100px;" alt="Tilmann Meyer"/><br /><sub><b>Tilmann Meyer</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=NieLeben" title="Code">💻</a></td>
<td align="center"><a href="http://www.jubeat.net"><img src="https://avatars3.githubusercontent.com/u/11289158?v=4" width="100px;" alt=""/><br /><sub><b>PM Extra</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/issues?q=author%3APMExtra" title="Bug reports">🐛</a></td> <td align="center"><a href="http://www.jubeat.net"><img src="https://avatars3.githubusercontent.com/u/11289158?v=4" width="100px;" alt="PM Extra"/><br /><sub><b>PM Extra</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/issues?q=author%3APMExtra" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://jjuhas.keybase.pub//"><img src="https://avatars1.githubusercontent.com/u/6438760?v=4" width="100px;" alt=""/><br /><sub><b>Jonathan</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=IgnusG" title="Code">💻</a></td> <td align="center"><a href="https://jjuhas.keybase.pub//"><img src="https://avatars1.githubusercontent.com/u/6438760?v=4" width="100px;" alt="Jonathan"/><br /><sub><b>Jonathan</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=IgnusG" title="Code">💻</a></td>
<td align="center"><a href="https://hans-koch.me"><img src="https://avatars0.githubusercontent.com/u/1093709?v=4" width="100px;" alt=""/><br /><sub><b>Hans Koch</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=hammster" title="Code">💻</a></td> <td align="center"><a href="https://hans-koch.me"><img src="https://avatars0.githubusercontent.com/u/1093709?v=4" width="100px;" alt="Hans Koch"/><br /><sub><b>Hans Koch</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=hammster" title="Code">💻</a></td>
<td align="center"><a href="http://thepuzzlemaker.info"><img src="https://avatars3.githubusercontent.com/u/12666617?v=4" width="100px;" alt=""/><br /><sub><b>Dak Smyth</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=ThePuzzlemaker" title="Code">💻</a></td> <td align="center"><a href="http://thepuzzlemaker.info"><img src="https://avatars3.githubusercontent.com/u/12666617?v=4" width="100px;" alt="Dak Smyth"/><br /><sub><b>Dak Smyth</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=ThePuzzlemaker" title="Code">💻</a></td>
<td align="center"><a href="http://yfwz100.github.io"><img src="https://avatars2.githubusercontent.com/u/983211?v=4" width="100px;" alt=""/><br /><sub><b>Wang Zhi</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=yfwz100" title="Code">💻</a></td> <td align="center"><a href="http://yfwz100.github.io"><img src="https://avatars2.githubusercontent.com/u/983211?v=4" width="100px;" alt="Wang Zhi"/><br /><sub><b>Wang Zhi</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=yfwz100" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/jack1142"><img src="https://avatars0.githubusercontent.com/u/6032823?v=4" width="100px;" alt=""/><br /><sub><b>jack1142</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=jack1142" title="Code">💻</a></td> <td align="center"><a href="https://github.com/jack1142"><img src="https://avatars0.githubusercontent.com/u/6032823?v=4" width="100px;" alt="jack1142"/><br /><sub><b>jack1142</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=jack1142" title="Code">💻</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/hdougie"><img src="https://avatars1.githubusercontent.com/u/450799?v=4" width="100px;" alt=""/><br /><sub><b>Howie Douglas</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=hdougie" title="Code">💻</a></td> <td align="center"><a href="https://github.com/hdougie"><img src="https://avatars1.githubusercontent.com/u/450799?v=4" width="100px;" alt="Howie Douglas"/><br /><sub><b>Howie Douglas</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=hdougie" title="Code">💻</a></td>
<td align="center"><a href="https://chriskaczor.com"><img src="https://avatars2.githubusercontent.com/u/180906?v=4" width="100px;" alt=""/><br /><sub><b>Chris Kaczor</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=ckaczor" title="Code">💻</a></td> <td align="center"><a href="https://chriskaczor.com"><img src="https://avatars2.githubusercontent.com/u/180906?v=4" width="100px;" alt="Chris Kaczor"/><br /><sub><b>Chris Kaczor</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=ckaczor" title="Code">💻</a></td>
<td align="center"><a href="https://www.boxmein.net"><img src="https://avatars1.githubusercontent.com/u/358714?v=4" width="100px;" alt=""/><br /><sub><b>Johannes Kadak</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=boxmein" title="Code">💻</a></td> <td align="center"><a href="https://www.boxmein.net"><img src="https://avatars1.githubusercontent.com/u/358714?v=4" width="100px;" alt="Johannes Kadak"/><br /><sub><b>Johannes Kadak</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=boxmein" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/LeSeulArtichaut"><img src="https://avatars1.githubusercontent.com/u/38361244?v=4" width="100px;" alt=""/><br /><sub><b>LeSeulArtichaut</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=LeSeulArtichaut" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/CyrilTaylor"><img src="https://avatars0.githubusercontent.com/u/12631466?v=4" width="100px;" alt=""/><br /><sub><b>Cyril Taylor</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=CyrilTaylor" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/nstefanou"><img src="https://avatars3.githubusercontent.com/u/51129173?v=4" width="100px;" alt=""/><br /><sub><b>nstefanou</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=nstefanou" title="Code">💻</a> <a href="#plugin-nstefanou" title="Plugin/utility libraries">🔌</a></td>
<td align="center"><a href="https://github.com/orin220444"><img src="https://avatars3.githubusercontent.com/u/30747229?v=4" width="100px;" alt=""/><br /><sub><b>orin220444</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=orin220444" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/Goobles"><img src="https://avatars3.githubusercontent.com/u/8776771?v=4" width="100px;" alt=""/><br /><sub><b>Gobius Dolhain</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=Goobles" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/3l0w"><img src="https://avatars2.githubusercontent.com/u/37798980?v=4" width="100px;" alt=""/><br /><sub><b>Gwilherm Folliot</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=3l0w" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Dimitory"><img src="https://avatars0.githubusercontent.com/u/475955?v=4" width="100px;" alt=""/><br /><sub><b>Dmitry Pronin</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=dimitory" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/JonathanBeverley"><img src="https://avatars1.githubusercontent.com/u/20328966?v=4" width="100px;" alt=""/><br /><sub><b>Jonathan Beverley</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=JonathanBeverley" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/zend"><img src="https://avatars1.githubusercontent.com/u/25160?v=4" width="100px;" alt=""/><br /><sub><b>Zenghai Liang</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=zend" title="Code">💻</a></td>
<td align="center"><a href="https://about.me/matishadow"><img src="https://avatars0.githubusercontent.com/u/9083085?v=4" width="100px;" alt=""/><br /><sub><b>Mateusz Tracz</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=matishadow" title="Code">💻</a></td>
<td align="center"><a href="https://zergpool.com"><img src="https://avatars3.githubusercontent.com/u/36234677?v=4" width="100px;" alt=""/><br /><sub><b>pinpin</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=pinpins" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/TakuroOnoda"><img src="https://avatars0.githubusercontent.com/u/1407926?v=4" width="100px;" alt=""/><br /><sub><b>Takuro Onoda</b></sub></a><br /><a href="https://github.com/Eugeny/terminus/commits?author=TakuroOnoda" title="Code">💻</a></td>
</tr> </tr>
</table> </table>
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END --> <!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -8,15 +8,17 @@ html
window.nodeRequire = require window.nodeRequire = require
script(src='./preload.js') script(src='./preload.js')
script(src='./bundle.js', defer) script(src='./bundle.js', defer)
style#custom-css
style. style.
body { transition: 0.5s background; } body { transition: 0.5s background; }
body body
style#custom-css
app-root app-root
.preload-logo .preload-logo
div div
.terminus-logo .terminus-logo
h1.terminus-title Terminus h1.terminus-title Terminus
sup α sup α
.progress .progress
.bar(style='width: 0%') .bar(style='width: 0%')

View File

@@ -1,5 +1,4 @@
import { app, ipcMain, Menu, Tray, shell, globalShortcut } from 'electron' import { app, ipcMain, Menu, Tray, shell } from 'electron'
// eslint-disable-next-line no-duplicate-imports
import * as electron from 'electron' import * as electron from 'electron'
import { loadConfig } from './config' import { loadConfig } from './config'
import { Window, WindowOptions } from './window' import { Window, WindowOptions } from './window'
@@ -9,38 +8,25 @@ export class Application {
private windows: Window[] = [] private windows: Window[] = []
constructor () { constructor () {
ipcMain.on('app:config-change', (_event, config) => { ipcMain.on('app:config-change', () => {
this.broadcast('host:config-change', config) this.broadcast('host:config-change')
})
ipcMain.on('app:register-global-hotkey', (_event, specs) => {
globalShortcut.unregisterAll()
for (let spec of specs) {
globalShortcut.register(spec, () => {
this.onGlobalHotkey()
})
}
}) })
const configData = loadConfig() const configData = loadConfig()
if (process.platform === 'linux') { if (process.platform === 'linux' && ((configData.appearance || {}).opacity || 1) !== 1) {
app.commandLine.appendSwitch('no-sandbox') app.commandLine.appendSwitch('enable-transparent-visuals')
if (((configData.appearance || {}).opacity || 1) !== 1) { app.disableHardwareAcceleration()
app.commandLine.appendSwitch('enable-transparent-visuals')
app.disableHardwareAcceleration()
}
} }
app.commandLine.appendSwitch('disable-http-cache') app.commandLine.appendSwitch('disable-http-cache')
app.commandLine.appendSwitch('lang', 'EN') app.commandLine.appendSwitch('lang', 'EN')
app.allowRendererProcessReuse = false
for (const flag of configData.flags || [['force_discrete_gpu', '0']]) { for (const flag of configData.flags || [['force_discrete_gpu', '0']]) {
app.commandLine.appendSwitch(flag[0], flag[1]) app.commandLine.appendSwitch(flag[0], flag[1])
} }
} }
init (): void { init () {
electron.screen.on('display-metrics-changed', () => this.broadcast('host:display-metrics-changed')) electron.screen.on('display-metrics-changed', () => this.broadcast('host:display-metrics-changed'))
} }
@@ -54,9 +40,6 @@ export class Application {
this.enableTray() this.enableTray()
} }
}) })
window.closed$.subscribe(() => {
this.windows = this.windows.filter(x => x !== window)
})
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
this.setupMenu() this.setupMenu()
} }
@@ -64,38 +47,20 @@ export class Application {
return window return window
} }
onGlobalHotkey (): void { broadcast (event, ...args) {
if (this.windows.some(x => x.isFocused())) {
for (let window of this.windows) {
window.hide()
}
} else {
for (let window of this.windows) {
window.present()
}
}
}
presentAllWindows (): void {
for (let window of this.windows) { for (let window of this.windows) {
window.present()
}
}
broadcast (event: string, ...args): void {
for (const window of this.windows) {
window.send(event, ...args) window.send(event, ...args)
} }
} }
async send (event: string, ...args): Promise<void> { async send (event, ...args) {
if (!this.hasWindows()) { if (!this.hasWindows()) {
await this.newWindow() await this.newWindow()
} }
this.windows.filter(w => !w.isDestroyed())[0].send(event, ...args) this.windows.filter(w => !w.isDestroyed())[0].send(event, ...args)
} }
enableTray (): void { enableTray () {
if (this.tray) { if (this.tray) {
return return
} }
@@ -106,7 +71,7 @@ export class Application {
this.tray = new Tray(`${app.getAppPath()}/assets/tray.png`) this.tray = new Tray(`${app.getAppPath()}/assets/tray.png`)
} }
this.tray.on('click', () => setTimeout(() => this.focus())) this.tray.on('click', () => setTimeout(() => this.focus()));
const contextMenu = Menu.buildFromTemplate([{ const contextMenu = Menu.buildFromTemplate([{
label: 'Show', label: 'Show',
@@ -120,28 +85,23 @@ export class Application {
this.tray.setToolTip(`Terminus ${app.getVersion()}`) this.tray.setToolTip(`Terminus ${app.getVersion()}`)
} }
disableTray (): void { disableTray () {
if (this.tray) { if (this.tray) {
this.tray.destroy() this.tray.destroy()
this.tray = null this.tray = null
} }
} }
hasWindows (): boolean { hasWindows () {
return !!this.windows.length return !!this.windows.length
} }
focus (): void { focus () {
for (let window of this.windows) { for (let window of this.windows) {
window.show() window.show()
} }
} }
handleSecondInstance (argv: string[], cwd: string): void {
this.presentAllWindows()
this.windows[this.windows.length - 1].handleSecondInstance(argv, cwd)
}
private setupMenu () { private setupMenu () {
let template: Electron.MenuItemConstructorOptions[] = [ let template: Electron.MenuItemConstructorOptions[] = [
{ {
@@ -222,7 +182,7 @@ export class Application {
}, },
}, },
], ],
}, }
] ]
Menu.setApplicationMenu(Menu.buildFromTemplate(template)) Menu.setApplicationMenu(Menu.buildFromTemplate(template))

View File

@@ -1,6 +1,6 @@
import { app } from 'electron' import { app } from 'electron'
export function parseArgs (argv: string[], cwd: string): any { export function parseArgs (argv, cwd) {
if (argv[0].includes('node')) { if (argv[0].includes('node')) {
argv = argv.slice(1) argv = argv.slice(1)
} }
@@ -20,25 +20,25 @@ export function parseArgs (argv: string[], cwd: string): any {
return yargs.option('escape', { return yargs.option('escape', {
alias: 'e', alias: 'e',
type: 'boolean', type: 'boolean',
describe: 'Perform shell escaping', describe: 'Perform shell escaping'
}).positional('text', { }).positional('text', {
type: 'string', type: 'string'
}) })
}) })
.version('version', '', app.getVersion()) .version('version', '', app.getVersion())
.option('debug', { .option('debug', {
alias: 'd', alias: 'd',
describe: 'Show DevTools on start', describe: 'Show DevTools on start',
type: 'boolean', type: 'boolean'
}) })
.option('hidden', { .option('hidden', {
describe: 'Start minimized', describe: 'Start minimized',
type: 'boolean', type: 'boolean'
}) })
.option('version', { .option('version', {
alias: 'v', alias: 'v',
describe: 'Show version and exit', describe: 'Show version and exit',
type: 'boolean', type: 'boolean'
}) })
.help('help') .help('help')
.parse(argv.slice(1)) .parse(argv.slice(1))

View File

@@ -1,10 +1,9 @@
import './portable'
import './sentry' import './sentry'
import './lru' import './lru'
import { app, ipcMain, Menu } from 'electron' import { app, ipcMain, Menu } from 'electron'
import { parseArgs } from './cli' import { parseArgs } from './cli'
import { Application } from './app' import { Application } from './app'
import electronDebug = require('electron-debug') import * as electronDebug from 'electron-debug'
if (!process.env.TERMINUS_PLUGINS) { if (!process.env.TERMINUS_PLUGINS) {
process.env.TERMINUS_PLUGINS = '' process.env.TERMINUS_PLUGINS = ''
@@ -34,7 +33,7 @@ process.on('uncaughtException' as any, err => {
}) })
app.on('second-instance', (_event, argv, cwd) => { app.on('second-instance', (_event, argv, cwd) => {
application.handleSecondInstance(argv, cwd) application.send('host:second-instance', parseArgs(argv, cwd), cwd)
}) })
const argv = parseArgs(process.argv, process.cwd()) const argv = parseArgs(process.argv, process.cwd())
@@ -59,8 +58,8 @@ app.on('ready', () => {
label: 'New window', label: 'New window',
click () { click () {
this.app.newWindow() this.app.newWindow()
}, }
}, }
])) ]))
} }
application.init() application.init()

View File

@@ -1,15 +1,13 @@
import * as createLRU from 'lru-cache' let lru = require('lru-cache')({ max: 256, maxAge: 250 })
import * as fs from 'fs'
const lru = createLRU({ max: 256, maxAge: 250 }) let fs = require('fs')
const origLstat = fs.realpathSync.bind(fs) let origLstat = fs.realpathSync.bind(fs)
// NB: The biggest offender of thrashing realpathSync is the node module system // NB: The biggest offender of thrashing realpathSync is the node module system
// itself, which we can't get into via any sane means. // itself, which we can't get into via any sane means.
require('fs').realpathSync = function (p) { require('fs').realpathSync = function (p) {
let r = lru.get(p) let r = lru.get(p)
if (r) { if (r) return r
return r
}
r = origLstat(p) r = origLstat(p)
lru.set(p, r) lru.set(p, r)

View File

@@ -1,24 +0,0 @@
import * as path from 'path'
import * as fs from 'fs'
let appPath: string | null = null
try {
appPath = path.dirname(require('electron').app.getPath('exe'))
} catch {
appPath = path.dirname(require('electron').remote.app.getPath('exe'))
}
if (null != appPath) {
if(fs.existsSync(path.join(appPath, 'terminus-data'))) {
fs.renameSync(path.join(appPath, 'terminus-data'), path.join(appPath, 'data'))
}
const portableData = path.join(appPath, 'data')
if (fs.existsSync(portableData)) {
console.log('reset user data to ' + portableData)
try {
require('electron').app.setPath('userData', portableData)
} catch {
require('electron').remote.app.setPath('userData', portableData)
}
}
}

17
app/lib/sentry.ts Executable file → Normal file
View File

@@ -1,5 +1,4 @@
const { init } = process.type === 'main' ? require('@sentry/electron/dist/main') : require('@sentry/electron/dist/renderer') const { init } = process.type === 'main' ? require('@sentry/electron/dist/main') : require('@sentry/electron/dist/renderer')
import * as isDev from 'electron-is-dev'
const SENTRY_DSN = 'https://4717a0a7ee0b4429bd3a0f06c3d7eec3@sentry.io/181876' const SENTRY_DSN = 'https://4717a0a7ee0b4429bd3a0f06c3d7eec3@sentry.io/181876'
@@ -10,12 +9,10 @@ try {
release = require('electron').remote.app.getVersion() release = require('electron').remote.app.getVersion()
} }
if (!isDev) { init({
init({ dsn: SENTRY_DSN,
dsn: SENTRY_DSN, release,
release, integrations (integrations) {
integrations (integrations) { return integrations.filter(integration => integration.name !== 'Breadcrumbs')
return integrations.filter(integration => integration.name !== 'Breadcrumbs') },
}, })
})
}

View File

@@ -1,20 +1,18 @@
import * as glasstron from 'glasstron'
if (process.platform === 'win32' || process.platform === 'linux') {
glasstron.init()
}
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
import { debounceTime } from 'rxjs/operators' import { debounceTime } from 'rxjs/operators'
import { BrowserWindow, app, ipcMain, Rectangle, Menu, screen } from 'electron' import { BrowserWindow, app, ipcMain, Rectangle, screen } from 'electron'
import ElectronConfig = require('electron-config') import * as ElectronConfig from 'electron-config'
import * as os from 'os' import * as os from 'os'
import * as path from 'path' import * as path from 'path'
import { parseArgs } from './cli'
import { loadConfig } from './config' import { loadConfig } from './config'
let SetWindowCompositionAttribute: any
let AccentState: any
let DwmEnableBlurBehindWindow: any let DwmEnableBlurBehindWindow: any
if (process.platform === 'win32') { if (process.platform === 'win32') {
SetWindowCompositionAttribute = require('windows-swca').SetWindowCompositionAttribute
AccentState = require('windows-swca').ACCENT_STATE
DwmEnableBlurBehindWindow = require('windows-blurbehind').DwmEnableBlurBehindWindow DwmEnableBlurBehindWindow = require('windows-blurbehind').DwmEnableBlurBehindWindow
} }
@@ -25,20 +23,15 @@ export interface WindowOptions {
export class Window { export class Window {
ready: Promise<void> ready: Promise<void>
private visible = new Subject<boolean>() private visible = new Subject<boolean>()
private closed = new Subject<void>()
private window: BrowserWindow private window: BrowserWindow
private windowConfig: ElectronConfig private windowConfig: ElectronConfig
private windowBounds: Rectangle private windowBounds: Rectangle
private closing = false private closing = false
private lastVibrancy: {enabled: boolean, type?: string} | null = null
private disableVibrancyWhileDragging = false
private configStore: any
get visible$ (): Observable<boolean> { return this.visible } get visible$ (): Observable<boolean> { return this.visible }
get closed$ (): Observable<void> { return this.closed }
constructor (options?: WindowOptions) { constructor (options?: WindowOptions) {
this.configStore = loadConfig() let configData = loadConfig()
options = options || {} options = options || {}
@@ -55,7 +48,6 @@ export class Window {
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
preload: path.join(__dirname, 'sentry.js'), preload: path.join(__dirname, 'sentry.js'),
backgroundThrottling: false,
}, },
frame: false, frame: false,
show: false, show: false,
@@ -64,18 +56,18 @@ export class Window {
if (this.windowBounds) { if (this.windowBounds) {
Object.assign(bwOptions, this.windowBounds) Object.assign(bwOptions, this.windowBounds)
const closestDisplay = screen.getDisplayNearestPoint( { x: this.windowBounds.x, y: this.windowBounds.y } ) const closestDisplay = screen.getDisplayNearestPoint( {x: this.windowBounds.x, y: this.windowBounds.y} )
const [left1, top1, right1, bottom1] = [this.windowBounds.x, this.windowBounds.y, this.windowBounds.x + this.windowBounds.width, this.windowBounds.y + this.windowBounds.height] const [left1, top1, right1, bottom1] = [this.windowBounds.x, this.windowBounds.y, this.windowBounds.x + this.windowBounds.width, this.windowBounds.y + this.windowBounds.height];
const [left2, top2, right2, bottom2] = [closestDisplay.bounds.x, closestDisplay.bounds.y, closestDisplay.bounds.x + closestDisplay.bounds.width, closestDisplay.bounds.y + closestDisplay.bounds.height] const [left2, top2, right2, bottom2] = [closestDisplay.bounds.x, closestDisplay.bounds.y, closestDisplay.bounds.x + closestDisplay.bounds.width, closestDisplay.bounds.y + closestDisplay.bounds.height];
if ((left2 > right1 || right2 < left1 || top2 > bottom1 || bottom2 < top1) && !maximized) { if ((left2 > right1 || right2 < left1 || top2 > bottom1 || bottom2 < top1) && !maximized) {
bwOptions.x = closestDisplay.bounds.width / 2 - bwOptions.width / 2 bwOptions.x = closestDisplay.bounds.width / 2 - bwOptions.width / 2;
bwOptions.y = closestDisplay.bounds.height / 2 - bwOptions.height / 2 bwOptions.y = closestDisplay.bounds.height / 2 - bwOptions.height / 2;
} }
} }
if ((this.configStore.appearance || {}).frame === 'native') { if ((configData.appearance || {}).frame === 'native') {
bwOptions.frame = true bwOptions.frame = true
} else { } else {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
@@ -83,12 +75,15 @@ export class Window {
} }
} }
this.window = new BrowserWindow(bwOptions) if (process.platform === 'linux') {
bwOptions.backgroundColor = '#131d27'
}
this.window = new BrowserWindow(bwOptions)
this.window.once('ready-to-show', () => { this.window.once('ready-to-show', () => {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
this.window.setVibrancy('window') this.window.setVibrancy('window')
} else if (process.platform === 'win32' && (this.configStore.appearance || {}).vibrancy) { } else if (process.platform === 'win32' && (configData.appearance || {}).vibrancy) {
this.setVibrancy(true) this.setVibrancy(true)
} }
@@ -99,13 +94,6 @@ export class Window {
this.window.show() this.window.show()
} }
this.window.focus() this.window.focus()
this.window.moveTop()
}
})
this.window.on('blur', () => {
if (this.configStore.appearance?.dockHideOnBlur) {
this.hide()
} }
}) })
@@ -128,90 +116,41 @@ export class Window {
}) })
} }
setVibrancy (enabled: boolean, type?: string): void { setVibrancy (enabled: boolean, type?: string) {
this.lastVibrancy = { enabled, type }
if (process.platform === 'win32') { if (process.platform === 'win32') {
if (parseFloat(os.release()) >= 10) { if (parseFloat(os.release()) >= 10) {
glasstron.update(this.window, { let attribValue = AccentState.ACCENT_DISABLED
windows: { blurType: enabled ? type === 'fluent' ? 'acrylic' : 'blurbehind' : null }, if (enabled) {
}) if (parseInt(os.release().split('.')[2]) >= 17063 && type === 'fluent') {
attribValue = AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND
} else {
attribValue = AccentState.ACCENT_ENABLE_BLURBEHIND
}
}
SetWindowCompositionAttribute(this.window.getNativeWindowHandle(), attribValue, 0x00000000)
} else { } else {
DwmEnableBlurBehindWindow(this.window, enabled) DwmEnableBlurBehindWindow(this.window, enabled)
} }
} else if (process.platform ==='linux') {
glasstron.update(this.window, {
linux: { requestBlur: enabled },
})
this.window.setBackgroundColor(enabled ? '#00000000' : '#131d27')
} else {
this.window.setVibrancy(enabled ? 'dark' : null as any) // electron issue 20269
} }
} }
show (): void { show () {
this.window.show() this.window.show()
this.window.moveTop()
} }
focus (): void { focus () {
this.window.focus() this.window.focus()
} }
send (event: string, ...args): void { send (event, ...args) {
if (!this.window) { if (!this.window) {
return return
} }
this.window.webContents.send(event, ...args) this.window.webContents.send(event, ...args)
if (event === 'host:config-change') {
this.configStore = args[0]
}
} }
isDestroyed (): boolean { isDestroyed () {
return !this.window || this.window.isDestroyed() return !this.window || this.window.isDestroyed();
}
isFocused (): boolean {
return this.window.isFocused()
}
hide (): void {
if (process.platform === 'darwin') {
// Lose focus
Menu.sendActionToFirstResponder('hide:')
}
this.window.blur()
if (process.platform !== 'darwin') {
this.window.hide()
}
}
present (): void {
if (!this.window.isVisible()) {
// unfocused, invisible
this.window.show()
this.window.focus()
} else {
if (!this.configStore.appearance?.dock || this.configStore.appearance?.dock === 'off') {
// not docked, visible
setTimeout(() => {
this.window.show()
this.window.focus()
})
} else {
if (this.configStore.appearance?.dockAlwaysOnTop) {
// docked, visible, on top
this.window.hide()
} else {
// docked, visible, not on top
this.window.focus()
}
}
}
}
handleSecondInstance (argv: string[], cwd: string): void {
this.send('host:second-instance', parseArgs(argv, cwd), cwd)
} }
private setupWindowManagement () { private setupWindowManagement () {
@@ -263,10 +202,6 @@ export class Window {
} }
}) })
this.window.on('focus', () => {
this.send('host:window-focused')
})
ipcMain.on('window-focus', event => { ipcMain.on('window-focus', event => {
if (!this.window || event.sender !== this.window.webContents) { if (!this.window || event.sender !== this.window.webContents) {
return return
@@ -354,35 +289,10 @@ export class Window {
}) })
this.window.webContents.on('new-window', event => event.preventDefault()) this.window.webContents.on('new-window', event => event.preventDefault())
ipcMain.on('window-set-disable-vibrancy-while-dragging', (_event, value) => {
this.disableVibrancyWhileDragging = value
})
this.window.on('will-move', () => {
if (!this.lastVibrancy?.enabled || !this.disableVibrancyWhileDragging) {
return
}
let timeout: number|null = null
const oldVibrancy = this.lastVibrancy
this.setVibrancy(false)
const onMove = () => {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
this.window.off('move', onMove)
this.setVibrancy(oldVibrancy.enabled, oldVibrancy.type)
}, 500)
}
this.window.on('move', onMove)
})
} }
private destroy () { private destroy () {
this.window = null this.window = null
this.closed.next()
this.visible.complete() this.visible.complete()
this.closed.complete()
} }
} }

View File

@@ -13,42 +13,43 @@
"watch": "webpack --progress --color --watch" "watch": "webpack --progress --color --watch"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "9.1.9", "@angular/animations": "9.0.0-rc.5",
"@angular/common": "9.1.11", "@angular/common": "9.0.0-rc.5",
"@angular/compiler": "9.1.9", "@angular/compiler": "9.0.0-rc.5",
"@angular/core": "9.1.9", "@angular/core": "9.0.0-rc.5",
"@angular/forms": "9.1.11", "@angular/forms": "9.0.0-rc.5",
"@angular/platform-browser": "9.1.9", "@angular/platform-browser": "9.0.0-rc.5",
"@angular/platform-browser-dynamic": "9.1.9", "@angular/platform-browser-dynamic": "9.0.0-rc.5",
"@ng-bootstrap/ng-bootstrap": "^6.1.0", "@ng-bootstrap/ng-bootstrap": "^5.1.4",
"devtron": "1.4.0", "devtron": "1.4.0",
"electron-config": "2.0.0", "electron-config": "2.0.0",
"electron-debug": "^3.0.1", "electron-debug": "^3.0.1",
"electron-is-dev": "1.1.0", "electron-is-dev": "1.1.0",
"electron-updater": "^4.2.0",
"fontmanager-redux": "0.4.0", "fontmanager-redux": "0.4.0",
"glasstron": "sentialx/Glasstron#n-api", "js-yaml": "3.13.1",
"js-yaml": "3.14.0", "keytar": "^5.0.0",
"keytar": "^6.0.1",
"mz": "^2.7.0", "mz": "^2.7.0",
"ngx-toastr": "^12.0.1", "ngx-toastr": "^11.2.1",
"@terminus-term/node-pty": "0.10.0-beta9", "node-pty": "^0.10.0-beta2",
"npm": "6.9.0", "npm": "6.9.0",
"path": "0.12.7", "path": "0.12.7",
"rxjs": "^6.5.5", "rxjs": "^6.5.3",
"rxjs-compat": "^6.6.0", "rxjs-compat": "^6.5.3",
"yargs": "^15.4.1", "yargs": "^15.0.2",
"zone.js": "^0.10.3" "zone.js": "^0.10.2"
}, },
"optionalDependencies": { "optionalDependencies": {
"macos-native-processlist": "^2.0.0", "macos-native-processlist": "^1.0.2",
"serialport": "^9.0.0",
"windows-blurbehind": "^1.0.1", "windows-blurbehind": "^1.0.1",
"windows-native-registry": "^3.0.0", "windows-native-registry": "^1.0.16",
"windows-process-tree": "^0.2.4" "windows-process-tree": "^0.2.4",
"windows-swca": "^2.0.2"
}, },
"devDependencies": { "devDependencies": {
"@angular/localize": "^9.0.0-rc.7",
"@types/mz": "0.0.32", "@types/mz": "0.0.32",
"@types/node": "12.7.12", "@types/node": "12.7.12",
"node-abi": "^2.18.0" "node-abi": "^2.13.0"
} }
} }

View File

@@ -1,13 +1,14 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { NgModule, Compiler, Inject, Injector, ɵcreateInjector as createInjector } from '@angular/core'
import { NgModule } from '@angular/core' import '@angular/localize/init'
import { CommonModule } from '@angular/common'
import { BrowserModule } from '@angular/platform-browser' import { BrowserModule } from '@angular/platform-browser'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { ToastrModule } from 'ngx-toastr' import { ToastrModule } from 'ngx-toastr'
export function getRootModule (plugins: any[]) { @NgModule({
const imports = [ imports: [
BrowserModule, BrowserModule,
...plugins, CommonModule,
NgbModule, NgbModule,
ToastrModule.forRoot({ ToastrModule.forRoot({
positionClass: 'toast-bottom-center', positionClass: 'toast-bottom-center',
@@ -15,7 +16,33 @@ export function getRootModule (plugins: any[]) {
preventDuplicates: true, preventDuplicates: true,
extendedTimeOut: 5000, extendedTimeOut: 5000,
}), }),
] ],
})
export class RootModule {
constructor (
private compiler: Compiler,
private injector: Injector,
@Inject('plugins') private plugins: any[],
) { }
async ngDoBootstrap (app) {
console.log('bootstrap', app)
for (let plugin of this.plugins) {
console.log(plugin)
// try {
const injector = createInjector(plugin, this.injector)
console.log(injector)
const module = await this.compiler.compileModuleAsync(plugin)
console.log(module)
// } catch (e) {
// console.error('Failed loading', plugin, e)
// }
}
}
}
export function setupRootModule (plugins: any[]) {
const bootstrap = [ const bootstrap = [
...plugins.filter(x => x.bootstrap).map(x => x.bootstrap), ...plugins.filter(x => x.bootstrap).map(x => x.bootstrap),
] ]
@@ -23,11 +50,4 @@ export function getRootModule (plugins: any[]) {
if (bootstrap.length === 0) { if (bootstrap.length === 0) {
throw new Error('Did not find any bootstrap components. Are there any plugins installed?') throw new Error('Did not find any bootstrap components. Are there any plugins installed?')
} }
@NgModule({
imports,
bootstrap,
}) class RootModule { } // eslint-disable-line @typescript-eslint/no-extraneous-class
return RootModule
} }

View File

@@ -1,9 +1,77 @@
import '../lib/lru' import '../lib/lru'
import 'core-js/proposals/reflect-metadata'
import 'source-sans-pro/source-sans-pro.css' import 'source-sans-pro/source-sans-pro.css'
import 'source-code-pro/source-code-pro.css' import 'source-code-pro/source-code-pro.css'
import '@fortawesome/fontawesome-free/css/solid.css' import '@fortawesome/fontawesome-free/css/solid.css'
import '@fortawesome/fontawesome-free/css/brands.css' import '@fortawesome/fontawesome-free/css/brands.css'
import '@fortawesome/fontawesome-free/css/regular.css'
import '@fortawesome/fontawesome-free/css/fontawesome.css' import '@fortawesome/fontawesome-free/css/fontawesome.css'
import 'ngx-toastr/toastr.css' import 'ngx-toastr/toastr.css'
import './preload.scss' import './preload.scss'
import * as path from 'path'
const nodeModule = require('module') // eslint-disable-line @typescript-eslint/no-var-requires
const nodeRequire = (global as any).require
const builtinModules = [
'@angular/animations',
'@angular/common',
'@angular/compiler',
'@angular/core',
'@angular/forms',
'@angular/localize',
'@angular/platform-browser',
'@angular/platform-browser-dynamic',
'@ng-bootstrap/ng-bootstrap',
'ngx-toastr',
'rxjs',
'rxjs/operators',
'rxjs/internal/observable/fromEvent',
'rxjs/internal/observable/merge',
'rxjs-compat/Subject',
'zone.js/dist/zone.js',
'terminus-core',
// 'terminus-settings',
// 'terminus-terminal',
]
const cachedBuiltinModules = {}
if (process.env.TERMINUS_DEV) {
console.info(path.dirname(require('electron').remote.app.getAppPath()))
nodeModule.globalPaths.unshift(path.dirname(require('electron').remote.app.getAppPath()))
nodeModule.globalPaths.unshift(path.join(require('electron').remote.app.getAppPath(), 'node_modules'))
}
const originalRequire = (global as any).require
;(global as any).require = function (query: string) {
if (cachedBuiltinModules[query]) {
return cachedBuiltinModules[query]
}
return originalRequire.apply(this, arguments)
}
const originalModuleRequire = nodeModule.prototype.require
nodeModule.prototype.require = function (query: string) {
if (cachedBuiltinModules[query]) {
return cachedBuiltinModules[query]
}
return originalModuleRequire.call(this, query)
}
global['require'].resolve = originalRequire.resolve
nodeModule.prototype.require.resolve = originalModuleRequire.resolve
builtinModules.forEach(m => {
const label = 'Caching ' + m
console.time(label)
try {
console.log(m + '/__ivy_ngcc__/fesm5/' + m.split('/')[1] + '.js')
cachedBuiltinModules[m] = nodeRequire(m + '/__ivy_ngcc__/fesm5/' + m.split('/')[1] + '.js')
console.log('loaded ivy')
} catch (e) {
console.error(e)
cachedBuiltinModules[m] = nodeRequire(m)
}
console.timeEnd(label)
})

View File

@@ -1,17 +1,12 @@
import 'zone.js'
import 'core-js/proposals/reflect-metadata'
import 'rxjs'
import * as isDev from 'electron-is-dev' import * as isDev from 'electron-is-dev'
import './global.scss' import './global.scss'
import './toastr.scss' import './toastr.scss'
import { enableProdMode, NgModuleRef, ApplicationRef } from '@angular/core' import { enableProdMode, NgModuleRef } from '@angular/core'
import { enableDebugTools } from '@angular/platform-browser'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { getRootModule } from './app.module' import { setupRootModule, RootModule } from './app.module'
import { findPlugins, loadPlugins, PluginInfo } from './plugins' import { findPlugins, loadPlugins, PluginInfo } from './plugins'
// Always land on the start view // Always land on the start view
@@ -33,19 +28,14 @@ async function bootstrap (plugins: PluginInfo[], safeMode = false): Promise<NgMo
if (safeMode) { if (safeMode) {
plugins = plugins.filter(x => x.isBuiltin) plugins = plugins.filter(x => x.isBuiltin)
} }
const pluginsModules = await loadPlugins(plugins, (current, total) => { const pluginModules = await loadPlugins(plugins, (current, total) => {
(document.querySelector('.progress .bar') as HTMLElement).style.width = `${100 * current / total}%` // eslint-disable-line (document.querySelector('.progress .bar') as HTMLElement).style.width = `${100 * current / total}%` // eslint-disable-line
}) })
const module = getRootModule(pluginsModules) setupRootModule(pluginModules)
window['rootModule'] = module window['rootModule'] = RootModule
return platformBrowserDynamic().bootstrapModule(module).then(moduleRef => { return platformBrowserDynamic([
if (isDev) { { provide: 'plugins', useValue: pluginModules },
const applicationRef = moduleRef.injector.get(ApplicationRef) ]).bootstrapModule(RootModule)
const componentRef = applicationRef.components[0]
enableDebugTools(componentRef)
}
return moduleRef
})
} }
findPlugins().then(async plugins => { findPlugins().then(async plugins => {

View File

@@ -14,14 +14,12 @@ function normalizePath (path: string): string {
global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x))) global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x)))
if (process.env.TERMINUS_DEV) {
nodeModule.globalPaths.unshift(path.dirname(require('electron').remote.app.getAppPath()))
}
const builtinPluginsPath = process.env.TERMINUS_DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins') const builtinPluginsPath = process.env.TERMINUS_DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
const userPluginsPath = path.join( const userPluginsPath = path.join(
require('electron').remote.app.getPath('userData'), require('electron').remote.app.getPath('appData'),
'terminus',
'plugins', 'plugins',
) )
@@ -51,49 +49,6 @@ export interface PluginInfo {
info?: any info?: any
} }
const builtinModules = [
'@angular/animations',
'@angular/common',
'@angular/compiler',
'@angular/core',
'@angular/forms',
'@angular/platform-browser',
'@angular/platform-browser-dynamic',
'@ng-bootstrap/ng-bootstrap',
'ngx-toastr',
'rxjs',
'rxjs/operators',
'rxjs-compat/Subject',
'terminus-core',
'terminus-settings',
'terminus-terminal',
'zone.js/dist/zone.js',
]
const cachedBuiltinModules = {}
builtinModules.forEach(m => {
const label = 'Caching ' + m
console.time(label)
cachedBuiltinModules[m] = nodeRequire(m)
console.timeEnd(label)
})
const originalRequire = (global as any).require
;(global as any).require = function (query: string) {
if (cachedBuiltinModules[query]) {
return cachedBuiltinModules[query]
}
return originalRequire.apply(this, arguments)
}
const originalModuleRequire = nodeModule.prototype.require
nodeModule.prototype.require = function (query: string) {
if (cachedBuiltinModules[query]) {
return cachedBuiltinModules[query]
}
return originalModuleRequire.call(this, query)
}
export async function findPlugins (): Promise<PluginInfo[]> { export async function findPlugins (): Promise<PluginInfo[]> {
const paths = nodeModule.globalPaths const paths = nodeModule.globalPaths
let foundPlugins: PluginInfo[] = [] let foundPlugins: PluginInfo[] = []
@@ -166,6 +121,7 @@ export async function loadPlugins (foundPlugins: PluginInfo[], progress: Progres
progress(0, 1) progress(0, 1)
let index = 0 let index = 0
for (const foundPlugin of foundPlugins) { for (const foundPlugin of foundPlugins) {
if (foundPlugin.name !== 'core') continue
console.info(`Loading ${foundPlugin.name}: ${nodeRequire.resolve(foundPlugin.path)}`) console.info(`Loading ${foundPlugin.name}: ${nodeRequire.resolve(foundPlugin.path)}`)
progress(index, foundPlugins.length) progress(index, foundPlugins.length)
try { try {

View File

@@ -1,6 +0,0 @@
import { Component } from '@angular/core'
@Component({
template: '<app-root></app-root>',
})
export class RootComponent { } // eslint-disable-line @typescript-eslint/no-extraneous-class

View File

@@ -1,7 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": "./src", "baseUrl": "./src",
"module": "commonjs", "module": "es2015",
"moduleResolution": "node",
"target": "es2015", "target": "es2015",
"declaration": false, "declaration": false,
"noImplicitAny": false, "noImplicitAny": false,
@@ -19,7 +20,10 @@
"es2015.iterable", "es2015.iterable",
"es2017", "es2017",
"es7" "es7"
] ],
"paths": {
"*": ["../../app/node_modules/*"]
}
}, },
"compileOnSave": false, "compileOnSave": false,
"exclude": [ "exclude": [
@@ -28,5 +32,9 @@
"*/node_modules", "*/node_modules",
"terminus*", "terminus*",
"platforms" "platforms"
] ],
"angularCompilerOptions": {
"enableIvy": true,
"disableTypeScriptVersionCheck": true
}
} }

View File

@@ -1,5 +1,6 @@
const path = require('path') const path = require('path')
const webpack = require('webpack') const webpack = require('webpack')
const { AngularCompilerPlugin } = require('@ngtools/webpack')
module.exports = { module.exports = {
name: 'terminus', name: 'terminus',
@@ -28,13 +29,8 @@ module.exports = {
module: { module: {
rules: [ rules: [
{ {
test: /\.ts$/, test: /(?:\.ngfactory\.js|\.ngfactory|\.ngstyle\.js|\.ts)$/,
use: { loader: '@ngtools/webpack',
loader: 'awesome-typescript-loader',
options: {
configFileName: path.resolve(__dirname, 'tsconfig.json'),
},
},
}, },
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{ test: /\.css$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.css$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
@@ -82,5 +78,10 @@ module.exports = {
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.type': '"renderer"' 'process.type': '"renderer"'
}), }),
new AngularCompilerPlugin({
tsConfigPath: path.resolve(__dirname, 'tsconfig.json'),
entryModule: 'src/index#default',
sourceMap: true,
}),
], ],
} }

View File

@@ -37,7 +37,6 @@ module.exports = {
'electron-config': 'commonjs electron-config', 'electron-config': 'commonjs electron-config',
'electron-vibrancy': 'commonjs electron-vibrancy', 'electron-vibrancy': 'commonjs electron-vibrancy',
fs: 'commonjs fs', fs: 'commonjs fs',
glasstron: 'commonjs glasstron',
mz: 'commonjs mz', mz: 'commonjs mz',
path: 'commonjs path', path: 'commonjs path',
yargs: 'commonjs yargs', yargs: 'commonjs yargs',

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,4 @@
#!/bin/bash #!/bin/bash
cat > '/usr/bin/${executable}' << END
#!/bin/sh
'/opt/${productFilename}/${executable}' --no-sandbox $@
END
chmod +x '/usr/bin/${executable}' # Link to the binary
ln -sf '/opt/${productFilename}/${executable}' '/usr/bin/${executable}'

View File

@@ -1,16 +0,0 @@
const fs = require('fs')
const signHook = require('./afterSignHook')
module.exports = async function (params) {
// notarize the app on Mac OS only.
if (process.platform !== 'darwin' || !process.env.GITHUB_REF || !process.env.GITHUB_REF.startsWith('refs/tags/')) {
return
}
console.log('afterBuild hook triggered')
let pkgName = fs.readdirSync('dist').find(x => x.endsWith('.pkg'))
signHook({
appOutDir: 'dist',
_pathOverride: pkgName,
})
}

View File

@@ -6,14 +6,14 @@ const notarizer = require('electron-notarize')
module.exports = async function (params) { module.exports = async function (params) {
// notarize the app on Mac OS only. // notarize the app on Mac OS only.
if (process.platform !== 'darwin' || !process.env.GITHUB_REF || !process.env.GITHUB_REF.startsWith('refs/tags/')) { if (process.platform !== 'darwin' || process.env.GITHUB_REF !== 'refs/heads/master' || process.env.GITHUB_REF && !process.env.GITHUB_REF.startsWith('refs/tags/')) {
return return
} }
console.log('afterSign hook triggered', params) console.log('afterSign hook triggered', params)
let appId = 'org.terminus' let appId = 'org.terminus'
let appPath = path.join(params.appOutDir, params._pathOverride || `${params.packager.appInfo.productFilename}.app`) let appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`)
if (!fs.existsSync(appPath)) { if (!fs.existsSync(appPath)) {
throw new Error(`Cannot find application at: ${appPath}`) throw new Error(`Cannot find application at: ${appPath}`)
} }

View File

@@ -10,9 +10,5 @@
<true/> <true/>
<key>com.apple.security.cs.disable-library-validation</key> <key>com.apple.security.cs.disable-library-validation</key>
<true/> <true/>
<key>com.apple.security.device.microphone</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@@ -1,71 +0,0 @@
---
appId: org.terminus
productName: Terminus
compression: normal
afterSign: "./build/mac/afterSignHook.js"
afterAllArtifactBuild: "./build/mac/afterBuildHook.js"
files:
- "**/*"
- dist
extraResources:
- builtin-plugins
- extras
publish:
- provider: github
win:
icon: "./build/windows/icon.ico"
artifactName: terminus-${version}-portable.${ext}
rfc3161TimeStampServer: http://sha256timestamp.ws.symantec.com/sha256/timestamp
nsis:
oneClick: false
artifactName: terminus-${version}-setup.${ext}
installerIcon: "./build/windows/icon.ico"
mac:
category: public.app-category.video
icon: "./build/mac/icon.icns"
artifactName: terminus-${version}-macos.${ext}
hardenedRuntime: true
entitlements: "./build/mac/entitlements.plist"
entitlementsInherit: "./build/mac/entitlements.plist"
extendInfo:
NSRequiresAquaSystemAppearance: false
NSCameraUsageDescription: "A subprocess requests access to the device's camera."
NSMicrophoneUsageDescription: "A subprocess requests access to the device's microphone."
NSLocationUsageDescription: "A subprocess requests access to the user's location information."
NSDesktopFolderUsageDescription: "A subprocess requests access to the user's Desktop folder."
NSDocumentsFolderUsageDescription: "A subprocess requests access to the user's Documents folder."
NSDownloadsFolderUsageDescription: "A subprocess requests access to the user's Downloads folder."
NSNetworkVolumesUsageDescription: 'A subprocess requests access to files on a network volume.'
NSRemovableVolumesUsageDescription: 'A subprocess requests access to files on a removable volume.'
pkg:
artifactName: terminus-${version}-macos.pkg
linux:
category: Utilities
icon: "./build/icons"
artifactName: terminus-${version}-linux.${ext}
executableArgs:
- "--no-sandbox"
snap:
plugs:
- default
- system-files
- system-observe
deb:
depends:
- gconf2
- gconf-service
- gnome-keyring
- libnotify4
- libsecret-1-0
- libappindicator1
- libxtst6
- libnss3
afterInstall: build/linux/after-install.tpl
rpm:
depends:
- screen
- gnome-keyring

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,35 +1,39 @@
{ {
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-free": "^5.13.0", "@angular/compiler": "9.0.0-rc.5",
"@sentry/cli": "^1.52.3", "@angular/compiler-cli": "9.0.0-rc.5",
"@sentry/electron": "^1.5.1", "@angular/core": "9.0.0-rc.5",
"@fortawesome/fontawesome-free": "^5.11.2",
"@ngtools/webpack": "9.0.0-rc.5",
"@sentry/cli": "^1.49.0",
"@sentry/electron": "^1.0.0",
"@types/electron-config": "^3.2.2", "@types/electron-config": "^3.2.2",
"@types/electron-debug": "^2.1.0", "@types/electron-debug": "^2.1.0",
"@types/js-yaml": "^3.12.4", "@types/js-yaml": "^3.12.1",
"@types/node": "12.7.12", "@types/node": "12.7.12",
"@types/webpack-env": "^1.15.2", "@types/webpack-env": "1.14.1",
"@typescript-eslint/eslint-plugin": "^2.26.0", "@typescript-eslint/eslint-plugin": "^2.9.0",
"@typescript-eslint/parser": "^3.8.0", "@typescript-eslint/parser": "^2.10.0",
"apply-loader": "2.0.0", "apply-loader": "2.0.0",
"awesome-typescript-loader": "^5.0.0", "awesome-typescript-loader": "^5.0.0",
"core-js": "^3.6.5", "core-js": "^3.4.2",
"cross-env": "7.0.2", "cross-env": "6.0.3",
"css-loader": "3.4.2", "css-loader": "3.4.0",
"electron": "^8.2.5", "electron": "^7.1.3",
"electron-builder": "22.6.1", "electron-builder": "22.1.0",
"electron-download": "^4.1.1", "electron-download": "^4.1.1",
"electron-installer-snap": "^5.0.0", "electron-installer-snap": "^4.1.0",
"electron-notarize": "^1.0.0", "electron-notarize": "^0.1.1",
"electron-rebuild": "^1.10.1", "electron-rebuild": "^1.8.5",
"eslint": "^7.6.0", "eslint": "^6.7.1",
"eslint-plugin-import": "^2.21.1", "eslint-plugin-import": "^2.18.2",
"file-loader": "^5.0.2", "file-loader": "^5.0.2",
"graceful-fs": "^4.2.4", "graceful-fs": "^4.2.2",
"html-loader": "0.5.5", "html-loader": "0.5.5",
"json-loader": "0.5.7", "json-loader": "0.5.7",
"node-abi": "^2.18.0", "node-abi": "^2.12.0",
"node-gyp": "^7.0.0", "node-gyp": "^6.0.1",
"node-sass": "^4.14.1", "node-sass": "^4.13.0",
"npmlog": "4.1.2", "npmlog": "4.1.2",
"npx": "^10.2.0", "npx": "^10.2.0",
"pug": "^2.0.4", "pug": "^2.0.4",
@@ -37,38 +41,104 @@
"pug-lint": "^2.6.0", "pug-lint": "^2.6.0",
"pug-loader": "^2.4.0", "pug-loader": "^2.4.0",
"pug-static-loader": "2.0.0", "pug-static-loader": "2.0.0",
"raw-loader": "4.0.1", "raw-loader": "4.0.0",
"sass-loader": "^8.0.0", "sass-loader": "^8.0.0",
"shelljs": "0.8.4", "shelljs": "0.8.3",
"source-code-pro": "^2.30.2", "source-code-pro": "^2.30.2",
"source-sans-pro": "3.6.0", "source-sans-pro": "3.6.0",
"style-loader": "^1.1.4", "style-loader": "^1.0.1",
"svg-inline-loader": "^0.8.0", "svg-inline-loader": "^0.8.0",
"to-string-loader": "1.1.6", "to-string-loader": "1.1.6",
"tslib": "^2.0.0", "tslib": "^1.10.0",
"typedoc": "^0.18.0", "typedoc": "^0.15.3",
"typescript": "^3.9.3", "typescript": "^3.7.3",
"url-loader": "^3.0.0", "url-loader": "^3.0.0",
"val-loader": "2.1.1", "val-loader": "2.1.0",
"webpack": "^5.0.0-beta.18", "webpack": "4",
"webpack-cli": "^3.3.12", "webpack-cli": "^3.3.10",
"yaml-loader": "0.6.0" "yaml-loader": "0.5.0"
}, },
"resolutions": { "resolutions": {
"*/node-abi": "^2.14.0" "*/node-abi": "^2.8.0"
},
"build": {
"appId": "org.terminus",
"productName": "Terminus",
"compression": "normal",
"afterSign": "./build/mac/afterSignHook.js",
"files": [
"**/*",
"dist"
],
"extraResources": [
"builtin-plugins",
"extras"
],
"win": {
"icon": "./build/windows/icon.ico",
"artifactName": "terminus-${version}-setup.exe",
"rfc3161TimeStampServer": "http://sha256timestamp.ws.symantec.com/sha256/timestamp"
},
"nsis": {
"oneClick": false,
"artifactName": "terminus-${version}-setup.${ext}",
"installerIcon": "./build/windows/icon.ico"
},
"publish": [
{
"provider": "github"
}
],
"portable": {
"artifactName": "terminus-${version}-portable.exe"
},
"mac": {
"category": "public.app-category.video",
"icon": "./build/mac/icon.icns",
"artifactName": "terminus-${version}-macos.${ext}",
"hardenedRuntime": true,
"entitlements": "./build/mac/entitlements.plist",
"entitlementsInherit": "./build/mac/entitlements.plist",
"extendInfo": {
"NSRequiresAquaSystemAppearance": false
}
},
"pkg": {
"artifactName": "terminus-${version}-macos.pkg"
},
"linux": {
"category": "Utilities",
"icon": "./build/icons",
"artifactName": "terminus-${version}-linux.${ext}"
},
"deb": {
"depends": [
"gconf2",
"gconf-service",
"libnotify4",
"libsecret-1-0",
"libappindicator1",
"libxtst6",
"libnss3"
],
"afterInstall": "build/linux/after-install.tpl"
},
"rpm": {
"depends": [
"screen",
"gnome-python2-gnomekeyring"
]
}
}, },
"scripts": { "scripts": {
"build": "npm run build:typings && webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js && webpack --color --config terminus-serial/webpack.config.js", "build": "npm run build:typings && webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js",
"build:typings": "node scripts/build-typings.js", "build:typings": "tsc --project terminus-core/tsconfig.typings.json && tsc --project terminus-settings/tsconfig.typings.json && tsc --project terminus-terminal/tsconfig.typings.json && tsc --project terminus-plugin-manager/tsconfig.typings.json && tsc --project terminus-ssh/tsconfig.typings.json",
"watch": "cross-env TERMINUS_DEV=1 webpack --progress --color --watch", "watch": "cross-env TERMINUS_DEV=1 webpack --progress --color --watch",
"start": "cross-env TERMINUS_DEV=1 electron app --debug", "start": "cross-env TERMINUS_DEV=1 electron app --debug",
"start:prod": "electron app --debug",
"prod": "cross-env TERMINUS_DEV=1 electron app", "prod": "cross-env TERMINUS_DEV=1 electron app",
"docs": "typedoc --out docs/api terminus-core/src && typedoc --out docs/api/terminal --tsconfig terminus-terminal/tsconfig.typings.json terminus-terminal/src && typedoc --out docs/api/settings --tsconfig terminus-settings/tsconfig.typings.json terminus-settings/src", "docs": "typedoc --out docs/api terminus-core/src && typedoc --out docs/api/terminal --tsconfig terminus-terminal/tsconfig.typings.json terminus-terminal/src && typedoc --out docs/api/settings --tsconfig terminus-settings/tsconfig.typings.json terminus-settings/src",
"lint": "eslint --ext ts */src */lib", "lint": "eslint --ext ts */src",
"postinstall": "node ./scripts/install-deps.js" "postinstall": "node ./scripts/install-deps.js"
}, },
"repository": "eugeny/terminus", "repository": "eugeny/terminus"
"author": "Eugene Pankov",
"license": "MIT"
} }

View File

@@ -1,10 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
const builder = require('electron-builder').build const builder = require('electron-builder').build
const vars = require('./vars') const vars = require('./vars')
const fs = require('fs')
const signHook = require('../build/mac/afterSignHook')
const isTag = (process.env.GITHUB_REF || '').startsWith('refs/tags/') const isTag = (process.env.GITHUB_REF || '').startsWith('refs/tags/')
const isCI = !!process.env.GITHUB_REF
builder({ builder({
dir: true, dir: true,
@@ -15,7 +14,4 @@ builder({
}, },
}, },
publish: isTag ? 'always' : 'onTag', publish: isTag ? 'always' : 'onTag',
}).catch(e => { }).catch(() => process.exit(1))
console.error(e)
process.exit(1)
})

View File

@@ -1,9 +0,0 @@
#!/usr/bin/env node
const sh = require('shelljs')
const vars = require('./vars')
const log = require('npmlog')
vars.builtinPlugins.forEach(plugin => {
log.info('typings', plugin)
sh.exec(`npx tsc --project ${plugin}/tsconfig.typings.json`)
})

View File

@@ -7,7 +7,7 @@ const isCI = !!process.env.GITHUB_REF
builder({ builder({
dir: true, dir: true,
win: ['nsis', 'zip'], win: ['nsis', 'portable'],
config: { config: {
extraMetadata: { extraMetadata: {
version: vars.version, version: vars.version,

View File

@@ -21,10 +21,5 @@ exports.builtinPlugins = [
'terminus-community-color-schemes', 'terminus-community-color-schemes',
'terminus-plugin-manager', 'terminus-plugin-manager',
'terminus-ssh', 'terminus-ssh',
'terminus-serial',
]
exports.bundledModules = [
'@angular',
'@ng-bootstrap',
] ]
exports.electronVersion = electronInfo.version exports.electronVersion = electronInfo.version

View File

@@ -1,6 +1,6 @@
{ {
"name": "terminus-community-color-schemes", "name": "terminus-community-color-schemes",
"version": "1.0.104-nightly.0", "version": "1.0.98-nightly.0",
"description": "Community color schemes for Terminus", "description": "Community color schemes for Terminus",
"keywords": [ "keywords": [
"terminus-builtin-plugin" "terminus-builtin-plugin"

View File

@@ -1,14 +0,0 @@
{
"extends": "../tsconfig.json",
"exclude": ["node_modules", "dist", "typings"],
"compilerOptions": {
"baseUrl": "src",
"emitDeclarationOnly": true,
"declaration": true,
"declarationDir": "./typings",
"paths": {
"terminus-*": ["../../terminus-*"],
"*": ["../../app/node_modules/*"]
}
}
}

View File

@@ -4,7 +4,7 @@ module.exports = {
target: 'node', target: 'node',
entry: 'src/index.ts', entry: 'src/index.ts',
context: __dirname, context: __dirname,
devtool: 'cheap-module-source-map', devtool: 'eval-cheap-module-source-map',
output: { output: {
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
filename: 'index.js', filename: 'index.js',

View File

@@ -1,6 +1,6 @@
{ {
"name": "terminus-core", "name": "terminus-core",
"version": "1.0.104-nightly.0", "version": "1.0.98-nightly.0",
"description": "Terminus core", "description": "Terminus core",
"keywords": [ "keywords": [
"terminus-builtin-plugin" "terminus-builtin-plugin"
@@ -30,7 +30,7 @@
"ng2-dnd": "^5.0.2", "ng2-dnd": "^5.0.2",
"ngx-perfect-scrollbar": "^8.0.0", "ngx-perfect-scrollbar": "^8.0.0",
"shell-escape": "^0.2.0", "shell-escape": "^0.2.0",
"uuid": "^8.0.0", "uuid": "^3.3.2",
"winston": "^3.2.1" "winston": "^3.2.1"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -1,13 +1,11 @@
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component' export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
export { TabHeaderComponent } from '../components/tabHeader.component'
export { SplitTabComponent, SplitContainer } from '../components/splitTab.component' export { SplitTabComponent, SplitContainer } from '../components/splitTab.component'
export { TabRecoveryProvider, RecoveredTab, RecoveryToken } from './tabRecovery' export { TabRecoveryProvider, RecoveredTab } from './tabRecovery'
export { ToolbarButtonProvider, ToolbarButton } from './toolbarButtonProvider' export { ToolbarButtonProvider, ToolbarButton } from './toolbarButtonProvider'
export { ConfigProvider } from './configProvider' export { ConfigProvider } from './configProvider'
export { HotkeyProvider, HotkeyDescription } from './hotkeyProvider' export { HotkeyProvider, HotkeyDescription } from './hotkeyProvider'
export { Theme } from './theme' export { Theme } from './theme'
export { TabContextMenuItemProvider } from './tabContextMenuProvider' export { TabContextMenuItemProvider } from './tabContextMenuProvider'
export { SelectorOption } from './selector'
export { AppService } from '../services/app.service' export { AppService } from '../services/app.service'
export { ConfigService } from '../services/config.service' export { ConfigService } from '../services/config.service'
@@ -20,4 +18,3 @@ export { HostAppService, Platform } from '../services/hostApp.service'
export { ShellIntegrationService } from '../services/shellIntegration.service' export { ShellIntegrationService } from '../services/shellIntegration.service'
export { ThemesService } from '../services/themes.service' export { ThemesService } from '../services/themes.service'
export { TabsService } from '../services/tabs.service' export { TabsService } from '../services/tabs.service'
export * from '../utils'

View File

@@ -1,8 +0,0 @@
export interface SelectorOption<T> {
name: string
description?: string
result?: T
icon?: string
freeInputPattern?: string
callback?: (string?) => void
}

View File

@@ -12,12 +12,6 @@ export interface RecoveredTab {
options?: any options?: any
} }
export interface RecoveryToken {
[_: string]: any
type: string
tabColor?: string|null
}
/** /**
* Extend to enable recovery for your custom tab. * Extend to enable recovery for your custom tab.
* This works in conjunction with [[getRecoveryToken()]] * This works in conjunction with [[getRecoveryToken()]]
@@ -40,5 +34,5 @@ export abstract class TabRecoveryProvider {
* @returns [[RecoveredTab]] descriptor containing tab type and component inputs * @returns [[RecoveredTab]] descriptor containing tab type and component inputs
* or `null` if this token is from a different tab type or is not supported * or `null` if this token is from a different tab type or is not supported
*/ */
abstract async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab|null> abstract async recover (recoveryToken: any): Promise<RecoveredTab|null>
} }

View File

@@ -4,18 +4,14 @@ title-bar(
) )
.content( .content(
[class.tabs-on-top]='config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "left"', [class.tabs-on-top]='config.store.appearance.tabsLocation == "top"'
[class.tabs-on-side]='hasVerticalTabs()',
) )
.tab-bar .tab-bar
.inset.background(*ngIf='hostApp.platform == Platform.macOS \ .inset.background(*ngIf='hostApp.platform == Platform.macOS && config.store.appearance.frame == "thin" && config.store.appearance.tabsLocation == "top"')
&& config.store.appearance.frame == "thin" \
&& (config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "left")')
.tabs( .tabs(
*ngIf='config.store.appearance.tabsLocation != "bottom"'
dnd-sortable-container, dnd-sortable-container,
[sortableData]='app.tabs',
) )
//- [sortableData]='app.tabs',
tab-header( tab-header(
*ngFor='let tab of app.tabs; let idx = index', *ngFor='let tab of app.tabs; let idx = index',
dnd-sortable, dnd-sortable,
@@ -28,7 +24,6 @@ title-bar(
[active]='tab == app.activeTab', [active]='tab == app.activeTab',
[hasActivity]='tab.activity$|async', [hasActivity]='tab.activity$|async',
@animateTab, @animateTab,
[@.disabled]='hasVerticalTabs()',
(click)='app.selectTab(tab)', (click)='app.selectTab(tab)',
[class.fully-draggable]='hostApp.platform != Platform.macOS', [class.fully-draggable]='hostApp.platform != Platform.macOS',
[class.drag-region]='hostApp.platform == Platform.macOS && !tabsDragging', [class.drag-region]='hostApp.platform == Platform.macOS && !tabsDragging',
@@ -43,7 +38,7 @@ title-bar(
button.btn.btn-secondary.btn-tab-bar( button.btn.btn-secondary.btn-tab-bar(
[title]='button.title', [title]='button.title',
(click)='button.click && button.click()', (click)='button.click && button.click()',
[fastHtmlBind]='button.icon', [innerHTML]='sanitizeIcon(button.icon)',
ngbDropdownToggle, ngbDropdownToggle,
) )
div(*ngIf='button.submenu', ngbDropdownMenu) div(*ngIf='button.submenu', ngbDropdownMenu)
@@ -54,7 +49,7 @@ title-bar(
) )
.icon-wrapper( .icon-wrapper(
*ngIf='hasIcons(button.submenuItems)', *ngIf='hasIcons(button.submenuItems)',
[fastHtmlBind]='item.icon' [innerHTML]='sanitizeIcon(item.icon)'
) )
div([class.ml-3]='hasIcons(button.submenuItems)') {{item.title}} div([class.ml-3]='hasIcons(button.submenuItems)') {{item.title}}
@@ -69,7 +64,7 @@ title-bar(
button.btn.btn-secondary.btn-tab-bar( button.btn.btn-secondary.btn-tab-bar(
[title]='button.title', [title]='button.title',
(click)='button.click && button.click()', (click)='button.click && button.click()',
[fastHtmlBind]='button.icon', [innerHTML]='sanitizeIcon(button.icon)',
ngbDropdownToggle, ngbDropdownToggle,
) )
div(*ngIf='button.submenu', ngbDropdownMenu) div(*ngIf='button.submenu', ngbDropdownMenu)
@@ -80,7 +75,7 @@ title-bar(
) )
.icon-wrapper( .icon-wrapper(
*ngIf='hasIcons(button.submenuItems)', *ngIf='hasIcons(button.submenuItems)',
[fastHtmlBind]='item.icon' [innerHTML]='sanitizeIcon(item.icon)'
) )
div([class.ml-3]='hasIcons(button.submenuItems)') {{item.title}} div([class.ml-3]='hasIcons(button.submenuItems)') {{item.title}}
@@ -88,12 +83,11 @@ title-bar(
*ngIf='updatesAvailable', *ngIf='updatesAvailable',
title='Update available - Click to install', title='Update available - Click to install',
(click)='updateApp()', (click)='updateApp()',
[fastHtmlBind]='updateIcon' [innerHTML]='sanitizeIcon(updateIcon)'
) )
window-controls.background( window-controls.background(
*ngIf='config.store.appearance.frame == "thin" \ *ngIf='config.store.appearance.frame == "thin" && (hostApp.platform == Platform.Windows || hostApp.platform == Platform.Linux)',
&& (hostApp.platform == Platform.Windows || hostApp.platform == Platform.Linux)',
) )
start-page(*ngIf='ready && app.tabs.length == 0') start-page(*ngIf='ready && app.tabs.length == 0')

View File

@@ -15,18 +15,10 @@
$tabs-height: 38px; $tabs-height: 38px;
$tab-border-radius: 4px; $tab-border-radius: 4px;
$side-tab-width: 200px;
.wrap {
display: flex;
width: 100vw;
height: 100vh;
flex-direction: row;
}
.content { .content {
width: 100vw; height: 100%;
height: 100vh;
flex: auto; flex: auto;
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column-reverse;
@@ -34,50 +26,15 @@ $side-tab-width: 200px;
&.tabs-on-top { &.tabs-on-top {
flex-direction: column; flex-direction: column;
} }
&.tabs-on-side {
flex-direction: row-reverse;
&.tabs-on-top {
flex-direction: row;
}
}
} }
.content.tabs-on-side > .tab-bar {
height: 100%;
width: $side-tab-width;
overflow-y: auto;
overflow-x: hidden;
flex-direction: column;
background: rgba(0, 0, 0, 0.25);
.tabs {
width: $side-tab-width;
flex: none;
flex-direction: column;
tab-header {
flex: 0 0 $tabs-height;
}
}
.drag-space {
flex: auto;
}
}
.tab-bar { .tab-bar {
flex: none; flex: none;
height: $tabs-height; height: $tabs-height;
display: flex; display: flex;
width: 100%;
.btn-tab-bar { .btn-tab-bar {
line-height: $tabs-height + 2px; line-height: $tabs-height + 2px;
height: $tabs-height;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@@ -107,7 +64,6 @@ $side-tab-width: 200px;
&>.drag-space { &>.drag-space {
min-width: 1px; min-width: 1px;
flex: 1 0 1%; flex: 1 0 1%;
margin-top: 2px; // for window resizing
-webkit-app-region: drag; -webkit-app-region: drag;
&.persistent { &.persistent {
@@ -117,10 +73,7 @@ $side-tab-width: 200px;
& > .inset { & > .inset {
width: 85px; width: 85px;
height: $tabs-height;
flex: none; flex: none;
opacity: 0;
-webkit-app-region: drag;
} }
window-controls { window-controls {

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component, Inject, Input, HostListener, HostBinding } from '@angular/core' import { Component, Inject, Input, HostListener, HostBinding } from '@angular/core'
import { trigger, style, animate, transition, state } from '@angular/animations' import { trigger, style, animate, transition, state } from '@angular/animations'
import { DomSanitizer } from '@angular/platform-browser'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { ElectronService } from '../services/electron.service' import { ElectronService } from '../services/electron.service'
@@ -20,8 +20,8 @@ import { AppService, ToolbarButton, ToolbarButtonProvider } from '../api'
/** @hidden */ /** @hidden */
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
template: require('./appRoot.component.pug'), templateUrl: './appRoot.component.pug',
styles: [require('./appRoot.component.scss')], styleUrls: ['./appRoot.component.scss'],
animations: [ animations: [
trigger('animateTab', [ trigger('animateTab', [
state('in', style({ state('in', style({
@@ -75,6 +75,7 @@ export class AppRootComponent {
public hostApp: HostAppService, public hostApp: HostAppService,
public config: ConfigService, public config: ConfigService,
public app: AppService, public app: AppService,
private domSanitizer: DomSanitizer,
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[], @Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
log: LogService, log: LogService,
ngbModal: NgbModal, ngbModal: NgbModal,
@@ -108,15 +109,6 @@ export class AppRootComponent {
if (hotkey === 'previous-tab') { if (hotkey === 'previous-tab') {
this.app.previousTab() this.app.previousTab()
} }
if (hotkey === 'move-tab-left') {
this.app.moveSelectedTabLeft()
}
if (hotkey === 'move-tab-right') {
this.app.moveSelectedTabRight()
}
if (hotkey === 'reopen-tab') {
this.app.reopenLastTab()
}
} }
if (hotkey === 'toggle-fullscreen') { if (hotkey === 'toggle-fullscreen') {
this.hostApp.toggleFullscreen() this.hostApp.toggleFullscreen()
@@ -128,8 +120,17 @@ export class AppRootComponent {
this.docking.dock() this.docking.dock()
}) })
this.hostApp.secondInstance$.subscribe(() => {
this.presentWindow()
})
this.hotkeys.globalHotkey.subscribe(() => {
this.onGlobalHotkey()
})
this.hostApp.windowCloseRequest$.subscribe(async () => { this.hostApp.windowCloseRequest$.subscribe(async () => {
this.app.closeWindow() if (await this.app.closeAllTabs()) {
this.hostApp.closeWindow()
}
}) })
if (window['safeModeReason']) { if (window['safeModeReason']) {
@@ -168,6 +169,40 @@ export class AppRootComponent {
}) })
} }
onGlobalHotkey () {
if (this.hostApp.getWindow().isFocused()) {
this.hideWindow()
} else {
this.presentWindow()
}
}
presentWindow () {
if (!this.hostApp.getWindow().isVisible()) {
// unfocused, invisible
this.hostApp.getWindow().show()
this.hostApp.getWindow().focus()
} else {
if (this.config.store.appearance.dock === 'off') {
// not docked, visible
setTimeout(() => {
this.hostApp.getWindow().focus()
})
} else {
// docked, visible
this.hostApp.getWindow().hide()
}
}
}
hideWindow () {
this.electron.loseFocus()
this.hostApp.getWindow().blur()
if (this.hostApp.platform !== Platform.macOS) {
this.hostApp.getWindow().hide()
}
}
async ngOnInit () { async ngOnInit () {
this.ready = true this.ready = true
@@ -184,10 +219,6 @@ export class AppRootComponent {
return false return false
} }
hasVerticalTabs () {
return this.config.store.appearance.tabsLocation === 'left' || this.config.store.appearance.tabsLocation === 'right'
}
async updateApp () { async updateApp () {
if ((await this.electron.showMessageBox( if ((await this.electron.showMessageBox(
this.hostApp.getWindow(), this.hostApp.getWindow(),
@@ -223,6 +254,10 @@ export class AppRootComponent {
return submenuItems.some(x => !!x.icon) return submenuItems.some(x => !!x.icon)
} }
sanitizeIcon (icon: string): any {
return this.domSanitizer.bypassSecurityTrustHtml(icon || '')
}
private getToolbarButtons (aboveZero: boolean): ToolbarButton[] { private getToolbarButtons (aboveZero: boolean): ToolbarButton[] {
let buttons: ToolbarButton[] = [] let buttons: ToolbarButton[] = []
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => { this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {

View File

@@ -1,6 +1,5 @@
import { Observable, Subject } from 'rxjs' import { Observable, Subject } from 'rxjs'
import { ViewRef } from '@angular/core' import { ViewRef } from '@angular/core'
import { RecoveryToken } from '../api/tabRecovery'
/** /**
* Represents an active "process" inside a tab, * Represents an active "process" inside a tab,
@@ -14,11 +13,6 @@ export interface BaseTabProcess {
* Abstract base class for custom tab components * Abstract base class for custom tab components
*/ */
export abstract class BaseTabComponent { export abstract class BaseTabComponent {
/**
* Parent tab (usually a SplitTabComponent)
*/
parent: BaseTabComponent|null = null
/** /**
* Current tab title * Current tab title
*/ */
@@ -44,7 +38,7 @@ export abstract class BaseTabComponent {
*/ */
color: string|null = null color: string|null = null
hasFocus = false protected hasFocus = false
/** /**
* Ping this if your recovery state has been changed and you want * Ping this if your recovery state has been changed and you want
@@ -68,7 +62,7 @@ export abstract class BaseTabComponent {
get destroyed$ (): Observable<void> { return this.destroyed } get destroyed$ (): Observable<void> { return this.destroyed }
get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint } get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint }
protected constructor () { constructor () {
this.focused$.subscribe(() => { this.focused$.subscribe(() => {
this.hasFocus = true this.hasFocus = true
}) })
@@ -77,7 +71,7 @@ export abstract class BaseTabComponent {
}) })
} }
setTitle (title: string): void { setTitle (title: string) {
this.title = title this.title = title
if (!this.customTitle) { if (!this.customTitle) {
this.titleChange.next(title) this.titleChange.next(title)
@@ -89,7 +83,7 @@ export abstract class BaseTabComponent {
* *
* @param {type} progress: value between 0 and 1, or `null` to remove * @param {type} progress: value between 0 and 1, or `null` to remove
*/ */
setProgress (progress: number|null): void { setProgress (progress: number|null) {
this.progress.next(progress) this.progress.next(progress)
if (progress) { if (progress) {
if (this.progressClearTimeout) { if (this.progressClearTimeout) {
@@ -124,7 +118,7 @@ export abstract class BaseTabComponent {
* @return JSON serializable tab state representation * @return JSON serializable tab state representation
* for your [[TabRecoveryProvider]] to parse * for your [[TabRecoveryProvider]] to parse
*/ */
async getRecoveryToken (): Promise<RecoveryToken|null> { async getRecoveryToken (): Promise<any> {
return null return null
} }
@@ -142,11 +136,11 @@ export abstract class BaseTabComponent {
return true return true
} }
emitFocused (): void { emitFocused () {
this.focused.next() this.focused.next()
} }
emitBlurred (): void { emitBlurred () {
this.blurred.next() this.blurred.next()
} }

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { NgZone, Component, Input, HostBinding, HostListener } from '@angular/core' import { NgZone, Component, Input, HostBinding, HostListener } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'

View File

@@ -1,11 +1,10 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component, Input, ElementRef, ViewChild } from '@angular/core' import { Component, Input, ElementRef, ViewChild } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
/** @hidden */ /** @hidden */
@Component({ @Component({
selector: 'rename-tab-modal', selector: 'rename-tab-modal',
template: require('./renameTabModal.component.pug'), templateUrl: './renameTabModal.component.pug',
}) })
export class RenameTabModalComponent { export class RenameTabModalComponent {
@Input() value: string @Input() value: string

View File

@@ -3,7 +3,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
/** @hidden */ /** @hidden */
@Component({ @Component({
template: require('./safeModeModal.component.pug'), templateUrl: './safeModeModal.component.pug',
}) })
export class SafeModeModalComponent { export class SafeModeModalComponent {
@Input() error: Error @Input() error: Error
@@ -14,7 +14,7 @@ export class SafeModeModalComponent {
this.error = window['safeModeReason'] this.error = window['safeModeReason']
} }
close (): void { close () {
this.modalInstance.dismiss() this.modalInstance.dismiss()
} }
} }

View File

@@ -1,26 +0,0 @@
.modal-body
input.form-control(
type='text',
[(ngModel)]='filter',
autofocus,
[placeholder]='name',
(ngModelChange)='onFilterChange()'
)
.list-group.mt-3(*ngIf='filteredOptions.length')
a.list-group-item.list-group-item-action.d-flex.align-items-center(
#item,
(click)='selectOption(option)',
[class.active]='selectedIndex == i',
*ngFor='let option of filteredOptions; let i = index'
)
i.icon(
class='fa-fw fas fa-{{option.icon}}',
*ngIf='!iconIsSVG(option.icon)'
)
.icon(
[fastHtmlBind]='option.icon',
*ngIf='iconIsSVG(option.icon)'
)
.mr-2.title {{getOptionText(option)}}
.text-muted {{option.description}}

View File

@@ -1,13 +0,0 @@
.list-group {
max-height: 70vh;
overflow: auto;
}
.icon {
width: 1.25rem;
margin-right: 0.25rem;
}
.title {
margin-left: 10px;
}

View File

@@ -1,78 +0,0 @@
import { Component, Input, HostListener, ViewChildren, QueryList, ElementRef } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { SelectorOption } from '../api/selector'
/** @hidden */
@Component({
template: require('./selectorModal.component.pug'),
styles: [require('./selectorModal.component.scss')],
})
export class SelectorModalComponent<T> {
@Input() options: SelectorOption<T>[]
@Input() filteredOptions: SelectorOption<T>[]
@Input() filter = ''
@Input() name: string
@Input() selectedIndex = 0
@ViewChildren('item') itemChildren: QueryList<ElementRef>
constructor (
public modalInstance: NgbActiveModal,
) { }
ngOnInit (): void {
this.onFilterChange()
}
@HostListener('keyup', ['$event']) onKeyUp (event: KeyboardEvent): void {
if (event.key === 'ArrowUp') {
this.selectedIndex--
}
if (event.key === 'ArrowDown') {
this.selectedIndex++
}
if (event.key === 'Enter') {
this.selectOption(this.filteredOptions[this.selectedIndex])
}
if (event.key === 'Escape') {
this.close()
}
this.selectedIndex = (this.selectedIndex + this.filteredOptions.length) % this.filteredOptions.length
Array.from(this.itemChildren)[this.selectedIndex]?.nativeElement.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
})
}
onFilterChange (): void {
const f = this.filter.trim().toLowerCase()
if (!f) {
this.filteredOptions = this.options.filter(x => !x.freeInputPattern)
} else {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
this.filteredOptions = this.options.filter(x => x.freeInputPattern || (x.name + (x.description || '')).toLowerCase().includes(f))
}
this.selectedIndex = Math.max(0, this.selectedIndex)
this.selectedIndex = Math.min(this.filteredOptions.length - 1, this.selectedIndex)
}
getOptionText (option: SelectorOption<T>): string {
if (option.freeInputPattern) {
return option.freeInputPattern.replace('%s', this.filter)
}
return option.name
}
selectOption (option: SelectorOption<T>): void {
option.callback?.(this.filter)
this.modalInstance.close(option.result)
}
close (): void {
this.modalInstance.dismiss()
}
iconIsSVG (icon: string): boolean {
return icon?.startsWith('<')
}
}

View File

@@ -3,24 +3,3 @@
position: relative; position: relative;
flex: auto; flex: auto;
} }
::ng-deep split-tab > .child {
position: absolute;
transition: 0.125s all;
opacity: .75;
&.focused {
opacity: 1;
}
&.minimized {
opacity: .1;
}
&.maximized {
z-index: 2;
box-shadow: rgba(0, 0, 0, 0.25) 0px 0px 30px;
backdrop-filter: blur(10px);
border-radius: 10px;
}
}

View File

@@ -1,7 +1,7 @@
import { Observable, Subject, Subscription } from 'rxjs' import { Observable, Subject, Subscription } from 'rxjs'
import { Component, Injectable, ViewChild, ViewContainerRef, EmbeddedViewRef, AfterViewInit, OnDestroy } from '@angular/core' import { Component, Injectable, ViewChild, ViewContainerRef, EmbeddedViewRef, OnInit, OnDestroy } from '@angular/core'
import { BaseTabComponent, BaseTabProcess } from './baseTab.component' import { BaseTabComponent, BaseTabProcess } from './baseTab.component'
import { TabRecoveryProvider, RecoveredTab, RecoveryToken } from '../api/tabRecovery' import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery'
import { TabsService } from '../services/tabs.service' import { TabsService } from '../services/tabs.service'
import { HotkeysService } from '../services/hotkeys.service' import { HotkeysService } from '../services/hotkeys.service'
import { TabRecoveryService } from '../services/tabRecovery.service' import { TabRecoveryService } from '../services/tabRecovery.service'
@@ -48,7 +48,7 @@ export class SplitContainer {
/** /**
* Remove unnecessarily nested child containers and renormalizes [[ratios]] * Remove unnecessarily nested child containers and renormalizes [[ratios]]
*/ */
normalize (): void { normalize () {
for (let i = 0; i < this.children.length; i++) { for (let i = 0; i < this.children.length; i++) {
const child = this.children[i] const child = this.children[i]
@@ -93,7 +93,7 @@ export class SplitContainer {
return s return s
} }
async serialize (): Promise<RecoveryToken> { async serialize () {
const children: any[] = [] const children: any[] = []
for (const child of this.children) { for (const child of this.children) {
if (child instanceof SplitContainer) { if (child instanceof SplitContainer) {
@@ -138,11 +138,9 @@ export interface SplitSpannerInfo {
(change)='onSpannerAdjusted(spanner)' (change)='onSpannerAdjusted(spanner)'
></split-tab-spanner> ></split-tab-spanner>
`, `,
styles: [require('./splitTab.component.scss')], styleUrls: ['./splitTab.component.scss'],
}) })
export class SplitTabComponent extends BaseTabComponent implements AfterViewInit, OnDestroy { export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
static DIRECTIONS: SplitDirection[] = ['t', 'r', 'b', 'l']
/** @hidden */ /** @hidden */
@ViewChild('vc', { read: ViewContainerRef }) viewContainer: ViewContainerRef @ViewChild('vc', { read: ViewContainerRef }) viewContainer: ViewContainerRef
@@ -157,12 +155,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/** @hidden */ /** @hidden */
_spanners: SplitSpannerInfo[] = [] _spanners: SplitSpannerInfo[] = []
/** @hidden */
_allFocusMode = false
/** @hidden */
private focusedTab: BaseTabComponent private focusedTab: BaseTabComponent
private maximizedTab: BaseTabComponent|null = null
private hotkeysSubscription: Subscription private hotkeysSubscription: Subscription
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map() private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
@@ -170,7 +163,6 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
private tabRemoved = new Subject<BaseTabComponent>() private tabRemoved = new Subject<BaseTabComponent>()
private splitAdjusted = new Subject<SplitSpannerInfo>() private splitAdjusted = new Subject<SplitSpannerInfo>()
private focusChanged = new Subject<BaseTabComponent>() private focusChanged = new Subject<BaseTabComponent>()
private initialized = new Subject<void>()
get tabAdded$ (): Observable<BaseTabComponent> { return this.tabAdded } get tabAdded$ (): Observable<BaseTabComponent> { return this.tabAdded }
get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved } get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
@@ -185,11 +177,6 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
*/ */
get focusChanged$ (): Observable<BaseTabComponent> { return this.focusChanged } get focusChanged$ (): Observable<BaseTabComponent> { return this.focusChanged }
/**
* Fired once tab layout is created and child tabs can be added
*/
get initialized$ (): Observable<void> { return this.initialized }
/** @hidden */ /** @hidden */
constructor ( constructor (
private hotkeys: HotkeysService, private hotkeys: HotkeysService,
@@ -239,13 +226,6 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
case 'pane-nav-down': case 'pane-nav-down':
this.navigate('b') this.navigate('b')
break break
case 'pane-maximize':
if (this.maximizedTab) {
this.maximize(null)
} else if (this.getAllTabs().length > 1) {
this.maximize(this.focusedTab)
}
break
case 'close-pane': case 'close-pane':
this.removeTab(this.focusedTab) this.removeTab(this.focusedTab)
break break
@@ -254,29 +234,26 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
} }
/** @hidden */ /** @hidden */
async ngAfterViewInit (): Promise<void> { async ngOnInit () {
if (this._recoveredState) { if (this._recoveredState) {
await this.recoverContainer(this.root, this._recoveredState) await this.recoverContainer(this.root, this._recoveredState)
this.layout() this.layout()
setTimeout(() => { setImmediate(() => {
if (this.hasFocus) { if (this.hasFocus) {
for (const tab of this.getAllTabs()) { this.getAllTabs().forEach(x => x.emitFocused())
this.focus(tab) this.focusAnyIn(this.root)
}
} }
}, 100) })
} }
this.initialized.next()
this.initialized.complete()
} }
/** @hidden */ /** @hidden */
ngOnDestroy (): void { ngOnDestroy () {
this.hotkeysSubscription.unsubscribe() this.hotkeysSubscription.unsubscribe()
} }
/** @returns Flat list of all sub-tabs */ /** @returns Flat list of all sub-tabs */
getAllTabs (): BaseTabComponent[] { getAllTabs () {
return this.root.getAllTabs() return this.root.getAllTabs()
} }
@@ -284,11 +261,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
return this.focusedTab return this.focusedTab
} }
getMaximizedTab (): BaseTabComponent|null { focus (tab: BaseTabComponent) {
return this.maximizedTab
}
focus (tab: BaseTabComponent): void {
this.focusedTab = tab this.focusedTab = tab
for (const x of this.getAllTabs()) { for (const x of this.getAllTabs()) {
if (x !== tab) { if (x !== tab) {
@@ -299,22 +272,13 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
tab.emitFocused() tab.emitFocused()
this.focusChanged.next(tab) this.focusChanged.next(tab)
} }
if (this.maximizedTab !== tab) {
this.maximizedTab = null
}
this.layout()
}
maximize (tab: BaseTabComponent|null): void {
this.maximizedTab = tab
this.layout() this.layout()
} }
/** /**
* Focuses the first available tab inside the given [[SplitContainer]] * Focuses the first available tab inside the given [[SplitContainer]]
*/ */
focusAnyIn (parent: BaseTabComponent | SplitContainer): void { focusAnyIn (parent: BaseTabComponent | SplitContainer) {
if (!parent) { if (!parent) {
return return
} }
@@ -328,9 +292,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/** /**
* Inserts a new `tab` to the `side` of the `relative` tab * Inserts a new `tab` to the `side` of the `relative` tab
*/ */
async addTab (tab: BaseTabComponent, relative: BaseTabComponent|null, side: SplitDirection): Promise<void> { addTab (tab: BaseTabComponent, relative: BaseTabComponent|null, side: SplitDirection) {
tab.parent = this
let target = (relative ? this.getParentOf(relative) : null) || this.root let target = (relative ? this.getParentOf(relative) : null) || this.root
let insertIndex = relative ? target.children.indexOf(relative) : -1 let insertIndex = relative ? target.children.indexOf(relative) : -1
@@ -360,9 +322,6 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
target.children.splice(insertIndex, 0, tab) target.children.splice(insertIndex, 0, tab)
this.recoveryStateChangedHint.next() this.recoveryStateChangedHint.next()
await this.initialized$.toPromise()
this.attachTabView(tab) this.attachTabView(tab)
setImmediate(() => { setImmediate(() => {
@@ -372,7 +331,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
}) })
} }
removeTab (tab: BaseTabComponent): void { removeTab (tab: BaseTabComponent) {
const parent = this.getParentOf(tab) const parent = this.getParentOf(tab)
if (!parent) { if (!parent) {
return return
@@ -382,11 +341,11 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
parent.children.splice(index, 1) parent.children.splice(index, 1)
this.detachTabView(tab) this.detachTabView(tab)
tab.parent = null
this.layout() this.layout()
this.tabRemoved.next(tab) this.tabRemoved.next(tab)
if (this.root.children.length === 0) { if (this.root.children.length === 0) {
this.destroy() this.destroy()
} else { } else {
@@ -397,7 +356,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/** /**
* Moves focus in the given direction * Moves focus in the given direction
*/ */
navigate (dir: SplitDirection): void { navigate (dir: SplitDirection) {
let rel: BaseTabComponent | SplitContainer = this.focusedTab let rel: BaseTabComponent | SplitContainer = this.focusedTab
let parent = this.getParentOf(rel) let parent = this.getParentOf(rel)
if (!parent) { if (!parent) {
@@ -430,12 +389,11 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
} }
} }
async splitTab (tab: BaseTabComponent, dir: SplitDirection): Promise<BaseTabComponent|null> { async splitTab (tab: BaseTabComponent, dir: SplitDirection) {
const newTab = await this.tabsService.duplicate(tab) const newTab = await this.tabsService.duplicate(tab)
if (newTab) { if (newTab) {
this.addTab(newTab, tab, dir) this.addTab(newTab, tab, dir)
} }
return newTab
} }
/** /**
@@ -473,24 +431,11 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
} }
/** @hidden */ /** @hidden */
onSpannerAdjusted (spanner: SplitSpannerInfo): void { onSpannerAdjusted (spanner: SplitSpannerInfo) {
this.layout() this.layout()
this.splitAdjusted.next(spanner) this.splitAdjusted.next(spanner)
} }
destroy (): void {
super.destroy()
for (const x of this.getAllTabs()) {
x.destroy()
}
}
layout (): void {
this.root.normalize()
this._spanners = []
this.layoutInternal(this.root, 0, 0, 100, 100)
}
private attachTabView (tab: BaseTabComponent) { private attachTabView (tab: BaseTabComponent) {
const ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any> // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion const ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any> // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
this.viewRefs.set(tab, ref) this.viewRefs.set(tab, ref)
@@ -516,6 +461,12 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
} }
} }
private layout () {
this.root.normalize()
this._spanners = []
this.layoutInternal(this.root, 0, 0, 100, 100)
}
private layoutInternal (root: SplitContainer, x: number, y: number, w: number, h: number) { private layoutInternal (root: SplitContainer, x: number, y: number, w: number, h: number) {
const size = root.orientation === 'v' ? h : w const size = root.orientation === 'v' ? h : w
const sizes = root.ratios.map(x => x * size) const sizes = root.ratios.map(x => x * size)
@@ -534,25 +485,14 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
if (child instanceof SplitContainer) { if (child instanceof SplitContainer) {
this.layoutInternal(child, childX, childY, childW, childH) this.layoutInternal(child, childX, childY, childW, childH)
} else { } else {
const viewRef = this.viewRefs.get(child) const element = this.viewRefs.get(child)!.rootNodes[0]
if (viewRef) { element.style.position = 'absolute'
const element = viewRef.rootNodes[0] element.style.left = `${childX}%`
element.classList.toggle('child', true) element.style.top = `${childY}%`
element.classList.toggle('maximized', child === this.maximizedTab) element.style.width = `${childW}%`
element.classList.toggle('minimized', this.maximizedTab && child !== this.maximizedTab) element.style.height = `${childH}%`
element.classList.toggle('focused', this._allFocusMode || child === this.focusedTab)
element.style.left = `${childX}%`
element.style.top = `${childY}%`
element.style.width = `${childW}%`
element.style.height = `${childH}%`
if (child === this.maximizedTab) { element.style.opacity = child === this.focusedTab ? 1 : 0.75
element.style.left = '5%'
element.style.top = '5%'
element.style.width = '90%'
element.style.height = '90%'
}
}
} }
offset += sizes[i] offset += sizes[i]
@@ -580,24 +520,19 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
if (recovered) { if (recovered) {
const tab = this.tabsService.create(recovered.type, recovered.options) const tab = this.tabsService.create(recovered.type, recovered.options)
children.push(tab) children.push(tab)
tab.parent = this
this.attachTabView(tab) this.attachTabView(tab)
} else { } else {
state.ratios.splice(state.children.indexOf(childState), 0) state.ratios.splice(state.children.indexOf(childState), 0)
} }
} }
} }
while (root.ratios.length < root.children.length) {
root.ratios.push(1)
}
root.normalize()
} }
} }
/** @hidden */ /** @hidden */
@Injectable() @Injectable()
export class SplitTabRecoveryProvider extends TabRecoveryProvider { export class SplitTabRecoveryProvider extends TabRecoveryProvider {
async recover (recoveryToken: RecoveryToken): Promise<RecoveredTab|null> { async recover (recoveryToken: any): Promise<RecoveredTab|null> {
if (recoveryToken && recoveryToken.type === 'app:split-tab') { if (recoveryToken && recoveryToken.type === 'app:split-tab') {
return { return {
type: SplitTabComponent, type: SplitTabComponent,

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core' import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
import { SplitContainer } from './splitTab.component' import { SplitContainer } from './splitTab.component'
@@ -6,7 +5,7 @@ import { SplitContainer } from './splitTab.component'
@Component({ @Component({
selector: 'split-tab-spanner', selector: 'split-tab-spanner',
template: '', template: '',
styles: [require('./splitTabSpanner.component.scss')], styleUrls: ['./splitTabSpanner.component.scss'],
}) })
export class SplitTabSpannerComponent { export class SplitTabSpannerComponent {
@Input() container: SplitContainer @Input() container: SplitContainer
@@ -17,17 +16,13 @@ export class SplitTabSpannerComponent {
@HostBinding('class.v') isVertical = true @HostBinding('class.v') isVertical = true
@HostBinding('style.left') cssLeft: string @HostBinding('style.left') cssLeft: string
@HostBinding('style.top') cssTop: string @HostBinding('style.top') cssTop: string
@HostBinding('style.width') cssWidth: string | null @HostBinding('style.width') cssWidth: string
@HostBinding('style.height') cssHeight: string | null @HostBinding('style.height') cssHeight: string
private marginOffset = -5 private marginOffset = -5
constructor (private element: ElementRef) { } constructor (private element: ElementRef) { }
ngAfterViewInit () { ngAfterViewInit () {
this.element.nativeElement.addEventListener('dblclick', () => {
this.reset()
})
this.element.nativeElement.addEventListener('mousedown', (e: MouseEvent) => { this.element.nativeElement.addEventListener('mousedown', (e: MouseEvent) => {
this.isActive = true this.isActive = true
const start = this.isVertical ? e.pageY : e.pageX const start = this.isVertical ? e.pageY : e.pageX
@@ -54,16 +49,14 @@ export class SplitTabSpannerComponent {
diff = Math.max(diff, -this.container.ratios[this.index - 1] + 0.1) diff = Math.max(diff, -this.container.ratios[this.index - 1] + 0.1)
diff = Math.min(diff, this.container.ratios[this.index] - 0.1) diff = Math.min(diff, this.container.ratios[this.index] - 0.1)
if (diff) { this.container.ratios[this.index - 1] += diff
this.container.ratios[this.index - 1] += diff this.container.ratios[this.index] -= diff
this.container.ratios[this.index] -= diff this.change.emit()
this.change.emit()
}
} }
document.addEventListener('mouseup', offHandler, { passive: true }) document.addEventListener('mouseup', offHandler)
this.element.nativeElement.parentElement.addEventListener('mousemove', dragHandler) this.element.nativeElement.parentElement.addEventListener('mousemove', dragHandler)
}, { passive: true }) })
} }
ngOnChanges () { ngOnChanges () {
@@ -86,17 +79,10 @@ export class SplitTabSpannerComponent {
} }
} }
reset () {
const ratio = (this.container.ratios[this.index - 1] + this.container.ratios[this.index]) / 2
this.container.ratios[this.index - 1] = ratio
this.container.ratios[this.index] = ratio
this.change.emit()
}
private setDimensions (x: number, y: number, w: number, h: number) { private setDimensions (x: number, y: number, w: number, h: number) {
this.cssLeft = `${x}%` this.cssLeft = `${x}%`
this.cssTop = `${y}%` this.cssTop = `${y}%`
this.cssWidth = w ? `${w}%` : null this.cssWidth = w ? `${w}%` : 'initial'
this.cssHeight = h ? `${h}%` : null this.cssHeight = h ? `${h}%` : 'initial'
} }
} }

View File

@@ -7,13 +7,13 @@ import { ToolbarButton, ToolbarButtonProvider } from '../api'
/** @hidden */ /** @hidden */
@Component({ @Component({
selector: 'start-page', selector: 'start-page',
template: require('./startPage.component.pug'), templateUrl: './startPage.component.pug',
styles: [require('./startPage.component.scss')], styleUrls: ['./startPage.component.scss'],
}) })
export class StartPageComponent { export class StartPageComponent {
version: string version: string
private constructor ( constructor (
private config: ConfigService, private config: ConfigService,
private domSanitizer: DomSanitizer, private domSanitizer: DomSanitizer,
public homeBase: HomeBaseService, public homeBase: HomeBaseService,

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component, Input, ViewChild, HostBinding, ViewContainerRef, OnChanges } from '@angular/core' import { Component, Input, ViewChild, HostBinding, ViewContainerRef, OnChanges } from '@angular/core'
import { BaseTabComponent } from '../components/baseTab.component' import { BaseTabComponent } from '../components/baseTab.component'
@@ -11,9 +10,9 @@ import { BaseTabComponent } from '../components/baseTab.component'
</perfect-scrollbar--> </perfect-scrollbar-->
<ng-template #placeholder></ng-template> <ng-template #placeholder></ng-template>
`, `,
styles: [ styleUrls: [
require('./tabBody.component.scss'), './tabBody.component.scss',
require('./tabBody.deep.component.css'), './tabBody.deep.component.css',
], ],
}) })
export class TabBodyComponent implements OnChanges { export class TabBodyComponent implements OnChanges {

View File

@@ -1,7 +1,7 @@
.progressbar([style.width]='progress + "%"', *ngIf='progress != null') .progressbar([style.width]='progress + "%"', *ngIf='progress != null')
.index(*ngIf='!config.store.terminal.hideTabIndex', .index(
#handle, #handle,
[style.background-color]='tab.color', [style.background-color]='tab.color',
) {{index + 1}} ) {{index + 1}}
.name([title]='tab.customTitle || tab.title') {{tab.customTitle || tab.title}} .name([title]='tab.customTitle || tab.title') {{tab.customTitle || tab.title}}
button(*ngIf='!config.store.terminal.hideCloseButton',(click)='app.closeTab(tab, true)') &times; button((click)='app.closeTab(tab, true)') &times;

View File

@@ -13,11 +13,6 @@ $tabs-height: 38px;
overflow: hidden; overflow: hidden;
&.vertical {
flex: none;
height: $tabs-height;
}
.index { .index {
flex: none; flex: none;
font-weight: bold; font-weight: bold;
@@ -25,7 +20,7 @@ $tabs-height: 38px;
cursor: -webkit-grab; cursor: -webkit-grab;
margin-left: 10px; margin-left: 10px;
width: 22px; width: 20px;
border-radius: 10px; border-radius: 10px;
text-align: center; text-align: center;
transition: 0.25s all; transition: 0.25s all;

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef } from '@angular/core' import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef } from '@angular/core'
import { SortableComponent } from 'ng2-dnd' import { SortableComponent } from 'ng2-dnd'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@@ -9,7 +8,6 @@ import { HotkeysService } from '../services/hotkeys.service'
import { ElectronService } from '../services/electron.service' import { ElectronService } from '../services/electron.service'
import { AppService } from '../services/app.service' import { AppService } from '../services/app.service'
import { HostAppService, Platform } from '../services/hostApp.service' import { HostAppService, Platform } from '../services/hostApp.service'
import { ConfigService } from '../services/config.service'
/** @hidden */ /** @hidden */
export interface SortableComponentProxy { export interface SortableComponentProxy {
@@ -19,8 +17,8 @@ export interface SortableComponentProxy {
/** @hidden */ /** @hidden */
@Component({ @Component({
selector: 'tab-header', selector: 'tab-header',
template: require('./tabHeader.component.pug'), templateUrl: './tabHeader.component.pug',
styles: [require('./tabHeader.component.scss')], styleUrls: ['./tabHeader.component.scss'],
}) })
export class TabHeaderComponent { export class TabHeaderComponent {
@Input() index: number @Input() index: number
@@ -32,7 +30,6 @@ export class TabHeaderComponent {
private constructor ( private constructor (
public app: AppService, public app: AppService,
public config: ConfigService,
private electron: ElectronService, private electron: ElectronService,
private hostApp: HostAppService, private hostApp: HostAppService,
private ngbModal: NgbModal, private ngbModal: NgbModal,
@@ -51,15 +48,12 @@ export class TabHeaderComponent {
} }
ngOnInit () { ngOnInit () {
this.tab.progress$.subscribe(progress => {
this.progress = progress
})
}
ngAfterViewInit () {
if (this.hostApp.platform === Platform.macOS) { if (this.hostApp.platform === Platform.macOS) {
this.parentDraggable.setDragHandle(this.handle.nativeElement) this.parentDraggable.setDragHandle(this.handle.nativeElement)
} }
this.tab.progress$.subscribe(progress => {
this.progress = progress
})
} }
showRenameTabModal (): void { showRenameTabModal (): void {

View File

@@ -1,9 +1,12 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { HostAppService } from '../services/hostApp.service'
/** @hidden */ /** @hidden */
@Component({ @Component({
selector: 'title-bar', selector: 'title-bar',
template: require('./titleBar.component.pug'), templateUrl: './titleBar.component.pug',
styles: [require('./titleBar.component.scss')], styleUrls: ['./titleBar.component.scss'],
}) })
export class TitleBarComponent { } // eslint-disable-line @typescript-eslint/no-extraneous-class export class TitleBarComponent {
constructor (public hostApp: HostAppService) { }
}

View File

@@ -11,7 +11,7 @@ import { CheckboxComponent } from './checkbox.component'
<label class="custom-control-label"></label> <label class="custom-control-label"></label>
</div> </div>
`, `,
styles: [require('./toggle.component.scss')], styleUrls: ['./toggle.component.scss'],
providers: [ providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: ToggleComponent, multi: true }, { provide: NG_VALUE_ACCESSOR, useExisting: ToggleComponent, multi: true },
], ],

View File

@@ -1,36 +1,19 @@
.container.mt-5.mb-5 .mb-4
.mb-4 .terminus-logo
.terminus-logo h1.terminus-title Terminus
h1.terminus-title Terminus sup α
sup α
.container
.text-center.mb-5 Thank you for downloading Terminus! .text-center.mb-5 Thank you for downloading Terminus!
.form-line .form-line
.header .header
.title Enable analytics .title Enable analytics
.description Help track the number of Terminus installs across the world! .description Help us track the number of Terminus installs across the world!
toggle([(ngModel)]='config.store.enableAnalytics') toggle(
[(ngModel)]='config.store.enableAnalytics',
(ngModelChange)='config.save(); config.requestRestart()',
.form-line )
.header
.title Enable global hotkey (#[strong Ctrl-Space])
.description Toggles the Terminus window visibility
toggle([(ngModel)]='enableGlobalHotkey')
.form-line
.header
.title Enable #[strong SSH] plugin
.description Adds an SSH connection manager UI to Terminus
toggle([(ngModel)]='enableSSH')
.form-line
.header
.title Enable #[strong Serial] plugin
.description Allows attaching Terminus to serial ports
toggle([(ngModel)]='enableSerial')
.text-center.mt-5 .text-center.mt-5
button.btn.btn-primary((click)='closeAndDisable()') Close and never show again button.btn.btn-primary((click)='closeAndDisable()') Close and never show again

View File

@@ -2,7 +2,5 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: auto; margin: auto;
flex: auto; flex: 0 1 500px;
max-height: 100%;
overflow-y: auto;
} }

View File

@@ -1,43 +1,26 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { BaseTabComponent } from './baseTab.component' import { BaseTabComponent } from './baseTab.component'
import { ConfigService } from '../services/config.service' import { ConfigService } from '../services/config.service'
import { HostAppService } from '../services/hostApp.service' import { AppService } from '../services/app.service'
/** @hidden */ /** @hidden */
@Component({ @Component({
selector: 'welcome-page', selector: 'welcome-page',
template: require('./welcomeTab.component.pug'), templateUrl: './welcomeTab.component.pug',
styles: [require('./welcomeTab.component.scss')], styleUrls: ['./welcomeTab.component.scss'],
}) })
export class WelcomeTabComponent extends BaseTabComponent { export class WelcomeTabComponent extends BaseTabComponent {
enableSSH = false
enableSerial = false
enableGlobalHotkey = true
constructor ( constructor (
private hostApp: HostAppService, private app: AppService,
public config: ConfigService, public config: ConfigService,
) { ) {
super() super()
this.setTitle('Welcome') this.setTitle('Welcome')
this.enableSSH = !config.store.pluginBlacklist.includes('ssh')
this.enableSerial = !config.store.pluginBlacklist.includes('serial')
} }
closeAndDisable () { closeAndDisable () {
this.config.store.enableWelcomeTab = false this.config.store.enableWelcomeTab = false
this.config.store.pluginBlacklist = []
if (!this.enableSSH) {
this.config.store.pluginBlacklist.push('ssh')
}
if (!this.enableSerial) {
this.config.store.pluginBlacklist.push('serial')
}
if (!this.enableGlobalHotkey) {
this.config.store.hotkeys['toggle-window'] = []
}
this.config.save() this.config.save()
this.hostApp.getWindow().reload() this.app.closeTab(this)
} }
} }

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { HostAppService } from '../services/hostApp.service' import { HostAppService } from '../services/hostApp.service'
import { AppService } from '../services/app.service' import { AppService } from '../services/app.service'
@@ -6,13 +5,15 @@ import { AppService } from '../services/app.service'
/** @hidden */ /** @hidden */
@Component({ @Component({
selector: 'window-controls', selector: 'window-controls',
template: require('./windowControls.component.pug'), templateUrl: './windowControls.component.pug',
styles: [require('./windowControls.component.scss')], styleUrls: ['./windowControls.component.scss'],
}) })
export class WindowControlsComponent { export class WindowControlsComponent {
private constructor (public hostApp: HostAppService, public app: AppService) { } constructor (public hostApp: HostAppService, public app: AppService) { }
async closeWindow () { async closeWindow () {
this.app.closeWindow() if (await this.app.closeAllTabs()) {
this.hostApp.closeWindow()
}
} }
} }

View File

@@ -7,8 +7,6 @@ hotkeys:
- 'F11' - 'F11'
close-tab: close-tab:
- 'Ctrl-Shift-W' - 'Ctrl-Shift-W'
reopen-tab:
- 'Ctrl-Shift-T'
toggle-last-tab: [] toggle-last-tab: []
rename-tab: rename-tab:
- 'Ctrl-Shift-R' - 'Ctrl-Shift-R'
@@ -18,10 +16,6 @@ hotkeys:
previous-tab: previous-tab:
- 'Ctrl-Shift-Left' - 'Ctrl-Shift-Left'
- 'Ctrl-Shift-Tab' - 'Ctrl-Shift-Tab'
move-tab-left:
- 'Ctrl-Shift-PageUp'
move-tab-right:
- 'Ctrl-Shift-PageDown'
tab-1: tab-1:
- 'Alt-1' - 'Alt-1'
tab-2: tab-2:
@@ -56,7 +50,5 @@ hotkeys:
- 'Ctrl-Alt-Up' - 'Ctrl-Alt-Up'
pane-nav-left: pane-nav-left:
- 'Ctrl-Alt-Left' - 'Ctrl-Alt-Left'
pane-maximize:
- 'Ctrl-Alt-Enter'
close-pane: [] close-pane: []
pluginBlacklist: ['ssh'] pluginBlacklist: ['ssh']

View File

@@ -7,8 +7,6 @@ hotkeys:
- 'Ctrl+⌘+F' - 'Ctrl+⌘+F'
close-tab: close-tab:
- '⌘-W' - '⌘-W'
reopen-tab:
- '⌘-Shift-T'
toggle-last-tab: [] toggle-last-tab: []
rename-tab: rename-tab:
- '⌘-R' - '⌘-R'
@@ -16,10 +14,6 @@ hotkeys:
- 'Ctrl-Tab' - 'Ctrl-Tab'
previous-tab: previous-tab:
- 'Ctrl-Shift-Tab' - 'Ctrl-Shift-Tab'
move-tab-left:
- '⌘-Shift-Left'
move-tab-right:
- '⌘-Shift-Right'
tab-1: tab-1:
- '⌘-1' - '⌘-1'
tab-2: tab-2:
@@ -54,8 +48,6 @@ hotkeys:
- '⌘-⌥-Up' - '⌘-⌥-Up'
pane-nav-left: pane-nav-left:
- '⌘-⌥-Left' - '⌘-⌥-Left'
pane-maximize:
- '⌘-⌥-Enter'
close-pane: close-pane:
- '⌘-Shift-W' - '⌘-Shift-W'
pluginBlacklist: ['ssh'] pluginBlacklist: ['ssh']

View File

@@ -5,11 +5,8 @@ hotkeys:
- 'Ctrl+Space' - 'Ctrl+Space'
toggle-fullscreen: toggle-fullscreen:
- 'F11' - 'F11'
- 'Alt-Enter'
close-tab: close-tab:
- 'Ctrl-Shift-W' - 'Ctrl-Shift-W'
reopen-tab:
- 'Ctrl-Shift-T'
toggle-last-tab: [] toggle-last-tab: []
rename-tab: rename-tab:
- 'Ctrl-Shift-R' - 'Ctrl-Shift-R'
@@ -19,10 +16,6 @@ hotkeys:
previous-tab: previous-tab:
- 'Ctrl-Shift-Left' - 'Ctrl-Shift-Left'
- 'Ctrl-Shift-Tab' - 'Ctrl-Shift-Tab'
move-tab-left:
- 'Ctrl-Shift-PageUp'
move-tab-right:
- 'Ctrl-Shift-PageDown'
tab-1: tab-1:
- 'Alt-1' - 'Alt-1'
tab-2: tab-2:
@@ -57,7 +50,5 @@ hotkeys:
- 'Ctrl-Alt-Up' - 'Ctrl-Alt-Up'
pane-nav-left: pane-nav-left:
- 'Ctrl-Alt-Left' - 'Ctrl-Alt-Left'
pane-maximize:
- 'Ctrl-Alt-Enter'
close-pane: [] close-pane: []
pluginBlacklist: [] pluginBlacklist: []

View File

@@ -2,15 +2,13 @@ appearance:
dock: off dock: off
dockScreen: current dockScreen: current
dockFill: 0.5 dockFill: 0.5
dockHideOnBlur: false
dockAlwaysOnTop: true
tabsLocation: top tabsLocation: top
cycleTabs: true cycleTabs: true
theme: Standard theme: Standard
frame: thin frame: thin
css: '/* * { color: blue !important; } */' css: '/* * { color: blue !important; } */'
opacity: 1.0 opacity: 1.0
vibrancy: true vibrancy: false
vibrancyType: 'blur' vibrancyType: 'blur'
enableAnalytics: true enableAnalytics: true
enableWelcomeTab: true enableWelcomeTab: true

View File

@@ -7,7 +7,7 @@ import { Directive, AfterViewInit, ElementRef } from '@angular/core'
export class AutofocusDirective implements AfterViewInit { export class AutofocusDirective implements AfterViewInit {
constructor (private el: ElementRef) { } constructor (private el: ElementRef) { }
ngAfterViewInit (): void { ngAfterViewInit () {
this.el.nativeElement.blur() this.el.nativeElement.blur()
setTimeout(() => { setTimeout(() => {
this.el.nativeElement.focus() this.el.nativeElement.focus()

View File

@@ -1,14 +0,0 @@
import { Directive, Input, ElementRef, OnChanges } from '@angular/core'
/** @hidden */
@Directive({
selector: '[fastHtmlBind]',
})
export class FastHtmlBindDirective implements OnChanges {
@Input() fastHtmlBind: string
constructor (private el: ElementRef) { }
ngOnChanges (): void {
this.el.nativeElement.innerHTML = this.fastHtmlBind || ''
}
}

View File

@@ -25,10 +25,6 @@ export class AppHotkeyProvider extends HotkeyProvider {
id: 'close-tab', id: 'close-tab',
name: 'Close tab', name: 'Close tab',
}, },
{
id: 'reopen-tab',
name: 'Reopen last tab',
},
{ {
id: 'toggle-last-tab', id: 'toggle-last-tab',
name: 'Toggle last tab', name: 'Toggle last tab',
@@ -41,14 +37,6 @@ export class AppHotkeyProvider extends HotkeyProvider {
id: 'previous-tab', id: 'previous-tab',
name: 'Previous tab', name: 'Previous tab',
}, },
{
id: 'move-tab-left',
name: 'Move tab to the left',
},
{
id: 'move-tab-right',
name: 'Move tab to the right',
},
{ {
id: 'tab-1', id: 'tab-1',
name: 'Tab 1', name: 'Tab 1',
@@ -105,10 +93,6 @@ export class AppHotkeyProvider extends HotkeyProvider {
id: 'split-top', id: 'split-top',
name: 'Split to the top', name: 'Split to the top',
}, },
{
id: 'pane-maximize',
name: 'Maximize the active pane',
},
{ {
id: 'pane-nav-up', id: 'pane-nav-up',
name: 'Focus the pane above', name: 'Focus the pane above',

View File

@@ -1,10 +1,11 @@
import { NgModule, ModuleWithProviders } from '@angular/core' import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser' import { BrowserModule } from '@angular/platform-browser'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar' import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
import { DndModule } from 'ng2-dnd' // import { DndModule } from 'ng2-dnd'
import { AppRootComponent } from './components/appRoot.component' import { AppRootComponent } from './components/appRoot.component'
import { CheckboxComponent } from './components/checkbox.component' import { CheckboxComponent } from './components/checkbox.component'
@@ -16,18 +17,16 @@ import { TitleBarComponent } from './components/titleBar.component'
import { ToggleComponent } from './components/toggle.component' import { ToggleComponent } from './components/toggle.component'
import { WindowControlsComponent } from './components/windowControls.component' import { WindowControlsComponent } from './components/windowControls.component'
import { RenameTabModalComponent } from './components/renameTabModal.component' import { RenameTabModalComponent } from './components/renameTabModal.component'
import { SelectorModalComponent } from './components/selectorModal.component'
import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component' import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component'
import { SplitTabSpannerComponent } from './components/splitTabSpanner.component' import { SplitTabSpannerComponent } from './components/splitTabSpanner.component'
import { WelcomeTabComponent } from './components/welcomeTab.component' import { WelcomeTabComponent } from './components/welcomeTab.component'
import { AutofocusDirective } from './directives/autofocus.directive' import { AutofocusDirective } from './directives/autofocus.directive'
import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
import { HotkeyProvider } from './api/hotkeyProvider' import { HotkeyProvider } from './api/hotkeyProvider'
import { ConfigProvider } from './api/configProvider' import { ConfigProvider } from './api/configProvider'
import { Theme } from './api/theme' import { Theme } from './api/theme'
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider' // import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
import { TabRecoveryProvider } from './api/tabRecovery' import { TabRecoveryProvider } from './api/tabRecovery'
import { AppService } from './services/app.service' import { AppService } from './services/app.service'
@@ -36,7 +35,7 @@ import { ConfigService } from './services/config.service'
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme' import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
import { CoreConfigProvider } from './config' import { CoreConfigProvider } from './config'
import { AppHotkeyProvider } from './hotkeys' import { AppHotkeyProvider } from './hotkeys'
import { TaskCompletionContextMenu, CommonOptionsContextMenu, TabManagementContextMenu } from './tabContextMenu' // import { TaskCompletionContextMenu, CommonOptionsContextMenu, CloseContextMenu } from './tabContextMenu'
import 'perfect-scrollbar/css/perfect-scrollbar.css' import 'perfect-scrollbar/css/perfect-scrollbar.css'
import 'ng2-dnd/bundles/style.css' import 'ng2-dnd/bundles/style.css'
@@ -53,9 +52,9 @@ const PROVIDERS = [
{ provide: Theme, useClass: StandardCompactTheme, multi: true }, { provide: Theme, useClass: StandardCompactTheme, multi: true },
{ provide: Theme, useClass: PaperTheme, multi: true }, { provide: Theme, useClass: PaperTheme, multi: true },
{ provide: ConfigProvider, useClass: CoreConfigProvider, multi: true }, { provide: ConfigProvider, useClass: CoreConfigProvider, multi: true },
{ provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true }, // { provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true },
{ provide: TabContextMenuItemProvider, useClass: TabManagementContextMenu, multi: true }, // { provide: TabContextMenuItemProvider, useClass: CloseContextMenu, multi: true },
{ provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true }, // { provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true },
{ provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true }, { provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true },
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } }, { provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } },
] ]
@@ -68,8 +67,9 @@ const PROVIDERS = [
FormsModule, FormsModule,
NgbModule, NgbModule,
PerfectScrollbarModule, PerfectScrollbarModule,
DndModule.forRoot(), // DndModule,
], ],
providers: PROVIDERS,
declarations: [ declarations: [
AppRootComponent as any, AppRootComponent as any,
CheckboxComponent, CheckboxComponent,
@@ -82,8 +82,6 @@ const PROVIDERS = [
RenameTabModalComponent, RenameTabModalComponent,
SafeModeModalComponent, SafeModeModalComponent,
AutofocusDirective, AutofocusDirective,
FastHtmlBindDirective,
SelectorModalComponent,
SplitTabComponent, SplitTabComponent,
SplitTabSpannerComponent, SplitTabSpannerComponent,
WelcomeTabComponent, WelcomeTabComponent,
@@ -91,7 +89,6 @@ const PROVIDERS = [
entryComponents: [ entryComponents: [
RenameTabModalComponent, RenameTabModalComponent,
SafeModeModalComponent, SafeModeModalComponent,
SelectorModalComponent,
SplitTabComponent, SplitTabComponent,
WelcomeTabComponent, WelcomeTabComponent,
], ],
@@ -101,7 +98,7 @@ const PROVIDERS = [
AutofocusDirective, AutofocusDirective,
], ],
}) })
export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class export class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
constructor (app: AppService, config: ConfigService) { constructor (app: AppService, config: ConfigService) {
app.ready$.subscribe(() => { app.ready$.subscribe(() => {
if (config.store.enableWelcomeTab) { if (config.store.enableWelcomeTab) {
@@ -109,18 +106,17 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
} }
}) })
} }
static forRoot (): ModuleWithProviders {
return {
ngModule: AppModule,
providers: PROVIDERS,
}
}
} }
export default AppModule
export { AppRootComponent as bootstrap } export { AppRootComponent as bootstrap }
export * from './api' export * from './api'
// Deprecations // Deprecations
export { ToolbarButton as IToolbarButton } from './api' export { ToolbarButton as IToolbarButton } from './api'
export { HotkeyDescription as IHotkeyDescription } from './api' export { HotkeyDescription as IHotkeyDescription } from './api'
export function fakeBootstrap () {
return platformBrowserDynamic().bootstrapModule(AppModule)
}

View File

@@ -2,13 +2,9 @@
import { Observable, Subject, AsyncSubject } from 'rxjs' import { Observable, Subject, AsyncSubject } from 'rxjs'
import { takeUntil } from 'rxjs/operators' import { takeUntil } from 'rxjs/operators'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { BaseTabComponent } from '../components/baseTab.component' import { BaseTabComponent } from '../components/baseTab.component'
import { SplitTabComponent } from '../components/splitTab.component' import { SplitTabComponent } from '../components/splitTab.component'
import { SelectorModalComponent } from '../components/selectorModal.component'
import { SelectorOption } from '../api/selector'
import { RecoveryToken } from '../api/tabRecovery'
import { ConfigService } from './config.service' import { ConfigService } from './config.service'
import { HostAppService } from './hostApp.service' import { HostAppService } from './hostApp.service'
@@ -50,7 +46,6 @@ export class AppService {
private lastTabIndex = 0 private lastTabIndex = 0
private _activeTab: BaseTabComponent private _activeTab: BaseTabComponent
private closedTabsStack: RecoveryToken[] = []
private activeTabChange = new Subject<BaseTabComponent>() private activeTabChange = new Subject<BaseTabComponent>()
private tabsChanged = new Subject<void>() private tabsChanged = new Subject<void>()
@@ -69,53 +64,38 @@ export class AppService {
get ready$ (): Observable<void> { return this.ready } get ready$ (): Observable<void> { return this.ready }
/** @hidden */ /** @hidden */
private constructor ( constructor (
private config: ConfigService, private config: ConfigService,
private hostApp: HostAppService, private hostApp: HostAppService,
private tabRecovery: TabRecoveryService, private tabRecovery: TabRecoveryService,
private tabsService: TabsService, private tabsService: TabsService,
private ngbModal: NgbModal,
) { ) {
this.tabsChanged$.subscribe(() => {
this.tabRecovery.saveTabs(this.tabs)
})
setInterval(() => {
this.tabRecovery.saveTabs(this.tabs)
}, 30000)
if (hostApp.getWindow().id === 1) { if (hostApp.getWindow().id === 1) {
if (config.store.terminal.recoverTabs) { if (config.store.terminal.recoverTabs) {
this.tabRecovery.recoverTabs().then(tabs => { this.tabRecovery.recoverTabs().then(tabs => {
for (const tab of tabs) { for (const tab of tabs) {
this.openNewTabRaw(tab.type, tab.options) this.openNewTabRaw(tab.type, tab.options)
} }
this.tabRecovery.enabled = true this.startTabStorage()
}) })
} else { } else {
/** Continue to store the tabs even if the setting is currently off */ /** Continue to store the tabs even if the setting is currently off */
this.tabRecovery.enabled = true this.startTabStorage()
} }
} }
hostApp.windowFocused$.subscribe(() => {
this._activeTab?.emitFocused()
})
this.tabClosed$.subscribe(async tab => {
const token = await tab.getRecoveryToken()
if (token) {
this.closedTabsStack.push(token)
}
})
} }
addTabRaw (tab: BaseTabComponent, index: number|null = null): void { startTabStorage () {
if (index !== null) { this.tabsChanged$.subscribe(() => {
this.tabs.splice(index, 0, tab) this.tabRecovery.saveTabs(this.tabs)
} else { })
this.tabs.push(tab) setInterval(() => {
} this.tabRecovery.saveTabs(this.tabs)
}, 30000)
}
addTabRaw (tab: BaseTabComponent) {
this.tabs.push(tab)
this.selectTab(tab) this.selectTab(tab)
this.tabsChanged.next() this.tabsChanged.next()
this.tabOpened.next(tab) this.tabOpened.next(tab)
@@ -141,18 +121,13 @@ export class AppService {
this.tabsChanged.next() this.tabsChanged.next()
this.tabClosed.next(tab) this.tabClosed.next(tab)
}) })
if (tab instanceof SplitTabComponent) {
tab.tabAdded$.subscribe(() => this.emitTabsChanged())
tab.tabRemoved$.subscribe(() => this.emitTabsChanged())
}
} }
/** /**
* Adds a new tab **without** wrapping it in a SplitTabComponent * Adds a new tab **without** wrapping it in a SplitTabComponent
* @param inputs Properties to be assigned on the new tab component instance * @param inputs Properties to be assigned on the new tab component instance
*/ */
openNewTabRaw (type: TabComponentType, inputs?: Record<string, any>): BaseTabComponent { openNewTabRaw (type: TabComponentType, inputs?: any): BaseTabComponent {
const tab = this.tabsService.create(type, inputs) const tab = this.tabsService.create(type, inputs)
this.addTabRaw(tab) this.addTabRaw(tab)
return tab return tab
@@ -162,7 +137,7 @@ export class AppService {
* Adds a new tab while wrapping it in a SplitTabComponent * Adds a new tab while wrapping it in a SplitTabComponent
* @param inputs Properties to be assigned on the new tab component instance * @param inputs Properties to be assigned on the new tab component instance
*/ */
openNewTab (type: TabComponentType, inputs?: Record<string, any>): BaseTabComponent { openNewTab (type: TabComponentType, inputs?: any): BaseTabComponent {
const splitTab = this.tabsService.create(SplitTabComponent) as SplitTabComponent const splitTab = this.tabsService.create(SplitTabComponent) as SplitTabComponent
const tab = this.tabsService.create(type, inputs) const tab = this.tabsService.create(type, inputs)
splitTab.addTab(tab, null, 'r') splitTab.addTab(tab, null, 'r')
@@ -170,24 +145,7 @@ export class AppService {
return tab return tab
} }
async reopenLastTab (): Promise<BaseTabComponent|null> { selectTab (tab: BaseTabComponent) {
const token = this.closedTabsStack.pop()
if (token) {
const recoveredTab = await this.tabRecovery.recoverTab(token)
if (recoveredTab) {
const tab = this.tabsService.create(recoveredTab.type, recoveredTab.options)
if (this.activeTab) {
this.addTabRaw(tab, this.tabs.indexOf(this.activeTab) + 1)
} else {
this.addTabRaw(tab)
}
return tab
}
}
return null
}
selectTab (tab: BaseTabComponent): void {
if (this._activeTab === tab) { if (this._activeTab === tab) {
this._activeTab.emitFocused() this._activeTab.emitFocused()
return return
@@ -211,26 +169,15 @@ export class AppService {
} }
} }
getParentTab (tab: BaseTabComponent): SplitTabComponent|null {
for (const topLevelTab of this.tabs) {
if (topLevelTab instanceof SplitTabComponent) {
if (topLevelTab.getAllTabs().includes(tab)) {
return topLevelTab
}
}
}
return null
}
/** Switches between the current tab and the previously active one */ /** Switches between the current tab and the previously active one */
toggleLastTab (): void { toggleLastTab () {
if (!this.lastTabIndex || this.lastTabIndex >= this.tabs.length) { if (!this.lastTabIndex || this.lastTabIndex >= this.tabs.length) {
this.lastTabIndex = 0 this.lastTabIndex = 0
} }
this.selectTab(this.tabs[this.lastTabIndex]) this.selectTab(this.tabs[this.lastTabIndex])
} }
nextTab (): void { nextTab () {
if (this.tabs.length > 1) { if (this.tabs.length > 1) {
const tabIndex = this.tabs.indexOf(this._activeTab) const tabIndex = this.tabs.indexOf(this._activeTab)
if (tabIndex < this.tabs.length - 1) { if (tabIndex < this.tabs.length - 1) {
@@ -241,7 +188,7 @@ export class AppService {
} }
} }
previousTab (): void { previousTab () {
if (this.tabs.length > 1) { if (this.tabs.length > 1) {
const tabIndex = this.tabs.indexOf(this._activeTab) const tabIndex = this.tabs.indexOf(this._activeTab)
if (tabIndex > 0) { if (tabIndex > 0) {
@@ -252,37 +199,8 @@ export class AppService {
} }
} }
moveSelectedTabLeft (): void {
if (this.tabs.length > 1) {
const tabIndex = this.tabs.indexOf(this._activeTab)
if (tabIndex > 0) {
this.swapTabs(this._activeTab, this.tabs[tabIndex - 1])
} else if (this.config.store.appearance.cycleTabs) {
this.swapTabs(this._activeTab, this.tabs[this.tabs.length - 1])
}
}
}
moveSelectedTabRight (): void {
if (this.tabs.length > 1) {
const tabIndex = this.tabs.indexOf(this._activeTab)
if (tabIndex < this.tabs.length - 1) {
this.swapTabs(this._activeTab, this.tabs[tabIndex + 1])
} else if (this.config.store.appearance.cycleTabs) {
this.swapTabs(this._activeTab, this.tabs[0])
}
}
}
swapTabs (a: BaseTabComponent, b: BaseTabComponent): void {
const i1 = this.tabs.indexOf(a)
const i2 = this.tabs.indexOf(b)
this.tabs[i1] = b
this.tabs[i2] = a
}
/** @hidden */ /** @hidden */
emitTabsChanged (): void { emitTabsChanged () {
this.tabsChanged.next() this.tabsChanged.next()
} }
@@ -296,12 +214,11 @@ export class AppService {
tab.destroy() tab.destroy()
} }
async duplicateTab (tab: BaseTabComponent): Promise<BaseTabComponent|null> { async duplicateTab (tab: BaseTabComponent) {
const dup = await this.tabsService.duplicate(tab) const dup = await this.tabsService.duplicate(tab)
if (dup) { if (dup) {
this.addTabRaw(dup, this.tabs.indexOf(tab) + 1) this.addTabRaw(dup)
} }
return dup
} }
/** /**
@@ -319,18 +236,8 @@ export class AppService {
return true return true
} }
async closeWindow (): Promise<void> {
this.tabRecovery.enabled = false
await this.tabRecovery.saveTabs(this.tabs)
if (await this.closeAllTabs()) {
this.hostApp.closeWindow()
} else {
this.tabRecovery.enabled = true
}
}
/** @hidden */ /** @hidden */
emitReady (): void { emitReady () {
this.ready.next() this.ready.next()
this.ready.complete() this.ready.complete()
this.hostApp.emitReady() this.hostApp.emitReady()
@@ -351,15 +258,7 @@ export class AppService {
return this.completionObservers.get(tab)!.done$ return this.completionObservers.get(tab)!.done$
} }
stopObservingTabCompletion (tab: BaseTabComponent): void { stopObservingTabCompletion (tab: BaseTabComponent) {
this.completionObservers.delete(tab) this.completionObservers.delete(tab)
} }
showSelector <T> (name: string, options: SelectorOption<T>[]): Promise<T> {
const modal = this.ngbModal.open(SelectorModalComponent)
const instance: SelectorModalComponent<T> = modal.componentInstance
instance.name = name
instance.options = options
return modal.result as Promise<T>
}
} }

View File

@@ -20,7 +20,7 @@ function isNonStructuralObjectMember (v): boolean {
/** @hidden */ /** @hidden */
export class ConfigProxy { export class ConfigProxy {
constructor (real: Record<string, any>, defaults: Record<string, any>) { constructor (real: any, defaults: any) {
for (const key in defaults) { for (const key in defaults) {
if (isStructuralMember(defaults[key])) { if (isStructuralMember(defaults[key])) {
if (!real[key]) { if (!real[key]) {
@@ -71,10 +71,8 @@ export class ConfigProxy {
} }
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function getValue (_key: string): any { } // eslint-disable-line @typescript-eslint/no-empty-function
getValue (_key: string): any { } setValue (_key: string, _value: any) { } // eslint-disable-line @typescript-eslint/no-empty-function
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-empty-function
setValue (_key: string, _value: any) { }
} }
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
@@ -102,7 +100,7 @@ export class ConfigService {
get changed$ (): Observable<void> { return this.changed } get changed$ (): Observable<void> { return this.changed }
/** @hidden */ /** @hidden */
private constructor ( constructor (
electron: ElectronService, electron: ElectronService,
private hostApp: HostAppService, private hostApp: HostAppService,
@Inject(ConfigProvider) configProviders: ConfigProvider[], @Inject(ConfigProvider) configProviders: ConfigProvider[],
@@ -126,23 +124,8 @@ export class ConfigService {
}) })
} }
getDefaults (): Record<string, any> { getDefaults () {
const cleanup = o => { return this.defaults
if (o instanceof Array) {
return o.map(cleanup)
} else if (o instanceof Object) {
const r = {}
for (const k of Object.keys(o)) {
if (k !== '__nonStructural') {
r[k] = cleanup(o[k])
}
}
return r
} else {
return o
}
}
return cleanup(this.defaults)
} }
load (): void { load (): void {
@@ -155,11 +138,9 @@ export class ConfigService {
} }
save (): void { save (): void {
// Scrub undefined values
this._store = JSON.parse(JSON.stringify(this._store))
fs.writeFileSync(this.path, yaml.safeDump(this._store), 'utf8') fs.writeFileSync(this.path, yaml.safeDump(this._store), 'utf8')
this.emitChange() this.emitChange()
this.hostApp.broadcastConfigChange(this.store) this.hostApp.broadcastConfigChange()
} }
/** /**
@@ -192,11 +173,11 @@ export class ConfigService {
enabledServices<T extends object> (services: T[]): T[] { enabledServices<T extends object> (services: T[]): T[] {
if (!this.servicesCache) { if (!this.servicesCache) {
this.servicesCache = {} this.servicesCache = {}
const ngModule = window['rootModule'].ɵinj const ngModule = window['rootModule'].ngInjectorDef
for (const imp of ngModule.imports) { for (const imp of ngModule.imports) {
const module = imp['ngModule'] || imp const module = imp['ngModule'] || imp
if (module.ɵinj?.providers) { if (module.ngInjectorDef && module.ngInjectorDef.providers) {
this.servicesCache[module['pluginName']] = module.ɵinj.providers.map(provider => { this.servicesCache[module['pluginName']] = module.ngInjectorDef.providers.map(provider => {
return provider['useClass'] || provider return provider['useClass'] || provider
}) })
} }

View File

@@ -6,7 +6,7 @@ import { HostAppService, Bounds } from '../services/hostApp.service'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class DockingService { export class DockingService {
/** @hidden */ /** @hidden */
private constructor ( constructor (
private electron: ElectronService, private electron: ElectronService,
private config: ConfigService, private config: ConfigService,
private hostApp: HostAppService, private hostApp: HostAppService,
@@ -15,7 +15,7 @@ export class DockingService {
electron.screen.on('display-metrics-changed', () => this.repositionWindow()) electron.screen.on('display-metrics-changed', () => this.repositionWindow())
} }
dock (): void { dock () {
const dockSide = this.config.store.appearance.dock const dockSide = this.config.store.appearance.dock
if (dockSide === 'off') { if (dockSide === 'off') {
@@ -53,25 +53,22 @@ export class DockingService {
newBounds.y = display.bounds.y newBounds.y = display.bounds.y
} }
const alwaysOnTop = this.config.store.appearance.dockAlwaysOnTop this.hostApp.setAlwaysOnTop(true)
this.hostApp.setAlwaysOnTop(alwaysOnTop)
setImmediate(() => { setImmediate(() => {
this.hostApp.setBounds(newBounds) this.hostApp.setBounds(newBounds)
}) })
} }
getCurrentScreen (): Electron.Display { getCurrentScreen () {
return this.electron.screen.getDisplayNearestPoint(this.electron.screen.getCursorScreenPoint()) return this.electron.screen.getDisplayNearestPoint(this.electron.screen.getCursorScreenPoint())
} }
getScreens (): Electron.Display[] { getScreens () {
const primaryDisplayID = this.electron.screen.getPrimaryDisplay().id const primaryDisplayID = this.electron.screen.getPrimaryDisplay().id
return this.electron.screen.getAllDisplays().sort((a, b) => return this.electron.screen.getAllDisplays().sort((a, b) =>
a.bounds.x === b.bounds.x ? a.bounds.y - b.bounds.y : a.bounds.x - b.bounds.x a.bounds.x === b.bounds.x ? a.bounds.y - b.bounds.y : a.bounds.x - b.bounds.x
).map((display, index) => { ).map((display,index) => {
return { return {
...display,
id: display.id, id: display.id,
name: display.id === primaryDisplayID ? 'Primary Display' : `Display ${index +1}`, name: display.id === primaryDisplayID ? 'Primary Display' : `Display ${index +1}`,
} }

View File

@@ -25,7 +25,7 @@ export class ElectronService {
private electron: any private electron: any
/** @hidden */ /** @hidden */
private constructor () { constructor () {
this.electron = require('electron') this.electron = require('electron')
this.remote = this.electron.remote this.remote = this.electron.remote
this.app = this.remote.app this.app = this.remote.app
@@ -43,6 +43,15 @@ export class ElectronService {
this.MenuItem = this.remote.MenuItem this.MenuItem = this.remote.MenuItem
} }
/**
* Removes OS focus from Terminus' window
*/
loseFocus () {
if (process.platform === 'darwin') {
this.remote.Menu.sendActionToFirstResponder('hide:')
}
}
async showMessageBox ( async showMessageBox (
browserWindow: Electron.BrowserWindow, browserWindow: Electron.BrowserWindow,
options: Electron.MessageBoxOptions options: Electron.MessageBoxOptions

View File

@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core'
import { ElectronService } from './electron.service' import { ElectronService } from './electron.service'
import { ConfigService } from './config.service' import { ConfigService } from './config.service'
import * as mixpanel from 'mixpanel' import * as mixpanel from 'mixpanel'
import { v4 as uuidv4 } from 'uuid' import * as uuidv4 from 'uuid/v4'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class HomeBaseService { export class HomeBaseService {
@@ -11,7 +11,7 @@ export class HomeBaseService {
mixpanel: any mixpanel: any
/** @hidden */ /** @hidden */
private constructor ( constructor (
private electron: ElectronService, private electron: ElectronService,
private config: ConfigService, private config: ConfigService,
) { ) {
@@ -22,29 +22,24 @@ export class HomeBaseService {
} }
} }
openGitHub (): void { openGitHub () {
this.electron.shell.openExternal('https://github.com/eugeny/terminus') this.electron.shell.openExternal('https://github.com/eugeny/terminus')
} }
reportBug (): void { reportBug () {
let body = `Version: ${this.appVersion}\n` let body = `Version: ${this.appVersion}\n`
body += `Platform: ${os.platform()} ${os.release()}\n` body += `Platform: ${os.platform()} ${os.release()}\n`
const label = { const label = {
aix: 'OS: IBM AIX',
android: 'OS: Android',
darwin: 'OS: macOS', darwin: 'OS: macOS',
freebsd: 'OS: FreeBSD', windows: 'OS: Windows',
linux: 'OS: Linux', linux: 'OS: Linux',
openbsd: 'OS: OpenBSD',
sunos: 'OS: Solaris',
win32: 'OS: Windows',
}[os.platform()] }[os.platform()]
const plugins = (window as any).installedPlugins.filter(x => !x.isBuiltin).map(x => x.name) const plugins = (window as any).installedPlugins.filter(x => !x.isBuiltin).map(x => x.name)
body += `Plugins: ${plugins.join(', ') || 'none'}\n\n` body += `Plugins: ${plugins.join(', ') || 'none'}\n\n`
this.electron.shell.openExternal(`https://github.com/eugeny/terminus/issues/new?body=${encodeURIComponent(body)}&labels=${label}`) this.electron.shell.openExternal(`https://github.com/eugeny/terminus/issues/new?body=${encodeURIComponent(body)}&labels=${label}`)
} }
enableAnalytics (): void { enableAnalytics () {
if (!window.localStorage.analyticsUserID) { if (!window.localStorage.analyticsUserID) {
window.localStorage.analyticsUserID = uuidv4() window.localStorage.analyticsUserID = uuidv4()
} }
@@ -56,7 +51,7 @@ export class HomeBaseService {
this.mixpanel.track('launch', this.getAnalyticsProperties()) this.mixpanel.track('launch', this.getAnalyticsProperties())
} }
getAnalyticsProperties (): Record<string, string> { getAnalyticsProperties () {
return { return {
distinct_id: window.localStorage.analyticsUserID, // eslint-disable-line @typescript-eslint/camelcase distinct_id: window.localStorage.analyticsUserID, // eslint-disable-line @typescript-eslint/camelcase
platform: process.platform, platform: process.platform,

View File

@@ -4,7 +4,6 @@ import { Observable, Subject } from 'rxjs'
import { Injectable, NgZone, EventEmitter } from '@angular/core' import { Injectable, NgZone, EventEmitter } from '@angular/core'
import { ElectronService } from './electron.service' import { ElectronService } from './electron.service'
import { Logger, LogService } from './log.service' import { Logger, LogService } from './log.service'
import { isWindowsBuild, WIN_BUILD_FLUENT_BG_MOVE_BUG_FIXED, WIN_BUILD_FLUENT_BG_SUPPORTED } from '../utils'
export enum Platform { export enum Platform {
Linux, macOS, Windows, Linux, macOS, Windows,
@@ -40,7 +39,6 @@ export class HostAppService {
private configChangeBroadcast = new Subject<void>() private configChangeBroadcast = new Subject<void>()
private windowCloseRequest = new Subject<void>() private windowCloseRequest = new Subject<void>()
private windowMoved = new Subject<void>() private windowMoved = new Subject<void>()
private windowFocused = new Subject<void>()
private displayMetricsChanged = new Subject<void>() private displayMetricsChanged = new Subject<void>()
private logger: Logger private logger: Logger
private windowId: number private windowId: number
@@ -87,8 +85,6 @@ export class HostAppService {
get windowMoved$ (): Observable<void> { return this.windowMoved } get windowMoved$ (): Observable<void> { return this.windowMoved }
get windowFocused$ (): Observable<void> { return this.windowFocused }
get displayMetricsChanged$ (): Observable<void> { return this.displayMetricsChanged } get displayMetricsChanged$ (): Observable<void> { return this.displayMetricsChanged }
private constructor ( private constructor (
@@ -132,10 +128,6 @@ export class HostAppService {
this.zone.run(() => this.windowMoved.next()) this.zone.run(() => this.windowMoved.next())
}) })
electron.ipcRenderer.on('host:window-focused', () => {
this.zone.run(() => this.windowFocused.next())
})
electron.ipcRenderer.on('host:display-metrics-changed', () => { electron.ipcRenderer.on('host:display-metrics-changed', () => {
this.zone.run(() => this.displayMetricsChanged.next()) this.zone.run(() => this.displayMetricsChanged.next())
}) })
@@ -165,60 +157,53 @@ export class HostAppService {
electron.ipcRenderer.on('host:config-change', () => this.zone.run(() => { electron.ipcRenderer.on('host:config-change', () => this.zone.run(() => {
this.configChangeBroadcast.next() this.configChangeBroadcast.next()
})) }))
if (
isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED) &&
!isWindowsBuild(WIN_BUILD_FLUENT_BG_MOVE_BUG_FIXED)
) {
electron.ipcRenderer.send('window-set-disable-vibrancy-while-dragging', true)
}
} }
/** /**
* Returns the current remote [[BrowserWindow]] * Returns the current remote [[BrowserWindow]]
*/ */
getWindow (): Electron.BrowserWindow { getWindow () {
return this.electron.BrowserWindow.fromId(this.windowId) return this.electron.BrowserWindow.fromId(this.windowId)
} }
newWindow (): void { newWindow () {
this.electron.ipcRenderer.send('app:new-window') this.electron.ipcRenderer.send('app:new-window')
} }
toggleFullscreen (): void { toggleFullscreen () {
const window = this.getWindow() const window = this.getWindow()
window.setFullScreen(!this.isFullScreen) window.setFullScreen(!this.isFullScreen)
} }
openDevTools (): void { openDevTools () {
this.getWindow().webContents.openDevTools({ mode: 'undocked' }) this.getWindow().webContents.openDevTools({ mode: 'undocked' })
} }
focusWindow (): void { focusWindow () {
this.electron.ipcRenderer.send('window-focus') this.electron.ipcRenderer.send('window-focus')
} }
minimize (): void { minimize () {
this.electron.ipcRenderer.send('window-minimize') this.electron.ipcRenderer.send('window-minimize')
} }
maximize (): void { maximize () {
this.electron.ipcRenderer.send('window-maximize') this.electron.ipcRenderer.send('window-maximize')
} }
unmaximize (): void { unmaximize () {
this.electron.ipcRenderer.send('window-unmaximize') this.electron.ipcRenderer.send('window-unmaximize')
} }
toggleMaximize (): void { toggleMaximize () {
this.electron.ipcRenderer.send('window-toggle-maximize') this.electron.ipcRenderer.send('window-toggle-maximize')
} }
setBounds (bounds: Bounds): void { setBounds (bounds: Bounds) {
this.electron.ipcRenderer.send('window-set-bounds', bounds) this.electron.ipcRenderer.send('window-set-bounds', bounds)
} }
setAlwaysOnTop (flag: boolean): void { setAlwaysOnTop (flag: boolean) {
this.electron.ipcRenderer.send('window-set-always-on-top', flag) this.electron.ipcRenderer.send('window-set-always-on-top', flag)
} }
@@ -227,50 +212,48 @@ export class HostAppService {
* *
* @param type `null`, or `fluent` when supported (Windowd only) * @param type `null`, or `fluent` when supported (Windowd only)
*/ */
setVibrancy (enable: boolean, type: string|null): void { setVibrancy (enable: boolean, type: string) {
if (!isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)) {
type = null
}
document.body.classList.toggle('vibrant', enable) document.body.classList.toggle('vibrant', enable)
this.electron.ipcRenderer.send('window-set-vibrancy', enable, type) if (this.platform === Platform.macOS) {
this.getWindow().setVibrancy(enable ? 'dark' : null as any) // electron issue 20269
}
if (this.platform === Platform.Windows) {
this.electron.ipcRenderer.send('window-set-vibrancy', enable, type)
}
} }
setTitle (title: string): void { setTitle (title: string) {
this.electron.ipcRenderer.send('window-set-title', title) this.electron.ipcRenderer.send('window-set-title', title)
} }
setTouchBar (touchBar: Electron.TouchBar): void { setTouchBar (touchBar: Electron.TouchBar) {
this.getWindow().setTouchBar(touchBar) this.getWindow().setTouchBar(touchBar)
} }
popupContextMenu (menuDefinition: Electron.MenuItemConstructorOptions[]): void { popupContextMenu (menuDefinition: Electron.MenuItemConstructorOptions[]) {
this.electron.Menu.buildFromTemplate(menuDefinition).popup({}) this.electron.Menu.buildFromTemplate(menuDefinition).popup({})
} }
/** /**
* Notifies other windows of config file changes * Notifies other windows of config file changes
*/ */
broadcastConfigChange (configStore: {[k: string]: any}): void { broadcastConfigChange () {
this.electron.ipcRenderer.send('app:config-change', configStore) this.electron.ipcRenderer.send('app:config-change')
} }
emitReady (): void { emitReady () {
this.electron.ipcRenderer.send('app:ready') this.electron.ipcRenderer.send('app:ready')
} }
bringToFront (): void { bringToFront () {
this.electron.ipcRenderer.send('window-bring-to-front') this.electron.ipcRenderer.send('window-bring-to-front')
} }
closeWindow (): void { closeWindow () {
this.electron.ipcRenderer.send('window-close') this.electron.ipcRenderer.send('window-close')
} }
registerGlobalHotkey (specs: string[]): void { relaunch () {
this.electron.ipcRenderer.send('app:register-global-hotkey', specs)
}
relaunch (): void {
if (this.isPortable) { if (this.isPortable) {
this.electron.app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE }) this.electron.app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE })
} else { } else {
@@ -279,7 +262,7 @@ export class HostAppService {
this.electron.app.exit() this.electron.app.exit()
} }
quit (): void { quit () {
this.logger.info('Quitting') this.logger.info('Quitting')
this.electron.app.quit() this.electron.app.quit()
} }

View File

@@ -1,10 +1,8 @@
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core' import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
import { Observable, Subject } from 'rxjs'
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider' import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
import { stringifyKeySequence } from './hotkeys.util' import { stringifyKeySequence } from './hotkeys.util'
import { ConfigService } from './config.service' import { ConfigService } from '../services/config.service'
import { ElectronService } from './electron.service' import { ElectronService } from '../services/electron.service'
import { HostAppService } from './hostApp.service'
export interface PartialHotkeyMatch { export interface PartialHotkeyMatch {
id: string id: string
@@ -22,23 +20,14 @@ interface EventBufferEntry {
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class HotkeysService { export class HotkeysService {
key = new EventEmitter<KeyboardEvent>() key = new EventEmitter<KeyboardEvent>()
/** @hidden */
matchedHotkey = new EventEmitter<string>() matchedHotkey = new EventEmitter<string>()
globalHotkey = new EventEmitter<void>()
/**
* Fired for each recognized hotkey
*/
get hotkey$ (): Observable<string> { return this._hotkey }
private _hotkey = new Subject<string>()
private currentKeystrokes: EventBufferEntry[] = [] private currentKeystrokes: EventBufferEntry[] = []
private disabledLevel = 0 private disabledLevel = 0
private hotkeyDescriptions: HotkeyDescription[] = [] private hotkeyDescriptions: HotkeyDescription[] = []
private constructor ( private constructor (
private zone: NgZone, private zone: NgZone,
private hostApp: HostAppService,
private electron: ElectronService, private electron: ElectronService,
private config: ConfigService, private config: ConfigService,
@Inject(HotkeyProvider) private hotkeyProviders: HotkeyProvider[], @Inject(HotkeyProvider) private hotkeyProviders: HotkeyProvider[],
@@ -60,9 +49,6 @@ export class HotkeysService {
this.getHotkeyDescriptions().then(hotkeys => { this.getHotkeyDescriptions().then(hotkeys => {
this.hotkeyDescriptions = hotkeys this.hotkeyDescriptions = hotkeys
}) })
// deprecated
this.hotkey$.subscribe(h => this.matchedHotkey.emit(h))
} }
/** /**
@@ -71,7 +57,7 @@ export class HotkeysService {
* @param name DOM event name * @param name DOM event name
* @param nativeEvent event object * @param nativeEvent event object
*/ */
pushKeystroke (name: string, nativeEvent: KeyboardEvent): void { pushKeystroke (name: string, nativeEvent: KeyboardEvent) {
(nativeEvent as any).event = name (nativeEvent as any).event = name
this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() }) this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() })
} }
@@ -79,26 +65,26 @@ export class HotkeysService {
/** /**
* Check the buffer for new complete keystrokes * Check the buffer for new complete keystrokes
*/ */
processKeystrokes (): void { processKeystrokes () {
if (this.isEnabled()) { if (this.isEnabled()) {
this.zone.run(() => { this.zone.run(() => {
const matched = this.getCurrentFullyMatchedHotkey() const matched = this.getCurrentFullyMatchedHotkey()
if (matched) { if (matched) {
console.log('Matched hotkey', matched) console.log('Matched hotkey', matched)
this._hotkey.next(matched) this.matchedHotkey.emit(matched)
this.clearCurrentKeystrokes() this.clearCurrentKeystrokes()
} }
}) })
} }
} }
emitKeyEvent (nativeEvent: KeyboardEvent): void { emitKeyEvent (nativeEvent: KeyboardEvent) {
this.zone.run(() => { this.zone.run(() => {
this.key.emit(nativeEvent) this.key.emit(nativeEvent)
}) })
} }
clearCurrentKeystrokes (): void { clearCurrentKeystrokes () {
this.currentKeystrokes = [] this.currentKeystrokes = []
} }
@@ -156,15 +142,15 @@ export class HotkeysService {
return this.hotkeyDescriptions.filter((x) => x.id === id)[0] return this.hotkeyDescriptions.filter((x) => x.id === id)[0]
} }
enable (): void { enable () {
this.disabledLevel-- this.disabledLevel--
} }
disable (): void { disable () {
this.disabledLevel++ this.disabledLevel++
} }
isEnabled (): boolean { isEnabled () {
return this.disabledLevel === 0 return this.disabledLevel === 0
} }
@@ -183,23 +169,21 @@ export class HotkeysService {
if (typeof value === 'string') { if (typeof value === 'string') {
value = [value] value = [value]
} }
const specs: string[] = []
value.forEach((item: string | string[]) => { value.forEach((item: string | string[]) => {
item = typeof item === 'string' ? [item] : item item = typeof item === 'string' ? [item] : item
try { try {
let electronKeySpec = item[0] let electronKeySpec = item[0]
electronKeySpec = electronKeySpec.replace('Meta', 'Super')
electronKeySpec = electronKeySpec.replace('⌘', 'Command') electronKeySpec = electronKeySpec.replace('⌘', 'Command')
electronKeySpec = electronKeySpec.replace('⌥', 'Alt') electronKeySpec = electronKeySpec.replace('⌥', 'Alt')
electronKeySpec = electronKeySpec.replace(/-/g, '+') electronKeySpec = electronKeySpec.replace(/-/g, '+')
specs.push(electronKeySpec) this.electron.globalShortcut.register(electronKeySpec, () => {
this.globalHotkey.emit()
})
} catch (err) { } catch (err) {
console.error('Could not register the global hotkey:', err) console.error('Could not register the global hotkey:', err)
} }
}) })
this.hostApp.registerGlobalHotkey(specs)
} }
private getHotkeysConfig () { private getHotkeysConfig () {
@@ -219,9 +203,6 @@ export class HotkeysService {
if (typeof value === 'string') { if (typeof value === 'string') {
value = [value] value = [value]
} }
if (!(value instanceof Array)) {
continue
}
if (value) { if (value) {
value = value.map((item: string | string[]) => typeof item === 'string' ? [item] : item) value = value.map((item: string | string[]) => typeof item === 'string' ? [item] : item)
keys[key] = value keys[key] = value

View File

@@ -32,27 +32,27 @@ export class Logger {
private name: string, private name: string,
) {} ) {}
debug (...args: any[]): void { debug (...args: any[]) {
this.doLog('debug', ...args) this.doLog('debug', ...args)
} }
info (...args: any[]): void { info (...args: any[]) {
this.doLog('info', ...args) this.doLog('info', ...args)
} }
warn (...args: any[]): void { warn (...args: any[]) {
this.doLog('warn', ...args) this.doLog('warn', ...args)
} }
error (...args: any[]): void { error (...args: any[]) {
this.doLog('error', ...args) this.doLog('error', ...args)
} }
log (...args: any[]): void { log (...args: any[]) {
this.doLog('log', ...args) this.doLog('log', ...args)
} }
private doLog (level: string, ...args: any[]): void { private doLog (level: string, ...args: any[]) {
console[level](`%c[${this.name}]`, 'color: #aaa', ...args) console[level](`%c[${this.name}]`, 'color: #aaa', ...args)
if (this.winstonLogger) { if (this.winstonLogger) {
this.winstonLogger[level](...args) this.winstonLogger[level](...args)
@@ -65,7 +65,7 @@ export class LogService {
private log: any private log: any
/** @hidden */ /** @hidden */
private constructor (electron: ElectronService) { constructor (electron: ElectronService) {
this.log = initializeWinston(electron) this.log = initializeWinston(electron)
} }

View File

@@ -8,7 +8,7 @@ import { HostAppService, Platform } from './hostApp.service'
/* eslint-disable block-scoped-var */ /* eslint-disable block-scoped-var */
try { try {
var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires
} catch (_) { } } catch (_) { }
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
@@ -18,22 +18,15 @@ export class ShellIntegrationService {
private automatorWorkflowsDestination: string private automatorWorkflowsDestination: string
private registryKeys = [ private registryKeys = [
{ {
path: 'Software\\Classes\\Directory\\Background\\shell\\Terminus', path: 'Software\\Classes\\Directory\\Background\\shell\\Open Terminus here',
value: 'Open Terminus here',
command: 'open "%V"', command: 'open "%V"',
}, },
{ {
path: 'SOFTWARE\\Classes\\Directory\\shell\\Terminus', path: 'Software\\Classes\\*\\shell\\Paste path into Terminus',
value: 'Open Terminus here',
command: 'open "%V"',
},
{
path: 'Software\\Classes\\*\\shell\\Terminus',
value: 'Paste path into Terminus',
command: 'paste "%V"', command: 'paste "%V"',
}, },
] ]
private constructor ( constructor (
private electron: ElectronService, private electron: ElectronService,
private hostApp: HostAppService, private hostApp: HostAppService,
) { ) {
@@ -58,7 +51,7 @@ export class ShellIntegrationService {
return true return true
} }
async install (): Promise<void> { async install () {
const exe: string = process.env.PORTABLE_EXECUTABLE_FILE || this.electron.app.getPath('exe') const exe: string = process.env.PORTABLE_EXECUTABLE_FILE || this.electron.app.getPath('exe')
if (this.hostApp.platform === Platform.macOS) { if (this.hostApp.platform === Platform.macOS) {
for (const wf of this.automatorWorkflows) { for (const wf of this.automatorWorkflows) {
@@ -68,21 +61,13 @@ export class ShellIntegrationService {
for (const registryKey of this.registryKeys) { for (const registryKey of this.registryKeys) {
wnr.createRegistryKey(wnr.HK.CU, registryKey.path) wnr.createRegistryKey(wnr.HK.CU, registryKey.path)
wnr.createRegistryKey(wnr.HK.CU, registryKey.path + '\\command') wnr.createRegistryKey(wnr.HK.CU, registryKey.path + '\\command')
wnr.setRegistryValue(wnr.HK.CU, registryKey.path, '', wnr.REG.SZ, registryKey.value)
wnr.setRegistryValue(wnr.HK.CU, registryKey.path, 'Icon', wnr.REG.SZ, exe) wnr.setRegistryValue(wnr.HK.CU, registryKey.path, 'Icon', wnr.REG.SZ, exe)
wnr.setRegistryValue(wnr.HK.CU, registryKey.path + '\\command', '', wnr.REG.SZ, exe + ' ' + registryKey.command) wnr.setRegistryValue(wnr.HK.CU, registryKey.path + '\\command', '', wnr.REG.SZ, exe + ' ' + registryKey.command)
} }
if(wnr.getRegistryKey(wnr.HK.CU, 'Software\\Classes\\Directory\\Background\\shell\\Open Terminus here')) {
wnr.deleteRegistryKey(wnr.HK.CU, 'Software\\Classes\\Directory\\Background\\shell\\Open Terminus here')
}
if(wnr.getRegistryKey(wnr.HK.CU, 'Software\\Classes\\*\\shell\\Paste path into Terminus')) {
wnr.deleteRegistryKey(wnr.HK.CU, 'Software\\Classes\\*\\shell\\Paste path into Terminus')
}
} }
} }
async remove (): Promise<void> { async remove () {
if (this.hostApp.platform === Platform.macOS) { if (this.hostApp.platform === Platform.macOS) {
for (const wf of this.automatorWorkflows) { for (const wf of this.automatorWorkflows) {
await exec(`rm -rf "${this.automatorWorkflowsDestination}/${wf}"`) await exec(`rm -rf "${this.automatorWorkflowsDestination}/${wf}"`)

View File

@@ -1,5 +1,5 @@
import { Injectable, Inject } from '@angular/core' import { Injectable, Inject } from '@angular/core'
import { TabRecoveryProvider, RecoveredTab, RecoveryToken } from '../api/tabRecovery' import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery'
import { BaseTabComponent } from '../components/baseTab.component' import { BaseTabComponent } from '../components/baseTab.component'
import { Logger, LogService } from '../services/log.service' import { Logger, LogService } from '../services/log.service'
import { ConfigService } from '../services/config.service' import { ConfigService } from '../services/config.service'
@@ -8,9 +8,8 @@ import { ConfigService } from '../services/config.service'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class TabRecoveryService { export class TabRecoveryService {
logger: Logger logger: Logger
enabled = false
private constructor ( constructor (
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[], @Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[],
private config: ConfigService, private config: ConfigService,
log: LogService log: LogService
@@ -18,41 +17,21 @@ export class TabRecoveryService {
this.logger = log.create('tabRecovery') this.logger = log.create('tabRecovery')
} }
async saveTabs (tabs: BaseTabComponent[]): Promise<void> { async saveTabs (tabs: BaseTabComponent[]) {
if (!this.enabled) {
return
}
window.localStorage.tabsRecovery = JSON.stringify( window.localStorage.tabsRecovery = JSON.stringify(
await Promise.all( await Promise.all(
tabs tabs
.map(tab => { .map(tab => tab.getRecoveryToken())
let token = tab.getRecoveryToken()
if (token) {
token = token.then(r => {
if (r) {
r.tabTitle = tab.title
if (tab.color) {
r.tabColor = tab.color
}
}
return r
})
}
return token
})
.filter(token => !!token) .filter(token => !!token)
) )
) )
} }
async recoverTab (token: RecoveryToken): Promise<RecoveredTab|null> { async recoverTab (token: any): Promise<RecoveredTab|null> {
for (const provider of this.config.enabledServices(this.tabRecoveryProviders)) { for (const provider of this.config.enabledServices(this.tabRecoveryProviders)) {
try { try {
const tab = await provider.recover(token) const tab = await provider.recover(token)
if (tab !== null) { if (tab) {
tab.options = tab.options || {}
tab.options.color = token.tabColor || null
tab.options.title = token.tabTitle || ''
return tab return tab
} }
} catch (error) { } catch (error) {

View File

@@ -8,7 +8,7 @@ export type TabComponentType = new (...args: any[]) => BaseTabComponent
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class TabsService { export class TabsService {
/** @hidden */ /** @hidden */
private constructor ( constructor (
private componentFactoryResolver: ComponentFactoryResolver, private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector, private injector: Injector,
private tabRecovery: TabRecoveryService, private tabRecovery: TabRecoveryService,
@@ -17,7 +17,7 @@ export class TabsService {
/** /**
* Instantiates a tab component and assigns given inputs * Instantiates a tab component and assigns given inputs
*/ */
create (type: TabComponentType, inputs?: Record<string, any>): BaseTabComponent { create (type: TabComponentType, inputs?: any): BaseTabComponent {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(type) const componentFactory = this.componentFactoryResolver.resolveComponentFactory(type)
const componentRef = componentFactory.create(this.injector) const componentRef = componentFactory.create(this.injector)
const tab = componentRef.instance const tab = componentRef.instance

View File

@@ -7,7 +7,7 @@ export class ThemesService {
private styleElement: HTMLElement|null = null private styleElement: HTMLElement|null = null
/** @hidden */ /** @hidden */
private constructor ( constructor (
private config: ConfigService, private config: ConfigService,
@Inject(Theme) private themes: Theme[], @Inject(Theme) private themes: Theme[],
) { ) {

View File

@@ -14,7 +14,7 @@ export class TouchbarService {
private tabSegments: SegmentedControlSegment[] = [] private tabSegments: SegmentedControlSegment[] = []
private nsImageCache: {[id: string]: Electron.NativeImage} = {} private nsImageCache: {[id: string]: Electron.NativeImage} = {}
private constructor ( constructor (
private app: AppService, private app: AppService,
private hostApp: HostAppService, private hostApp: HostAppService,
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[], @Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
@@ -48,7 +48,7 @@ export class TouchbarService {
}) })
} }
updateTabs (): void { updateTabs () {
this.tabSegments = this.app.tabs.map(tab => ({ this.tabSegments = this.app.tabs.map(tab => ({
label: this.shortenTitle(tab.title), label: this.shortenTitle(tab.title),
})) }))
@@ -56,7 +56,7 @@ export class TouchbarService {
this.tabsSegmentedControl.selectedIndex = this.app.tabs.indexOf(this.app.activeTab) this.tabsSegmentedControl.selectedIndex = this.app.tabs.indexOf(this.app.activeTab)
} }
update (): void { update () {
if (this.hostApp.platform !== Platform.macOS) { if (this.hostApp.platform !== Platform.macOS) {
return return
} }

View File

@@ -1,4 +1,8 @@
import axios from 'axios' import axios from 'axios'
import * as fs from 'fs'
import os from 'os'
import { spawn } from 'mz/child_process'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Logger, LogService } from './log.service' import { Logger, LogService } from './log.service'
@@ -14,38 +18,35 @@ export class UpdaterService {
private downloaded: Promise<boolean> private downloaded: Promise<boolean>
private electronUpdaterAvailable = true private electronUpdaterAvailable = true
private updateURL: string private updateURL: string
private autoUpdater
private constructor ( constructor (
log: LogService, log: LogService,
private electron: ElectronService, private electron: ElectronService,
private config: ConfigService, config: ConfigService,
) { ) {
this.logger = log.create('updater') this.logger = log.create('updater')
if (process.platform === 'linux') { this.autoUpdater = electron.remote.require('electron-updater').autoUpdater
this.electronUpdaterAvailable = false
return
}
electron.autoUpdater.on('update-available', () => { this.autoUpdater.autoInstallOnAppQuit = !!config.store.enableAutomaticUpdates
this.autoUpdater.on('update-available', () => {
this.logger.info('Update available') this.logger.info('Update available')
}) })
this.autoUpdater.once('update-not-available', () => {
electron.autoUpdater.once('update-not-available', () => {
this.logger.info('No updates') this.logger.info('No updates')
}) })
this.downloaded = new Promise<boolean>(resolve => { this.downloaded = new Promise<boolean>(resolve => {
electron.autoUpdater.once('update-downloaded', () => resolve(true)) this.autoUpdater.once('update-downloaded', () => resolve(true))
}) })
if (config.store.enableAutomaticUpdates && this.electronUpdaterAvailable && !process.env.TERMINUS_DEV) { this.logger.debug('Checking for updates')
this.logger.debug('Checking for updates')
if (this.electronUpdaterAvailable && !process.env.TERMINUS_DEV) {
try { try {
electron.autoUpdater.setFeedURL({ this.autoUpdater.checkForUpdates()
url: `https://update.electronjs.org/eugeny/terminus/${process.platform}-${process.arch}/${electron.app.getVersion()}`,
})
electron.autoUpdater.checkForUpdates()
} catch (e) { } catch (e) {
this.electronUpdaterAvailable = false this.electronUpdaterAvailable = false
this.logger.info('Electron updater unavailable, falling back', e) this.logger.info('Electron updater unavailable, falling back', e)
@@ -54,9 +55,6 @@ export class UpdaterService {
} }
async check (): Promise<boolean> { async check (): Promise<boolean> {
if (!this.config.store.enableAutomaticUpdates) {
return false
}
if (!this.electronUpdaterAvailable) { if (!this.electronUpdaterAvailable) {
this.logger.debug('Checking for updates through fallback method.') this.logger.debug('Checking for updates through fallback method.')
const response = await axios.get(UPDATES_URL) const response = await axios.get(UPDATES_URL)
@@ -73,12 +71,25 @@ export class UpdaterService {
return this.downloaded return this.downloaded
} }
async update (): Promise<void> { async update () {
if (!this.electronUpdaterAvailable) { if (!this.electronUpdaterAvailable) {
this.electron.shell.openExternal(this.updateURL) this.electron.shell.openExternal(this.updateURL)
} else { } else {
await this.downloaded if (process.platform === 'win32') {
this.electron.autoUpdater.quitAndInstall() let downloadpath = await this.autoUpdater.downloadUpdate()
fs.exists(downloadpath[0], (exists) => {
if (exists) {
fs.copyFile(downloadpath[0], os.tmpdir() + 'terminus-installer-temp.exe', (err) => {
if (!err) {
spawn(os.tmpdir() + 'terminus-installer-temp.exe', ['--force-run'], { detached: true, stdio: 'ignore' })
}
})
}
})
} else {
await this.downloaded
this.autoUpdater.quitAndInstall(false, true)
}
} }
} }
} }

View File

@@ -1,16 +1,13 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Injectable, NgZone } from '@angular/core' import { Injectable, NgZone } from '@angular/core'
import { Subscription } from 'rxjs'
import { AppService } from './services/app.service' import { AppService } from './services/app.service'
import { BaseTabComponent } from './components/baseTab.component' import { BaseTabComponent } from './components/baseTab.component'
import { TabHeaderComponent } from './components/tabHeader.component' import { TabHeaderComponent } from './components/tabHeader.component'
import { SplitTabComponent, SplitDirection } from './components/splitTab.component'
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider' import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
/** @hidden */ /** @hidden */
@Injectable() @Injectable({ providedIn: 'root' })
export class TabManagementContextMenu extends TabContextMenuItemProvider { export class CloseContextMenu extends TabContextMenuItemProvider {
weight = 99 weight = -5
constructor ( constructor (
private app: AppService, private app: AppService,
@@ -19,67 +16,39 @@ export class TabManagementContextMenu extends TabContextMenuItemProvider {
super() super()
} }
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]> { async getItems (tab: BaseTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
let items: Electron.MenuItemConstructorOptions[] = [ return [
{ {
label: 'Close', label: 'Close',
click: () => this.zone.run(() => { click: () => this.zone.run(() => {
if (this.app.tabs.includes(tab)) { this.app.closeTab(tab, true)
this.app.closeTab(tab, true) }),
} else { },
tab.destroy() {
label: 'Close other tabs',
click: () => this.zone.run(() => {
for (const t of this.app.tabs.filter(x => x !== tab)) {
this.app.closeTab(t, true)
}
}),
},
{
label: 'Close tabs to the right',
click: () => this.zone.run(() => {
for (const t of this.app.tabs.slice(this.app.tabs.indexOf(tab) + 1)) {
this.app.closeTab(t, true)
}
}),
},
{
label: 'Close tabs to the left',
click: () => this.zone.run(() => {
for (const t of this.app.tabs.slice(0, this.app.tabs.indexOf(tab))) {
this.app.closeTab(t, true)
} }
}), }),
}, },
] ]
if (tabHeader) {
items = [
...items,
{
label: 'Close other tabs',
click: () => this.zone.run(() => {
for (const t of this.app.tabs.filter(x => x !== tab)) {
this.app.closeTab(t, true)
}
}),
},
{
label: 'Close tabs to the right',
click: () => this.zone.run(() => {
for (const t of this.app.tabs.slice(this.app.tabs.indexOf(tab) + 1)) {
this.app.closeTab(t, true)
}
}),
},
{
label: 'Close tabs to the left',
click: () => this.zone.run(() => {
for (const t of this.app.tabs.slice(0, this.app.tabs.indexOf(tab))) {
this.app.closeTab(t, true)
}
}),
},
]
} else {
if (tab.parent instanceof SplitTabComponent) {
const directions: SplitDirection[] = ['r', 'b', 'l', 't']
items.push({
label: 'Split',
submenu: directions.map(dir => ({
label: {
r: 'Right',
b: 'Down',
l: 'Left',
t: 'Up',
}[dir],
click: () => this.zone.run(() => {
(tab.parent as SplitTabComponent).splitTab(tab, dir)
}),
})) as Electron.MenuItemConstructorOptions[],
})
}
}
return items
} }
} }
@@ -94,7 +63,7 @@ const COLORS = [
] ]
/** @hidden */ /** @hidden */
@Injectable() @Injectable({ providedIn: 'root' })
export class CommonOptionsContextMenu extends TabContextMenuItemProvider { export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
weight = -1 weight = -1
@@ -106,38 +75,33 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
} }
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]> { async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]> {
let items: Electron.MenuItemConstructorOptions[] = [] return [
if (tabHeader) { {
items = [ label: 'Rename',
...items, click: () => this.zone.run(() => tabHeader?.showRenameTabModal()),
{ },
label: 'Rename', {
click: () => this.zone.run(() => tabHeader?.showRenameTabModal()), label: 'Duplicate',
}, click: () => this.zone.run(() => this.app.duplicateTab(tab)),
{ },
label: 'Duplicate', {
click: () => this.zone.run(() => this.app.duplicateTab(tab)), label: 'Color',
}, sublabel: COLORS.find(x => x.value === tab.color)!.name,
{ submenu: COLORS.map(color => ({
label: 'Color', label: color.name,
sublabel: COLORS.find(x => x.value === tab.color)?.name, type: 'radio',
submenu: COLORS.map(color => ({ checked: tab.color === color.value,
label: color.name, click: () => this.zone.run(() => {
type: 'radio', tab.color = color.value
checked: tab.color === color.value, }),
click: () => this.zone.run(() => { })) as Electron.MenuItemConstructorOptions[],
tab.color = color.value },
}), ]
})) as Electron.MenuItemConstructorOptions[],
},
]
}
return items
} }
} }
/** @hidden */ /** @hidden */
@Injectable() @Injectable({ providedIn: 'root' })
export class TaskCompletionContextMenu extends TabContextMenuItemProvider { export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
constructor ( constructor (
private app: AppService, private app: AppService,
@@ -148,61 +112,36 @@ export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
async getItems (tab: BaseTabComponent): Promise<Electron.MenuItemConstructorOptions[]> { async getItems (tab: BaseTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
const process = await tab.getCurrentProcess() const process = await tab.getCurrentProcess()
let items: Electron.MenuItemConstructorOptions[] = []
const extTab: (BaseTabComponent & { __completionNotificationEnabled?: boolean, __outputNotificationSubscription?: Subscription|null }) = tab
if (process) { if (process) {
items.push({ return [
id: 'process-name', {
enabled: false, id: 'process-name',
label: 'Current process: ' + process.name, enabled: false,
}) label: 'Current process: ' + process.name,
items.push({ },
label: 'Notify when done', {
type: 'checkbox', label: 'Notify when done',
checked: extTab.__completionNotificationEnabled, type: 'checkbox',
click: () => this.zone.run(() => { checked: (tab as any).__completionNotificationEnabled,
extTab.__completionNotificationEnabled = !extTab.__completionNotificationEnabled click: () => this.zone.run(() => {
(tab as any).__completionNotificationEnabled = !(tab as any).__completionNotificationEnabled
if (extTab.__completionNotificationEnabled) { if ((tab as any).__completionNotificationEnabled) {
this.app.observeTabCompletion(tab).subscribe(() => { this.app.observeTabCompletion(tab).subscribe(() => {
new Notification('Process completed', { new Notification('Process completed', {
body: process.name, body: process.name,
}).addEventListener('click', () => { }).addEventListener('click', () => {
this.app.selectTab(tab) this.app.selectTab(tab)
}) })
extTab.__completionNotificationEnabled = false ;(tab as any).__completionNotificationEnabled = false
})
} else {
this.app.stopObservingTabCompletion(tab)
}
}),
})
}
items.push({
label: 'Notify on activity',
type: 'checkbox',
checked: !!extTab.__outputNotificationSubscription,
click: () => this.zone.run(() => {
if (extTab.__outputNotificationSubscription) {
extTab.__outputNotificationSubscription.unsubscribe()
extTab.__outputNotificationSubscription = null
} else {
extTab.__outputNotificationSubscription = tab.activity$.subscribe(active => {
if (extTab.__outputNotificationSubscription && active) {
extTab.__outputNotificationSubscription.unsubscribe()
extTab.__outputNotificationSubscription = null
new Notification('Tab activity', {
body: tab.title,
}).addEventListener('click', () => {
this.app.selectTab(tab)
}) })
} else {
this.app.stopObservingTabCompletion(tab)
} }
}) }),
} },
}), ]
}) }
return items return []
} }
} }

View File

@@ -16,12 +16,4 @@ app-root {
terminaltab .content { terminaltab .content {
margin: 5px !important; margin: 5px !important;
} }
ssh-tab .content {
margin: 5px !important;
}
serial-tab .content {
margin: 5px !important;
}
} }

View File

@@ -29,7 +29,7 @@ body {
background: $body-bg; background: $body-bg;
&.vibrant { &.vibrant {
background: rgba(0,0,0,.65); background: rgba(0,0,0,.4);
} }
} }
@@ -246,7 +246,7 @@ ngb-tabset .tab-content {
} }
.list-group-item { .list-group-item {
transition: 0.0625s background; transition: 0.25s background;
i + * { i + * {
margin-left: 10px; margin-left: 10px;
@@ -262,29 +262,6 @@ ngb-tabset .tab-content {
} }
} }
.list-group-light {
.list-group-item {
background: transparent;
border: none;
border-top: 1px solid rgba(255, 255, 255, .1);
&:not(.combi) {
padding: $list-group-item-padding-y $list-group-item-padding-x;
}
&:first-child {
border-top: none;
}
&.list-group-item-action {
&:hover, &.active {
background: $list-group-hover-bg;
}
}
}
}
checkbox i.on { checkbox i.on {
color: $blue; color: $blue;
} }
@@ -415,7 +392,3 @@ search-panel {
border-color: $nav-tabs-link-active-border-color; border-color: $nav-tabs-link-active-border-color;
} }
} }
hr {
border-color: $list-group-border-color;
}

View File

@@ -142,7 +142,7 @@ $nav-tabs-link-active-border-color: #eee;
$navbar-padding-y: 0; $navbar-padding-y: 0;
$navbar-padding-x: 0; $navbar-padding-x: 0;
$dropdown-bg: $content-bg-solid; $dropdown-bg: $table-bg;
$dropdown-color: $body-color; $dropdown-color: $body-color;
$dropdown-border-width: 1px; $dropdown-border-width: 1px;
$dropdown-box-shadow: 0 .5rem 1rem rgba($black,.175); $dropdown-box-shadow: 0 .5rem 1rem rgba($black,.175);

View File

@@ -2,6 +2,9 @@
"extends": "../tsconfig.json", "extends": "../tsconfig.json",
"exclude": ["node_modules", "dist"], "exclude": ["node_modules", "dist"],
"compilerOptions": { "compilerOptions": {
"baseUrl": "src" "baseUrl": "src",
"paths": {
"*": ["../../app/node_modules/*"]
}
} }
} }

View File

@@ -1,4 +1,5 @@
const path = require('path') const path = require('path')
const { AngularCompilerPlugin } = require('@ngtools/webpack')
module.exports = { module.exports = {
target: 'node', target: 'node',
@@ -23,22 +24,26 @@ module.exports = {
module: { module: {
rules: [ rules: [
{ {
test: /\.ts$/, test: /(?:\.ngfactory\.js|\.ngfactory|\.ngstyle\.js|\.ts)$/,
use: { loader: '@ngtools/webpack',
loader: 'awesome-typescript-loader',
options: {
configFileName: path.resolve(__dirname, 'tsconfig.json'),
typeRoots: [
path.resolve(__dirname, 'node_modules/@types'),
path.resolve(__dirname, '../node_modules/@types'),
],
paths: {
"terminus-*": [path.resolve(__dirname, '../terminus-*')],
"*": [path.resolve(__dirname, '../app/node_modules/*')],
},
},
},
}, },
// {
// test: /\.ts$/,
// use: {
// loader: 'awesome-typescript-loader',
// options: {
// configFileName: path.resolve(__dirname, 'tsconfig.json'),
// typeRoots: [
// path.resolve(__dirname, 'node_modules/@types'),
// path.resolve(__dirname, '../node_modules/@types'),
// ],
// paths: {
// "terminus-*": [path.resolve(__dirname, '../terminus-*')],
// "*": [path.resolve(__dirname, '../app/node_modules/*')],
// },
// },
// },
// },
{ test: /\.pug$/, use: ['apply-loader', 'pug-loader'] }, { test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
{ test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] }, { test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] },
{ test: /\.css$/, use: ['to-string-loader', 'css-loader'], include: /component\.css/ }, { test: /\.css$/, use: ['to-string-loader', 'css-loader'], include: /component\.css/ },
@@ -57,4 +62,12 @@ module.exports = {
/^@angular/, /^@angular/,
/^@ng-bootstrap/, /^@ng-bootstrap/,
], ],
plugins: [
new AngularCompilerPlugin({
tsConfigPath: path.resolve(__dirname, 'tsconfig.json'),
entryModule: './terminus-core/src/index#AppModule',
sourceMap: true,
directTemplateLoading: true,
}),
],
} }

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