Compare commits
43 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
afdb06df40 | ||
![]() |
2227f2cff3 | ||
![]() |
1a46f0ec3c | ||
![]() |
e246e22bfd | ||
![]() |
89e4a80a37 | ||
![]() |
3a522f4f73 | ||
![]() |
a01238d34c | ||
![]() |
75fea5e390 | ||
![]() |
af792399da | ||
![]() |
7d86a7e4cc | ||
![]() |
cf6a7d10b3 | ||
![]() |
4bc03319e7 | ||
![]() |
8cf7851801 | ||
![]() |
c70e6fde35 | ||
![]() |
2ed35cb400 | ||
![]() |
cf5e31be79 | ||
![]() |
2c4c094941 | ||
![]() |
d25751abe7 | ||
![]() |
bcc4a262e2 | ||
![]() |
27fb611166 | ||
![]() |
7279ba13ac | ||
![]() |
70b463b086 | ||
![]() |
ef4e1e5a0d | ||
![]() |
04d621d62b | ||
![]() |
dc813b8edb | ||
![]() |
426bedd7b7 | ||
![]() |
34b7c80243 | ||
![]() |
33281b5caf | ||
![]() |
e6e88272c2 | ||
![]() |
05bb07e37d | ||
![]() |
61726cad36 | ||
![]() |
77c253594f | ||
![]() |
478d715d10 | ||
![]() |
1675312f75 | ||
![]() |
128aa618f0 | ||
![]() |
6b56155ca5 | ||
![]() |
3ee1f1b023 | ||
![]() |
db54f4af13 | ||
![]() |
59d1a2fc23 | ||
![]() |
808e7f4699 | ||
![]() |
e338bc417a | ||
![]() |
3039a65757 | ||
![]() |
714f181be5 |
3
.gitignore
vendored
@@ -18,3 +18,6 @@ npm-debug.log
|
|||||||
builtin-plugins
|
builtin-plugins
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
||||||
|
docs/api
|
||||||
|
.travis.ssh.key
|
||||||
|
BIN
.travis.ssh.key.enc
Normal file
1
.travis.ssh.key.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDDFM4nHSbET5V7EYNgjA8NeVfOxV0wVMdZ2YvsDzD+qPJ4+MYbvsL7ZPaSxQSn7n6ATkLHjKje5RpF/Rl9K3kucGs0P6cqJVeE0qryEteQ3Q+fYAk+bD2J9ZQ/hv/0NtLl8T+7lJUZ3WUxFH73sgph77Sw0z+kMpPaK7U2vqMBQD/7+6iJgya31wP0qW0XKDz1BjKeXgwTg10Pm4vcGsR4c2q7YIzSzBHffcyo0vJyFvOX/ZKHlZRcq/wnQMeOl/hPgf1xCENjQZmFVReQlYSw5cNNDT9HZPKekOAZFFez7/AbPiTIo/bnBYIv0mdUjr3nw8nXF505q8LiD3z/ksaaWDqe9CCLM4W0Bh7/dhP7IGPdfX0fVHLhOnYIOsG21D8rWJjMPkVRSLyEvWNAnVuObJNHoQu8VATnOxfPNnMun72IHyyFWVoADk5JcsMbzcP7gZB+5oJO7U1qpcdndtBOA3ZlF0Uz2jVZnqavoEBWT39tl3vs69hAA3aTPGclg7HMuAJOl4HsKmaUgDxqV2wCX/S4pDqmKMbmumDLX+MM0xl0gXj/zpVJp9BzdnrArkC40ivmC6TSA4wrdN0tNBlqApkH5/jxGWrcu2AXVn9PGF3+QrjW0iu+QMZCaKWDhLIQC835uFwzhnNGlx41B7uxMLuNFxKXdQ3f/cC9QMG8ew== TravisCIDeployKey
|
60
.travis.yml
@@ -1,28 +1,48 @@
|
|||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: linux
|
|
||||||
env: BUILD_FOR=linux
|
|
||||||
- os: osx
|
|
||||||
env: BUILD_FOR=macos
|
|
||||||
|
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: 10
|
node_js: 11
|
||||||
|
|
||||||
cache:
|
stages:
|
||||||
directories:
|
- Build
|
||||||
- node_modules
|
- name: Docs
|
||||||
- app/node_modules
|
if: branch = master
|
||||||
|
|
||||||
before_install:
|
jobs:
|
||||||
- yarn
|
include:
|
||||||
|
- stage: 'Build'
|
||||||
|
os: linux
|
||||||
|
before_install:
|
||||||
|
- yarn
|
||||||
|
- rm app/node_modules/.yarn-integrity || true
|
||||||
|
- scripts/install-deps.js
|
||||||
|
script:
|
||||||
|
- scripts/build-native.js
|
||||||
|
- yarn run build
|
||||||
|
- scripts/prepackage-plugins.js
|
||||||
|
- scripts/build-linux.js
|
||||||
|
|
||||||
script:
|
- stage: 'Build'
|
||||||
- scripts/build-native.js
|
os: osx
|
||||||
- yarn run build
|
before_install:
|
||||||
- scripts/prepackage-plugins.js
|
- rm app/node_modules/.yarn-integrity || true
|
||||||
- scripts/build-$BUILD_FOR.js
|
- yarn
|
||||||
|
script:
|
||||||
|
- scripts/build-native.js
|
||||||
|
- yarn run build
|
||||||
|
- scripts/prepackage-plugins.js
|
||||||
|
- scripts/build-macos.js
|
||||||
|
|
||||||
dist: trusty
|
- stage: 'Docs'
|
||||||
|
os: linux
|
||||||
|
script:
|
||||||
|
- openssl aes-256-cbc -K $encrypted_4e2fb4889ef8_key -iv $encrypted_4e2fb4889ef8_iv -in .travis.ssh.key.enc -out .travis.ssh.key -d
|
||||||
|
- eval "$(ssh-agent -s)"
|
||||||
|
- chmod 600 .travis.ssh.key
|
||||||
|
- ssh-add .travis.ssh.key
|
||||||
|
- yarn
|
||||||
|
- yarn run docs
|
||||||
|
- rsync -e "ssh -o StrictHostKeyChecking=no" -arv docs/api/ root@ajenti.org:/srv/terminus-docs/
|
||||||
|
|
||||||
|
dist: xenial
|
||||||
sudo: false
|
sudo: false
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/Eugeny/terminus/releases/latest">Downloads</a> | <a href="https://t.me/joinchat/HgLqPhRg9Inhmm7WD3H1BQ">Community</a> | <a href="https://ci.appveyor.com/project/Eugeny/terminus/build/artifacts">Latest Windows nightly</a>
|
<a href="https://github.com/Eugeny/terminus/releases/latest">Downloads</a> | <a href="https://gitter.im/terminus-terminal/community">Community</a> | <a href="https://ci.appveyor.com/project/Eugeny/terminus/build/artifacts">Latest Windows nightly</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
----
|
----
|
||||||
@@ -47,8 +47,7 @@ Plugins can be installed directly from the Settings view inside Terminus.
|
|||||||
|
|
||||||
Pull requests and plugins are welcome!
|
Pull requests and plugins are welcome!
|
||||||
|
|
||||||
See [HACKING.md](https://github.com/Eugeny/terminus/blob/master/HACKING.md) for information of how the project is laid out, and a very brief plugin development tutorial.
|
See [HACKING.md](https://github.com/Eugeny/terminus/blob/master/HACKING.md) and [API docs](http://ajenti.org/terminus-docs/) for information of how the project is laid out, and a very brief plugin development tutorial.
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
[](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FEugeny%2Fterminus?ref=badge_large)
|
[](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FEugeny%2Fterminus?ref=badge_large)
|
||||||
|
@@ -19,6 +19,7 @@ export class Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.commandLine.appendSwitch('disable-http-cache')
|
app.commandLine.appendSwitch('disable-http-cache')
|
||||||
|
app.commandLine.appendSwitch('force_discrete_gpu', '0')
|
||||||
app.commandLine.appendSwitch('lang', 'EN')
|
app.commandLine.appendSwitch('lang', 'EN')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -44,7 +44,9 @@ export class Window {
|
|||||||
title: 'Terminus',
|
title: 'Terminus',
|
||||||
minWidth: 400,
|
minWidth: 400,
|
||||||
minHeight: 300,
|
minHeight: 300,
|
||||||
webPreferences: { webSecurity: false },
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
},
|
||||||
frame: false,
|
frame: false,
|
||||||
show: false,
|
show: false,
|
||||||
backgroundColor: '#00000000'
|
backgroundColor: '#00000000'
|
||||||
|
@@ -13,13 +13,13 @@
|
|||||||
"watch": "webpack --progress --color --watch"
|
"watch": "webpack --progress --color --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "7.2.0-rc.0",
|
"@angular/animations": "7.2.8",
|
||||||
"@angular/common": "7.2.0-rc.0",
|
"@angular/common": "7.2.8",
|
||||||
"@angular/compiler": "7.2.0-rc.0",
|
"@angular/compiler": "7.2.8",
|
||||||
"@angular/core": "7.2.0-rc.0",
|
"@angular/core": "7.2.8",
|
||||||
"@angular/forms": "7.2.0-rc.0",
|
"@angular/forms": "7.2.8",
|
||||||
"@angular/platform-browser": "7.2.0-rc.0",
|
"@angular/platform-browser": "7.2.8",
|
||||||
"@angular/platform-browser-dynamic": "7.2.0-rc.0",
|
"@angular/platform-browser-dynamic": "7.2.8",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^3.3.1",
|
"@ng-bootstrap/ng-bootstrap": "^3.3.1",
|
||||||
"devtron": "1.4.0",
|
"devtron": "1.4.0",
|
||||||
"electron-config": "0.2.1",
|
"electron-config": "0.2.1",
|
||||||
|
@@ -2,52 +2,52 @@
|
|||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
"@angular/animations@7.2.0-rc.0":
|
"@angular/animations@7.2.8":
|
||||||
version "7.2.0-rc.0"
|
version "7.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.2.0-rc.0.tgz#12849f8ab104d309ec99c0ceb170a895c15d3d44"
|
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.2.8.tgz#0285364c839c660a934ab0f753ec21424bfb292e"
|
||||||
integrity sha512-CRQNQ6QVTuf4nCHVLVpKQx7YPpNPfnTF79KVWzHefkkyS3URRuEgvE4jCED4oTJ4BEsmkjXyt51VeDV0FgqQFg==
|
integrity sha512-dJn9koYukyz15TouBc+z5z9fdThDk+bKgdlij25eYSu5Mpmtk04gB4eIMQA97K0UDh1d4YukgSJ5w3ZIk0m8DQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.9.0"
|
tslib "^1.9.0"
|
||||||
|
|
||||||
"@angular/common@7.2.0-rc.0":
|
"@angular/common@7.2.8":
|
||||||
version "7.2.0-rc.0"
|
version "7.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.2.0-rc.0.tgz#60d3540c6cdcf3440f67e2c15cf8f1c7b1160d9d"
|
resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.2.8.tgz#660c816b6f08cd2919a6efb7465397e4ff14d265"
|
||||||
integrity sha512-Xv60KEP1kpF74kpN1xtps0W++PUXLUMK/0tDblUZH7tBWvS0XwEwtuK5B6wcs+I5nqZkPgvlvOyiVZvOLraWOg==
|
integrity sha512-LgOhf68+LPndGZhtnUlGFd2goReXYmHzaFZW8gCEi9aC+H+Io8bjYh0gkH3xDreevEOe3f0z6coXNFLIxSmTuA==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.9.0"
|
tslib "^1.9.0"
|
||||||
|
|
||||||
"@angular/compiler@7.2.0-rc.0":
|
"@angular/compiler@7.2.8":
|
||||||
version "7.2.0-rc.0"
|
version "7.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.2.0-rc.0.tgz#603dbec25d6c2beea08a293c68c39b40e2ea81e2"
|
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.2.8.tgz#9d9c1515e99914399e6915c1c90484b1d255560b"
|
||||||
integrity sha512-tvgGJx0urSz/qn6upmcjX3N3dyWQ9m5mQOwJxmN4qekxjOtSRml5yt2KtlaUTkGsjkEmEVfSHel+X1TwzBdhYw==
|
integrity sha512-PrU97cTsOdofpaDkxK0rWUA/CGd0u6ESOI6XvFVm5xH9zJInsdY8ShSHklnr1JJnss70e1dGKZbZq32OChxWMw==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.9.0"
|
tslib "^1.9.0"
|
||||||
|
|
||||||
"@angular/core@7.2.0-rc.0":
|
"@angular/core@7.2.8":
|
||||||
version "7.2.0-rc.0"
|
version "7.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.2.0-rc.0.tgz#57c0e26130288c3b58466f079828c028bdf6221f"
|
resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.2.8.tgz#6586d9b6c6321c80119b3f3e2bd0edbb32d0b649"
|
||||||
integrity sha512-2u11TNlLorw3JhuczCPwl8UmxE+ja2Q/ghBl8iYi4SIBWiBO1K0wVT13Ts7eojk63yZcg60lyYYCegXBmHLTuw==
|
integrity sha512-QKwug2kWJC00zm2rvmD9mCJzsOkMVhSu8vqPWf83poWTh8+F9aIVWcy29W0VoGpBkSchOnK8hf9DnKVv28j9nw==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.9.0"
|
tslib "^1.9.0"
|
||||||
|
|
||||||
"@angular/forms@7.2.0-rc.0":
|
"@angular/forms@7.2.8":
|
||||||
version "7.2.0-rc.0"
|
version "7.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.2.0-rc.0.tgz#4eb473018084bb81be3e2e1ae8afa8d2b2117a6c"
|
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.2.8.tgz#adf194088495822d55dcf3e5bf69196dcf19465d"
|
||||||
integrity sha512-OWP1zzYQiuqtoltdlhkcVjHxg78exbt7z1lr8RSjybr/Snc5zSFhnZF6byasd/4lzVySuujsMXkTK7D8x6hedA==
|
integrity sha512-lbSX4IHFHz/c4e2RHiPpL8MJlzDkCuQEHnqsujDaV2X9o9fApS6+C1X4x7Z2XDKqonmeX+aHQwv9+SLejX6OyQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.9.0"
|
tslib "^1.9.0"
|
||||||
|
|
||||||
"@angular/platform-browser-dynamic@7.2.0-rc.0":
|
"@angular/platform-browser-dynamic@7.2.8":
|
||||||
version "7.2.0-rc.0"
|
version "7.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.0-rc.0.tgz#5ea47d094c53a0ba34ecbb0dfdcef452fa05dc9a"
|
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.8.tgz#e82768900cedfa75bf453263f931a9f90f7aaab2"
|
||||||
integrity sha512-uqT27oh9m58L6MUjgvT+7NpAFbigOnnTUWMsCLijNUKd7i37T6UxTVKPvuqNHlaLXsmDRxVHN3INI0IrWZ3R+w==
|
integrity sha512-nOJt28A5pRn4mdL8y98V7bA6OOdMRjsQAcWCr/isGYF0l1yDC0ijUGWkHuRtj3z1/9tmERN0BLXx+xs1h4JhCQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.9.0"
|
tslib "^1.9.0"
|
||||||
|
|
||||||
"@angular/platform-browser@7.2.0-rc.0":
|
"@angular/platform-browser@7.2.8":
|
||||||
version "7.2.0-rc.0"
|
version "7.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.2.0-rc.0.tgz#c6d1f0b2328b1d81649bea70c23edc33de729015"
|
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.2.8.tgz#11096727b99bf3d7fd82a00a3a468b933c9713bd"
|
||||||
integrity sha512-r0ak7SVLWrivd4S0MXWmqNLeF6NNOBAopnjrhUu2j5I00u7/QfLrX0E5zRlJ8JkARVjer6Wm+D1ztlOWw5jHag==
|
integrity sha512-SizCRMc7Or27g2CugcqWnaAikRPfgLgRvb9GFFGpcgoq8CRfOVwkyR5dFZuqN39H+uwtwuTMP5OUYhZcrFNKug==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.9.0"
|
tslib "^1.9.0"
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 2.6 MiB |
BIN
docs/dist/assets/background.jpeg
vendored
Before Width: | Height: | Size: 2.6 MiB |
BIN
docs/dist/assets/terminal.png
vendored
Before Width: | Height: | Size: 21 KiB |
1
docs/dist/bundle.js
vendored
BIN
docs/dist/fonts/background.jpeg
vendored
Before Width: | Height: | Size: 2.6 MiB |
@@ -1,9 +0,0 @@
|
|||||||
<!DOCTYPE html><html><head><base href="dist/"><meta name="viewport" content="initial-scale=1, minimal-ui, shrink-to-fit=no"><link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400" rel="stylesheet"><script src="bundle.js"></script><title>Terminus</title></head><body><div class="mt-5 mb-5" id="header"><div class="text-center"><h1>Terminus</h1><div class="subtitle mb-3">A terminal for a more modern age</div><a class="btn btn-lg btn-outline-dark mt-4" href="https://github.com/Eugeny/terminus/releases/latest" target="_blank"><strong>DOWNLOAD</strong></a><a class="btn btn-lg btn-outline-secondary mt-4 ml-3" href="https://github.com/Eugeny/terminus" target="_blank"><strong>GITHUB</strong></a></div></div><div class="background-stripe"><div class="overlay overlay1"></div><div class="overlay overlay2"></div><div class="terminal"></div></div><div class="container mt-5 mb-5"><div class="d-flex flex-wrap flex-md-nowrap"><div class="w-100"><div class="feature">windows</div><div class="feature">linux</div><div class="feature">macos</div><br><div class="feature">powershell</div><div class="feature">wsl</div><div class="feature">cygwin</div><div class="feature">git-bash</div><div class="feature">cmder</div><div class="feature">clink</div></div><div class="w-100"><div class="feature">full unicode</div><div class="feature">global hotkey</div><div class="feature">plugins</div><div class="feature">tab recovery</div><div class="feature">custom css</div><div class="feature">themes</div><div class="feature">font ligatures</div><div class="feature">clickable paths</div><div class="feature">tabs on top/bottom</div><div class="feature">vibrancy</div><div class="feature">bracketed paste</div></div></div></div><div class="container mt-5 mb-5"><div class="text-center mt-5"><div class="mb-4 mt-5"><script type="text/javascript" src="https://ko-fi.com/widgets/widget_2.js"></script><script type="text/javascript">kofiwidget2.init('Buy me a coffee', '#46b798', 'J3J8KWTF')
|
|
||||||
kofiwidget2.draw()
|
|
||||||
</script></div><a class="btn btn-lg btn-outline-secondary mt-3" href="/terminus/#header"><strong>BEAM ME UP</strong></a></div></div><div class="background-stripe2"><div class="overlay overlay1"></div></div><script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
|
||||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
|
||||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
|
||||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
|
||||||
|
|
||||||
ga('create', 'UA-3278102-18', 'auto');
|
|
||||||
ga('send', 'pageview');</script></body></html>
|
|
@@ -1 +0,0 @@
|
|||||||
import './styles.scss'
|
|
@@ -1,75 +0,0 @@
|
|||||||
doctype html
|
|
||||||
html
|
|
||||||
head
|
|
||||||
base(href='dist/')
|
|
||||||
meta(name='viewport', content='initial-scale=1, minimal-ui, shrink-to-fit=no')
|
|
||||||
link(href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400", rel="stylesheet")
|
|
||||||
script(src='bundle.js')
|
|
||||||
title Terminus
|
|
||||||
body
|
|
||||||
.mt-5.mb-5#header
|
|
||||||
.text-center
|
|
||||||
h1 Terminus
|
|
||||||
.subtitle.mb-3 A terminal for a more modern age
|
|
||||||
|
|
||||||
a.btn.btn-lg.btn-outline-dark.mt-4(href='https://github.com/Eugeny/terminus/releases/latest', target='_blank')
|
|
||||||
strong DOWNLOAD
|
|
||||||
|
|
||||||
a.btn.btn-lg.btn-outline-secondary.mt-4.ml-3(href='https://github.com/Eugeny/terminus', target='_blank')
|
|
||||||
strong GITHUB
|
|
||||||
|
|
||||||
|
|
||||||
.background-stripe
|
|
||||||
.overlay.overlay1
|
|
||||||
.overlay.overlay2
|
|
||||||
.terminal
|
|
||||||
|
|
||||||
.container.mt-5.mb-5
|
|
||||||
.d-flex.flex-wrap.flex-md-nowrap
|
|
||||||
.w-100
|
|
||||||
.feature windows
|
|
||||||
.feature linux
|
|
||||||
.feature macos
|
|
||||||
br
|
|
||||||
.feature powershell
|
|
||||||
.feature wsl
|
|
||||||
.feature cygwin
|
|
||||||
.feature git-bash
|
|
||||||
.feature cmder
|
|
||||||
.feature clink
|
|
||||||
|
|
||||||
.w-100
|
|
||||||
.feature full unicode
|
|
||||||
.feature global hotkey
|
|
||||||
.feature plugins
|
|
||||||
.feature tab recovery
|
|
||||||
.feature custom css
|
|
||||||
.feature themes
|
|
||||||
.feature font ligatures
|
|
||||||
.feature clickable paths
|
|
||||||
.feature tabs on top/bottom
|
|
||||||
.feature vibrancy
|
|
||||||
.feature bracketed paste
|
|
||||||
|
|
||||||
.container.mt-5.mb-5
|
|
||||||
.text-center.mt-5
|
|
||||||
.mb-4.mt-5
|
|
||||||
script(type='text/javascript', src='https://ko-fi.com/widgets/widget_2.js')
|
|
||||||
script(type='text/javascript').
|
|
||||||
kofiwidget2.init('Buy me a coffee', '#46b798', 'J3J8KWTF')
|
|
||||||
kofiwidget2.draw()
|
|
||||||
|
|
||||||
a.btn.btn-lg.btn-outline-secondary.mt-3(href='/terminus/#header')
|
|
||||||
strong BEAM ME UP
|
|
||||||
|
|
||||||
.background-stripe2
|
|
||||||
.overlay.overlay1
|
|
||||||
|
|
||||||
script.
|
|
||||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
|
||||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
|
||||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
|
||||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
|
||||||
|
|
||||||
ga('create', 'UA-3278102-18', 'auto');
|
|
||||||
ga('send', 'pageview');
|
|
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "docs",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"build": "webpack --progress",
|
|
||||||
"watch": "webpack --progress --watch"
|
|
||||||
},
|
|
||||||
"private": true,
|
|
||||||
"devDependencies": {
|
|
||||||
"bootstrap": "^4.1.3",
|
|
||||||
"css-loader": "^1.0.0",
|
|
||||||
"file-loader": "^1.1.11",
|
|
||||||
"node-sass": "^4.9.3",
|
|
||||||
"pug": "^2.0.3",
|
|
||||||
"pug-cli": "^1.0.0-alpha6",
|
|
||||||
"pug-html-loader": "^1.1.5",
|
|
||||||
"sass-loader": "^7.1.0",
|
|
||||||
"style-loader": "^0.22.1",
|
|
||||||
"val-loader": "^1.1.1",
|
|
||||||
"webpack": "^4.16.5",
|
|
||||||
"webpack-cli": "^3.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
BIN
docs/readme.png
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.6 MiB |
141
docs/styles.scss
@@ -1,141 +0,0 @@
|
|||||||
$font-family-sans-serif: "Source Sans Pro";
|
|
||||||
$border-radius-lg: 0;
|
|
||||||
$btn-border-width: 3px;
|
|
||||||
|
|
||||||
@import "node_modules/bootstrap/scss/bootstrap";
|
|
||||||
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 10vw;
|
|
||||||
font-weight: 200;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-style: italic;
|
|
||||||
color: #999;
|
|
||||||
font-size: 5vw;
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.background-stripe {
|
|
||||||
width: 100vw;
|
|
||||||
background-image: url('./background.jpeg');
|
|
||||||
background-size: cover;
|
|
||||||
height: 30vw;
|
|
||||||
margin: 200px 0 150px;
|
|
||||||
min-height: 1000px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
position: absolute;
|
|
||||||
width: 100vw;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
|
|
||||||
&.overlay1 {
|
|
||||||
top: -1px;
|
|
||||||
left: 0;
|
|
||||||
border-top: 10vw solid white;
|
|
||||||
border-right: 100vw solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.overlay2 {
|
|
||||||
bottom: -1px;
|
|
||||||
right: 0;
|
|
||||||
border-bottom: 10vw solid white;
|
|
||||||
border-left: 100vw solid transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 5vw;
|
|
||||||
|
|
||||||
width: 1304px;
|
|
||||||
margin-left: -652px;
|
|
||||||
height: 972px;
|
|
||||||
border-radius: 9px;
|
|
||||||
|
|
||||||
box-shadow: 0 0 100px black;
|
|
||||||
background: url('./terminal.png');
|
|
||||||
background-size: cover;
|
|
||||||
|
|
||||||
animation: slideIn ease-out 1s;
|
|
||||||
opacity: .95;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media(max-width: 1500px) {
|
|
||||||
min-height: 500px;
|
|
||||||
margin: 200px 0 100px;
|
|
||||||
|
|
||||||
.terminal {
|
|
||||||
width: 652px;
|
|
||||||
top: -100px;
|
|
||||||
margin-left: -326px;
|
|
||||||
height: 486px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media(max-width: 750px) {
|
|
||||||
min-height: 250px;
|
|
||||||
margin: 100px 0 50px;
|
|
||||||
|
|
||||||
.terminal {
|
|
||||||
width: 326px;
|
|
||||||
top: -50px;
|
|
||||||
margin-left: -163px;
|
|
||||||
height: 243px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature {
|
|
||||||
font-size: 45px;
|
|
||||||
line-height: 40px;
|
|
||||||
opacity: .5;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
margin-top: 200px;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: .95;
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.background-stripe2 {
|
|
||||||
width: 100vw;
|
|
||||||
background-image: url('./background.jpeg');
|
|
||||||
background-size: cover;
|
|
||||||
height: 30vw;
|
|
||||||
margin: 100px 0 0;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
position: absolute;
|
|
||||||
width: 100vw;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
|
|
||||||
&.overlay1 {
|
|
||||||
top: -1px;
|
|
||||||
right: 0;
|
|
||||||
border-top: 10vw solid white;
|
|
||||||
border-left: 100vw solid transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,27 +0,0 @@
|
|||||||
const path = require('path')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: {
|
|
||||||
'index.ignore': 'file-loader?name=../index.html!pug-html-loader!' + path.resolve(__dirname, './index.pug'),
|
|
||||||
'bundle': path.resolve(__dirname, 'index.js'),
|
|
||||||
},
|
|
||||||
context: __dirname,
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, 'dist'),
|
|
||||||
filename: '[name].js'
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
|
|
||||||
{
|
|
||||||
test: /\.(jpeg|png)?$/,
|
|
||||||
use: {
|
|
||||||
loader: 'file-loader',
|
|
||||||
options: {
|
|
||||||
name: 'assets/[name].[ext]'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
@@ -14,7 +14,7 @@
|
|||||||
"core-js": "2.4.1",
|
"core-js": "2.4.1",
|
||||||
"cross-env": "4.0.0",
|
"cross-env": "4.0.0",
|
||||||
"css-loader": "0.28.0",
|
"css-loader": "0.28.0",
|
||||||
"electron": "4.0.0",
|
"electron": "4.0.5",
|
||||||
"electron-builder": "^20.38.4",
|
"electron-builder": "^20.38.4",
|
||||||
"electron-builder-squirrel-windows": "^20.28.3",
|
"electron-builder-squirrel-windows": "^20.28.3",
|
||||||
"electron-installer-snap": "^3.0.0",
|
"electron-installer-snap": "^3.0.0",
|
||||||
@@ -39,12 +39,13 @@
|
|||||||
"shelljs": "0.7.7",
|
"shelljs": "0.7.7",
|
||||||
"source-code-pro": "^2.30.1",
|
"source-code-pro": "^2.30.1",
|
||||||
"source-sans-pro": "2.0.10",
|
"source-sans-pro": "2.0.10",
|
||||||
"style-loader": "0.13.1",
|
"style-loader": "^0.23.1",
|
||||||
"svg-inline-loader": "^0.8.0",
|
"svg-inline-loader": "^0.8.0",
|
||||||
"to-string-loader": "1.1.5",
|
"to-string-loader": "1.1.5",
|
||||||
"tslint": "^5.12.0",
|
"tslint": "^5.12.0",
|
||||||
"tslint-config-standard": "^8.0.1",
|
"tslint-config-standard": "^8.0.1",
|
||||||
"tslint-eslint-rules": "^5.4.0",
|
"tslint-eslint-rules": "^5.4.0",
|
||||||
|
"typedoc": "^0.14.2",
|
||||||
"typescript": "^3.1.3",
|
"typescript": "^3.1.3",
|
||||||
"url-loader": "^1.1.1",
|
"url-loader": "^1.1.1",
|
||||||
"val-loader": "0.5.0",
|
"val-loader": "0.5.0",
|
||||||
@@ -130,6 +131,7 @@
|
|||||||
"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",
|
||||||
"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 terminus-terminal/src && typedoc --out docs/api/settings terminus-settings/src",
|
||||||
"lint": "tslint -c tslint.json -t stylish terminus-*/src/**/*.ts terminus-*/src/*.ts app/src/*.ts",
|
"lint": "tslint -c tslint.json -t stylish terminus-*/src/**/*.ts terminus-*/src/*.ts app/src/*.ts",
|
||||||
"postinstall": "node ./scripts/install-deps.js"
|
"postinstall": "node ./scripts/install-deps.js"
|
||||||
},
|
},
|
||||||
|
31
terminus-core/README.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Terminus Core Plugin
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
See also: [Settings plugin API](./settings/), [Terminal plugin API](./settings/)
|
||||||
|
|
||||||
|
* tabbed interface services
|
||||||
|
* toolbar UI
|
||||||
|
* config file management
|
||||||
|
* hotkeys
|
||||||
|
* tab recovery
|
||||||
|
* logging
|
||||||
|
* theming
|
||||||
|
|
||||||
|
Using the API:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { AppService, TabContextMenuItemProvider } from 'terminus-core'
|
||||||
|
```
|
||||||
|
|
||||||
|
Exporting your subclasses:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
@NgModule({
|
||||||
|
...
|
||||||
|
providers: [
|
||||||
|
...
|
||||||
|
{ provide: TabContextMenuItemProvider, useClass: MyContextMenu, multi: true },
|
||||||
|
...
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
@@ -1,4 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Extend to add your own config options
|
||||||
|
*/
|
||||||
export abstract class ConfigProvider {
|
export abstract class ConfigProvider {
|
||||||
|
/**
|
||||||
|
* Default values, e.g.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* defaults = {
|
||||||
|
* myPlugin: {
|
||||||
|
* foo: 1
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
defaults: any = {}
|
defaults: any = {}
|
||||||
platformDefaults: any = {}
|
|
||||||
|
/**
|
||||||
|
* [[Platform]] specific defaults, e.g.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* platformDefaults = {
|
||||||
|
* [Platform.Windows]: {
|
||||||
|
* myPlugin: {
|
||||||
|
* bar: true
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* [Platform.macOS]: {
|
||||||
|
* myPlugin: {
|
||||||
|
* bar: false
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
platformDefaults: {[platform: string]: any} = {}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,12 @@
|
|||||||
export interface IHotkeyDescription {
|
export interface IHotkeyDescription {
|
||||||
id: string,
|
id: string
|
||||||
name: string,
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend to provide your own hotkeys. A corresponding [[ConfigProvider]]
|
||||||
|
* must also provide the `hotkeys.foo` config options with the default values
|
||||||
|
*/
|
||||||
export abstract class HotkeyProvider {
|
export abstract class HotkeyProvider {
|
||||||
hotkeys: IHotkeyDescription[] = []
|
hotkeys: IHotkeyDescription[] = []
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
|
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
|
||||||
|
export { SplitTabComponent, SplitContainer } from '../components/splitTab.component'
|
||||||
export { TabRecoveryProvider, RecoveredTab } from './tabRecovery'
|
export { TabRecoveryProvider, RecoveredTab } from './tabRecovery'
|
||||||
export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
|
export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
|
||||||
export { ConfigProvider } from './configProvider'
|
export { ConfigProvider } from './configProvider'
|
||||||
@@ -16,3 +17,4 @@ export { HotkeysService } from '../services/hotkeys.service'
|
|||||||
export { HostAppService, Platform } from '../services/hostApp.service'
|
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'
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
import { BaseTabComponent } from '../components/baseTab.component'
|
import { BaseTabComponent } from '../components/baseTab.component'
|
||||||
import { TabHeaderComponent } from '../components/tabHeader.component'
|
import { TabHeaderComponent } from '../components/tabHeader.component'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend to add items to the tab header's context menu
|
||||||
|
*/
|
||||||
export abstract class TabContextMenuItemProvider {
|
export abstract class TabContextMenuItemProvider {
|
||||||
weight = 0
|
weight = 0
|
||||||
|
|
||||||
|
@@ -1,10 +1,38 @@
|
|||||||
import { TabComponentType } from '../services/app.service'
|
import { TabComponentType } from '../services/tabs.service'
|
||||||
|
|
||||||
export interface RecoveredTab {
|
export interface RecoveredTab {
|
||||||
type: TabComponentType,
|
/**
|
||||||
options?: any,
|
* Component type to be instantiated
|
||||||
|
*/
|
||||||
|
type: TabComponentType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component instance inputs
|
||||||
|
*/
|
||||||
|
options?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend to enable recovery for your custom tab.
|
||||||
|
* This works in conjunction with [[getRecoveryToken()]]
|
||||||
|
*
|
||||||
|
* Terminus will try to find any [[TabRecoveryProvider]] that is able to process
|
||||||
|
* the recovery token previously returned by [[getRecoveryToken]].
|
||||||
|
*
|
||||||
|
* Recommended token format:
|
||||||
|
*
|
||||||
|
* ```json
|
||||||
|
* {
|
||||||
|
* type: 'my-tab-type',
|
||||||
|
* foo: 'bar',
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
export abstract class TabRecoveryProvider {
|
export abstract class TabRecoveryProvider {
|
||||||
|
/**
|
||||||
|
* @param recoveryToken a recovery token found in the saved tabs list
|
||||||
|
* @returns [[RecoveredTab]] descriptor containing tab type and component inputs
|
||||||
|
* or `null` if this token is from a different tab type or is not supported
|
||||||
|
*/
|
||||||
abstract async recover (recoveryToken: any): Promise<RecoveredTab | null>
|
abstract async recover (recoveryToken: any): Promise<RecoveredTab | null>
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Extend to add a custom CSS theme
|
||||||
|
*/
|
||||||
export abstract class Theme {
|
export abstract class Theme {
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete CSS stylesheet
|
||||||
|
*/
|
||||||
css: string
|
css: string
|
||||||
|
|
||||||
terminalBackground: string
|
terminalBackground: string
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,34 @@
|
|||||||
import { SafeHtml } from '@angular/platform-browser'
|
import { SafeHtml } from '@angular/platform-browser'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [[ToolbarButtonProvider]]
|
||||||
|
*/
|
||||||
export interface IToolbarButton {
|
export interface IToolbarButton {
|
||||||
|
/**
|
||||||
|
* Raw SVG icon code
|
||||||
|
*/
|
||||||
icon: SafeHtml
|
icon: SafeHtml
|
||||||
touchBarNSImage?: string
|
|
||||||
title: string
|
title: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional Touch Bar icon ID
|
||||||
|
*/
|
||||||
|
touchBarNSImage?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional Touch Bar button label
|
||||||
|
*/
|
||||||
touchBarTitle?: string
|
touchBarTitle?: string
|
||||||
|
|
||||||
weight?: number
|
weight?: number
|
||||||
|
|
||||||
click: () => void
|
click: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend to add buttons to the toolbar
|
||||||
|
*/
|
||||||
export abstract class ToolbarButtonProvider {
|
export abstract class ToolbarButtonProvider {
|
||||||
abstract provide (): IToolbarButton[]
|
abstract provide (): IToolbarButton[]
|
||||||
}
|
}
|
||||||
|
@@ -17,6 +17,7 @@ import { BaseTabComponent } from './baseTab.component'
|
|||||||
import { SafeModeModalComponent } from './safeModeModal.component'
|
import { SafeModeModalComponent } from './safeModeModal.component'
|
||||||
import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
|
import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
template: require('./appRoot.component.pug'),
|
template: require('./appRoot.component.pug'),
|
||||||
@@ -126,6 +127,11 @@ export class AppRootComponent {
|
|||||||
this.onGlobalHotkey()
|
this.onGlobalHotkey()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.hostApp.windowCloseRequest$.subscribe(async () => {
|
||||||
|
await this.app.closeAllTabs()
|
||||||
|
this.hostApp.closeWindow()
|
||||||
|
})
|
||||||
|
|
||||||
if (window['safeModeReason']) {
|
if (window['safeModeReason']) {
|
||||||
ngbModal.open(SafeModeModalComponent)
|
ngbModal.open(SafeModeModalComponent)
|
||||||
}
|
}
|
||||||
|
@@ -1,27 +1,58 @@
|
|||||||
import { Observable, Subject } from 'rxjs'
|
import { Observable, Subject } from 'rxjs'
|
||||||
import { ViewRef } from '@angular/core'
|
import { ViewRef } from '@angular/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an active "process" inside a tab,
|
||||||
|
* for example, a user process running inside a terminal tab
|
||||||
|
*/
|
||||||
export interface BaseTabProcess {
|
export interface BaseTabProcess {
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for custom tab components
|
||||||
|
*/
|
||||||
export abstract class BaseTabComponent {
|
export abstract class BaseTabComponent {
|
||||||
private static lastTabID = 0
|
/**
|
||||||
id: number
|
* Current tab title
|
||||||
|
*/
|
||||||
title: string
|
title: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-defined title override
|
||||||
|
*/
|
||||||
customTitle: string
|
customTitle: string
|
||||||
hasFocus = false
|
|
||||||
|
/**
|
||||||
|
* Last tab activity state
|
||||||
|
*/
|
||||||
hasActivity = false
|
hasActivity = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewRef to the tab DOM element
|
||||||
|
*/
|
||||||
hostView: ViewRef
|
hostView: ViewRef
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSS color override for the tab's header
|
||||||
|
*/
|
||||||
color: string = null
|
color: string = null
|
||||||
protected titleChange = new Subject<string>()
|
|
||||||
protected focused = new Subject<void>()
|
protected hasFocus = false
|
||||||
protected blurred = new Subject<void>()
|
|
||||||
protected progress = new Subject<number>()
|
/**
|
||||||
protected activity = new Subject<boolean>()
|
* Ping this if your recovery state has been changed and you want
|
||||||
protected destroyed = new Subject<void>()
|
* your tab state to be saved sooner
|
||||||
|
*/
|
||||||
|
protected recoveryStateChangedHint = new Subject<void>()
|
||||||
|
|
||||||
private progressClearTimeout: number
|
private progressClearTimeout: number
|
||||||
|
private titleChange = new Subject<string>()
|
||||||
|
private focused = new Subject<void>()
|
||||||
|
private blurred = new Subject<void>()
|
||||||
|
private progress = new Subject<number>()
|
||||||
|
private activity = new Subject<boolean>()
|
||||||
|
private destroyed = new Subject<void>()
|
||||||
|
|
||||||
get focused$ (): Observable<void> { return this.focused }
|
get focused$ (): Observable<void> { return this.focused }
|
||||||
get blurred$ (): Observable<void> { return this.blurred }
|
get blurred$ (): Observable<void> { return this.blurred }
|
||||||
@@ -29,9 +60,9 @@ export abstract class BaseTabComponent {
|
|||||||
get progress$ (): Observable<number> { return this.progress }
|
get progress$ (): Observable<number> { return this.progress }
|
||||||
get activity$ (): Observable<boolean> { return this.activity }
|
get activity$ (): Observable<boolean> { return this.activity }
|
||||||
get destroyed$ (): Observable<void> { return this.destroyed }
|
get destroyed$ (): Observable<void> { return this.destroyed }
|
||||||
|
get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint }
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
this.id = BaseTabComponent.lastTabID++
|
|
||||||
this.focused$.subscribe(() => {
|
this.focused$.subscribe(() => {
|
||||||
this.hasFocus = true
|
this.hasFocus = true
|
||||||
})
|
})
|
||||||
@@ -47,6 +78,11 @@ export abstract class BaseTabComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets visual progressbar on the tab
|
||||||
|
*
|
||||||
|
* @param {type} progress: value between 0 and 1, or `null` to remove
|
||||||
|
*/
|
||||||
setProgress (progress: number) {
|
setProgress (progress: number) {
|
||||||
this.progress.next(progress)
|
this.progress.next(progress)
|
||||||
if (progress) {
|
if (progress) {
|
||||||
@@ -59,24 +95,43 @@ export abstract class BaseTabComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the acticity marker on the tab header
|
||||||
|
*/
|
||||||
displayActivity (): void {
|
displayActivity (): void {
|
||||||
this.hasActivity = true
|
this.hasActivity = true
|
||||||
this.activity.next(true)
|
this.activity.next(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the acticity marker from the tab header
|
||||||
|
*/
|
||||||
clearActivity (): void {
|
clearActivity (): void {
|
||||||
this.hasActivity = false
|
this.hasActivity = false
|
||||||
this.activity.next(false)
|
this.activity.next(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override this and implement a [[TabRecoveryProvider]] to enable recovery
|
||||||
|
* for your custom tab
|
||||||
|
*
|
||||||
|
* @return JSON serializable tab state representation
|
||||||
|
* for your [[TabRecoveryProvider]] to parse
|
||||||
|
*/
|
||||||
async getRecoveryToken (): Promise<any> {
|
async getRecoveryToken (): Promise<any> {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override this to enable task completion notifications for the tab
|
||||||
|
*/
|
||||||
async getCurrentProcess (): Promise<BaseTabProcess> {
|
async getCurrentProcess (): Promise<BaseTabProcess> {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return false to prevent the tab from being closed
|
||||||
|
*/
|
||||||
async canClose (): Promise<boolean> {
|
async canClose (): Promise<boolean> {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -89,11 +144,15 @@ export abstract class BaseTabComponent {
|
|||||||
this.blurred.next()
|
this.blurred.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before the tab is closed
|
||||||
|
*/
|
||||||
destroy (): void {
|
destroy (): void {
|
||||||
this.focused.complete()
|
this.focused.complete()
|
||||||
this.blurred.complete()
|
this.blurred.complete()
|
||||||
this.titleChange.complete()
|
this.titleChange.complete()
|
||||||
this.progress.complete()
|
this.progress.complete()
|
||||||
|
this.recoveryStateChangedHint.complete()
|
||||||
this.destroyed.next()
|
this.destroyed.next()
|
||||||
this.destroyed.complete()
|
this.destroyed.complete()
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
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'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'checkbox',
|
selector: 'checkbox',
|
||||||
template: require('./checkbox.component.pug'),
|
template: require('./checkbox.component.pug'),
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
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 */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rename-tab-modal',
|
selector: 'rename-tab-modal',
|
||||||
template: require('./renameTabModal.component.pug'),
|
template: require('./renameTabModal.component.pug'),
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
template: require('./safeModeModal.component.pug'),
|
template: require('./safeModeModal.component.pug'),
|
||||||
})
|
})
|
||||||
|
5
terminus-core/src/components/splitTab.component.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
flex: auto;
|
||||||
|
}
|
519
terminus-core/src/components/splitTab.component.ts
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
import { Observable, Subject, Subscription } from 'rxjs'
|
||||||
|
import { Component, Injectable, ViewChild, ViewContainerRef, EmbeddedViewRef, OnInit, OnDestroy } from '@angular/core'
|
||||||
|
import { BaseTabComponent, BaseTabProcess } from './baseTab.component'
|
||||||
|
import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery'
|
||||||
|
import { TabsService } from '../services/tabs.service'
|
||||||
|
import { HotkeysService } from '../services/hotkeys.service'
|
||||||
|
import { TabRecoveryService } from '../services/tabRecovery.service'
|
||||||
|
|
||||||
|
export declare type SplitOrientation = 'v' | 'h'
|
||||||
|
export declare type SplitDirection = 'r' | 't' | 'b' | 'l'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes a horizontal or vertical split row or column
|
||||||
|
*/
|
||||||
|
export class SplitContainer {
|
||||||
|
orientation: SplitOrientation = 'h'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Children could be tabs or other containers
|
||||||
|
*/
|
||||||
|
children: (BaseTabComponent | SplitContainer)[] = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relative sizes of children, between 0 and 1. Total sum is 1
|
||||||
|
*/
|
||||||
|
ratios: number[] = []
|
||||||
|
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Flat list of all tabs inside this container
|
||||||
|
*/
|
||||||
|
getAllTabs () {
|
||||||
|
let r = []
|
||||||
|
for (let child of this.children) {
|
||||||
|
if (child instanceof SplitContainer) {
|
||||||
|
r = r.concat(child.getAllTabs())
|
||||||
|
} else {
|
||||||
|
r.push(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove unnecessarily nested child containers and renormalizes [[ratios]]
|
||||||
|
*/
|
||||||
|
normalize () {
|
||||||
|
for (let i = 0; i < this.children.length; i++) {
|
||||||
|
let child = this.children[i]
|
||||||
|
|
||||||
|
if (child instanceof SplitContainer) {
|
||||||
|
child.normalize()
|
||||||
|
|
||||||
|
if (child.children.length === 0) {
|
||||||
|
this.children.splice(i, 1)
|
||||||
|
this.ratios.splice(i, 1)
|
||||||
|
i--
|
||||||
|
continue
|
||||||
|
} else if (child.children.length === 1) {
|
||||||
|
this.children[i] = child.children[0]
|
||||||
|
} else if (child.orientation === this.orientation) {
|
||||||
|
let ratio = this.ratios[i]
|
||||||
|
this.children.splice(i, 1)
|
||||||
|
this.ratios.splice(i, 1)
|
||||||
|
for (let j = 0; j < child.children.length; j++) {
|
||||||
|
this.children.splice(i, 0, child.children[j])
|
||||||
|
this.ratios.splice(i, 0, child.ratios[j] * ratio)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = 0
|
||||||
|
for (let x of this.ratios) {
|
||||||
|
s += x
|
||||||
|
}
|
||||||
|
this.ratios = this.ratios.map(x => x / s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the left/top side offset for the given element index (between 0 and 1)
|
||||||
|
*/
|
||||||
|
getOffsetRatio (index: number): number {
|
||||||
|
let s = 0
|
||||||
|
for (let i = 0; i < index; i++) {
|
||||||
|
s += this.ratios[i]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
async serialize () {
|
||||||
|
let children = []
|
||||||
|
for (let child of this.children) {
|
||||||
|
if (child instanceof SplitContainer) {
|
||||||
|
children.push(await child.serialize())
|
||||||
|
} else {
|
||||||
|
children.push(await child.getRecoveryToken())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'app:split-tab',
|
||||||
|
ratios: this.ratios,
|
||||||
|
orientation: this.orientation,
|
||||||
|
children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a spanner (draggable border between two split areas)
|
||||||
|
*/
|
||||||
|
export interface SplitSpannerInfo {
|
||||||
|
container: SplitContainer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of the right/bottom split in the container
|
||||||
|
*/
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split tab is a tab that contains other tabs and allows further splitting them
|
||||||
|
* You'll mainly encounter it inside [[AppService]].tabs
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'split-tab',
|
||||||
|
template: `
|
||||||
|
<ng-container #vc></ng-container>
|
||||||
|
<split-tab-spanner
|
||||||
|
*ngFor='let spanner of _spanners'
|
||||||
|
[container]='spanner.container'
|
||||||
|
[index]='spanner.index'
|
||||||
|
(change)='onSpannerAdjusted(spanner)'
|
||||||
|
></split-tab-spanner>
|
||||||
|
`,
|
||||||
|
styles: [require('./splitTab.component.scss')],
|
||||||
|
})
|
||||||
|
export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
|
||||||
|
/** @hidden */
|
||||||
|
@ViewChild('vc', { read: ViewContainerRef }) viewContainer: ViewContainerRef
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top-level split container
|
||||||
|
*/
|
||||||
|
root: SplitContainer
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
_recoveredState: any
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
_spanners: SplitSpannerInfo[] = []
|
||||||
|
|
||||||
|
private focusedTab: BaseTabComponent
|
||||||
|
private hotkeysSubscription: Subscription
|
||||||
|
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
|
||||||
|
|
||||||
|
private tabAdded = new Subject<BaseTabComponent>()
|
||||||
|
private tabRemoved = new Subject<BaseTabComponent>()
|
||||||
|
private splitAdjusted = new Subject<SplitSpannerInfo>()
|
||||||
|
private focusChanged = new Subject<BaseTabComponent>()
|
||||||
|
|
||||||
|
get tabAdded$ (): Observable<BaseTabComponent> { return this.tabAdded }
|
||||||
|
get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when split ratio is changed for a given spanner
|
||||||
|
*/
|
||||||
|
get splitAdjusted$ (): Observable<SplitSpannerInfo> { return this.splitAdjusted }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when a different sub-tab gains focus
|
||||||
|
*/
|
||||||
|
get focusChanged$ (): Observable<BaseTabComponent> { return this.focusChanged }
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
constructor (
|
||||||
|
private hotkeys: HotkeysService,
|
||||||
|
private tabsService: TabsService,
|
||||||
|
private tabRecovery: TabRecoveryService,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this.root = new SplitContainer()
|
||||||
|
this.setTitle('')
|
||||||
|
|
||||||
|
this.focused$.subscribe(() => {
|
||||||
|
this.getAllTabs().forEach(x => x.emitFocused())
|
||||||
|
this.focus(this.focusedTab)
|
||||||
|
})
|
||||||
|
this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred()))
|
||||||
|
|
||||||
|
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
||||||
|
if (!this.hasFocus) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (hotkey) {
|
||||||
|
case 'split-right':
|
||||||
|
this.splitTab(this.focusedTab, 'r')
|
||||||
|
break
|
||||||
|
case 'split-bottom':
|
||||||
|
this.splitTab(this.focusedTab, 'b')
|
||||||
|
break
|
||||||
|
case 'split-top':
|
||||||
|
this.splitTab(this.focusedTab, 't')
|
||||||
|
break
|
||||||
|
case 'split-left':
|
||||||
|
this.splitTab(this.focusedTab, 'l')
|
||||||
|
break
|
||||||
|
case 'split-nav-left':
|
||||||
|
this.navigate('l')
|
||||||
|
break
|
||||||
|
case 'split-nav-right':
|
||||||
|
this.navigate('r')
|
||||||
|
break
|
||||||
|
case 'split-nav-up':
|
||||||
|
this.navigate('t')
|
||||||
|
break
|
||||||
|
case 'split-nav-down':
|
||||||
|
this.navigate('b')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
async ngOnInit () {
|
||||||
|
if (this._recoveredState) {
|
||||||
|
await this.recoverContainer(this.root, this._recoveredState)
|
||||||
|
this.layout()
|
||||||
|
setImmediate(() => {
|
||||||
|
this.getAllTabs().forEach(x => x.emitFocused())
|
||||||
|
this.focusAnyIn(this.root)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
ngOnDestroy () {
|
||||||
|
this.hotkeysSubscription.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns Flat list of all sub-tabs */
|
||||||
|
getAllTabs () {
|
||||||
|
return this.root.getAllTabs()
|
||||||
|
}
|
||||||
|
|
||||||
|
getFocusedTab (): BaseTabComponent {
|
||||||
|
return this.focusedTab
|
||||||
|
}
|
||||||
|
|
||||||
|
focus (tab: BaseTabComponent) {
|
||||||
|
this.focusedTab = tab
|
||||||
|
for (let x of this.getAllTabs()) {
|
||||||
|
if (x !== tab) {
|
||||||
|
x.emitBlurred()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tab) {
|
||||||
|
tab.emitFocused()
|
||||||
|
this.focusChanged.next(tab)
|
||||||
|
}
|
||||||
|
this.layout()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focuses the first available tab inside the given [[SplitContainer]]
|
||||||
|
*/
|
||||||
|
focusAnyIn (parent: BaseTabComponent | SplitContainer) {
|
||||||
|
if (!parent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (parent instanceof SplitContainer) {
|
||||||
|
this.focusAnyIn(parent.children[0])
|
||||||
|
} else {
|
||||||
|
this.focus(parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a new `tab` to the `side` of the `relative` tab
|
||||||
|
*/
|
||||||
|
addTab (tab: BaseTabComponent, relative: BaseTabComponent, side: SplitDirection) {
|
||||||
|
let target = this.getParentOf(relative) || this.root
|
||||||
|
let insertIndex = target.children.indexOf(relative)
|
||||||
|
|
||||||
|
if (
|
||||||
|
(target.orientation === 'v' && ['l', 'r'].includes(side)) ||
|
||||||
|
(target.orientation === 'h' && ['t', 'b'].includes(side))
|
||||||
|
) {
|
||||||
|
let newContainer = new SplitContainer()
|
||||||
|
newContainer.orientation = (target.orientation === 'v') ? 'h' : 'v'
|
||||||
|
newContainer.children = [relative]
|
||||||
|
newContainer.ratios = [1]
|
||||||
|
target.children[insertIndex] = newContainer
|
||||||
|
target = newContainer
|
||||||
|
insertIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insertIndex === -1) {
|
||||||
|
insertIndex = 0
|
||||||
|
} else {
|
||||||
|
insertIndex += (side === 'l' || side === 't') ? 0 : 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < target.children.length; i++) {
|
||||||
|
target.ratios[i] *= target.children.length / (target.children.length + 1)
|
||||||
|
}
|
||||||
|
target.ratios.splice(insertIndex, 0, 1 / (target.children.length + 1))
|
||||||
|
target.children.splice(insertIndex, 0, tab)
|
||||||
|
|
||||||
|
this.recoveryStateChangedHint.next()
|
||||||
|
this.attachTabView(tab)
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
this.layout()
|
||||||
|
this.tabAdded.next(tab)
|
||||||
|
this.focus(tab)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTab (tab: BaseTabComponent) {
|
||||||
|
let parent = this.getParentOf(tab)
|
||||||
|
let index = parent.children.indexOf(tab)
|
||||||
|
parent.ratios.splice(index, 1)
|
||||||
|
parent.children.splice(index, 1)
|
||||||
|
|
||||||
|
this.detachTabView(tab)
|
||||||
|
|
||||||
|
this.layout()
|
||||||
|
|
||||||
|
this.tabRemoved.next(tab)
|
||||||
|
|
||||||
|
if (this.root.children.length === 0) {
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves focus in the given direction
|
||||||
|
*/
|
||||||
|
navigate (dir: SplitDirection) {
|
||||||
|
let rel: BaseTabComponent | SplitContainer = this.focusedTab
|
||||||
|
let parent = this.getParentOf(rel)
|
||||||
|
let orientation = ['l', 'r'].includes(dir) ? 'h' : 'v'
|
||||||
|
|
||||||
|
while (parent !== this.root && parent.orientation !== orientation) {
|
||||||
|
rel = parent
|
||||||
|
parent = this.getParentOf(rel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent.orientation !== orientation) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = parent.children.indexOf(rel)
|
||||||
|
if (['l', 't'].includes(dir)) {
|
||||||
|
if (index > 0) {
|
||||||
|
this.focusAnyIn(parent.children[index - 1])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < parent.children.length - 1) {
|
||||||
|
this.focusAnyIn(parent.children[index + 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async splitTab (tab: BaseTabComponent, dir: SplitDirection) {
|
||||||
|
let newTab = await this.tabsService.duplicate(tab)
|
||||||
|
this.addTab(newTab, tab, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the immediate parent of `tab`
|
||||||
|
*/
|
||||||
|
getParentOf (tab: BaseTabComponent | SplitContainer, root?: SplitContainer): SplitContainer {
|
||||||
|
root = root || this.root
|
||||||
|
for (let child of root.children) {
|
||||||
|
if (child instanceof SplitContainer) {
|
||||||
|
let r = this.getParentOf(tab, child)
|
||||||
|
if (r) {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (child === tab) {
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
async canClose (): Promise<boolean> {
|
||||||
|
return !(await Promise.all(this.getAllTabs().map(x => x.canClose()))).some(x => !x)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
async getRecoveryToken (): Promise<any> {
|
||||||
|
return this.root.serialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
async getCurrentProcess (): Promise<BaseTabProcess> {
|
||||||
|
return (await Promise.all(this.getAllTabs().map(x => x.getCurrentProcess()))).find(x => !!x)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
onSpannerAdjusted (spanner: SplitSpannerInfo) {
|
||||||
|
this.layout()
|
||||||
|
this.splitAdjusted.next(spanner)
|
||||||
|
}
|
||||||
|
|
||||||
|
private attachTabView (tab: BaseTabComponent) {
|
||||||
|
let ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any>
|
||||||
|
this.viewRefs.set(tab, ref)
|
||||||
|
|
||||||
|
ref.rootNodes[0].addEventListener('click', () => this.focus(tab))
|
||||||
|
|
||||||
|
tab.titleChange$.subscribe(t => this.setTitle(t))
|
||||||
|
tab.activity$.subscribe(a => a ? this.displayActivity() : this.clearActivity())
|
||||||
|
tab.progress$.subscribe(p => this.setProgress(p))
|
||||||
|
if (tab.title) {
|
||||||
|
this.setTitle(tab.title)
|
||||||
|
}
|
||||||
|
tab.destroyed$.subscribe(() => {
|
||||||
|
this.removeTab(tab)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private detachTabView (tab: BaseTabComponent) {
|
||||||
|
let ref = this.viewRefs.get(tab)
|
||||||
|
this.viewRefs.delete(tab)
|
||||||
|
this.viewContainer.remove(this.viewContainer.indexOf(ref))
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
let size = (root.orientation === 'v') ? h : w
|
||||||
|
let sizes = root.ratios.map(x => x * size)
|
||||||
|
|
||||||
|
root.x = x
|
||||||
|
root.y = y
|
||||||
|
root.w = w
|
||||||
|
root.h = h
|
||||||
|
|
||||||
|
let offset = 0
|
||||||
|
root.children.forEach((child, i) => {
|
||||||
|
let childX = (root.orientation === 'v') ? x : (x + offset)
|
||||||
|
let childY = (root.orientation === 'v') ? (y + offset) : y
|
||||||
|
let childW = (root.orientation === 'v') ? w : sizes[i]
|
||||||
|
let childH = (root.orientation === 'v') ? sizes[i] : h
|
||||||
|
if (child instanceof SplitContainer) {
|
||||||
|
this.layoutInternal(child, childX, childY, childW, childH)
|
||||||
|
} else {
|
||||||
|
let element = this.viewRefs.get(child).rootNodes[0]
|
||||||
|
element.style.position = 'absolute'
|
||||||
|
element.style.left = `${childX}%`
|
||||||
|
element.style.top = `${childY}%`
|
||||||
|
element.style.width = `${childW}%`
|
||||||
|
element.style.height = `${childH}%`
|
||||||
|
|
||||||
|
element.style.opacity = (child === this.focusedTab) ? 1 : 0.75
|
||||||
|
}
|
||||||
|
offset += sizes[i]
|
||||||
|
|
||||||
|
if (i !== 0) {
|
||||||
|
this._spanners.push({
|
||||||
|
container: root,
|
||||||
|
index: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async recoverContainer (root: SplitContainer, state: any) {
|
||||||
|
let children: (SplitContainer | BaseTabComponent)[] = []
|
||||||
|
root.orientation = state.orientation
|
||||||
|
root.ratios = state.ratios
|
||||||
|
root.children = children
|
||||||
|
for (let childState of state.children) {
|
||||||
|
if (childState.type === 'app:split-tab') {
|
||||||
|
let child = new SplitContainer()
|
||||||
|
await this.recoverContainer(child, childState)
|
||||||
|
children.push(child)
|
||||||
|
} else {
|
||||||
|
let recovered = await this.tabRecovery.recoverTab(childState)
|
||||||
|
if (recovered) {
|
||||||
|
let tab = this.tabsService.create(recovered.type, recovered.options)
|
||||||
|
children.push(tab)
|
||||||
|
this.attachTabView(tab)
|
||||||
|
} else {
|
||||||
|
state.ratios.splice(state.children.indexOf(childState), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Injectable()
|
||||||
|
export class SplitTabRecoveryProvider extends TabRecoveryProvider {
|
||||||
|
async recover (recoveryToken: any): Promise<RecoveredTab> {
|
||||||
|
if (recoveryToken && recoveryToken.type === 'app:split-tab') {
|
||||||
|
return {
|
||||||
|
type: SplitTabComponent,
|
||||||
|
options: { _recoveredState: recoveryToken },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
22
terminus-core/src/components/splitTabSpanner.component.scss
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5;
|
||||||
|
transition: 0.125s background;
|
||||||
|
|
||||||
|
&.v {
|
||||||
|
cursor: ns-resize;
|
||||||
|
height: 10px;
|
||||||
|
margin-top: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.h {
|
||||||
|
cursor: ew-resize;
|
||||||
|
width: 10px;
|
||||||
|
margin-left: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
background: rgba(255, 255, 255, .125);
|
||||||
|
}
|
||||||
|
}
|
88
terminus-core/src/components/splitTabSpanner.component.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
|
||||||
|
import { SplitContainer } from './splitTab.component'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Component({
|
||||||
|
selector: 'split-tab-spanner',
|
||||||
|
template: '',
|
||||||
|
styles: [require('./splitTabSpanner.component.scss')],
|
||||||
|
})
|
||||||
|
export class SplitTabSpannerComponent {
|
||||||
|
@Input() container: SplitContainer
|
||||||
|
@Input() index: number
|
||||||
|
@Output() change = new EventEmitter<void>()
|
||||||
|
@HostBinding('class.active') isActive = false
|
||||||
|
@HostBinding('class.h') isHorizontal = false
|
||||||
|
@HostBinding('class.v') isVertical = true
|
||||||
|
@HostBinding('style.left') cssLeft: string
|
||||||
|
@HostBinding('style.top') cssTop: string
|
||||||
|
@HostBinding('style.width') cssWidth: string
|
||||||
|
@HostBinding('style.height') cssHeight: string
|
||||||
|
private marginOffset = -5
|
||||||
|
|
||||||
|
constructor (private element: ElementRef) { }
|
||||||
|
|
||||||
|
ngAfterViewInit () {
|
||||||
|
this.element.nativeElement.addEventListener('mousedown', e => {
|
||||||
|
this.isActive = true
|
||||||
|
let start = this.isVertical ? e.pageY : e.pageX
|
||||||
|
let current = start
|
||||||
|
let oldPosition = this.isVertical ? this.element.nativeElement.offsetTop : this.element.nativeElement.offsetLeft
|
||||||
|
|
||||||
|
const dragHandler = e => {
|
||||||
|
current = this.isVertical ? e.pageY : e.pageX
|
||||||
|
let newPosition = oldPosition + (current - start)
|
||||||
|
if (this.isVertical) {
|
||||||
|
this.element.nativeElement.style.top = `${newPosition - this.marginOffset}px`
|
||||||
|
} else {
|
||||||
|
this.element.nativeElement.style.left = `${newPosition - this.marginOffset}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const offHandler = () => {
|
||||||
|
this.isActive = false
|
||||||
|
document.removeEventListener('mouseup', offHandler)
|
||||||
|
this.element.nativeElement.parentElement.removeEventListener('mousemove', dragHandler)
|
||||||
|
|
||||||
|
let diff = (current - start) / (this.isVertical ? this.element.nativeElement.parentElement.clientHeight : this.element.nativeElement.parentElement.clientWidth)
|
||||||
|
|
||||||
|
diff = Math.max(diff, -this.container.ratios[this.index - 1] + 0.1)
|
||||||
|
diff = Math.min(diff, this.container.ratios[this.index] - 0.1)
|
||||||
|
|
||||||
|
this.container.ratios[this.index - 1] += diff
|
||||||
|
this.container.ratios[this.index] -= diff
|
||||||
|
this.change.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', offHandler)
|
||||||
|
this.element.nativeElement.parentElement.addEventListener('mousemove', dragHandler)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges () {
|
||||||
|
this.isHorizontal = this.container.orientation === 'h'
|
||||||
|
this.isVertical = this.container.orientation === 'v'
|
||||||
|
if (this.isVertical) {
|
||||||
|
this.setDimensions(
|
||||||
|
this.container.x,
|
||||||
|
this.container.y + this.container.h * this.container.getOffsetRatio(this.index),
|
||||||
|
this.container.w,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.setDimensions(
|
||||||
|
this.container.x + this.container.w * this.container.getOffsetRatio(this.index),
|
||||||
|
this.container.y,
|
||||||
|
null,
|
||||||
|
this.container.h
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setDimensions (x: number, y: number, w: number, h: number) {
|
||||||
|
this.cssLeft = `${x}%`
|
||||||
|
this.cssTop = `${y}%`
|
||||||
|
this.cssWidth = w ? `${w}%` : null
|
||||||
|
this.cssHeight = h ? `${h}%` : null
|
||||||
|
}
|
||||||
|
}
|
@@ -3,6 +3,7 @@ import { ConfigService } from '../services/config.service'
|
|||||||
import { HomeBaseService } from '../services/homeBase.service'
|
import { HomeBaseService } from '../services/homeBase.service'
|
||||||
import { IToolbarButton, ToolbarButtonProvider } from '../api'
|
import { IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'start-page',
|
selector: 'start-page',
|
||||||
template: require('./startPage.component.pug'),
|
template: require('./startPage.component.pug'),
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
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'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tab-body',
|
selector: 'tab-body',
|
||||||
template: `
|
template: `
|
||||||
|
@@ -9,6 +9,7 @@ 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'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tab-header',
|
selector: 'tab-header',
|
||||||
template: require('./tabHeader.component.pug'),
|
template: require('./tabHeader.component.pug'),
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'title-bar',
|
selector: 'title-bar',
|
||||||
template: require('./titleBar.component.pug'),
|
template: require('./titleBar.component.pug'),
|
||||||
|
@@ -2,6 +2,7 @@ import { Component } from '@angular/core'
|
|||||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
import { CheckboxComponent } from './checkbox.component'
|
import { CheckboxComponent } from './checkbox.component'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'toggle',
|
selector: 'toggle',
|
||||||
template: `
|
template: `
|
||||||
|
@@ -9,7 +9,7 @@ button.btn.btn-secondary.btn-maximize(
|
|||||||
svg(version='1.1', width='10', height='10')
|
svg(version='1.1', width='10', height='10')
|
||||||
path(d='M 0,0 0,10 10,10 10,0 Z M 1,1 9,1 9,9 1,9 Z')
|
path(d='M 0,0 0,10 10,10 10,0 Z M 1,1 9,1 9,9 1,9 Z')
|
||||||
button.btn.btn-secondary.btn-close(
|
button.btn.btn-secondary.btn-close(
|
||||||
(click)='app.closeWindow()'
|
(click)='closeWindow()'
|
||||||
)
|
)
|
||||||
svg(version='1.1', width='10', height='10')
|
svg(version='1.1', width='10', height='10')
|
||||||
path(d='M 0,0 0,0.7 4.3,5 0,9.3 0,10 0.7,10 5,5.7 9.3,10 10,10 10,9.3 5.7,5 10,0.7 10,0 9.3,0 5,4.3 0.7,0 Z')
|
path(d='M 0,0 0,0.7 4.3,5 0,9.3 0,10 0.7,10 5,5.7 9.3,10 10,10 10,9.3 5.7,5 10,0.7 10,0 9.3,0 5,4.3 0.7,0 Z')
|
||||||
|
@@ -2,6 +2,7 @@ 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'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'window-controls',
|
selector: 'window-controls',
|
||||||
template: require('./windowControls.component.pug'),
|
template: require('./windowControls.component.pug'),
|
||||||
@@ -9,4 +10,9 @@ import { AppService } from '../services/app.service'
|
|||||||
})
|
})
|
||||||
export class WindowControlsComponent {
|
export class WindowControlsComponent {
|
||||||
constructor (public hostApp: HostAppService, public app: AppService) { }
|
constructor (public hostApp: HostAppService, public app: AppService) { }
|
||||||
|
|
||||||
|
async closeWindow () {
|
||||||
|
await this.app.closeAllTabs()
|
||||||
|
this.hostApp.closeWindow()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { ConfigProvider } from './api/configProvider'
|
import { ConfigProvider } from './api/configProvider'
|
||||||
import { Platform } from './services/hostApp.service'
|
import { Platform } from './services/hostApp.service'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
export class CoreConfigProvider extends ConfigProvider {
|
export class CoreConfigProvider extends ConfigProvider {
|
||||||
platformDefaults = {
|
platformDefaults = {
|
||||||
[Platform.macOS]: require('./configDefaults.macos.yaml'),
|
[Platform.macOS]: require('./configDefaults.macos.yaml'),
|
||||||
|
@@ -36,4 +36,18 @@ hotkeys:
|
|||||||
- 'Alt-9'
|
- 'Alt-9'
|
||||||
tab-10:
|
tab-10:
|
||||||
- 'Alt-0'
|
- 'Alt-0'
|
||||||
|
split-right:
|
||||||
|
- 'Ctrl-Shift-E'
|
||||||
|
split-bottom:
|
||||||
|
- 'Ctrl-Shift-D'
|
||||||
|
split-left: []
|
||||||
|
split-top: []
|
||||||
|
split-nav-right:
|
||||||
|
- 'Ctrl-Alt-ArrowRight'
|
||||||
|
split-nav-down:
|
||||||
|
- 'Ctrl-Alt-ArrowDown'
|
||||||
|
split-nav-up:
|
||||||
|
- 'Ctrl-Alt-ArrowUp'
|
||||||
|
split-nav-left:
|
||||||
|
- 'Ctrl-Alt-ArrowLeft'
|
||||||
pluginBlacklist: ['ssh']
|
pluginBlacklist: ['ssh']
|
||||||
|
@@ -34,4 +34,18 @@ hotkeys:
|
|||||||
- '⌘-9'
|
- '⌘-9'
|
||||||
tab-10:
|
tab-10:
|
||||||
- '⌘-0'
|
- '⌘-0'
|
||||||
|
split-right:
|
||||||
|
- '⌘-Shift-D'
|
||||||
|
split-bottom:
|
||||||
|
- '⌘-D'
|
||||||
|
split-left: []
|
||||||
|
split-top: []
|
||||||
|
split-nav-right:
|
||||||
|
- '⌘-⌥-ArrowRight'
|
||||||
|
split-nav-down:
|
||||||
|
- '⌘-⌥-ArrowDown'
|
||||||
|
split-nav-up:
|
||||||
|
- '⌘-⌥-ArrowUp'
|
||||||
|
split-nav-left:
|
||||||
|
- '⌘-⌥-ArrowLeft'
|
||||||
pluginBlacklist: ['ssh']
|
pluginBlacklist: ['ssh']
|
||||||
|
@@ -36,4 +36,18 @@ hotkeys:
|
|||||||
- 'Alt-9'
|
- 'Alt-9'
|
||||||
tab-10:
|
tab-10:
|
||||||
- 'Alt-0'
|
- 'Alt-0'
|
||||||
|
split-right:
|
||||||
|
- 'Ctrl-Shift-E'
|
||||||
|
split-bottom:
|
||||||
|
- 'Ctrl-Shift-D'
|
||||||
|
split-left: []
|
||||||
|
split-top: []
|
||||||
|
split-nav-right:
|
||||||
|
- 'Ctrl-Alt-ArrowRight'
|
||||||
|
split-nav-down:
|
||||||
|
- 'Ctrl-Alt-ArrowDown'
|
||||||
|
split-nav-up:
|
||||||
|
- 'Ctrl-Alt-ArrowUp'
|
||||||
|
split-nav-left:
|
||||||
|
- 'Ctrl-Alt-ArrowLeft'
|
||||||
pluginBlacklist: []
|
pluginBlacklist: []
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { Directive, AfterViewInit, ElementRef } from '@angular/core'
|
import { Directive, AfterViewInit, ElementRef } from '@angular/core'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[autofocus]'
|
selector: '[autofocus]'
|
||||||
})
|
})
|
||||||
|
117
terminus-core/src/hotkeys.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { IHotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
@Injectable()
|
||||||
|
export class AppHotkeyProvider extends HotkeyProvider {
|
||||||
|
hotkeys: IHotkeyDescription[] = [
|
||||||
|
{
|
||||||
|
id: 'new-window',
|
||||||
|
name: 'New window',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'toggle-window',
|
||||||
|
name: 'Toggle terminal window',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'toggle-fullscreen',
|
||||||
|
name: 'Toggle fullscreen mode',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rename-tab',
|
||||||
|
name: 'Rename Tab',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'close-tab',
|
||||||
|
name: 'Close tab',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'toggle-last-tab',
|
||||||
|
name: 'Toggle last tab',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'next-tab',
|
||||||
|
name: 'Next tab',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'previous-tab',
|
||||||
|
name: 'Previous tab',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-1',
|
||||||
|
name: 'Tab 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-2',
|
||||||
|
name: 'Tab 2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-3',
|
||||||
|
name: 'Tab 3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-4',
|
||||||
|
name: 'Tab 4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-5',
|
||||||
|
name: 'Tab 5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-6',
|
||||||
|
name: 'Tab 6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-7',
|
||||||
|
name: 'Tab 7',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-8',
|
||||||
|
name: 'Tab 8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-9',
|
||||||
|
name: 'Tab 9',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tab-10',
|
||||||
|
name: 'Tab 10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'split-right',
|
||||||
|
name: 'Split to the right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'split-bottom',
|
||||||
|
name: 'Split to the bottom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'split-left',
|
||||||
|
name: 'Split to the left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'split-top',
|
||||||
|
name: 'Split to the top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'split-nav-up',
|
||||||
|
name: 'Focus the pane above',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'split-nav-down',
|
||||||
|
name: 'Focus the pane below',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'split-nav-left',
|
||||||
|
name: 'Focus the pane on the left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'split-nav-right',
|
||||||
|
name: 'Focus the pane on the right',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async provide (): Promise<IHotkeyDescription[]> {
|
||||||
|
return this.hotkeys
|
||||||
|
}
|
||||||
|
}
|
@@ -6,8 +6,6 @@ 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 { AppHotkeyProvider } from './services/hotkeys.service'
|
|
||||||
|
|
||||||
import { AppRootComponent } from './components/appRoot.component'
|
import { AppRootComponent } from './components/appRoot.component'
|
||||||
import { CheckboxComponent } from './components/checkbox.component'
|
import { CheckboxComponent } from './components/checkbox.component'
|
||||||
import { TabBodyComponent } from './components/tabBody.component'
|
import { TabBodyComponent } from './components/tabBody.component'
|
||||||
@@ -18,6 +16,8 @@ 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 { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component'
|
||||||
|
import { SplitTabSpannerComponent } from './components/splitTabSpanner.component'
|
||||||
|
|
||||||
import { AutofocusDirective } from './directives/autofocus.directive'
|
import { AutofocusDirective } from './directives/autofocus.directive'
|
||||||
|
|
||||||
@@ -25,9 +25,11 @@ 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 { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
|
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
|
||||||
import { CoreConfigProvider } from './config'
|
import { CoreConfigProvider } from './config'
|
||||||
|
import { AppHotkeyProvider } from './hotkeys'
|
||||||
import { TaskCompletionContextMenu, CommonOptionsContextMenu, CloseContextMenu } from './tabContextMenu'
|
import { TaskCompletionContextMenu, CommonOptionsContextMenu, CloseContextMenu } from './tabContextMenu'
|
||||||
|
|
||||||
import 'perfect-scrollbar/css/perfect-scrollbar.css'
|
import 'perfect-scrollbar/css/perfect-scrollbar.css'
|
||||||
@@ -42,9 +44,11 @@ const PROVIDERS = [
|
|||||||
{ provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true },
|
{ provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true },
|
||||||
{ provide: TabContextMenuItemProvider, useClass: CloseContextMenu, 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: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } }
|
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@@ -66,10 +70,13 @@ const PROVIDERS = [
|
|||||||
RenameTabModalComponent,
|
RenameTabModalComponent,
|
||||||
SafeModeModalComponent,
|
SafeModeModalComponent,
|
||||||
AutofocusDirective,
|
AutofocusDirective,
|
||||||
|
SplitTabComponent,
|
||||||
|
SplitTabSpannerComponent,
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
RenameTabModalComponent,
|
RenameTabModalComponent,
|
||||||
SafeModeModalComponent,
|
SafeModeModalComponent,
|
||||||
|
SplitTabComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
CheckboxComponent,
|
CheckboxComponent,
|
||||||
|
@@ -1,13 +1,12 @@
|
|||||||
import { Observable, Subject, AsyncSubject } from 'rxjs'
|
import { Observable, Subject, AsyncSubject } from 'rxjs'
|
||||||
import { takeUntil } from 'rxjs/operators'
|
import { takeUntil } from 'rxjs/operators'
|
||||||
import { Injectable, ComponentFactoryResolver, Injector } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { BaseTabComponent } from '../components/baseTab.component'
|
import { BaseTabComponent } from '../components/baseTab.component'
|
||||||
import { Logger, LogService } from './log.service'
|
import { SplitTabComponent } from '../components/splitTab.component'
|
||||||
import { ConfigService } from './config.service'
|
import { ConfigService } from './config.service'
|
||||||
import { HostAppService } from './hostApp.service'
|
import { HostAppService } from './hostApp.service'
|
||||||
import { TabRecoveryService } from './tabRecovery.service'
|
import { TabRecoveryService } from './tabRecovery.service'
|
||||||
|
import { TabsService, TabComponentType } from './tabs.service'
|
||||||
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
|
|
||||||
|
|
||||||
class CompletionObserver {
|
class CompletionObserver {
|
||||||
get done$ (): Observable<void> { return this.done }
|
get done$ (): Observable<void> { return this.done }
|
||||||
@@ -39,9 +38,11 @@ class CompletionObserver {
|
|||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AppService {
|
export class AppService {
|
||||||
tabs: BaseTabComponent[] = []
|
tabs: BaseTabComponent[] = []
|
||||||
activeTab: BaseTabComponent
|
|
||||||
lastTabIndex = 0
|
get activeTab (): BaseTabComponent { return this._activeTab }
|
||||||
logger: Logger
|
|
||||||
|
private lastTabIndex = 0
|
||||||
|
private _activeTab: BaseTabComponent
|
||||||
|
|
||||||
private activeTabChange = new Subject<BaseTabComponent>()
|
private activeTabChange = new Subject<BaseTabComponent>()
|
||||||
private tabsChanged = new Subject<void>()
|
private tabsChanged = new Subject<void>()
|
||||||
@@ -55,23 +56,20 @@ export class AppService {
|
|||||||
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
|
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
|
||||||
get tabsChanged$ (): Observable<void> { return this.tabsChanged }
|
get tabsChanged$ (): Observable<void> { return this.tabsChanged }
|
||||||
get tabClosed$ (): Observable<BaseTabComponent> { return this.tabClosed }
|
get tabClosed$ (): Observable<BaseTabComponent> { return this.tabClosed }
|
||||||
|
|
||||||
|
/** Fires once when the app is ready */
|
||||||
get ready$ (): Observable<void> { return this.ready }
|
get ready$ (): Observable<void> { return this.ready }
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
constructor (
|
constructor (
|
||||||
private componentFactoryResolver: ComponentFactoryResolver,
|
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
private hostApp: HostAppService,
|
private hostApp: HostAppService,
|
||||||
private injector: Injector,
|
|
||||||
private tabRecovery: TabRecoveryService,
|
private tabRecovery: TabRecoveryService,
|
||||||
log: LogService,
|
private tabsService: TabsService,
|
||||||
) {
|
) {
|
||||||
this.logger = log.create('app')
|
|
||||||
|
|
||||||
this.hostApp.windowCloseRequest$.subscribe(() => this.closeWindow())
|
|
||||||
|
|
||||||
this.tabRecovery.recoverTabs().then(tabs => {
|
this.tabRecovery.recoverTabs().then(tabs => {
|
||||||
for (let tab of tabs) {
|
for (let tab of tabs) {
|
||||||
this.openNewTab(tab.type, tab.options)
|
this.openNewTabRaw(tab.type, tab.options)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tabsChanged$.subscribe(() => {
|
this.tabsChanged$.subscribe(() => {
|
||||||
@@ -83,50 +81,80 @@ export class AppService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
openNewTab (type: TabComponentType, inputs?: any): BaseTabComponent {
|
private addTabRaw (tab: BaseTabComponent) {
|
||||||
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(type)
|
|
||||||
let componentRef = componentFactory.create(this.injector)
|
|
||||||
let tab = componentRef.instance
|
|
||||||
tab.hostView = componentRef.hostView
|
|
||||||
Object.assign(tab, inputs || {})
|
|
||||||
|
|
||||||
this.tabs.push(tab)
|
this.tabs.push(tab)
|
||||||
this.selectTab(tab)
|
this.selectTab(tab)
|
||||||
this.tabsChanged.next()
|
this.tabsChanged.next()
|
||||||
this.tabOpened.next(tab)
|
this.tabOpened.next(tab)
|
||||||
|
|
||||||
|
tab.recoveryStateChangedHint$.subscribe(() => {
|
||||||
|
this.tabRecovery.saveTabs(this.tabs)
|
||||||
|
})
|
||||||
|
|
||||||
tab.titleChange$.subscribe(title => {
|
tab.titleChange$.subscribe(title => {
|
||||||
if (tab === this.activeTab) {
|
if (tab === this._activeTab) {
|
||||||
this.hostApp.setTitle(title)
|
this.hostApp.setTitle(title)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
tab.destroyed$.subscribe(() => {
|
||||||
|
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
||||||
|
this.tabs = this.tabs.filter((x) => x !== tab)
|
||||||
|
if (tab === this._activeTab) {
|
||||||
|
this.selectTab(this.tabs[newIndex])
|
||||||
|
}
|
||||||
|
this.tabsChanged.next()
|
||||||
|
this.tabClosed.next(tab)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new tab **without** wrapping it in a SplitTabComponent
|
||||||
|
* @param inputs Properties to be assigned on the new tab component instance
|
||||||
|
*/
|
||||||
|
openNewTabRaw (type: TabComponentType, inputs?: any): BaseTabComponent {
|
||||||
|
let tab = this.tabsService.create(type, inputs)
|
||||||
|
this.addTabRaw(tab)
|
||||||
|
return tab
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new tab while wrapping it in a SplitTabComponent
|
||||||
|
* @param inputs Properties to be assigned on the new tab component instance
|
||||||
|
*/
|
||||||
|
openNewTab (type: TabComponentType, inputs?: any): BaseTabComponent {
|
||||||
|
let splitTab = this.tabsService.create(SplitTabComponent) as SplitTabComponent
|
||||||
|
let tab = this.tabsService.create(type, inputs)
|
||||||
|
splitTab.addTab(tab, null, 'r')
|
||||||
|
this.addTabRaw(splitTab)
|
||||||
return tab
|
return tab
|
||||||
}
|
}
|
||||||
|
|
||||||
selectTab (tab: BaseTabComponent) {
|
selectTab (tab: BaseTabComponent) {
|
||||||
if (this.activeTab === tab) {
|
if (this._activeTab === tab) {
|
||||||
this.activeTab.emitFocused()
|
this._activeTab.emitFocused()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.tabs.includes(this.activeTab)) {
|
if (this.tabs.includes(this._activeTab)) {
|
||||||
this.lastTabIndex = this.tabs.indexOf(this.activeTab)
|
this.lastTabIndex = this.tabs.indexOf(this._activeTab)
|
||||||
} else {
|
} else {
|
||||||
this.lastTabIndex = null
|
this.lastTabIndex = null
|
||||||
}
|
}
|
||||||
if (this.activeTab) {
|
if (this._activeTab) {
|
||||||
this.activeTab.clearActivity()
|
this._activeTab.clearActivity()
|
||||||
this.activeTab.emitBlurred()
|
this._activeTab.emitBlurred()
|
||||||
}
|
}
|
||||||
this.activeTab = tab
|
this._activeTab = tab
|
||||||
this.activeTabChange.next(tab)
|
this.activeTabChange.next(tab)
|
||||||
if (this.activeTab) {
|
if (this._activeTab) {
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
this.activeTab.emitFocused()
|
this._activeTab.emitFocused()
|
||||||
})
|
})
|
||||||
this.hostApp.setTitle(this.activeTab.title)
|
this.hostApp.setTitle(this._activeTab.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Switches between the current tab and the previously active one */
|
||||||
toggleLastTab () {
|
toggleLastTab () {
|
||||||
if (!this.lastTabIndex || this.lastTabIndex >= this.tabs.length) {
|
if (!this.lastTabIndex || this.lastTabIndex >= this.tabs.length) {
|
||||||
this.lastTabIndex = 0
|
this.lastTabIndex = 0
|
||||||
@@ -136,7 +164,7 @@ export class AppService {
|
|||||||
|
|
||||||
nextTab () {
|
nextTab () {
|
||||||
if (this.tabs.length > 1) {
|
if (this.tabs.length > 1) {
|
||||||
let tabIndex = this.tabs.indexOf(this.activeTab)
|
let tabIndex = this.tabs.indexOf(this._activeTab)
|
||||||
if (tabIndex < this.tabs.length - 1) {
|
if (tabIndex < this.tabs.length - 1) {
|
||||||
this.selectTab(this.tabs[tabIndex + 1])
|
this.selectTab(this.tabs[tabIndex + 1])
|
||||||
} else if (this.config.store.appearance.cycleTabs) {
|
} else if (this.config.store.appearance.cycleTabs) {
|
||||||
@@ -147,7 +175,7 @@ export class AppService {
|
|||||||
|
|
||||||
previousTab () {
|
previousTab () {
|
||||||
if (this.tabs.length > 1) {
|
if (this.tabs.length > 1) {
|
||||||
let tabIndex = this.tabs.indexOf(this.activeTab)
|
let tabIndex = this.tabs.indexOf(this._activeTab)
|
||||||
if (tabIndex > 0) {
|
if (tabIndex > 0) {
|
||||||
this.selectTab(this.tabs[tabIndex - 1])
|
this.selectTab(this.tabs[tabIndex - 1])
|
||||||
} else if (this.config.store.appearance.cycleTabs) {
|
} else if (this.config.store.appearance.cycleTabs) {
|
||||||
@@ -156,6 +184,7 @@ export class AppService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
emitTabsChanged () {
|
emitTabsChanged () {
|
||||||
this.tabsChanged.next()
|
this.tabsChanged.next()
|
||||||
}
|
}
|
||||||
@@ -167,28 +196,17 @@ export class AppService {
|
|||||||
if (checkCanClose && !await tab.canClose()) {
|
if (checkCanClose && !await tab.canClose()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
|
||||||
this.tabs = this.tabs.filter((x) => x !== tab)
|
|
||||||
tab.destroy()
|
tab.destroy()
|
||||||
if (tab === this.activeTab) {
|
|
||||||
this.selectTab(this.tabs[newIndex])
|
|
||||||
}
|
|
||||||
this.tabsChanged.next()
|
|
||||||
this.tabClosed.next(tab)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async duplicateTab (tab: BaseTabComponent) {
|
async duplicateTab (tab: BaseTabComponent) {
|
||||||
let token = await tab.getRecoveryToken()
|
let dup = await this.tabsService.duplicate(tab)
|
||||||
if (!token) {
|
if (dup) {
|
||||||
return
|
this.addTabRaw(dup)
|
||||||
}
|
|
||||||
let recoveredTab = await this.tabRecovery.recoverTab(token)
|
|
||||||
if (recoveredTab) {
|
|
||||||
this.openNewTab(recoveredTab.type, recoveredTab.options)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeWindow () {
|
async closeAllTabs () {
|
||||||
for (let tab of this.tabs) {
|
for (let tab of this.tabs) {
|
||||||
if (!await tab.canClose()) {
|
if (!await tab.canClose()) {
|
||||||
return
|
return
|
||||||
@@ -197,15 +215,19 @@ export class AppService {
|
|||||||
for (let tab of this.tabs) {
|
for (let tab of this.tabs) {
|
||||||
tab.destroy()
|
tab.destroy()
|
||||||
}
|
}
|
||||||
this.hostApp.closeWindow()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
emitReady () {
|
emitReady () {
|
||||||
this.ready.next(null)
|
this.ready.next(null)
|
||||||
this.ready.complete()
|
this.ready.complete()
|
||||||
this.hostApp.emitReady()
|
this.hostApp.emitReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable that fires once
|
||||||
|
* the tab's internal "process" (see [[BaseTabProcess]]) completes
|
||||||
|
*/
|
||||||
observeTabCompletion (tab: BaseTabComponent): Observable<void> {
|
observeTabCompletion (tab: BaseTabComponent): Observable<void> {
|
||||||
if (!this.completionObservers.has(tab)) {
|
if (!this.completionObservers.has(tab)) {
|
||||||
let observer = new CompletionObserver(tab)
|
let observer = new CompletionObserver(tab)
|
||||||
|
@@ -18,6 +18,7 @@ function isNonStructuralObjectMember (v) {
|
|||||||
return v instanceof Object && !(v instanceof Array) && v.__nonStructural
|
return v instanceof Object && !(v instanceof Array) && v.__nonStructural
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
export class ConfigProxy {
|
export class ConfigProxy {
|
||||||
constructor (real: any, defaults: any) {
|
constructor (real: any, defaults: any) {
|
||||||
for (let key in defaults) {
|
for (let key in defaults) {
|
||||||
@@ -76,16 +77,29 @@ export class ConfigProxy {
|
|||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ConfigService {
|
export class ConfigService {
|
||||||
|
/**
|
||||||
|
* Contains the actual config values
|
||||||
|
*/
|
||||||
store: any
|
store: any
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether an app restart is required due to recent changes
|
||||||
|
*/
|
||||||
restartRequested: boolean
|
restartRequested: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full config file path
|
||||||
|
*/
|
||||||
|
path: string
|
||||||
|
|
||||||
private changed = new Subject<void>()
|
private changed = new Subject<void>()
|
||||||
private _store: any
|
private _store: any
|
||||||
private path: string
|
|
||||||
private defaults: any
|
private defaults: any
|
||||||
private servicesCache: { [id: string]: Function[] } = null
|
private servicesCache: { [id: string]: Function[] } = null
|
||||||
|
|
||||||
get changed$ (): Observable<void> { return this.changed }
|
get changed$ (): Observable<void> { return this.changed }
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
constructor (
|
constructor (
|
||||||
electron: ElectronService,
|
electron: ElectronService,
|
||||||
private hostApp: HostAppService,
|
private hostApp: HostAppService,
|
||||||
@@ -129,10 +143,16 @@ export class ConfigService {
|
|||||||
this.hostApp.broadcastConfigChange()
|
this.hostApp.broadcastConfigChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads config YAML as string
|
||||||
|
*/
|
||||||
readRaw (): string {
|
readRaw (): string {
|
||||||
return yaml.safeDump(this._store)
|
return yaml.safeDump(this._store)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes config YAML as string
|
||||||
|
*/
|
||||||
writeRaw (data: string): void {
|
writeRaw (data: string): void {
|
||||||
this._store = yaml.safeLoad(data)
|
this._store = yaml.safeLoad(data)
|
||||||
this.save()
|
this.save()
|
||||||
@@ -140,7 +160,7 @@ export class ConfigService {
|
|||||||
this.emitChange()
|
this.emitChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
emitChange (): void {
|
private emitChange (): void {
|
||||||
this.changed.next()
|
this.changed.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +168,12 @@ export class ConfigService {
|
|||||||
this.restartRequested = true
|
this.restartRequested = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters a list of Angular services to only include those provided
|
||||||
|
* by plugins that are enabled
|
||||||
|
*
|
||||||
|
* @typeparam T Base provider type
|
||||||
|
*/
|
||||||
enabledServices<T> (services: T[]): T[] {
|
enabledServices<T> (services: T[]): T[] {
|
||||||
if (!this.servicesCache) {
|
if (!this.servicesCache) {
|
||||||
this.servicesCache = {}
|
this.servicesCache = {}
|
||||||
|
@@ -10,6 +10,7 @@ export interface IScreen {
|
|||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class DockingService {
|
export class DockingService {
|
||||||
|
/** @hidden */
|
||||||
constructor (
|
constructor (
|
||||||
private electron: ElectronService,
|
private electron: ElectronService,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
@@ -20,21 +21,23 @@ export class DockingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dock () {
|
dock () {
|
||||||
|
let dockSide = this.config.store.appearance.dock
|
||||||
|
|
||||||
|
if (dockSide === 'off') {
|
||||||
|
this.hostApp.setAlwaysOnTop(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let display = this.electron.screen.getAllDisplays()
|
let display = this.electron.screen.getAllDisplays()
|
||||||
.filter((x) => x.id === this.config.store.appearance.dockScreen)[0]
|
.filter((x) => x.id === this.config.store.appearance.dockScreen)[0]
|
||||||
if (!display) {
|
if (!display) {
|
||||||
display = this.getCurrentScreen()
|
display = this.getCurrentScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
let dockSide = this.config.store.appearance.dock
|
|
||||||
let newBounds: Bounds = { x: 0, y: 0, width: 0, height: 0 }
|
let newBounds: Bounds = { x: 0, y: 0, width: 0, height: 0 }
|
||||||
let fill = this.config.store.appearance.dockFill
|
let fill = this.config.store.appearance.dockFill
|
||||||
let [minWidth, minHeight] = this.hostApp.getWindow().getMinimumSize()
|
let [minWidth, minHeight] = this.hostApp.getWindow().getMinimumSize()
|
||||||
|
|
||||||
if (dockSide === 'off') {
|
|
||||||
this.hostApp.setAlwaysOnTop(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (dockSide === 'left' || dockSide === 'right') {
|
if (dockSide === 'left' || dockSide === 'right') {
|
||||||
newBounds.width = Math.max(minWidth, Math.round(fill * display.bounds.width))
|
newBounds.width = Math.max(minWidth, Math.round(fill * display.bounds.width))
|
||||||
newBounds.height = display.bounds.height
|
newBounds.height = display.bounds.height
|
||||||
@@ -76,7 +79,7 @@ export class DockingService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
repositionWindow () {
|
private repositionWindow () {
|
||||||
let [x, y] = this.hostApp.getWindow().getPosition()
|
let [x, y] = this.hostApp.getWindow().getPosition()
|
||||||
for (let screen of this.electron.screen.getAllDisplays()) {
|
for (let screen of this.electron.screen.getAllDisplays()) {
|
||||||
let bounds = screen.bounds
|
let bounds = screen.bounds
|
||||||
|
@@ -24,6 +24,7 @@ export class ElectronService {
|
|||||||
MenuItem: typeof MenuItem
|
MenuItem: typeof MenuItem
|
||||||
private electron: any
|
private electron: any
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
constructor () {
|
constructor () {
|
||||||
this.electron = require('electron')
|
this.electron = require('electron')
|
||||||
this.remote = this.electron.remote
|
this.remote = this.electron.remote
|
||||||
@@ -42,18 +43,9 @@ export class ElectronService {
|
|||||||
this.MenuItem = this.remote.MenuItem
|
this.MenuItem = this.remote.MenuItem
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteRequire (name: string): any {
|
/**
|
||||||
return this.remote.require(name)
|
* Removes OS focus from Terminus' window
|
||||||
}
|
*/
|
||||||
|
|
||||||
remoteRequirePluginModule (plugin: string, module: string, globals: any): any {
|
|
||||||
return this.remoteRequire(this.remoteResolvePluginModule(plugin, module, globals))
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteResolvePluginModule (plugin: string, module: string, globals: any): any {
|
|
||||||
return globals.require.resolve(`${plugin}/node_modules/${module}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
loseFocus () {
|
loseFocus () {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
this.remote.Menu.sendActionToFirstResponder('hide:')
|
this.remote.Menu.sendActionToFirstResponder('hide:')
|
||||||
|
@@ -9,6 +9,7 @@ import uuidv4 = require('uuid/v4')
|
|||||||
export class HomeBaseService {
|
export class HomeBaseService {
|
||||||
appVersion: string
|
appVersion: string
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
constructor (
|
constructor (
|
||||||
private electron: ElectronService,
|
private electron: ElectronService,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
|
@@ -16,12 +16,19 @@ export interface Bounds {
|
|||||||
height: number
|
height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides interaction with the main process
|
||||||
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class HostAppService {
|
export class HostAppService {
|
||||||
platform: Platform
|
platform: Platform
|
||||||
nodePlatform: string
|
|
||||||
|
/**
|
||||||
|
* Fired once the window is visible
|
||||||
|
*/
|
||||||
shown = new EventEmitter<any>()
|
shown = new EventEmitter<any>()
|
||||||
isFullScreen = false
|
isFullScreen = false
|
||||||
|
|
||||||
private preferencesMenu = new Subject<void>()
|
private preferencesMenu = new Subject<void>()
|
||||||
private secondInstance = new Subject<void>()
|
private secondInstance = new Subject<void>()
|
||||||
private cliOpenDirectory = new Subject<string>()
|
private cliOpenDirectory = new Subject<string>()
|
||||||
@@ -35,29 +42,62 @@ export class HostAppService {
|
|||||||
private logger: Logger
|
private logger: Logger
|
||||||
private windowId: number
|
private windowId: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when Preferences is selected in the macOS menu
|
||||||
|
*/
|
||||||
get preferencesMenu$ (): Observable<void> { return this.preferencesMenu }
|
get preferencesMenu$ (): Observable<void> { return this.preferencesMenu }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when a second instance of Terminus is launched
|
||||||
|
*/
|
||||||
get secondInstance$ (): Observable<void> { return this.secondInstance }
|
get secondInstance$ (): Observable<void> { return this.secondInstance }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired for the `terminus open` CLI command
|
||||||
|
*/
|
||||||
get cliOpenDirectory$ (): Observable<string> { return this.cliOpenDirectory }
|
get cliOpenDirectory$ (): Observable<string> { return this.cliOpenDirectory }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired for the `terminus run` CLI command
|
||||||
|
*/
|
||||||
get cliRunCommand$ (): Observable<string[]> { return this.cliRunCommand }
|
get cliRunCommand$ (): Observable<string[]> { return this.cliRunCommand }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired for the `terminus paste` CLI command
|
||||||
|
*/
|
||||||
get cliPaste$ (): Observable<string> { return this.cliPaste }
|
get cliPaste$ (): Observable<string> { return this.cliPaste }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired for the `terminus profile` CLI command
|
||||||
|
*/
|
||||||
get cliOpenProfile$ (): Observable<string> { return this.cliOpenProfile }
|
get cliOpenProfile$ (): Observable<string> { return this.cliOpenProfile }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when another window modified the config file
|
||||||
|
*/
|
||||||
get configChangeBroadcast$ (): Observable<void> { return this.configChangeBroadcast }
|
get configChangeBroadcast$ (): Observable<void> { return this.configChangeBroadcast }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when the window close button is pressed
|
||||||
|
*/
|
||||||
get windowCloseRequest$ (): Observable<void> { return this.windowCloseRequest }
|
get windowCloseRequest$ (): Observable<void> { return this.windowCloseRequest }
|
||||||
|
|
||||||
get windowMoved$ (): Observable<void> { return this.windowMoved }
|
get windowMoved$ (): Observable<void> { return this.windowMoved }
|
||||||
|
|
||||||
get displayMetricsChanged$ (): Observable<void> { return this.displayMetricsChanged }
|
get displayMetricsChanged$ (): Observable<void> { return this.displayMetricsChanged }
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
constructor (
|
constructor (
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private electron: ElectronService,
|
private electron: ElectronService,
|
||||||
log: LogService,
|
log: LogService,
|
||||||
) {
|
) {
|
||||||
this.logger = log.create('hostApp')
|
this.logger = log.create('hostApp')
|
||||||
this.nodePlatform = require('os').platform()
|
|
||||||
this.platform = {
|
this.platform = {
|
||||||
win32: Platform.Windows,
|
win32: Platform.Windows,
|
||||||
darwin: Platform.macOS,
|
darwin: Platform.macOS,
|
||||||
linux: Platform.Linux
|
linux: Platform.Linux
|
||||||
}[this.nodePlatform]
|
}[process.platform]
|
||||||
|
|
||||||
this.windowId = parseInt(location.search.substring(1))
|
this.windowId = parseInt(location.search.substring(1))
|
||||||
this.logger.info('Window ID:', this.windowId)
|
this.logger.info('Window ID:', this.windowId)
|
||||||
@@ -117,6 +157,9 @@ export class HostAppService {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current remote [[BrowserWindow]]
|
||||||
|
*/
|
||||||
getWindow () {
|
getWindow () {
|
||||||
return this.electron.BrowserWindow.fromId(this.windowId)
|
return this.electron.BrowserWindow.fromId(this.windowId)
|
||||||
}
|
}
|
||||||
@@ -125,18 +168,6 @@ export class HostAppService {
|
|||||||
this.electron.ipcRenderer.send('app:new-window')
|
this.electron.ipcRenderer.send('app:new-window')
|
||||||
}
|
}
|
||||||
|
|
||||||
getShell () {
|
|
||||||
return this.electron.shell
|
|
||||||
}
|
|
||||||
|
|
||||||
getAppPath () {
|
|
||||||
return this.electron.app.getAppPath()
|
|
||||||
}
|
|
||||||
|
|
||||||
getPath (type: string) {
|
|
||||||
return this.electron.app.getPath(type)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleFullscreen () {
|
toggleFullscreen () {
|
||||||
let window = this.getWindow()
|
let window = this.getWindow()
|
||||||
window.setFullScreen(!this.isFullScreen)
|
window.setFullScreen(!this.isFullScreen)
|
||||||
@@ -174,6 +205,11 @@ export class HostAppService {
|
|||||||
this.electron.ipcRenderer.send('window-set-always-on-top', flag)
|
this.electron.ipcRenderer.send('window-set-always-on-top', flag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets window vibrancy mode (Windows, macOS)
|
||||||
|
*
|
||||||
|
* @param type `null`, or `fluent` when supported (Windowd only)
|
||||||
|
*/
|
||||||
setVibrancy (enable: boolean, type: string) {
|
setVibrancy (enable: boolean, type: string) {
|
||||||
document.body.classList.toggle('vibrant', enable)
|
document.body.classList.toggle('vibrant', enable)
|
||||||
if (this.platform === Platform.macOS) {
|
if (this.platform === Platform.macOS) {
|
||||||
@@ -196,6 +232,9 @@ export class HostAppService {
|
|||||||
this.electron.Menu.buildFromTemplate(menuDefinition).popup({})
|
this.electron.Menu.buildFromTemplate(menuDefinition).popup({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies other windows of config file changes
|
||||||
|
*/
|
||||||
broadcastConfigChange () {
|
broadcastConfigChange () {
|
||||||
this.electron.ipcRenderer.send('app:config-change')
|
this.electron.ipcRenderer.send('app:config-change')
|
||||||
}
|
}
|
||||||
|
@@ -5,16 +5,16 @@ import { ConfigService } from '../services/config.service'
|
|||||||
import { ElectronService } from '../services/electron.service'
|
import { ElectronService } from '../services/electron.service'
|
||||||
|
|
||||||
export interface PartialHotkeyMatch {
|
export interface PartialHotkeyMatch {
|
||||||
id: string,
|
id: string
|
||||||
strokes: string[],
|
strokes: string[]
|
||||||
matchedLength: number,
|
matchedLength: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const KEY_TIMEOUT = 2000
|
const KEY_TIMEOUT = 2000
|
||||||
|
|
||||||
interface EventBufferEntry {
|
interface EventBufferEntry {
|
||||||
event: NativeKeyEvent,
|
event: NativeKeyEvent
|
||||||
time: number,
|
time: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@@ -26,6 +26,7 @@ export class HotkeysService {
|
|||||||
private disabledLevel = 0
|
private disabledLevel = 0
|
||||||
private hotkeyDescriptions: IHotkeyDescription[] = []
|
private hotkeyDescriptions: IHotkeyDescription[] = []
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
constructor (
|
constructor (
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private electron: ElectronService,
|
private electron: ElectronService,
|
||||||
@@ -51,11 +52,20 @@ export class HotkeysService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new key event to the buffer
|
||||||
|
*
|
||||||
|
* @param name DOM event name
|
||||||
|
* @param nativeEvent event object
|
||||||
|
*/
|
||||||
pushKeystroke (name, nativeEvent) {
|
pushKeystroke (name, nativeEvent) {
|
||||||
nativeEvent.event = name
|
nativeEvent.event = name
|
||||||
this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() })
|
this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the buffer for new complete keystrokes
|
||||||
|
*/
|
||||||
processKeystrokes () {
|
processKeystrokes () {
|
||||||
if (this.isEnabled()) {
|
if (this.isEnabled()) {
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
@@ -84,7 +94,7 @@ export class HotkeysService {
|
|||||||
return stringifyKeySequence(this.currentKeystrokes.map(x => x.event))
|
return stringifyKeySequence(this.currentKeystrokes.map(x => x.event))
|
||||||
}
|
}
|
||||||
|
|
||||||
registerGlobalHotkey () {
|
private registerGlobalHotkey () {
|
||||||
this.electron.globalShortcut.unregisterAll()
|
this.electron.globalShortcut.unregisterAll()
|
||||||
let value = this.config.store.hotkeys['toggle-window'] || []
|
let value = this.config.store.hotkeys['toggle-window'] || []
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
@@ -103,11 +113,11 @@ export class HotkeysService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getHotkeysConfig () {
|
private getHotkeysConfig () {
|
||||||
return this.getHotkeysConfigRecursive(this.config.store.hotkeys)
|
return this.getHotkeysConfigRecursive(this.config.store.hotkeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
getHotkeysConfigRecursive (branch) {
|
private getHotkeysConfigRecursive (branch) {
|
||||||
let keys = {}
|
let keys = {}
|
||||||
for (let key in branch) {
|
for (let key in branch) {
|
||||||
let value = branch[key]
|
let value = branch[key]
|
||||||
@@ -129,7 +139,7 @@ export class HotkeysService {
|
|||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentFullyMatchedHotkey (): string {
|
private getCurrentFullyMatchedHotkey (): string {
|
||||||
let currentStrokes = this.getCurrentKeystrokes()
|
let currentStrokes = this.getCurrentKeystrokes()
|
||||||
let config = this.getHotkeysConfig()
|
let config = this.getHotkeysConfig()
|
||||||
for (let id in config) {
|
for (let id in config) {
|
||||||
@@ -199,85 +209,3 @@ export class HotkeysService {
|
|||||||
).reduce((a, b) => a.concat(b))
|
).reduce((a, b) => a.concat(b))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AppHotkeyProvider extends HotkeyProvider {
|
|
||||||
hotkeys: IHotkeyDescription[] = [
|
|
||||||
{
|
|
||||||
id: 'new-window',
|
|
||||||
name: 'New window',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'toggle-window',
|
|
||||||
name: 'Toggle terminal window',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'toggle-fullscreen',
|
|
||||||
name: 'Toggle fullscreen mode',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'rename-tab',
|
|
||||||
name: 'Rename Tab',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'close-tab',
|
|
||||||
name: 'Close tab',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'toggle-last-tab',
|
|
||||||
name: 'Toggle last tab',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'next-tab',
|
|
||||||
name: 'Next tab',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'previous-tab',
|
|
||||||
name: 'Previous tab',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tab-1',
|
|
||||||
name: 'Tab 1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tab-2',
|
|
||||||
name: 'Tab 2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tab-3',
|
|
||||||
name: 'Tab 3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tab-4',
|
|
||||||
name: 'Tab 4',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tab-5',
|
|
||||||
name: 'Tab 5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tab-6',
|
|
||||||
name: 'Tab 6',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tab-7',
|
|
||||||
name: 'Tab 7',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tab-8',
|
|
||||||
name: 'Tab 8',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tab-9',
|
|
||||||
name: 'Tab 9',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tab-10',
|
|
||||||
name: 'Tab 10',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
async provide (): Promise<IHotkeyDescription[]> {
|
|
||||||
return this.hotkeys
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -11,13 +11,13 @@ export const altKeyName = {
|
|||||||
}[process.platform]
|
}[process.platform]
|
||||||
|
|
||||||
export interface NativeKeyEvent {
|
export interface NativeKeyEvent {
|
||||||
event?: string,
|
event?: string
|
||||||
altKey: boolean,
|
altKey: boolean
|
||||||
ctrlKey: boolean,
|
ctrlKey: boolean
|
||||||
metaKey: boolean,
|
metaKey: boolean
|
||||||
shiftKey: boolean,
|
shiftKey: boolean
|
||||||
key: string,
|
key: string
|
||||||
keyCode: string,
|
keyCode: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringifyKeySequence (events: NativeKeyEvent[]): string[] {
|
export function stringifyKeySequence (events: NativeKeyEvent[]): string[] {
|
||||||
|
@@ -39,7 +39,7 @@ export class Logger {
|
|||||||
private name: string,
|
private name: string,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
doLog (level: string, ...args: any[]) {
|
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)
|
||||||
@@ -57,6 +57,7 @@ export class Logger {
|
|||||||
export class LogService {
|
export class LogService {
|
||||||
private log: any
|
private log: any
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
constructor (electron: ElectronService) {
|
constructor (electron: ElectronService) {
|
||||||
this.log = initializeWinston(electron)
|
this.log = initializeWinston(electron)
|
||||||
}
|
}
|
||||||
|
@@ -37,7 +37,7 @@ export class ShellIntegrationService {
|
|||||||
this.updatePaths()
|
this.updatePaths()
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePaths (): Promise<void> {
|
private async updatePaths (): Promise<void> {
|
||||||
// Update paths in case of an update
|
// Update paths in case of an update
|
||||||
if (this.hostApp.platform === Platform.Windows) {
|
if (this.hostApp.platform === Platform.Windows) {
|
||||||
if (await this.isInstalled()) {
|
if (await this.isInstalled()) {
|
||||||
|
@@ -4,6 +4,7 @@ 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'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class TabRecoveryService {
|
export class TabRecoveryService {
|
||||||
logger: Logger
|
logger: Logger
|
||||||
|
42
terminus-core/src/services/tabs.service.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Injectable, ComponentFactoryResolver, Injector } from '@angular/core'
|
||||||
|
import { BaseTabComponent } from '../components/baseTab.component'
|
||||||
|
import { TabRecoveryService } from './tabRecovery.service'
|
||||||
|
|
||||||
|
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class TabsService {
|
||||||
|
/** @hidden */
|
||||||
|
constructor (
|
||||||
|
private componentFactoryResolver: ComponentFactoryResolver,
|
||||||
|
private injector: Injector,
|
||||||
|
private tabRecovery: TabRecoveryService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a tab component and assigns given inputs
|
||||||
|
*/
|
||||||
|
create (type: TabComponentType, inputs?: any): BaseTabComponent {
|
||||||
|
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(type)
|
||||||
|
let componentRef = componentFactory.create(this.injector)
|
||||||
|
let tab = componentRef.instance
|
||||||
|
tab.hostView = componentRef.hostView
|
||||||
|
Object.assign(tab, inputs || {})
|
||||||
|
return tab
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicates an existing tab instance (using the tab recovery system)
|
||||||
|
*/
|
||||||
|
async duplicate (tab: BaseTabComponent): Promise<BaseTabComponent> {
|
||||||
|
let token = await tab.getRecoveryToken()
|
||||||
|
if (!token) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
let dup = await this.tabRecovery.recoverTab(token)
|
||||||
|
if (dup) {
|
||||||
|
return this.create(dup.type, dup.options)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
@@ -6,6 +6,7 @@ import { Theme } from '../api/theme'
|
|||||||
export class ThemesService {
|
export class ThemesService {
|
||||||
private styleElement: HTMLElement = null
|
private styleElement: HTMLElement = null
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
constructor (
|
constructor (
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
@Inject(Theme) private themes: Theme[],
|
@Inject(Theme) private themes: Theme[],
|
||||||
@@ -34,7 +35,7 @@ export class ThemesService {
|
|||||||
document.querySelector('style#custom-css').innerHTML = this.config.store.appearance.css
|
document.querySelector('style#custom-css').innerHTML = this.config.store.appearance.css
|
||||||
}
|
}
|
||||||
|
|
||||||
applyCurrentTheme (): void {
|
private applyCurrentTheme (): void {
|
||||||
this.applyTheme(this.findCurrentTheme())
|
this.applyTheme(this.findCurrentTheme())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,9 +3,10 @@ import { TouchBarSegmentedControl, SegmentedControlSegment } from 'electron'
|
|||||||
import { AppService } from './app.service'
|
import { AppService } from './app.service'
|
||||||
import { ConfigService } from './config.service'
|
import { ConfigService } from './config.service'
|
||||||
import { ElectronService } from './electron.service'
|
import { ElectronService } from './electron.service'
|
||||||
import { HostAppService } from './hostApp.service'
|
import { HostAppService, Platform } from './hostApp.service'
|
||||||
import { IToolbarButton, ToolbarButtonProvider } from '../api'
|
import { IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class TouchbarService {
|
export class TouchbarService {
|
||||||
private tabsSegmentedControl: TouchBarSegmentedControl
|
private tabsSegmentedControl: TouchBarSegmentedControl
|
||||||
@@ -20,6 +21,9 @@ export class TouchbarService {
|
|||||||
private electron: ElectronService,
|
private electron: ElectronService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
) {
|
) {
|
||||||
|
if (this.hostApp.platform !== Platform.macOS) {
|
||||||
|
return
|
||||||
|
}
|
||||||
app.tabsChanged$.subscribe(() => this.update())
|
app.tabsChanged$.subscribe(() => this.update())
|
||||||
app.activeTabChange$.subscribe(() => this.update())
|
app.activeTabChange$.subscribe(() => this.update())
|
||||||
app.tabOpened$.subscribe(tab => {
|
app.tabOpened$.subscribe(tab => {
|
||||||
@@ -31,6 +35,10 @@ export class TouchbarService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update () {
|
update () {
|
||||||
|
if (this.hostApp.platform !== Platform.macOS) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let buttons: IToolbarButton[] = []
|
let buttons: IToolbarButton[] = []
|
||||||
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
|
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
|
||||||
buttons = buttons.concat(provider.provide())
|
buttons = buttons.concat(provider.provide())
|
||||||
|
@@ -6,6 +6,7 @@ import { ElectronService } from './electron.service'
|
|||||||
|
|
||||||
const UPDATES_URL = 'https://api.github.com/repos/eugeny/terminus/releases/latest'
|
const UPDATES_URL = 'https://api.github.com/repos/eugeny/terminus/releases/latest'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class UpdaterService {
|
export class UpdaterService {
|
||||||
private logger: Logger
|
private logger: Logger
|
||||||
|
@@ -4,6 +4,7 @@ import { BaseTabComponent } from './components/baseTab.component'
|
|||||||
import { TabHeaderComponent } from './components/tabHeader.component'
|
import { TabHeaderComponent } from './components/tabHeader.component'
|
||||||
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
|
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CloseContextMenu extends TabContextMenuItemProvider {
|
export class CloseContextMenu extends TabContextMenuItemProvider {
|
||||||
weight = -5
|
weight = -5
|
||||||
@@ -61,6 +62,7 @@ const COLORS = [
|
|||||||
{ name: 'Yellow', value: '#ffd500' },
|
{ name: 'Yellow', value: '#ffd500' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
||||||
weight = -1
|
weight = -1
|
||||||
@@ -98,6 +100,7 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
|
export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
|
||||||
constructor (
|
constructor (
|
||||||
@@ -121,7 +124,7 @@ export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
|
|||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: (tab as any).__completionNotificationEnabled,
|
checked: (tab as any).__completionNotificationEnabled,
|
||||||
click: () => this.zone.run(() => {
|
click: () => this.zone.run(() => {
|
||||||
;(tab as any).__completionNotificationEnabled = !(tab as any).__completionNotificationEnabled
|
(tab as any).__completionNotificationEnabled = !(tab as any).__completionNotificationEnabled
|
||||||
|
|
||||||
if ((tab as any).__completionNotificationEnabled) {
|
if ((tab as any).__completionNotificationEnabled) {
|
||||||
this.app.observeTabCompletion(tab).subscribe(() => {
|
this.app.observeTabCompletion(tab).subscribe(() => {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { Theme } from './api'
|
import { Theme } from './api'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StandardTheme extends Theme {
|
export class StandardTheme extends Theme {
|
||||||
name = 'Standard'
|
name = 'Standard'
|
||||||
@@ -8,6 +9,7 @@ export class StandardTheme extends Theme {
|
|||||||
terminalBackground = '#222a33'
|
terminalBackground = '#222a33'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StandardCompactTheme extends Theme {
|
export class StandardCompactTheme extends Theme {
|
||||||
name = 'Compact'
|
name = 'Compact'
|
||||||
@@ -15,6 +17,7 @@ export class StandardCompactTheme extends Theme {
|
|||||||
terminalBackground = '#222a33'
|
terminalBackground = '#222a33'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PaperTheme extends Theme {
|
export class PaperTheme extends Theme {
|
||||||
name = 'Paper'
|
name = 'Paper'
|
||||||
|
@@ -31,13 +31,14 @@
|
|||||||
"@angular/forms": "4.0.1",
|
"@angular/forms": "4.0.1",
|
||||||
"@angular/platform-browser": "4.0.1",
|
"@angular/platform-browser": "4.0.1",
|
||||||
"@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.22",
|
"@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.22",
|
||||||
|
"rxjs": "5.3.0",
|
||||||
"terminus-core": "*",
|
"terminus-core": "*",
|
||||||
"terminus-settings": "*",
|
"terminus-settings": "*"
|
||||||
"rxjs": "5.3.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.16.2",
|
"axios": "^0.16.2",
|
||||||
"mz": "^2.6.0"
|
"mz": "^2.6.0",
|
||||||
|
"npm": "^6.7.0"
|
||||||
},
|
},
|
||||||
"false": {}
|
"false": {}
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,7 @@
|
|||||||
small {{plugin.description}}
|
small {{plugin.description}}
|
||||||
|
|
||||||
button.btn.btn-primary.ml-2(
|
button.btn.btn-primary.ml-2(
|
||||||
*ngIf='npmInstalled && knownUpgrades[plugin.name]',
|
*ngIf='knownUpgrades[plugin.name]',
|
||||||
(click)='upgradePlugin(plugin)',
|
(click)='upgradePlugin(plugin)',
|
||||||
[disabled]='busy[plugin.name] != undefined'
|
[disabled]='busy[plugin.name] != undefined'
|
||||||
)
|
)
|
||||||
@@ -42,24 +42,13 @@
|
|||||||
|
|
||||||
button.btn.btn-danger.ml-2(
|
button.btn.btn-danger.ml-2(
|
||||||
(click)='uninstallPlugin(plugin)',
|
(click)='uninstallPlugin(plugin)',
|
||||||
*ngIf='!plugin.isBuiltin && npmInstalled',
|
*ngIf='!plugin.isBuiltin',
|
||||||
[disabled]='busy[plugin.name] != undefined'
|
[disabled]='busy[plugin.name] != undefined'
|
||||||
)
|
)
|
||||||
i.fas.fa-fw.fa-trash(*ngIf='busy[plugin.name] != BusyState.Uninstalling')
|
i.fas.fa-fw.fa-trash(*ngIf='busy[plugin.name] != BusyState.Uninstalling')
|
||||||
i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Uninstalling')
|
i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Uninstalling')
|
||||||
|
|
||||||
.text-center.mt-5(*ngIf='npmMissing')
|
div
|
||||||
h4 npm not installed
|
|
||||||
p.mb-2 npm is required to install Terminus plugins.
|
|
||||||
.btn-group
|
|
||||||
button.btn.btn-outline-primary((click)='downloadNPM()')
|
|
||||||
i.fas.fa-download
|
|
||||||
span Get npm
|
|
||||||
button.btn.btn-outline-info((click)='checkNPM()')
|
|
||||||
i.fas.fa-refresh
|
|
||||||
span Try again
|
|
||||||
|
|
||||||
div(*ngIf='npmInstalled')
|
|
||||||
h3.mt-4 Available
|
h3.mt-4 Available
|
||||||
|
|
||||||
.input-group.mb-3
|
.input-group.mb-3
|
||||||
|
@@ -3,11 +3,12 @@ import { debounceTime, distinctUntilChanged, first, tap, flatMap } from 'rxjs/op
|
|||||||
import * as semver from 'semver'
|
import * as semver from 'semver'
|
||||||
|
|
||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { ConfigService, HostAppService, ElectronService } from 'terminus-core'
|
import { ConfigService, ElectronService } from 'terminus-core'
|
||||||
import { IPluginInfo, PluginManagerService } from '../services/pluginManager.service'
|
import { IPluginInfo, PluginManagerService } from '../services/pluginManager.service'
|
||||||
|
|
||||||
enum BusyState { Installing, Uninstalling }
|
enum BusyState { Installing, Uninstalling }
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
template: require('./pluginsSettingsTab.component.pug'),
|
template: require('./pluginsSettingsTab.component.pug'),
|
||||||
styles: [require('./pluginsSettingsTab.component.scss')],
|
styles: [require('./pluginsSettingsTab.component.scss')],
|
||||||
@@ -21,13 +22,10 @@ export class PluginsSettingsTabComponent {
|
|||||||
@Input() busy: {[id: string]: BusyState} = {}
|
@Input() busy: {[id: string]: BusyState} = {}
|
||||||
@Input() erroredPlugin: string
|
@Input() erroredPlugin: string
|
||||||
@Input() errorMessage: string
|
@Input() errorMessage: string
|
||||||
@Input() npmInstalled = false
|
|
||||||
@Input() npmMissing = false
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private electron: ElectronService,
|
private electron: ElectronService,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
private hostApp: HostAppService,
|
|
||||||
public pluginManager: PluginManagerService
|
public pluginManager: PluginManagerService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -50,20 +48,10 @@ export class PluginsSettingsTabComponent {
|
|||||||
this.knownUpgrades[plugin.name] = available.find(x => x.name === plugin.name && semver.gt(x.version, plugin.version))
|
this.knownUpgrades[plugin.name] = available.find(x => x.name === plugin.name && semver.gt(x.version, plugin.version))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.checkNPM()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
openPluginsFolder (): void {
|
openPluginsFolder (): void {
|
||||||
this.hostApp.getShell().openItem(this.pluginManager.userPluginsPath)
|
this.electron.shell.openItem(this.pluginManager.userPluginsPath)
|
||||||
}
|
|
||||||
|
|
||||||
downloadNPM (): void {
|
|
||||||
this.hostApp.getShell().openExternal('https://nodejs.org/en/download/current/')
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkNPM () {
|
|
||||||
this.npmInstalled = await this.pluginManager.isNPMInstalled()
|
|
||||||
this.npmMissing = !this.npmInstalled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searchAvailable (query: string) {
|
searchAvailable (query: string) {
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
import { ConfigProvider } from 'terminus-core'
|
|
||||||
|
|
||||||
export class PluginsConfigProvider extends ConfigProvider {
|
|
||||||
defaults = {
|
|
||||||
npm: 'npm',
|
|
||||||
}
|
|
||||||
}
|
|
@@ -4,12 +4,10 @@ import { FormsModule } from '@angular/forms'
|
|||||||
import { NgPipesModule } from 'ngx-pipes'
|
import { NgPipesModule } from 'ngx-pipes'
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
import { ConfigProvider } from 'terminus-core'
|
|
||||||
import { SettingsTabProvider } from 'terminus-settings'
|
import { SettingsTabProvider } from 'terminus-settings'
|
||||||
|
|
||||||
import { PluginsSettingsTabComponent } from './components/pluginsSettingsTab.component'
|
import { PluginsSettingsTabComponent } from './components/pluginsSettingsTab.component'
|
||||||
import { PluginManagerService } from './services/pluginManager.service'
|
import { PluginManagerService } from './services/pluginManager.service'
|
||||||
import { PluginsConfigProvider } from './config'
|
|
||||||
import { PluginsSettingsTabProvider } from './settings'
|
import { PluginsSettingsTabProvider } from './settings'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -21,7 +19,6 @@ import { PluginsSettingsTabProvider } from './settings'
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SettingsTabProvider, useClass: PluginsSettingsTabProvider, multi: true },
|
{ provide: SettingsTabProvider, useClass: PluginsSettingsTabProvider, multi: true },
|
||||||
{ provide: ConfigProvider, useClass: PluginsConfigProvider, multi: true },
|
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
PluginsSettingsTabComponent,
|
PluginsSettingsTabComponent,
|
||||||
|
@@ -1,11 +1,8 @@
|
|||||||
import * as path from 'path'
|
|
||||||
import * as fs from 'mz/fs'
|
|
||||||
import { exec } from 'mz/child_process'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { Observable, from } from 'rxjs'
|
import { Observable, from } from 'rxjs'
|
||||||
import { map } from 'rxjs/operators'
|
import { map } from 'rxjs/operators'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { Logger, LogService, ConfigService, HostAppService, Platform } from 'terminus-core'
|
import { Logger, LogService } from 'terminus-core'
|
||||||
|
|
||||||
const NAME_PREFIX = 'terminus-'
|
const NAME_PREFIX = 'terminus-'
|
||||||
const KEYWORD = 'terminus-plugin'
|
const KEYWORD = 'terminus-plugin'
|
||||||
@@ -29,45 +26,35 @@ export class PluginManagerService {
|
|||||||
builtinPluginsPath: string = (window as any).builtinPluginsPath
|
builtinPluginsPath: string = (window as any).builtinPluginsPath
|
||||||
userPluginsPath: string = (window as any).userPluginsPath
|
userPluginsPath: string = (window as any).userPluginsPath
|
||||||
installedPlugins: IPluginInfo[] = (window as any).installedPlugins
|
installedPlugins: IPluginInfo[] = (window as any).installedPlugins
|
||||||
npmPath: string
|
|
||||||
private envPath: string
|
private npmReady: Promise<void>
|
||||||
|
private npm: any
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
log: LogService,
|
log: LogService,
|
||||||
private config: ConfigService,
|
|
||||||
private hostApp: HostAppService,
|
|
||||||
) {
|
) {
|
||||||
this.logger = log.create('pluginManager')
|
this.logger = log.create('pluginManager')
|
||||||
this.detectPath()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async detectPath () {
|
async getNPM () {
|
||||||
this.npmPath = this.config.store.npm
|
if (!this.npm) {
|
||||||
this.envPath = process.env.PATH
|
if (!this.npmReady) {
|
||||||
if (await fs.exists(this.npmPath)) {
|
this.npmReady = new Promise(resolve => {
|
||||||
return
|
const npm = require('npm')
|
||||||
}
|
npm.load({
|
||||||
if (this.hostApp.platform !== Platform.Windows) {
|
prefix: this.userPluginsPath,
|
||||||
this.envPath = (await exec('$SHELL -i -c \'echo $PATH\''))[0].toString().trim()
|
}, err => {
|
||||||
let searchPaths = this.envPath.split(':')
|
if (err) {
|
||||||
for (let searchPath of searchPaths) {
|
this.logger.error(err)
|
||||||
if (await fs.exists(path.join(searchPath, 'npm'))) {
|
}
|
||||||
this.logger.debug('Found npm in', searchPath)
|
this.npm = npm
|
||||||
this.npmPath = path.join(searchPath, 'npm')
|
resolve()
|
||||||
return
|
})
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
await this.npmReady
|
||||||
}
|
}
|
||||||
}
|
return this.npm
|
||||||
|
|
||||||
async isNPMInstalled (): Promise<boolean> {
|
|
||||||
await this.detectPath()
|
|
||||||
try {
|
|
||||||
await exec(`${this.npmPath} -v`, { env: this.getEnv() })
|
|
||||||
return true
|
|
||||||
} catch (_) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
listAvailable (query?: string): Observable<IPluginInfo[]> {
|
listAvailable (query?: string): Observable<IPluginInfo[]> {
|
||||||
@@ -92,17 +79,21 @@ export class PluginManagerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async installPlugin (plugin: IPluginInfo) {
|
async installPlugin (plugin: IPluginInfo) {
|
||||||
await exec(`${this.npmPath} --prefix "${this.userPluginsPath}" install ${plugin.packageName}@${plugin.version}`, { env: this.getEnv() })
|
(await this.getNPM()).commands.install([`${plugin.packageName}@${plugin.version}`], err => {
|
||||||
this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
|
if (err) {
|
||||||
this.installedPlugins.push(plugin)
|
this.logger.error(err)
|
||||||
|
}
|
||||||
|
this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
|
||||||
|
this.installedPlugins.push(plugin)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async uninstallPlugin (plugin: IPluginInfo) {
|
async uninstallPlugin (plugin: IPluginInfo) {
|
||||||
await exec(`${this.npmPath} --prefix "${this.userPluginsPath}" remove ${plugin.packageName}`, { env: this.getEnv() })
|
(await this.getNPM()).commands.remove([plugin.packageName], err => {
|
||||||
this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
|
if (err) {
|
||||||
}
|
this.logger.error(err)
|
||||||
|
}
|
||||||
private getEnv (): any {
|
this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
|
||||||
return Object.assign(process.env, { PATH: this.envPath })
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ import { SettingsTabProvider } from 'terminus-settings'
|
|||||||
|
|
||||||
import { PluginsSettingsTabComponent } from './components/pluginsSettingsTab.component'
|
import { PluginsSettingsTabComponent } from './components/pluginsSettingsTab.component'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PluginsSettingsTabProvider extends SettingsTabProvider {
|
export class PluginsSettingsTabProvider extends SettingsTabProvider {
|
||||||
id = 'plugins'
|
id = 'plugins'
|
||||||
|
@@ -44,6 +44,7 @@ module.exports = {
|
|||||||
externals: [
|
externals: [
|
||||||
'fs',
|
'fs',
|
||||||
'font-manager',
|
'font-manager',
|
||||||
|
'npm',
|
||||||
'path',
|
'path',
|
||||||
'mz/fs',
|
'mz/fs',
|
||||||
'mz/child_process',
|
'mz/child_process',
|
||||||
|
23
terminus-settings/README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Terminus Settings Plugin
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
* tabbed settings interface
|
||||||
|
|
||||||
|
Using the API:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { SettingsTabProvider } from 'terminus-settings'
|
||||||
|
```
|
||||||
|
|
||||||
|
Exporting your subclasses:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
@NgModule({
|
||||||
|
...
|
||||||
|
providers: [
|
||||||
|
...
|
||||||
|
{ provide: SettingsTabProvider, useClass: MySettingsTab, multi: true },
|
||||||
|
...
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Extend to add your own settings tabs
|
||||||
|
*/
|
||||||
export abstract class SettingsTabProvider {
|
export abstract class SettingsTabProvider {
|
||||||
id: string
|
id: string
|
||||||
icon: string
|
icon: string
|
||||||
|
@@ -4,6 +4,7 @@ import { ToolbarButtonProvider, IToolbarButton, AppService, HostAppService, Hotk
|
|||||||
|
|
||||||
import { SettingsTabComponent } from './components/settingsTab.component'
|
import { SettingsTabComponent } from './components/settingsTab.component'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ButtonProvider extends ToolbarButtonProvider {
|
export class ButtonProvider extends ToolbarButtonProvider {
|
||||||
constructor (
|
constructor (
|
||||||
@@ -37,7 +38,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
|||||||
if (settingsTab) {
|
if (settingsTab) {
|
||||||
this.app.selectTab(settingsTab)
|
this.app.selectTab(settingsTab)
|
||||||
} else {
|
} else {
|
||||||
this.app.openNewTab(SettingsTabComponent)
|
this.app.openNewTabRaw(SettingsTabComponent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,7 @@ import { HotkeysService } from 'terminus-core'
|
|||||||
|
|
||||||
const INPUT_TIMEOUT = 1000
|
const INPUT_TIMEOUT = 1000
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hotkey-input-modal',
|
selector: 'hotkey-input-modal',
|
||||||
template: require('./hotkeyInputModal.component.pug'),
|
template: require('./hotkeyInputModal.component.pug'),
|
||||||
|
@@ -2,6 +2,7 @@ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from
|
|||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { HotkeyInputModalComponent } from './hotkeyInputModal.component'
|
import { HotkeyInputModalComponent } from './hotkeyInputModal.component'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'multi-hotkey-input',
|
selector: 'multi-hotkey-input',
|
||||||
template: require('./multiHotkeyInput.component.pug'),
|
template: require('./multiHotkeyInput.component.pug'),
|
||||||
|
@@ -300,10 +300,13 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
|
|||||||
[(ngModel)]='configDefaults',
|
[(ngModel)]='configDefaults',
|
||||||
readonly
|
readonly
|
||||||
)
|
)
|
||||||
.mt-3
|
.mt-2.mb-2.d-flex
|
||||||
button.btn.btn-primary((click)='saveConfigFile()', *ngIf='isConfigFileValid()')
|
button.btn.btn-primary((click)='saveConfigFile()', *ngIf='isConfigFileValid()')
|
||||||
i.fas.fa-check.mr-2
|
i.fas.fa-check.mr-2
|
||||||
| Save and apply
|
| Save and apply
|
||||||
button.btn.btn-primary(disabled, *ngIf='!isConfigFileValid()')
|
button.btn.btn-primary(disabled, *ngIf='!isConfigFileValid()')
|
||||||
i.fas.fa-exclamation-triangle.mr-2
|
i.fas.fa-exclamation-triangle.mr-2
|
||||||
| Invalid syntax
|
| Invalid syntax
|
||||||
|
button.btn.btn-secondary.ml-auto((click)='showConfigFile()')
|
||||||
|
i.fas.fa-external-link-square-alt.mr-2
|
||||||
|
| Show config file
|
||||||
|
@@ -18,6 +18,7 @@ import {
|
|||||||
|
|
||||||
import { SettingsTabProvider } from '../api'
|
import { SettingsTabProvider } from '../api'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'settings-tab',
|
selector: 'settings-tab',
|
||||||
template: require('./settingsTab.component.pug'),
|
template: require('./settingsTab.component.pug'),
|
||||||
@@ -100,6 +101,10 @@ export class SettingsTabComponent extends BaseTabComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showConfigFile () {
|
||||||
|
this.electron.shell.showItemInFolder(this.config.path)
|
||||||
|
}
|
||||||
|
|
||||||
isConfigFileValid () {
|
isConfigFileValid () {
|
||||||
try {
|
try {
|
||||||
yaml.safeLoad(this.configFile)
|
yaml.safeLoad(this.configFile)
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Component, Input, ViewContainerRef, ViewChild, ComponentFactoryResolver, ComponentRef } from '@angular/core'
|
import { Component, Input, ViewContainerRef, ViewChild, ComponentFactoryResolver, ComponentRef } from '@angular/core'
|
||||||
import { SettingsTabProvider } from '../api'
|
import { SettingsTabProvider } from '../api'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'settings-tab-body',
|
selector: 'settings-tab-body',
|
||||||
template: '<ng-template #placeholder></ng-template>',
|
template: '<ng-template #placeholder></ng-template>',
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { ConfigProvider, Platform } from 'terminus-core'
|
import { ConfigProvider, Platform } from 'terminus-core'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
export class SettingsConfigProvider extends ConfigProvider {
|
export class SettingsConfigProvider extends ConfigProvider {
|
||||||
defaults = { }
|
defaults = { }
|
||||||
platformDefaults = {
|
platformDefaults = {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { IHotkeyDescription, HotkeyProvider } from 'terminus-core'
|
import { IHotkeyDescription, HotkeyProvider } from 'terminus-core'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SettingsHotkeyProvider extends HotkeyProvider {
|
export class SettingsHotkeyProvider extends HotkeyProvider {
|
||||||
hotkeys: IHotkeyDescription[] = [
|
hotkeys: IHotkeyDescription[] = [
|
||||||
|
@@ -16,6 +16,7 @@ import { RecoveryProvider } from './recoveryProvider'
|
|||||||
import { SettingsHotkeyProvider } from './hotkeys'
|
import { SettingsHotkeyProvider } from './hotkeys'
|
||||||
import { SettingsConfigProvider } from './config'
|
import { SettingsConfigProvider } from './config'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@@ -3,6 +3,7 @@ import { TabRecoveryProvider, RecoveredTab } from 'terminus-core'
|
|||||||
|
|
||||||
import { SettingsTabComponent } from './components/settingsTab.component'
|
import { SettingsTabComponent } from './components/settingsTab.component'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RecoveryProvider extends TabRecoveryProvider {
|
export class RecoveryProvider extends TabRecoveryProvider {
|
||||||
async recover (recoveryToken: any): Promise<RecoveredTab> {
|
async recover (recoveryToken: any): Promise<RecoveredTab> {
|
||||||
|
@@ -4,6 +4,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
|||||||
import { HotkeysService, ToolbarButtonProvider, IToolbarButton } from 'terminus-core'
|
import { HotkeysService, ToolbarButtonProvider, IToolbarButton } from 'terminus-core'
|
||||||
import { SSHModalComponent } from './components/sshModal.component'
|
import { SSHModalComponent } from './components/sshModal.component'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ButtonProvider extends ToolbarButtonProvider {
|
export class ButtonProvider extends ToolbarButtonProvider {
|
||||||
constructor (
|
constructor (
|
||||||
|
@@ -5,6 +5,7 @@ import { PasswordStorageService } from '../services/passwordStorage.service'
|
|||||||
import { SSHConnection, LoginScript, SSHAlgorithmType } from '../api'
|
import { SSHConnection, LoginScript, SSHAlgorithmType } from '../api'
|
||||||
import { ALGORITHMS } from 'ssh2-streams/lib/constants'
|
import { ALGORITHMS } from 'ssh2-streams/lib/constants'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
template: require('./editConnectionModal.component.pug'),
|
template: require('./editConnectionModal.component.pug'),
|
||||||
})
|
})
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Component, Input, ViewChild, ElementRef } from '@angular/core'
|
import { Component, Input, ViewChild, ElementRef } from '@angular/core'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
template: require('./promptModal.component.pug'),
|
template: require('./promptModal.component.pug'),
|
||||||
})
|
})
|
||||||
|
@@ -6,6 +6,7 @@ import { SettingsTabComponent } from 'terminus-settings'
|
|||||||
import { SSHService } from '../services/ssh.service'
|
import { SSHService } from '../services/ssh.service'
|
||||||
import { SSHConnection, ISSHConnectionGroup } from '../api'
|
import { SSHConnection, ISSHConnectionGroup } from '../api'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
template: require('./sshModal.component.pug'),
|
template: require('./sshModal.component.pug'),
|
||||||
styles: [require('./sshModal.component.scss')],
|
styles: [require('./sshModal.component.scss')],
|
||||||
|
@@ -5,6 +5,7 @@ import { SSHConnection, ISSHConnectionGroup } from '../api'
|
|||||||
import { EditConnectionModalComponent } from './editConnectionModal.component'
|
import { EditConnectionModalComponent } from './editConnectionModal.component'
|
||||||
import { PromptModalComponent } from './promptModal.component'
|
import { PromptModalComponent } from './promptModal.component'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
template: require('./sshSettingsTab.component.pug'),
|
template: require('./sshSettingsTab.component.pug'),
|
||||||
})
|
})
|
||||||
|
@@ -4,6 +4,7 @@ import { BaseTerminalTabComponent } from 'terminus-terminal'
|
|||||||
import { SSHService } from '../services/ssh.service'
|
import { SSHService } from '../services/ssh.service'
|
||||||
import { SSHConnection, SSHSession } from '../api'
|
import { SSHConnection, SSHSession } from '../api'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<div
|
<div
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { ConfigProvider } from 'terminus-core'
|
import { ConfigProvider } from 'terminus-core'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
export class SSHConfigProvider extends ConfigProvider {
|
export class SSHConfigProvider extends ConfigProvider {
|
||||||
defaults = {
|
defaults = {
|
||||||
ssh: {
|
ssh: {
|
||||||
|
@@ -17,6 +17,7 @@ import { SSHConfigProvider } from './config'
|
|||||||
import { SSHSettingsTabProvider } from './settings'
|
import { SSHSettingsTabProvider } from './settings'
|
||||||
import { RecoveryProvider } from './recoveryProvider'
|
import { RecoveryProvider } from './recoveryProvider'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
NgbModule,
|
NgbModule,
|
||||||
|