Compare commits
195 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d42e070e6c | ||
![]() |
c5958bc9a0 | ||
![]() |
9dc2337787 | ||
![]() |
1a38cc30a8 | ||
![]() |
4949f14184 | ||
![]() |
11902020a5 | ||
![]() |
3f96c21f33 | ||
![]() |
9e81f0aa0e | ||
![]() |
1cce23cef5 | ||
![]() |
b7a56adb60 | ||
![]() |
eb02752cbf | ||
![]() |
3a6eb8cb2f | ||
![]() |
a7d62b0234 | ||
![]() |
b9cbe4f12d | ||
![]() |
5f74b35ba9 | ||
![]() |
affb439ab2 | ||
![]() |
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 | ||
![]() |
329d0448d3 | ||
![]() |
100436f511 | ||
![]() |
22d3e35723 | ||
![]() |
9cdcc8d8e5 | ||
![]() |
168e6f17dc | ||
![]() |
a2c636fdbf | ||
![]() |
413ca70729 | ||
![]() |
6f99e6c14b | ||
![]() |
e65811786d | ||
![]() |
aecd381b25 | ||
![]() |
89465f57d5 | ||
![]() |
3bf0ac43ef | ||
![]() |
a66dd43e1e | ||
![]() |
dd4566cf02 | ||
![]() |
f2be34d137 | ||
![]() |
e28c619bdc | ||
![]() |
04bf5dbcfb | ||
![]() |
a2128ca1f2 | ||
![]() |
bf0d02d1fc | ||
![]() |
792de65696 | ||
![]() |
fab21f6859 | ||
![]() |
b0b01b98be | ||
![]() |
24dff4b5b7 | ||
![]() |
78f8f4005e | ||
![]() |
38cfb3f036 | ||
![]() |
4e4d8a0e91 | ||
![]() |
21cfd14f1c | ||
![]() |
a64bbe145c | ||
![]() |
6a5dc79c5d | ||
![]() |
b799128427 | ||
![]() |
8b64a819e7 | ||
![]() |
5b78a5c1ed | ||
![]() |
91b318853f | ||
![]() |
ce3610c2da | ||
![]() |
d03430fb2e | ||
![]() |
caacc01aea | ||
![]() |
bcb6963c35 | ||
![]() |
deb99b0865 | ||
![]() |
2101c18657 | ||
![]() |
1a258f32b0 | ||
![]() |
3aaf490f57 | ||
![]() |
9faa346699 | ||
![]() |
d5b6a686f8 | ||
![]() |
492d006f64 | ||
![]() |
d999320c24 | ||
![]() |
5142d12e7e | ||
![]() |
453c613571 | ||
![]() |
ccc34ae4d9 | ||
![]() |
4362b5c50b | ||
![]() |
2d6023446c | ||
![]() |
dcd43dc019 | ||
![]() |
d8e70f9693 | ||
![]() |
7a26e8bd65 | ||
![]() |
d56287587c | ||
![]() |
8793613117 | ||
![]() |
92afec75e7 | ||
![]() |
ca71ec24f8 | ||
![]() |
524550f6e3 | ||
![]() |
fe31131fc1 | ||
![]() |
a7c1fe3425 | ||
![]() |
d7b305bf29 | ||
![]() |
0bd0c850da | ||
![]() |
88bb40f94b | ||
![]() |
120e2a2cd5 | ||
![]() |
cbb6821814 | ||
![]() |
75bf374a8f | ||
![]() |
bf995981d3 | ||
![]() |
a6fdabcd2f | ||
![]() |
0e6886d00a | ||
![]() |
459d6aadd9 | ||
![]() |
21d533c7cf | ||
![]() |
211566488d | ||
![]() |
282aab2e55 | ||
![]() |
6f41865474 | ||
![]() |
e4bcfd8f39 | ||
![]() |
504cfcf8ff | ||
![]() |
6e13914712 | ||
![]() |
9aaf670092 | ||
![]() |
c204f6d5a4 | ||
![]() |
91bba042b5 | ||
![]() |
2ca6135c06 | ||
![]() |
9ef3cbc177 | ||
![]() |
8a3906687a | ||
![]() |
3192a14c9d | ||
![]() |
b510a86f4d | ||
![]() |
fcf14eaa8b | ||
![]() |
137dd0bbe8 | ||
![]() |
4b5b75a57a | ||
![]() |
68c497e5fc | ||
![]() |
1da7c85973 | ||
![]() |
fe75aab724 | ||
![]() |
85bcac1fb7 | ||
![]() |
72287cc7cb | ||
![]() |
1f1d212c1d | ||
![]() |
cded1284de | ||
![]() |
df97e7ebb5 | ||
![]() |
d80c9a27d3 | ||
![]() |
3469ec9b6b | ||
![]() |
d4db8f4b18 | ||
![]() |
384744ec44 | ||
![]() |
76633db25e | ||
![]() |
6b823d0fa0 | ||
![]() |
798dda5236 | ||
![]() |
2b90a17d5e | ||
![]() |
6387539980 | ||
![]() |
cb17fd0866 | ||
![]() |
17bac5a904 | ||
![]() |
c34123ffe3 | ||
![]() |
c755885bbb | ||
![]() |
f49e3f0664 | ||
![]() |
7852ac2071 | ||
![]() |
60358e7ac4 | ||
![]() |
f32bdbdeac | ||
![]() |
c58c629d0e | ||
![]() |
a091f46100 | ||
![]() |
76e8652492 | ||
![]() |
2606b910f1 | ||
![]() |
9440d687d3 | ||
![]() |
216f5c2213 | ||
![]() |
dbadef1c4e | ||
![]() |
1baf80cfe6 | ||
![]() |
2e50bfccf4 | ||
![]() |
07a3d88397 | ||
![]() |
6e5ce8e0b1 | ||
![]() |
c1d1ddd3b7 | ||
![]() |
bd5f274cf3 | ||
![]() |
38045165d4 | ||
![]() |
9e7721d2a9 | ||
![]() |
1d593e0495 | ||
![]() |
9b263c7237 | ||
![]() |
ca05c1b819 | ||
![]() |
2107ed7ab7 | ||
![]() |
9fd69f668a | ||
![]() |
8800614bff | ||
![]() |
867ddb4809 | ||
![]() |
5cf31d3351 |
4
.gitignore
vendored
@@ -5,6 +5,7 @@ node_modules
|
||||
|
||||
build/files.wxs
|
||||
dist
|
||||
*/dist
|
||||
|
||||
*.xcworkspacedata
|
||||
*.xcuserstate
|
||||
@@ -17,3 +18,6 @@ npm-debug.log
|
||||
builtin-plugins
|
||||
package-lock.json
|
||||
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
|
61
.travis.yml
@@ -1,29 +1,48 @@
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
env: BUILD_FOR=linux
|
||||
- os: osx
|
||||
env: BUILD_FOR=macos
|
||||
|
||||
language: node_js
|
||||
node_js: 8
|
||||
node_js: 11
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
- app/node_modules
|
||||
stages:
|
||||
- Build
|
||||
- name: Docs
|
||||
if: branch = master
|
||||
|
||||
before_install:
|
||||
- yarn install
|
||||
- scripts/install-deps.js
|
||||
jobs:
|
||||
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:
|
||||
- scripts/build-native.js
|
||||
- yarn run build
|
||||
- scripts/prepackage-plugins.js
|
||||
- scripts/build-$BUILD_FOR.js
|
||||
- stage: 'Build'
|
||||
os: osx
|
||||
before_install:
|
||||
- rm app/node_modules/.yarn-integrity || true
|
||||
- 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
|
||||
|
||||
addons:
|
||||
|
28
README.md
@@ -1,30 +1,36 @@
|
||||

|
||||
|
||||
<p align="center">
|
||||
<a href="https://raw.githubusercontent.com/Eugeny/terminus/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg"/></a> <a href="https://travis-ci.org/Eugeny/terminus"><img src="https://travis-ci.org/Eugeny/terminus.svg?branch=master"/></a>
|
||||
<a href="https://ci.appveyor.com/project/Eugeny/terminus"><img src="https://ci.appveyor.com/api/projects/status/wnnq4hm5mbd9rgoy?svg=true"/></a>
|
||||
<a href="https://raw.githubusercontent.com/Eugeny/terminus/master/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/eugeny/terminus.svg?label=License&style=flat-square"></a> <a href="https://travis-ci.org/Eugeny/terminus"><img alt="Travis (.org)" src="https://img.shields.io/travis/Eugeny/terminus.svg?label=CI&logo=travis&logoColor=white&style=flat-square"></a>
|
||||
<a href="https://ci.appveyor.com/project/Eugeny/terminus"><img alt="AppVeyor" src="https://img.shields.io/appveyor/ci/eugeny/terminus.svg?label=CI&logo=appveyor&logoColor=white&style=flat-square"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Eugeny/terminus/releases/latest">Downloads</a> | <a href="https://t.me/joinchat/AAAAAEZuCv2WKKYcfyQ3QA">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"><img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/eugeny/terminus/total.svg?label=DOWNLOAD&logo=github&style=for-the-badge"></a> <a href="https://ci.appveyor.com/project/Eugeny/terminus/build/artifacts"><img src="https://img.shields.io/badge/download-nightly%20build-magenta.svg?logo=appveyor&style=for-the-badge"/></a> <a href="https://gitter.im/terminus-terminal/community"><img alt="Gitter" src="https://img.shields.io/gitter/room/terminus/community.svg?color=blue&logo=gitter&style=for-the-badge"></a>
|
||||
</p>
|
||||
|
||||
----
|
||||
|
||||
**Terminus** is a terminal heavily inspired by Hyper. It is, however, designed for people who need to get things done.
|
||||
**Terminus** is a highly configurable terminal emulator for Windows, macOS and Linux
|
||||
|
||||
* Runs on Windows, macOS and Linux
|
||||
* Theming and color schemes
|
||||
* Fully configurable shortcuts
|
||||
* Split panes
|
||||
* Remembers your tabs
|
||||
* PowerShell (and PS Core), WSL, Git-Bash, Cygwin, Cmder and CMD support
|
||||
* Integrated SSH client and connection manager
|
||||
* Full Unicode support including double-width characters
|
||||
* Doesn't choke on fast-flowing outputs
|
||||
* Proper shell-like experience on Windows including tab completion (via Clink)
|
||||
* PowerShell Core, WSL (Bash on Windows), PowerShell, Git-Bash, Cygwin, Cmder and CMD support
|
||||
* Tab persistence on macOS and Linux
|
||||
* Proper shell experience on Windows including tab completion (via Clink)
|
||||
|
||||
|
||||
[](https://ko-fi.com/eugeny)
|
||||
|
||||
---
|
||||
|
||||
* **Terminus is** an alternative to Windows' standard terminal (conhost), PowerShell ISE, PuTTY or iTerm
|
||||
|
||||
* **Terminus is not** a new shell or a MinGW or Cygwin replacement. Neither is it lightweight - if RAM usage is of importance, consider [Conemu](https://conemu.github.io) or [Alacritty](https://github.com/jwilm/alacritty)
|
||||
|
||||
---
|
||||
|
||||
@@ -37,7 +43,8 @@ Plugins can be installed directly from the Settings view inside Terminus.
|
||||
* [shell-selector](https://github.com/Eugeny/terminus-shell-selector) - a quick shell selector pane
|
||||
* [title-control](https://github.com/kbjr/terminus-title-control) - allows modifying the title of the terminal tabs by providing a prefix, suffix, and/or strings to be removed
|
||||
* [scrollbar](https://github.com/kbjr/terminus-scrollbar) - adds a scrollbar to terminal tabs
|
||||
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - quicklky send commands to one or all terminal tabs
|
||||
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - quickly send commands to one or all terminal tabs
|
||||
* [save-output](https://github.com/Eugeny/terminus-save-output) - record terminal output into a file
|
||||
|
||||
---
|
||||
|
||||
@@ -45,8 +52,7 @@ Plugins can be installed directly from the Settings view inside Terminus.
|
||||
|
||||
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
|
||||
[](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FEugeny%2Fterminus?ref=badge_large)
|
||||
|
@@ -1,91 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="150mm"
|
||||
height="150mm"
|
||||
viewBox="0 0 150 150"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:export-filename="/home/eugene/Work/term/build/icons/512x512.png"
|
||||
inkscape:export-xdpi="86.699997"
|
||||
inkscape:export-ydpi="86.699997">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.49497475"
|
||||
inkscape:cx="85.897128"
|
||||
inkscape:cy="375.72042"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="692"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:object-paths="true" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-10.356544,-82.309525)">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path138"
|
||||
style="opacity:0.9;fill:#bfd9f1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.12037313px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 33.048081,103.66303 101.30357,143.02426 80.80219,154.86063 33.048089,125.73315 Z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path116"
|
||||
style="opacity:0.9;fill:#6666af;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.12037313px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 141.59934,143.95811 0.051,23.16109 -87.420905,49.42651 -0.0034,-22.16232 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path118"
|
||||
style="opacity:0.9;fill:#bfd9f1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.12037313px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 33.233182,182.28294 20.992812,12.1202 0.0034,22.16208 -20.996251,-12.19239 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="opacity:0.9;fill:#9dbef0;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.12649226px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 52.236336,92.196079 -19.484508,11.249681 68.551742,39.5785 -68.366041,39.4708 21.107487,12.18633 68.366044,-39.4708 19.48451,-11.24968 z"
|
||||
id="path134"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
</g>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024 1024;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
.st1{opacity:0.16;fill:url(#SVGID_2_);}
|
||||
.st2{fill:url(#SVGID_3_);}
|
||||
.st3{opacity:0.16;fill:url(#SVGID_4_);}
|
||||
.st4{fill:url(#SVGID_5_);}
|
||||
.st5{opacity:0.15;fill:url(#SVGID_6_);}
|
||||
.st6{fill:url(#SVGID_7_);}
|
||||
</style>
|
||||
<g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="260.9675" y1="871.1813" x2="919.1845" y2="491.1596">
|
||||
<stop offset="0" style="stop-color:#669ABD"/>
|
||||
<stop offset="1" style="stop-color:#77DBDB"/>
|
||||
</linearGradient>
|
||||
<polygon class="st0" points="297.54,934.52 882.6,596.72 882.61,427.82 297.54,765.65 "/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="553.5051" y1="617.8278" x2="626.647" y2="744.5132">
|
||||
<stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
|
||||
<stop offset="1" style="stop-color:#000000"/>
|
||||
</linearGradient>
|
||||
<polygon class="st1" points="297.54,934.52 882.6,596.72 882.61,427.82 297.54,765.65 "/>
|
||||
</g>
|
||||
<g>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="114.6631" y1="744.5275" x2="334.0905" y2="871.2141">
|
||||
<stop offset="0" style="stop-color:#6A8FAD"/>
|
||||
<stop offset="1" style="stop-color:#669ABD"/>
|
||||
</linearGradient>
|
||||
<polygon class="st2" points="151.23,681.18 151.22,850.09 297.54,934.52 297.54,765.65 "/>
|
||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="260.9478" y1="744.5281" x2="187.8059" y2="871.2135">
|
||||
<stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
|
||||
<stop offset="1" style="stop-color:#000000"/>
|
||||
</linearGradient>
|
||||
<polygon class="st3" points="151.23,681.18 151.22,850.09 297.54,934.52 297.54,765.65 "/>
|
||||
</g>
|
||||
<g>
|
||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="114.663" y1="237.793" x2="553.5026" y2="491.1571">
|
||||
<stop offset="0" style="stop-color:#6A8FAD"/>
|
||||
<stop offset="1" style="stop-color:#669ABD"/>
|
||||
</linearGradient>
|
||||
<polygon class="st4" points="151.23,174.45 151.21,343.36 443.79,512.27 590.08,427.81 "/>
|
||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="370.6562" y1="301.1281" x2="297.5094" y2="427.8221">
|
||||
<stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
|
||||
<stop offset="1" style="stop-color:#000000"/>
|
||||
</linearGradient>
|
||||
<polygon class="st5" points="151.23,174.45 151.21,343.36 443.79,512.27 590.08,427.81 "/>
|
||||
</g>
|
||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="78.0912" y1="554.4979" x2="736.3375" y2="174.4593">
|
||||
<stop offset="0" style="stop-color:#CCECFF"/>
|
||||
<stop offset="1" style="stop-color:#9FECED"/>
|
||||
</linearGradient>
|
||||
<polygon class="st6" points="297.51,765.64 151.23,681.18 590.08,427.81 151.23,174.45 297.5,90 882.61,427.82 "/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 2.9 KiB |
@@ -1,4 +1,5 @@
|
||||
import { app, ipcMain, Menu, Tray, shell } from 'electron'
|
||||
import * as electron from 'electron'
|
||||
import { loadConfig } from './config'
|
||||
import { Window, WindowOptions } from './window'
|
||||
|
||||
@@ -18,6 +19,12 @@ export class Application {
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch('disable-http-cache')
|
||||
app.commandLine.appendSwitch('force_discrete_gpu', '0')
|
||||
app.commandLine.appendSwitch('lang', 'EN')
|
||||
}
|
||||
|
||||
init () {
|
||||
electron.screen.on('display-metrics-changed', () => this.broadcast('host:display-metrics-changed'))
|
||||
}
|
||||
|
||||
async newWindow (options?: WindowOptions): Promise<Window> {
|
||||
@@ -103,7 +110,7 @@ export class Application {
|
||||
{
|
||||
label: 'Preferences',
|
||||
accelerator: 'Cmd+,',
|
||||
async click () {
|
||||
click: async () => {
|
||||
if (!this.hasWindows()) {
|
||||
await this.newWindow()
|
||||
}
|
||||
|
@@ -13,6 +13,9 @@ export function parseArgs (argv, cwd) {
|
||||
.command('run [command...]', 'run a command in the terminal', {
|
||||
command: { type: 'string' },
|
||||
})
|
||||
.command('profile [profileName]', 'open a tab with specified profile', {
|
||||
profileName: { type: 'string' },
|
||||
})
|
||||
.command('paste [text]', 'paste stdin into the active tab', yargs => {
|
||||
return yargs.option('escape', {
|
||||
alias: 'e',
|
||||
|
@@ -12,7 +12,6 @@ if (!process.env.TERMINUS_PLUGINS) {
|
||||
const application = new Application()
|
||||
|
||||
ipcMain.on('app:new-window', () => {
|
||||
console.log('new-window')
|
||||
application.newWindow()
|
||||
})
|
||||
|
||||
@@ -59,5 +58,6 @@ app.on('ready', () => {
|
||||
}
|
||||
]))
|
||||
}
|
||||
application.init()
|
||||
application.newWindow({ hidden: argv.hidden })
|
||||
})
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { debounceTime } from 'rxjs/operators'
|
||||
import { BrowserWindow, app, ipcMain, Rectangle } from 'electron'
|
||||
import ElectronConfig = require('electron-config')
|
||||
import * as os from 'os'
|
||||
@@ -10,7 +11,7 @@ let AccentState: any
|
||||
let DwmEnableBlurBehindWindow: any
|
||||
if (process.platform === 'win32') {
|
||||
SetWindowCompositionAttribute = require('windows-swca').SetWindowCompositionAttribute
|
||||
AccentState = require('windows-swca').AccentState
|
||||
AccentState = require('windows-swca').ACCENT_STATE
|
||||
DwmEnableBlurBehindWindow = require('windows-blurbehind').DwmEnableBlurBehindWindow
|
||||
}
|
||||
|
||||
@@ -43,7 +44,9 @@ export class Window {
|
||||
title: 'Terminus',
|
||||
minWidth: 400,
|
||||
minHeight: 300,
|
||||
webPreferences: { webSecurity: false },
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
},
|
||||
frame: false,
|
||||
show: false,
|
||||
backgroundColor: '#00000000'
|
||||
@@ -102,16 +105,14 @@ export class Window {
|
||||
if (process.platform === 'win32') {
|
||||
if (parseFloat(os.release()) >= 10) {
|
||||
let attribValue = AccentState.ACCENT_DISABLED
|
||||
let color = 0x00000000
|
||||
if (enabled) {
|
||||
if (parseInt(os.release().split('.')[2]) >= 17063 && type === 'fluent') {
|
||||
attribValue = AccentState.ACCENT_ENABLE_FLUENT
|
||||
color = 0x01000000 // using a small alpha because acrylic bugs out at full transparency.
|
||||
attribValue = AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND
|
||||
} else {
|
||||
attribValue = AccentState.ACCENT_ENABLE_BLURBEHIND
|
||||
}
|
||||
}
|
||||
SetWindowCompositionAttribute(this.window, attribValue, color)
|
||||
SetWindowCompositionAttribute(this.window.getNativeWindowHandle(), attribValue, 0x00000000)
|
||||
} else {
|
||||
DwmEnableBlurBehindWindow(this.window, enabled)
|
||||
}
|
||||
@@ -143,6 +144,16 @@ export class Window {
|
||||
this.visible.next(false)
|
||||
})
|
||||
|
||||
let moveSubscription = new Observable<void>(observer => {
|
||||
this.window.on('move', () => observer.next())
|
||||
}).pipe(debounceTime(250)).subscribe(() => {
|
||||
this.window.webContents.send('host:window-moved')
|
||||
})
|
||||
|
||||
this.window.on('closed', () => {
|
||||
moveSubscription.unsubscribe()
|
||||
})
|
||||
|
||||
this.window.on('enter-full-screen', () => this.window.webContents.send('host:window-enter-full-screen'))
|
||||
this.window.on('leave-full-screen', () => this.window.webContents.send('host:window-leave-full-screen'))
|
||||
|
||||
@@ -173,28 +184,28 @@ export class Window {
|
||||
})
|
||||
|
||||
ipcMain.on('window-focus', event => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.window.focus()
|
||||
})
|
||||
|
||||
ipcMain.on('window-maximize', event => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.window.maximize()
|
||||
})
|
||||
|
||||
ipcMain.on('window-unmaximize', event => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.window.unmaximize()
|
||||
})
|
||||
|
||||
ipcMain.on('window-toggle-maximize', event => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
if (this.window.isMaximized()) {
|
||||
@@ -205,42 +216,42 @@ export class Window {
|
||||
})
|
||||
|
||||
ipcMain.on('window-minimize', event => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.window.minimize()
|
||||
})
|
||||
|
||||
ipcMain.on('window-set-bounds', (event, bounds) => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.window.setBounds(bounds)
|
||||
})
|
||||
|
||||
ipcMain.on('window-set-always-on-top', (event, flag) => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.window.setAlwaysOnTop(flag)
|
||||
})
|
||||
|
||||
ipcMain.on('window-set-vibrancy', (event, enabled, type) => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.setVibrancy(enabled, type)
|
||||
})
|
||||
|
||||
ipcMain.on('window-set-title', (event, title) => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.window.setTitle(title)
|
||||
})
|
||||
|
||||
ipcMain.on('window-bring-to-front', event => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
if (this.window.isMinimized()) {
|
||||
@@ -250,7 +261,10 @@ export class Window {
|
||||
this.window.moveTop()
|
||||
})
|
||||
|
||||
ipcMain.on('window-close', () => {
|
||||
ipcMain.on('window-close', event => {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.closing = true
|
||||
this.window.close()
|
||||
})
|
||||
|
@@ -13,13 +13,13 @@
|
||||
"watch": "webpack --progress --color --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "7.0.0",
|
||||
"@angular/common": "7.0.0",
|
||||
"@angular/compiler": "7.0.0",
|
||||
"@angular/core": "7.0.0",
|
||||
"@angular/forms": "7.0.0",
|
||||
"@angular/platform-browser": "7.0.0",
|
||||
"@angular/platform-browser-dynamic": "7.0.0",
|
||||
"@angular/animations": "7.2.8",
|
||||
"@angular/common": "7.2.8",
|
||||
"@angular/compiler": "7.2.8",
|
||||
"@angular/core": "7.2.8",
|
||||
"@angular/forms": "7.2.8",
|
||||
"@angular/platform-browser": "7.2.8",
|
||||
"@angular/platform-browser-dynamic": "7.2.8",
|
||||
"@ng-bootstrap/ng-bootstrap": "^3.3.1",
|
||||
"devtron": "1.4.0",
|
||||
"electron-config": "0.2.1",
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"windows-blurbehind": "^1.0.0",
|
||||
"windows-swca": "^1.1.1"
|
||||
"windows-swca": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mz": "0.0.31"
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import '../lib/lru'
|
||||
import 'source-sans-pro'
|
||||
import 'source-code-pro/source-code-pro.css'
|
||||
import 'font-awesome/css/font-awesome.css'
|
||||
import '@fortawesome/fontawesome-free/css/solid.css'
|
||||
import '@fortawesome/fontawesome-free/css/brands.css'
|
||||
import '@fortawesome/fontawesome-free/css/fontawesome.css'
|
||||
import 'ngx-toastr/toastr.css'
|
||||
import './preload.scss'
|
||||
|
||||
|
@@ -15,6 +15,8 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
||||
import { getRootModule } from './app.module'
|
||||
import { findPlugins, loadPlugins, IPluginInfo } from './plugins'
|
||||
|
||||
;(process as any).enablePromiseAPI = true
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
process.env.HOME = process.env.HOMEDRIVE + process.env.HOMEPATH
|
||||
}
|
||||
@@ -34,7 +36,7 @@ async function bootstrap (plugins: IPluginInfo[], safeMode = false): Promise<NgM
|
||||
})
|
||||
let module = getRootModule(pluginsModules)
|
||||
window['rootModule'] = module
|
||||
return await platformBrowserDynamic().bootstrapModule(module)
|
||||
return platformBrowserDynamic().bootstrapModule(module)
|
||||
}
|
||||
|
||||
findPlugins().then(async plugins => {
|
||||
|
@@ -16,11 +16,11 @@ function normalizePath (path: string): string {
|
||||
|
||||
nodeRequire.main.paths.map(x => nodeModule.globalPaths.push(normalizePath(x)))
|
||||
|
||||
if (process.env.DEV) {
|
||||
if (process.env.TERMINUS_DEV) {
|
||||
nodeModule.globalPaths.unshift(path.dirname(require('electron').remote.app.getAppPath()))
|
||||
}
|
||||
|
||||
const builtinPluginsPath = process.env.DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
|
||||
const builtinPluginsPath = process.env.TERMINUS_DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
|
||||
|
||||
const userPluginsPath = path.join(
|
||||
require('electron').remote.app.getPath('appData'),
|
||||
@@ -118,7 +118,7 @@ export async function findPlugins (): Promise<IPluginInfo[]> {
|
||||
}
|
||||
|
||||
try {
|
||||
let info = JSON.parse(await fs.readFile(infoPath, {encoding: 'utf-8'}))
|
||||
let info = JSON.parse(await fs.readFile(infoPath, { encoding: 'utf-8' }))
|
||||
if (!info.keywords || !(info.keywords.includes('terminus-plugin') || info.keywords.includes('terminus-builtin-plugin'))) {
|
||||
continue
|
||||
}
|
||||
|
@@ -9,7 +9,6 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedParameters": true,
|
||||
|
@@ -9,7 +9,7 @@ module.exports = {
|
||||
preload: path.resolve(__dirname, 'src/entry.preload.ts'),
|
||||
bundle: path.resolve(__dirname, 'src/entry.ts'),
|
||||
},
|
||||
mode: process.env.DEV ? 'development' : 'production',
|
||||
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
|
||||
optimization:{
|
||||
minimize: false,
|
||||
},
|
||||
|
@@ -7,7 +7,7 @@ module.exports = {
|
||||
entry: {
|
||||
main: path.resolve(__dirname, 'lib/index.ts'),
|
||||
},
|
||||
mode: process.env.DEV ? 'development' : 'production',
|
||||
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
|
||||
context: __dirname,
|
||||
devtool: 'source-map',
|
||||
output: {
|
||||
|
@@ -2,52 +2,52 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@angular/animations@7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.0.0.tgz#5c9e1683063c29df10253b7dc5bb9b13694ee396"
|
||||
integrity sha512-IYdryQXdYfPvhJpExLSAr0o9rlUeyVS++a6h/sjqN1dkUt/yJBHLRreuHx8Udvlj2nH70raHJgevk8FwhAkTdA==
|
||||
"@angular/animations@7.2.8":
|
||||
version "7.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.2.8.tgz#0285364c839c660a934ab0f753ec21424bfb292e"
|
||||
integrity sha512-dJn9koYukyz15TouBc+z5z9fdThDk+bKgdlij25eYSu5Mpmtk04gB4eIMQA97K0UDh1d4YukgSJ5w3ZIk0m8DQ==
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
"@angular/common@7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.0.0.tgz#29206614d2b8dc79e5207b7dc6f9fc559e9a24f2"
|
||||
integrity sha512-jp6MA6EOq/a1m+F0c1aZC345pAYYYFpN1m7GMM91JlqkjzJMhyYVk+Bod9xQOEWadcpY+RFudG+jRsPCMO8bvQ==
|
||||
"@angular/common@7.2.8":
|
||||
version "7.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.2.8.tgz#660c816b6f08cd2919a6efb7465397e4ff14d265"
|
||||
integrity sha512-LgOhf68+LPndGZhtnUlGFd2goReXYmHzaFZW8gCEi9aC+H+Io8bjYh0gkH3xDreevEOe3f0z6coXNFLIxSmTuA==
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
"@angular/compiler@7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.0.0.tgz#f953a213a01e4736e94fe1a370b07e13e2393b71"
|
||||
integrity sha512-4fkohfGyG1BEpeYenOartuJmduyZ/R3XQx46hDDiR/9A8/Go4qLGkgr9Bd/JL/gPIR1XAHH9D5ii2sh+28ZEmA==
|
||||
"@angular/compiler@7.2.8":
|
||||
version "7.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.2.8.tgz#9d9c1515e99914399e6915c1c90484b1d255560b"
|
||||
integrity sha512-PrU97cTsOdofpaDkxK0rWUA/CGd0u6ESOI6XvFVm5xH9zJInsdY8ShSHklnr1JJnss70e1dGKZbZq32OChxWMw==
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
"@angular/core@7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.0.0.tgz#01e9db9074a1db1c47a32f745b787d1c86f5d61a"
|
||||
integrity sha512-DjVyWNGBWKEeBvxeXy8FGBNlnr/W/tNygOZEd6/uCktcXTG4DNyNQrWuNZUKEpr7RuIT3YVMj+UNwgTq0jB/9g==
|
||||
"@angular/core@7.2.8":
|
||||
version "7.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.2.8.tgz#6586d9b6c6321c80119b3f3e2bd0edbb32d0b649"
|
||||
integrity sha512-QKwug2kWJC00zm2rvmD9mCJzsOkMVhSu8vqPWf83poWTh8+F9aIVWcy29W0VoGpBkSchOnK8hf9DnKVv28j9nw==
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
"@angular/forms@7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.0.0.tgz#672c306b13e94a20b72c096214642a326c43699a"
|
||||
integrity sha512-rTg1UHq9gHR6zY3Kkip1KCm/YTck/rlR8CvVFIMwF0bdQxUCT51SXVn58nXts9yDaieABcGaQHNkQn1mARslgw==
|
||||
"@angular/forms@7.2.8":
|
||||
version "7.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.2.8.tgz#adf194088495822d55dcf3e5bf69196dcf19465d"
|
||||
integrity sha512-lbSX4IHFHz/c4e2RHiPpL8MJlzDkCuQEHnqsujDaV2X9o9fApS6+C1X4x7Z2XDKqonmeX+aHQwv9+SLejX6OyQ==
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
"@angular/platform-browser-dynamic@7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.0.0.tgz#2b2a50b5a8176bee257f90ee47b1d873502f7182"
|
||||
integrity sha512-lH2KuH+Em1y/mTOE6yTJmsOxYkMbYKzKLP9gYzc9vZu3er1df6Jx6jxefeBmAr9v+kNCLnpnHWHz2y4GzAesJA==
|
||||
"@angular/platform-browser-dynamic@7.2.8":
|
||||
version "7.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.8.tgz#e82768900cedfa75bf453263f931a9f90f7aaab2"
|
||||
integrity sha512-nOJt28A5pRn4mdL8y98V7bA6OOdMRjsQAcWCr/isGYF0l1yDC0ijUGWkHuRtj3z1/9tmERN0BLXx+xs1h4JhCQ==
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
"@angular/platform-browser@7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.0.0.tgz#8c13a6380cf465b3628e5b576a1313e9b4976093"
|
||||
integrity sha512-XyvL30d6meJ+SXlOmdR+sxoLdSvkQdmVNvpdvUzAHC/EqwA/byg4V3bTe5lpZmypclgFCjkGoTsz6uOnnwlQhw==
|
||||
"@angular/platform-browser@7.2.8":
|
||||
version "7.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.2.8.tgz#11096727b99bf3d7fd82a00a3a468b933c9713bd"
|
||||
integrity sha512-SizCRMc7Or27g2CugcqWnaAikRPfgLgRvb9GFFGpcgoq8CRfOVwkyR5dFZuqN39H+uwtwuTMP5OUYhZcrFNKug==
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
@@ -70,6 +70,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.13.tgz#530f0f9254209b0335bf5cc6387822594ef47093"
|
||||
integrity sha512-Y3EAG7VA7NVNbZek/fjJtILnmTk/ZfpJuWZGDBqDZ1dVIxgJJJ82fXPW7pKnqyV9CD/9bcPOCi7eErUqGMHOrA==
|
||||
|
||||
"@types/node@^10.12.18":
|
||||
version "10.12.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
|
||||
integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==
|
||||
|
||||
accessibility-developer-tools@^2.11.0:
|
||||
version "2.12.0"
|
||||
resolved "https://registry.yarnpkg.com/accessibility-developer-tools/-/accessibility-developer-tools-2.12.0.tgz#3da0cce9d6ec6373964b84f35db7cfc3df7ab514"
|
||||
@@ -620,10 +625,12 @@ windows-blurbehind@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/windows-blurbehind/-/windows-blurbehind-1.0.0.tgz#050efb988704c44335bdc3efefd757f6e463b8ac"
|
||||
integrity sha512-lO+A7fhTHO7oy9zJM3o1AdzfSQrmtPkdwvleeuww840ghijjEA1f1Zp8bKA3mJu2DFNtVT40fwmqtgsCGat4UA==
|
||||
|
||||
windows-swca@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/windows-swca/-/windows-swca-1.1.1.tgz#0b3530278c67d408baac71c3a6aeb16d55318bf8"
|
||||
integrity sha512-hKmHrNYJD72Kg0u35fjkiFIuMKuC+Tztmf3Obnf4aTkNjstEpbSEspEeSo3ZNixaVCETA1dLbDkVUQVF1QxtWA==
|
||||
windows-swca@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/windows-swca/-/windows-swca-2.0.1.tgz#25d78ce25251292061494a0ad07c02282b28b4e3"
|
||||
integrity sha512-flj+HD6RUemZUvKKguLLnMUOYkQSgDu9qrhUIO4cydvtb/x+sxU8XmpZUtugYuydcdikB9zsCOMgKnAqIQ+7nw==
|
||||
dependencies:
|
||||
"@types/node" "^10.12.18"
|
||||
|
||||
wrap-ansi@^2.0.0:
|
||||
version "2.1.0"
|
||||
|
@@ -7,18 +7,18 @@ environment:
|
||||
nodejs_version: "10"
|
||||
|
||||
cache:
|
||||
- '%USERPROFILE%\.electron'
|
||||
- "%USERPROFILE%\\.electron"
|
||||
- "%LOCALAPPDATA%\\Yarn"
|
||||
|
||||
version: "{build}"
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version $env:platform
|
||||
- npm install
|
||||
- node scripts/install-deps.js
|
||||
- yarn
|
||||
- node scripts/build-native.js
|
||||
|
||||
build_script:
|
||||
- npm run build
|
||||
- yarn run build
|
||||
- node scripts/prepackage-plugins.js
|
||||
- node scripts/build-windows.js
|
||||
|
||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 644 B After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -1,124 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="150mm"
|
||||
height="150mm"
|
||||
viewBox="0 0 150 150"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
|
||||
sodipodi:docname="icon.svg"
|
||||
inkscape:export-filename="/home/eugene/Work/term/build/icons/512x512.png"
|
||||
inkscape:export-xdpi="86.699997"
|
||||
inkscape:export-ydpi="86.699997">
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4649">
|
||||
<stop
|
||||
style="stop-color:#000316;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop4645" />
|
||||
<stop
|
||||
style="stop-color:#190065;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop4647" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4649"
|
||||
id="linearGradient4651"
|
||||
x1="89.26284"
|
||||
y1="85.146751"
|
||||
x2="89.26284"
|
||||
y2="229.47229"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.82182032,0,0,0.82182032,15.208802,28.029361)" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.49497475"
|
||||
inkscape:cx="85.897128"
|
||||
inkscape:cy="375.72042"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="692"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:object-paths="true" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-10.356544,-82.309525)">
|
||||
<rect
|
||||
id="rect168"
|
||||
width="123.27305"
|
||||
height="123.27305"
|
||||
x="23.72002"
|
||||
y="95.673004"
|
||||
style="fill:url(#linearGradient4651);fill-opacity:1;stroke-width:0.21743995"
|
||||
rx="8.2182035"
|
||||
ry="8.2182035" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path138"
|
||||
style="opacity:0.9;fill:#bfd9f1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.82182032px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 47.511243,117.17807 50.067023,28.8724 -15.038249,8.68226 -35.028768,-21.3657 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path116"
|
||||
style="opacity:0.9;fill:#6666af;fill-rule:evenodd;stroke:none;stroke-width:0.82182032px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;fill-opacity:1"
|
||||
d="m 127.13617,146.73547 0.0374,16.98921 -64.125308,36.25552 -0.0025,-16.25659 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path118"
|
||||
style="opacity:0.9;fill:#bfd9f1;fill-rule:evenodd;stroke:none;stroke-width:0.82182032px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;fill-opacity:1"
|
||||
d="m 47.647019,174.84764 15.398727,8.89046 0.0025,16.25641 -15.401249,-8.94341 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="opacity:0.9;fill:#9dbef0;fill-rule:evenodd;stroke:none;stroke-width:0.82630885px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;fill-opacity:1"
|
||||
d="m 61.586284,108.76679 -14.292349,8.25191 50.284331,29.03177 -50.148115,28.95277 15.482843,8.93896 50.148116,-28.95277 14.29235,-8.25191 z"
|
||||
id="path134"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
</g>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024 1024;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{opacity:0.8;fill:#00232E;}
|
||||
.st1{fill:url(#SVGID_1_);}
|
||||
.st2{opacity:0.16;fill:url(#SVGID_2_);}
|
||||
.st3{fill:url(#SVGID_3_);}
|
||||
.st4{opacity:0.16;fill:url(#SVGID_4_);}
|
||||
.st5{fill:url(#SVGID_5_);}
|
||||
.st6{opacity:0.15;fill:url(#SVGID_6_);}
|
||||
.st7{fill:url(#SVGID_7_);}
|
||||
</style>
|
||||
<polygon class="st0" points="449.5,645.47 407.51,621.23 533.47,548.5 407.51,475.77 449.5,451.53 617.45,548.5 "/>
|
||||
<g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="439.0065" y1="603.0394" x2="627.9464" y2="493.9549">
|
||||
<stop offset="0" style="stop-color:#669ABD"/>
|
||||
<stop offset="1" style="stop-color:#77DBDB"/>
|
||||
</linearGradient>
|
||||
<polygon class="st1" points="449.5,621.22 617.45,524.25 617.45,475.77 449.5,572.75 "/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="522.9788" y1="530.3148" x2="543.9741" y2="566.6795">
|
||||
<stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
|
||||
<stop offset="1" style="stop-color:#000000"/>
|
||||
</linearGradient>
|
||||
<polygon class="st2" points="449.5,621.22 617.45,524.25 617.45,475.77 449.5,572.75 "/>
|
||||
</g>
|
||||
<g>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="397.0101" y1="566.6837" x2="459.9963" y2="603.0487">
|
||||
<stop offset="0" style="stop-color:#6A8FAD"/>
|
||||
<stop offset="1" style="stop-color:#669ABD"/>
|
||||
</linearGradient>
|
||||
<polygon class="st3" points="407.51,548.5 407.5,596.99 449.5,621.22 449.5,572.75 "/>
|
||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="439.0009" y1="566.6838" x2="418.0056" y2="603.0486">
|
||||
<stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
|
||||
<stop offset="1" style="stop-color:#000000"/>
|
||||
</linearGradient>
|
||||
<polygon class="st4" points="407.51,548.5 407.5,596.99 449.5,621.22 449.5,572.75 "/>
|
||||
</g>
|
||||
<g>
|
||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="397.0101" y1="421.2265" x2="522.9781" y2="493.9542">
|
||||
<stop offset="0" style="stop-color:#6A8FAD"/>
|
||||
<stop offset="1" style="stop-color:#669ABD"/>
|
||||
</linearGradient>
|
||||
<polygon class="st5" points="407.51,403.04 407.5,451.53 491.49,500.01 533.48,475.77 "/>
|
||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="470.4924" y1="439.4067" x2="449.4958" y2="475.774">
|
||||
<stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
|
||||
<stop offset="1" style="stop-color:#000000"/>
|
||||
</linearGradient>
|
||||
<polygon class="st6" points="407.51,403.04 407.5,451.53 491.49,500.01 533.48,475.77 "/>
|
||||
</g>
|
||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="386.5123" y1="512.136" x2="575.4605" y2="403.0467">
|
||||
<stop offset="0" style="stop-color:#CCECFF"/>
|
||||
<stop offset="1" style="stop-color:#9FECED"/>
|
||||
</linearGradient>
|
||||
<polygon class="st7" points="449.5,572.74 407.51,548.5 533.48,475.77 407.51,403.04 449.49,378.8 617.45,475.77 "/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 361 KiB |
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 426 426" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
|
||||
<g id="Layer-1" serif:id="Layer 1" transform="matrix(1,0,0,1,-29.3571,-233.318)">
|
||||
<g>
|
||||
<path id="path138" d="M93.68,293.848L287.16,405.423L229.046,438.975L93.68,356.409L93.68,293.848Z" style="fill:url(#_Linear1);"/>
|
||||
<path id="path118" d="M94.204,516.708L153.711,551.064L153.721,613.886L94.204,579.325L94.204,516.708Z" style="fill:url(#_Linear2);"/>
|
||||
</g>
|
||||
<path id="path116" d="M401.384,408.07L401.529,473.724L153.721,613.83L153.712,551.008L401.384,408.07Z" style="fill:rgb(0,94,167);fill-opacity:0.9;"/>
|
||||
<path id="path134" d="M148.072,261.343L92.84,293.232L287.16,405.423L93.366,517.309L153.198,551.853L346.992,439.967L402.224,408.078L148.072,261.343Z" style="fill:rgb(7,147,255);fill-opacity:0.9;"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(193.48,0,0,320.038,93.6796,453.867)"><stop offset="0" style="stop-color:rgb(0,121,215);stop-opacity:0.9"/><stop offset="1" style="stop-color:rgb(40,97,156);stop-opacity:0.9"/></linearGradient>
|
||||
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(193.48,0,0,320.038,93.6796,453.867)"><stop offset="0" style="stop-color:rgb(0,121,215);stop-opacity:0.9"/><stop offset="1" style="stop-color:rgb(40,97,156);stop-opacity:0.9"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.8 KiB |
BIN
build/windows/squirrel.gif
Normal file
After Width: | Height: | Size: 9.2 KiB |
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]'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
BIN
extras/UAC.exe
Normal file
35
package.json
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "term",
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.6.3",
|
||||
"@types/electron-config": "^0.2.1",
|
||||
"@types/electron-debug": "^1.1.0",
|
||||
"@types/fs-promise": "1.0.1",
|
||||
@@ -13,20 +14,17 @@
|
||||
"core-js": "2.4.1",
|
||||
"cross-env": "4.0.0",
|
||||
"css-loader": "0.28.0",
|
||||
"electron": "3.0.8",
|
||||
"electron-builder": "^20.27.1",
|
||||
"electron": "4.0.5",
|
||||
"electron-builder": "^20.38.4",
|
||||
"electron-builder-squirrel-windows": "^20.28.3",
|
||||
"electron-installer-snap": "^3.0.0",
|
||||
"electron-rebuild": "^1.8.2",
|
||||
"file-loader": "^1.1.11",
|
||||
"font-awesome": "4.7.0",
|
||||
"graceful-fs": "^4.1.11",
|
||||
"html-loader": "0.4.4",
|
||||
"json-loader": "0.5.4",
|
||||
"less": "2.7.1",
|
||||
"less-loader": "2.2.3",
|
||||
"node-abi": "^2.4.4",
|
||||
"node-gyp": "^3.6.2",
|
||||
"node-gyp": "^3.8.0",
|
||||
"node-sass": "^4.5.3",
|
||||
"npmlog": "4.1.0",
|
||||
"npx": "^10.2.0",
|
||||
@@ -41,20 +39,24 @@
|
||||
"shelljs": "0.7.7",
|
||||
"source-code-pro": "^2.30.1",
|
||||
"source-sans-pro": "2.0.10",
|
||||
"style-loader": "0.13.1",
|
||||
"style-loader": "^0.23.1",
|
||||
"svg-inline-loader": "^0.8.0",
|
||||
"to-string-loader": "1.1.5",
|
||||
"tslint": "5.1.0",
|
||||
"tslint-config-standard": "5.0.2",
|
||||
"tslint-eslint-rules": "4.0.0",
|
||||
"tslint": "^5.12.0",
|
||||
"tslint-config-standard": "^8.0.1",
|
||||
"tslint-eslint-rules": "^5.4.0",
|
||||
"typedoc": "^0.14.2",
|
||||
"typescript": "^3.1.3",
|
||||
"url-loader": "^1.1.1",
|
||||
"val-loader": "0.5.0",
|
||||
"webpack": "^4.22.0",
|
||||
"webpack": "^4.27.1",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"yaml-loader": "0.4.0",
|
||||
"yarn": "^1.10.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"*/node-abi": "^2.5.0"
|
||||
},
|
||||
"build": {
|
||||
"appId": "org.terminus",
|
||||
"productName": "Terminus",
|
||||
@@ -76,6 +78,7 @@
|
||||
},
|
||||
"squirrelWindows": {
|
||||
"iconUrl": "https://github.com/Eugeny/terminus/raw/master/build/windows/icon.ico",
|
||||
"loadingGif": "./build/windows/squirrel.gif",
|
||||
"artifactName": "terminus-${version}-setup.exe"
|
||||
},
|
||||
"portable": {
|
||||
@@ -84,6 +87,7 @@
|
||||
"mac": {
|
||||
"category": "public.app-category.video",
|
||||
"icon": "./build/mac/icon.icns",
|
||||
"artifactName": "terminus-${version}-macos.${ext}",
|
||||
"publish": [
|
||||
"github"
|
||||
],
|
||||
@@ -124,11 +128,12 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js",
|
||||
"watch": "cross-env DEV=1 webpack --progress --color --watch",
|
||||
"start": "cross-env DEV=1 electron app --debug",
|
||||
"prod": "cross-env DEV=1 electron app",
|
||||
"watch": "cross-env TERMINUS_DEV=1 webpack --progress --color --watch",
|
||||
"start": "cross-env TERMINUS_DEV=1 electron app --debug",
|
||||
"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",
|
||||
"postinstall": "install-app-deps"
|
||||
"postinstall": "node ./scripts/install-deps.js"
|
||||
},
|
||||
"repository": "eugeny/terminus"
|
||||
}
|
||||
|
@@ -4,24 +4,18 @@ const path = require('path')
|
||||
const vars = require('./vars')
|
||||
|
||||
lifecycles = []
|
||||
lifecycles.push(rebuild({
|
||||
buildPath: path.resolve(__dirname, '../app'),
|
||||
electronVersion: vars.electronVersion,
|
||||
force: true,
|
||||
}).lifecycle)
|
||||
lifecycles.push(rebuild({
|
||||
buildPath: path.resolve(__dirname, '../terminus-ssh'),
|
||||
electronVersion: vars.electronVersion,
|
||||
force: true,
|
||||
}).lifecycle)
|
||||
lifecycles.push(rebuild({
|
||||
buildPath: path.resolve(__dirname, '../terminus-terminal'),
|
||||
electronVersion: vars.electronVersion,
|
||||
force: true,
|
||||
}).lifecycle)
|
||||
for (let dir of ['app', 'terminus-ssh', 'terminus-terminal']) {
|
||||
lifecycles.push([rebuild({
|
||||
buildPath: path.resolve(__dirname, '../' + dir),
|
||||
electronVersion: vars.electronVersion,
|
||||
force: true,
|
||||
}).lifecycle, dir])
|
||||
}
|
||||
|
||||
for (let lc of lifecycles) {
|
||||
console.info('Building against Electron', vars.electronVersion)
|
||||
|
||||
for (let [lc, dir] of lifecycles) {
|
||||
lc.on('module-found', name => {
|
||||
console.info('Rebuilding', name)
|
||||
console.info('Rebuilding', dir + '/' + name)
|
||||
})
|
||||
}
|
||||
|
@@ -8,7 +8,6 @@ const localBinPath = path.resolve(__dirname, '../node_modules/.bin');
|
||||
const npx = `${localBinPath}/npx`;
|
||||
|
||||
log.info('deps', 'app')
|
||||
sh.exec(`${npx} yarn install`)
|
||||
|
||||
sh.cd('app')
|
||||
sh.exec(`${npx} yarn install`)
|
||||
|
@@ -3,10 +3,11 @@ const fs = require('fs')
|
||||
const childProcess = require('child_process')
|
||||
|
||||
const appInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../app/package.json')))
|
||||
const pkgInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json')))
|
||||
const electronInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../node_modules/electron/package.json')))
|
||||
|
||||
exports.version = childProcess.execSync('git describe --tags', {encoding:'utf-8'})
|
||||
exports.version = exports.version.substring(1, exports.version.length - 1)
|
||||
exports.version = exports.version.substring(1).trim()
|
||||
exports.version = exports.version.replace('-', '-c')
|
||||
|
||||
exports.builtinPlugins = [
|
||||
'terminus-core',
|
||||
@@ -20,4 +21,4 @@ exports.bundledModules = [
|
||||
'@angular',
|
||||
'@ng-bootstrap',
|
||||
]
|
||||
exports.electronVersion = pkgInfo.devDependencies.electron
|
||||
exports.electronVersion = electronInfo.version
|
||||
|
26
snap/snapcraft.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
name: terminus
|
||||
version: '1.0.0'
|
||||
summary: A terminal for a modern age
|
||||
description: |
|
||||
Terminus is a terminal heavily inspired by Hyper. It is, however, designed for people who need to get things done.
|
||||
|
||||
grade: devel
|
||||
confinement: devmode
|
||||
|
||||
apps:
|
||||
terminus:
|
||||
command: opt/terminus/terminus
|
||||
|
||||
parts:
|
||||
app:
|
||||
plugin: nodejs
|
||||
source: .
|
||||
build-packages:
|
||||
- libfontconfig-dev
|
||||
override-build: |
|
||||
yarn
|
||||
./scripts/build-native.js
|
||||
yarn run build
|
||||
./scripts/build-linux.js
|
||||
mkdir -p $SNAPCRAFT_PART_INSTALL/opt/terminus || true
|
||||
cp -ar dist/linux-unpacked/* $SNAPCRAFT_PART_INSTALL/opt/terminus/
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-community-color-schemes",
|
||||
"version": "1.0.0-alpha.55",
|
||||
"version": "1.0.73-c4-ga7d62b0",
|
||||
"description": "Community color schemes for Terminus",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
|
36
terminus-community-color-schemes/schemes/Relaxed
Normal file
@@ -0,0 +1,36 @@
|
||||
! special
|
||||
*.foreground: #d8d8d8
|
||||
*.background: #343a43
|
||||
*.cursorColor: #d8d8d8
|
||||
|
||||
! black
|
||||
*.color0: #2c3037
|
||||
*.color8: #626262
|
||||
|
||||
! red
|
||||
*.color1: #bb5653
|
||||
*.color9: #c35956
|
||||
|
||||
! green
|
||||
*.color2: #909d62
|
||||
*.color10: #9fab76
|
||||
|
||||
! yellow
|
||||
*.color3: #eac179
|
||||
*.color11: #ecc179
|
||||
|
||||
! blue
|
||||
*.color4: #698698
|
||||
*.color12: #7da9c7
|
||||
|
||||
! magenta
|
||||
*.color5: #b06597
|
||||
*.color13: #ba6ca0
|
||||
|
||||
! cyan
|
||||
*.color6: #c9dfff
|
||||
*.color14: #abbacf
|
||||
|
||||
! white
|
||||
*.color7: #d8d8d8
|
||||
*.color15: #f7f7f7
|
@@ -13,7 +13,7 @@ module.exports = {
|
||||
libraryTarget: 'umd',
|
||||
devtoolModuleFilenameTemplate: 'webpack-terminus-community-color-schemes:///[resource-path]',
|
||||
},
|
||||
mode: process.env.DEV ? 'development' : 'production',
|
||||
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
|
||||
optimization:{
|
||||
minimize: false,
|
||||
},
|
||||
|
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,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-core",
|
||||
"version": "1.0.0-alpha.55",
|
||||
"version": "1.0.73-c4-ga7d62b0",
|
||||
"description": "Terminus core",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
@@ -8,8 +8,8 @@
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && webpack --progress --color --display-modules",
|
||||
"watch": "rm -rf dist && webpack --progress --color --watch"
|
||||
"build": "webpack --progress --color --display-modules",
|
||||
"watch": "webpack --progress --color --watch"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -21,12 +21,13 @@
|
||||
"@types/node": "^7.0.37",
|
||||
"@types/webpack-env": "^1.13.0",
|
||||
"@types/winston": "^2.3.6",
|
||||
"axios": "^0.18.0",
|
||||
"bootstrap": "^4.1.3",
|
||||
"core-js": "^2.4.1",
|
||||
"electron-updater": "^2.8.9",
|
||||
"ng2-dnd": "^5.0.2",
|
||||
"ngx-perfect-scrollbar": "^6.0.0",
|
||||
"rage-edit-tmp": "^1.1.0",
|
||||
"rage-edit": "^1.2.0",
|
||||
"shell-escape": "^0.2.0",
|
||||
"universal-analytics": "^0.4.17"
|
||||
},
|
||||
@@ -44,6 +45,5 @@
|
||||
"deepmerge": "^1.5.0",
|
||||
"js-yaml": "^3.9.0",
|
||||
"winston": "^2.4.0"
|
||||
},
|
||||
"false": {}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,37 @@
|
||||
/**
|
||||
* Extend to add your own config options
|
||||
*/
|
||||
export abstract class ConfigProvider {
|
||||
/**
|
||||
* Default values, e.g.
|
||||
*
|
||||
* ```ts
|
||||
* defaults = {
|
||||
* myPlugin: {
|
||||
* foo: 1
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
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 {
|
||||
id: string,
|
||||
name: string,
|
||||
id: 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 {
|
||||
hotkeys: IHotkeyDescription[] = []
|
||||
|
||||
|
@@ -1,9 +1,11 @@
|
||||
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
|
||||
export { SplitTabComponent, SplitContainer } from '../components/splitTab.component'
|
||||
export { TabRecoveryProvider, RecoveredTab } from './tabRecovery'
|
||||
export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
|
||||
export { ConfigProvider } from './configProvider'
|
||||
export { HotkeyProvider, IHotkeyDescription } from './hotkeyProvider'
|
||||
export { Theme } from './theme'
|
||||
export { TabContextMenuItemProvider } from './tabContextMenuProvider'
|
||||
|
||||
export { AppService } from '../services/app.service'
|
||||
export { ConfigService } from '../services/config.service'
|
||||
@@ -15,3 +17,4 @@ export { HotkeysService } from '../services/hotkeys.service'
|
||||
export { HostAppService, Platform } from '../services/hostApp.service'
|
||||
export { ShellIntegrationService } from '../services/shellIntegration.service'
|
||||
export { ThemesService } from '../services/themes.service'
|
||||
export { TabsService } from '../services/tabs.service'
|
||||
|
11
terminus-core/src/api/tabContextMenuProvider.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { TabHeaderComponent } from '../components/tabHeader.component'
|
||||
|
||||
/**
|
||||
* Extend to add items to the tab header's context menu
|
||||
*/
|
||||
export abstract class TabContextMenuItemProvider {
|
||||
weight = 0
|
||||
|
||||
abstract async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]>
|
||||
}
|
@@ -1,10 +1,38 @@
|
||||
import { TabComponentType } from '../services/app.service'
|
||||
import { TabComponentType } from '../services/tabs.service'
|
||||
|
||||
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 {
|
||||
abstract async recover (recoveryToken: any): Promise<RecoveredTab|null>
|
||||
/**
|
||||
* @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>
|
||||
}
|
||||
|
@@ -1,5 +1,13 @@
|
||||
/**
|
||||
* Extend to add a custom CSS theme
|
||||
*/
|
||||
export abstract class Theme {
|
||||
name: string
|
||||
|
||||
/**
|
||||
* Complete CSS stylesheet
|
||||
*/
|
||||
css: string
|
||||
|
||||
terminalBackground: string
|
||||
}
|
||||
|
@@ -1,14 +1,34 @@
|
||||
import { SafeHtml } from '@angular/platform-browser'
|
||||
|
||||
/**
|
||||
* See [[ToolbarButtonProvider]]
|
||||
*/
|
||||
export interface IToolbarButton {
|
||||
/**
|
||||
* Raw SVG icon code
|
||||
*/
|
||||
icon: SafeHtml
|
||||
touchBarNSImage?: string
|
||||
|
||||
title: string
|
||||
|
||||
/**
|
||||
* Optional Touch Bar icon ID
|
||||
*/
|
||||
touchBarNSImage?: string
|
||||
|
||||
/**
|
||||
* Optional Touch Bar button label
|
||||
*/
|
||||
touchBarTitle?: string
|
||||
|
||||
weight?: number
|
||||
|
||||
click: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend to add buttons to the toolbar
|
||||
*/
|
||||
export abstract class ToolbarButtonProvider {
|
||||
abstract provide (): IToolbarButton[]
|
||||
}
|
||||
|
@@ -9,7 +9,6 @@ import { HotkeysService } from '../services/hotkeys.service'
|
||||
import { Logger, LogService } from '../services/log.service'
|
||||
import { ConfigService } from '../services/config.service'
|
||||
import { DockingService } from '../services/docking.service'
|
||||
import { TabRecoveryService } from '../services/tabRecovery.service'
|
||||
import { ThemesService } from '../services/themes.service'
|
||||
import { UpdaterService } from '../services/updater.service'
|
||||
import { TouchbarService } from '../services/touchbar.service'
|
||||
@@ -18,6 +17,7 @@ import { BaseTabComponent } from './baseTab.component'
|
||||
import { SafeModeModalComponent } from './safeModeModal.component'
|
||||
import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: require('./appRoot.component.pug'),
|
||||
@@ -69,7 +69,6 @@ export class AppRootComponent {
|
||||
constructor (
|
||||
private docking: DockingService,
|
||||
private electron: ElectronService,
|
||||
private tabRecovery: TabRecoveryService,
|
||||
private hotkeys: HotkeysService,
|
||||
private updater: UpdaterService,
|
||||
private touchbar: TouchbarService,
|
||||
@@ -128,12 +127,17 @@ export class AppRootComponent {
|
||||
this.onGlobalHotkey()
|
||||
})
|
||||
|
||||
this.hostApp.windowCloseRequest$.subscribe(async () => {
|
||||
await this.app.closeAllTabs()
|
||||
this.hostApp.closeWindow()
|
||||
})
|
||||
|
||||
if (window['safeModeReason']) {
|
||||
ngbModal.open(SafeModeModalComponent)
|
||||
}
|
||||
|
||||
this.updater.check().then(() => {
|
||||
this.updatesAvailable = true
|
||||
this.updater.check().then(available => {
|
||||
this.updatesAvailable = available
|
||||
})
|
||||
|
||||
this.touchbar.update()
|
||||
@@ -199,9 +203,7 @@ export class AppRootComponent {
|
||||
}
|
||||
|
||||
async ngOnInit () {
|
||||
await this.tabRecovery.recoverTabs()
|
||||
this.ready = true
|
||||
this.tabRecovery.saveTabs(this.app.tabs)
|
||||
|
||||
this.app.emitReady()
|
||||
}
|
||||
|
@@ -1,26 +1,58 @@
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
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 {
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for custom tab components
|
||||
*/
|
||||
export abstract class BaseTabComponent {
|
||||
private static lastTabID = 0
|
||||
id: number
|
||||
/**
|
||||
* Current tab title
|
||||
*/
|
||||
title: string
|
||||
|
||||
/**
|
||||
* User-defined title override
|
||||
*/
|
||||
customTitle: string
|
||||
hasFocus = false
|
||||
|
||||
/**
|
||||
* Last tab activity state
|
||||
*/
|
||||
hasActivity = false
|
||||
|
||||
/**
|
||||
* ViewRef to the tab DOM element
|
||||
*/
|
||||
hostView: ViewRef
|
||||
protected titleChange = new Subject<string>()
|
||||
protected focused = new Subject<void>()
|
||||
protected blurred = new Subject<void>()
|
||||
protected progress = new Subject<number>()
|
||||
protected activity = new Subject<boolean>()
|
||||
protected destroyed = new Subject<void>()
|
||||
|
||||
/**
|
||||
* CSS color override for the tab's header
|
||||
*/
|
||||
color: string = null
|
||||
|
||||
protected hasFocus = false
|
||||
|
||||
/**
|
||||
* Ping this if your recovery state has been changed and you want
|
||||
* your tab state to be saved sooner
|
||||
*/
|
||||
protected recoveryStateChangedHint = new Subject<void>()
|
||||
|
||||
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 blurred$ (): Observable<void> { return this.blurred }
|
||||
@@ -28,9 +60,9 @@ export abstract class BaseTabComponent {
|
||||
get progress$ (): Observable<number> { return this.progress }
|
||||
get activity$ (): Observable<boolean> { return this.activity }
|
||||
get destroyed$ (): Observable<void> { return this.destroyed }
|
||||
get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint }
|
||||
|
||||
constructor () {
|
||||
this.id = BaseTabComponent.lastTabID++
|
||||
this.focused$.subscribe(() => {
|
||||
this.hasFocus = true
|
||||
})
|
||||
@@ -46,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) {
|
||||
this.progress.next(progress)
|
||||
if (progress) {
|
||||
@@ -58,24 +95,43 @@ export abstract class BaseTabComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the acticity marker on the tab header
|
||||
*/
|
||||
displayActivity (): void {
|
||||
this.hasActivity = true
|
||||
this.activity.next(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the acticity marker from the tab header
|
||||
*/
|
||||
clearActivity (): void {
|
||||
this.hasActivity = false
|
||||
this.activity.next(false)
|
||||
}
|
||||
|
||||
getRecoveryToken (): any {
|
||||
/**
|
||||
* 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> {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this to enable task completion notifications for the tab
|
||||
*/
|
||||
async getCurrentProcess (): Promise<BaseTabProcess> {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Return false to prevent the tab from being closed
|
||||
*/
|
||||
async canClose (): Promise<boolean> {
|
||||
return true
|
||||
}
|
||||
@@ -88,11 +144,15 @@ export abstract class BaseTabComponent {
|
||||
this.blurred.next()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before the tab is closed
|
||||
*/
|
||||
destroy (): void {
|
||||
this.focused.complete()
|
||||
this.blurred.complete()
|
||||
this.titleChange.complete()
|
||||
this.progress.complete()
|
||||
this.recoveryStateChangedHint.complete()
|
||||
this.destroyed.next()
|
||||
this.destroyed.complete()
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
.icon(tabindex='0', [class.active]='model', (keyup.space)='click()')
|
||||
i.fa.fa-square-o.off
|
||||
i.fa.fa-check-square.on
|
||||
i.fas.fa-square.off
|
||||
i.fas.fa-check-square.on
|
||||
.text {{text}}
|
||||
|
@@ -20,6 +20,10 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.off {
|
||||
color: rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
flex: none;
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { NgZone, Component, Input, HostBinding, HostListener } from '@angular/core'
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'checkbox',
|
||||
template: require('./checkbox.component.pug'),
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Component, Input, ElementRef, ViewChild } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'rename-tab-modal',
|
||||
template: require('./renameTabModal.component.pug'),
|
||||
@@ -16,6 +17,7 @@ export class RenameTabModalComponent {
|
||||
ngOnInit () {
|
||||
setTimeout(() => {
|
||||
this.input.nativeElement.focus()
|
||||
this.input.nativeElement.select()
|
||||
}, 250)
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
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;
|
||||
}
|
524
terminus-core/src/components/splitTab.component.ts
Normal file
@@ -0,0 +1,524 @@
|
||||
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 'pane-nav-left':
|
||||
this.navigate('l')
|
||||
break
|
||||
case 'pane-nav-right':
|
||||
this.navigate('r')
|
||||
break
|
||||
case 'pane-nav-up':
|
||||
this.navigate('t')
|
||||
break
|
||||
case 'pane-nav-down':
|
||||
this.navigate('b')
|
||||
break
|
||||
case 'close-pane':
|
||||
this.removeTab(this.focusedTab)
|
||||
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()
|
||||
} else {
|
||||
this.focusAnyIn(parent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
@@ -14,10 +14,10 @@ div
|
||||
footer.d-flex.align-items-center
|
||||
.btn-group.mr-auto
|
||||
button.btn.btn-secondary((click)='homeBase.openGitHub()')
|
||||
i.fa.fa-github
|
||||
i.fab.fa-github
|
||||
span GitHub
|
||||
button.btn.btn-secondary((click)='homeBase.reportBug()')
|
||||
i.fa.fa-bug
|
||||
i.fas.fa-bug
|
||||
span Report a problem
|
||||
|
||||
.form-control-static.selectable.no-drag Version: {{homeBase.appVersion}}
|
||||
|
@@ -3,6 +3,7 @@ import { ConfigService } from '../services/config.service'
|
||||
import { HomeBaseService } from '../services/homeBase.service'
|
||||
import { IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'start-page',
|
||||
template: require('./startPage.component.pug'),
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Component, Input, ViewChild, HostBinding, ViewContainerRef, OnChanges } from '@angular/core'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'tab-body',
|
||||
template: `
|
||||
@@ -17,7 +18,7 @@ import { BaseTabComponent } from '../components/baseTab.component'
|
||||
export class TabBodyComponent implements OnChanges {
|
||||
@Input() @HostBinding('class.active') active: boolean
|
||||
@Input() tab: BaseTabComponent
|
||||
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef
|
||||
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
|
||||
|
||||
ngOnChanges (changes) {
|
||||
if (changes.tab) {
|
||||
|
@@ -1,4 +1,7 @@
|
||||
.progressbar([style.width]='progress + "%"', *ngIf='progress != null')
|
||||
.index(#handle) {{index + 1}}
|
||||
.index(
|
||||
#handle,
|
||||
[style.background-color]='tab.color',
|
||||
) {{index + 1}}
|
||||
.name([title]='tab.customTitle || tab.title') {{tab.customTitle || tab.title}}
|
||||
button((click)='app.closeTab(tab, true)') ×
|
||||
|
@@ -1,12 +1,15 @@
|
||||
import { Component, Input, HostBinding, HostListener, NgZone, ViewChild, ElementRef } from '@angular/core'
|
||||
import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef } from '@angular/core'
|
||||
import { SortableComponent } from 'ng2-dnd'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider'
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { RenameTabModalComponent } from './renameTabModal.component'
|
||||
import { HotkeysService } from '../services/hotkeys.service'
|
||||
import { ElectronService } from '../services/electron.service'
|
||||
import { AppService } from '../services/app.service'
|
||||
import { HostAppService, Platform } from '../services/hostApp.service'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'tab-header',
|
||||
template: require('./tabHeader.component.pug'),
|
||||
@@ -20,16 +23,24 @@ export class TabHeaderComponent {
|
||||
@Input() progress: number
|
||||
@ViewChild('handle') handle: ElementRef
|
||||
|
||||
private completionNotificationEnabled = false
|
||||
|
||||
constructor (
|
||||
public app: AppService,
|
||||
private electron: ElectronService,
|
||||
private zone: NgZone,
|
||||
private hostApp: HostAppService,
|
||||
private ngbModal: NgbModal,
|
||||
private hotkeys: HotkeysService,
|
||||
private parentDraggable: SortableComponent,
|
||||
) { }
|
||||
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
|
||||
) {
|
||||
this.hotkeys.matchedHotkey.subscribe((hotkey) => {
|
||||
if (this.app.activeTab === this.tab) {
|
||||
if (hotkey === 'rename-tab') {
|
||||
this.showRenameTabModal()
|
||||
}
|
||||
}
|
||||
})
|
||||
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
if (this.hostApp.platform === Platform.macOS) {
|
||||
@@ -40,7 +51,7 @@ export class TabHeaderComponent {
|
||||
})
|
||||
}
|
||||
|
||||
@HostListener('dblclick') onDoubleClick (): void {
|
||||
showRenameTabModal (): void {
|
||||
let modal = this.ngbModal.open(RenameTabModalComponent)
|
||||
modal.componentInstance.value = this.tab.customTitle || this.tab.title
|
||||
modal.result.then(result => {
|
||||
@@ -49,6 +60,19 @@ export class TabHeaderComponent {
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
||||
async buildContextMenu (): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||
let items: Electron.MenuItemConstructorOptions[] = []
|
||||
for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this.tab, this)))) {
|
||||
items.push({ type: 'separator' })
|
||||
items = items.concat(section)
|
||||
}
|
||||
return items.slice(1)
|
||||
}
|
||||
|
||||
@HostListener('dblclick') onDoubleClick (): void {
|
||||
this.showRenameTabModal()
|
||||
}
|
||||
|
||||
@HostListener('auxclick', ['$event']) async onAuxClick ($event: MouseEvent) {
|
||||
if ($event.which === 2) {
|
||||
this.app.closeTab(this.tab, true)
|
||||
@@ -56,73 +80,7 @@ export class TabHeaderComponent {
|
||||
if ($event.which === 3) {
|
||||
event.preventDefault()
|
||||
|
||||
let contextMenu = this.electron.remote.Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Close',
|
||||
click: () => this.zone.run(() => {
|
||||
this.app.closeTab(this.tab, true)
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Close other tabs',
|
||||
click: () => this.zone.run(() => {
|
||||
for (let tab of this.app.tabs.filter(x => x !== this.tab)) {
|
||||
this.app.closeTab(tab, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Close tabs to the right',
|
||||
click: () => this.zone.run(() => {
|
||||
for (let tab of this.app.tabs.slice(this.app.tabs.indexOf(this.tab) + 1)) {
|
||||
this.app.closeTab(tab, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Close tabs to the left',
|
||||
click: () => this.zone.run(() => {
|
||||
for (let tab of this.app.tabs.slice(0, this.app.tabs.indexOf(this.tab))) {
|
||||
this.app.closeTab(tab, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
])
|
||||
|
||||
let process = await this.tab.getCurrentProcess()
|
||||
if (process) {
|
||||
contextMenu.append(new this.electron.MenuItem({
|
||||
id: 'sep',
|
||||
type: 'separator',
|
||||
}))
|
||||
contextMenu.append(new this.electron.MenuItem({
|
||||
id: 'process-name',
|
||||
enabled: false,
|
||||
label: 'Current process: ' + process.name,
|
||||
}))
|
||||
contextMenu.append(new this.electron.MenuItem({
|
||||
id: 'completion',
|
||||
label: 'Notify when done',
|
||||
type: 'checkbox',
|
||||
checked: this.completionNotificationEnabled,
|
||||
click: () => this.zone.run(() => {
|
||||
this.completionNotificationEnabled = !this.completionNotificationEnabled
|
||||
|
||||
if (this.completionNotificationEnabled) {
|
||||
this.app.observeTabCompletion(this.tab).subscribe(() => {
|
||||
new Notification('Process completed', {
|
||||
body: process.name,
|
||||
}).addEventListener('click', () => {
|
||||
this.app.selectTab(this.tab)
|
||||
})
|
||||
this.completionNotificationEnabled = false
|
||||
})
|
||||
} else {
|
||||
this.app.stopObservingTabCompletion(this.tab)
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
const contextMenu = this.electron.remote.Menu.buildFromTemplate(await this.buildContextMenu())
|
||||
|
||||
contextMenu.popup({
|
||||
x: $event.pageX,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'title-bar',
|
||||
template: require('./titleBar.component.pug'),
|
||||
|
@@ -2,6 +2,7 @@ import { Component } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { CheckboxComponent } from './checkbox.component'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'toggle',
|
||||
template: `
|
||||
|
@@ -9,7 +9,7 @@ button.btn.btn-secondary.btn-maximize(
|
||||
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')
|
||||
button.btn.btn-secondary.btn-close(
|
||||
(click)='app.closeWindow()'
|
||||
(click)='closeWindow()'
|
||||
)
|
||||
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')
|
||||
|
@@ -2,6 +2,7 @@ import { Component } from '@angular/core'
|
||||
import { HostAppService } from '../services/hostApp.service'
|
||||
import { AppService } from '../services/app.service'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'window-controls',
|
||||
template: require('./windowControls.component.pug'),
|
||||
@@ -9,4 +10,9 @@ import { AppService } from '../services/app.service'
|
||||
})
|
||||
export class WindowControlsComponent {
|
||||
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 { Platform } from './services/hostApp.service'
|
||||
|
||||
/** @hidden */
|
||||
export class CoreConfigProvider extends ConfigProvider {
|
||||
platformDefaults = {
|
||||
[Platform.macOS]: require('./configDefaults.macos.yaml'),
|
||||
|
@@ -7,46 +7,48 @@ hotkeys:
|
||||
- 'F11'
|
||||
close-tab:
|
||||
- 'Ctrl-Shift-W'
|
||||
- ['Ctrl-A', 'K']
|
||||
toggle-last-tab:
|
||||
- ['Ctrl-A', 'A']
|
||||
- ['Ctrl-A', 'Ctrl-A']
|
||||
toggle-last-tab: []
|
||||
rename-tab:
|
||||
- 'Ctrl-Shift-R'
|
||||
next-tab:
|
||||
- 'Ctrl-Shift-ArrowRight'
|
||||
- ['Ctrl-A', 'N']
|
||||
- 'Ctrl-Tab'
|
||||
previous-tab:
|
||||
- 'Ctrl-Shift-ArrowLeft'
|
||||
- ['Ctrl-A', 'P']
|
||||
- 'Ctrl-Shift-Tab'
|
||||
tab-1:
|
||||
- 'Alt-1'
|
||||
- ['Ctrl-A', '1']
|
||||
tab-2:
|
||||
- 'Alt-2'
|
||||
- ['Ctrl-A', '2']
|
||||
tab-3:
|
||||
- 'Alt-3'
|
||||
- ['Ctrl-A', '3']
|
||||
tab-4:
|
||||
- 'Alt-4'
|
||||
- ['Ctrl-A', '4']
|
||||
tab-5:
|
||||
- 'Alt-5'
|
||||
- ['Ctrl-A', '5']
|
||||
tab-6:
|
||||
- 'Alt-6'
|
||||
- ['Ctrl-A', '6']
|
||||
tab-7:
|
||||
- 'Alt-7'
|
||||
- ['Ctrl-A', '7']
|
||||
tab-8:
|
||||
- 'Alt-8'
|
||||
- ['Ctrl-A', '8']
|
||||
tab-9:
|
||||
- 'Alt-9'
|
||||
- ['Ctrl-A', '9']
|
||||
tab-10:
|
||||
- 'Alt-0'
|
||||
- ['Ctrl-A', '0']
|
||||
split-right:
|
||||
- 'Ctrl-Shift-E'
|
||||
split-bottom:
|
||||
- 'Ctrl-Shift-D'
|
||||
split-left: []
|
||||
split-top: []
|
||||
pane-nav-right:
|
||||
- 'Ctrl-Alt-ArrowRight'
|
||||
pane-nav-down:
|
||||
- 'Ctrl-Alt-ArrowDown'
|
||||
pane-nav-up:
|
||||
- 'Ctrl-Alt-ArrowUp'
|
||||
pane-nav-left:
|
||||
- 'Ctrl-Alt-ArrowLeft'
|
||||
close-pane: []
|
||||
pluginBlacklist: ['ssh']
|
||||
|
@@ -8,6 +8,8 @@ hotkeys:
|
||||
close-tab:
|
||||
- '⌘-W'
|
||||
toggle-last-tab: []
|
||||
rename-tab:
|
||||
- '⌘-R'
|
||||
next-tab:
|
||||
- 'Ctrl-Tab'
|
||||
previous-tab:
|
||||
@@ -32,4 +34,20 @@ hotkeys:
|
||||
- '⌘-9'
|
||||
tab-10:
|
||||
- '⌘-0'
|
||||
split-right:
|
||||
- '⌘-Shift-D'
|
||||
split-bottom:
|
||||
- '⌘-D'
|
||||
split-left: []
|
||||
split-top: []
|
||||
pane-nav-right:
|
||||
- '⌘-⌥-ArrowRight'
|
||||
pane-nav-down:
|
||||
- '⌘-⌥-ArrowDown'
|
||||
pane-nav-up:
|
||||
- '⌘-⌥-ArrowUp'
|
||||
pane-nav-left:
|
||||
- '⌘-⌥-ArrowLeft'
|
||||
close-pane:
|
||||
- '⌘-Shift-W'
|
||||
pluginBlacklist: ['ssh']
|
||||
|
@@ -7,46 +7,48 @@ hotkeys:
|
||||
- 'F11'
|
||||
close-tab:
|
||||
- 'Ctrl-Shift-W'
|
||||
- ['Ctrl-A', 'K']
|
||||
toggle-last-tab:
|
||||
- ['Ctrl-A', 'A']
|
||||
- ['Ctrl-A', 'Ctrl-A']
|
||||
toggle-last-tab: []
|
||||
rename-tab:
|
||||
- 'Ctrl-Shift-R'
|
||||
next-tab:
|
||||
- 'Ctrl-Shift-ArrowRight'
|
||||
- ['Ctrl-A', 'N']
|
||||
- 'Ctrl-Tab'
|
||||
previous-tab:
|
||||
- 'Ctrl-Shift-ArrowLeft'
|
||||
- ['Ctrl-A', 'P']
|
||||
- 'Ctrl-Shift-Tab'
|
||||
tab-1:
|
||||
- 'Alt-1'
|
||||
- ['Ctrl-A', '1']
|
||||
tab-2:
|
||||
- 'Alt-2'
|
||||
- ['Ctrl-A', '2']
|
||||
tab-3:
|
||||
- 'Alt-3'
|
||||
- ['Ctrl-A', '3']
|
||||
tab-4:
|
||||
- 'Alt-4'
|
||||
- ['Ctrl-A', '4']
|
||||
tab-5:
|
||||
- 'Alt-5'
|
||||
- ['Ctrl-A', '5']
|
||||
tab-6:
|
||||
- 'Alt-6'
|
||||
- ['Ctrl-A', '6']
|
||||
tab-7:
|
||||
- 'Alt-7'
|
||||
- ['Ctrl-A', '7']
|
||||
tab-8:
|
||||
- 'Alt-8'
|
||||
- ['Ctrl-A', '8']
|
||||
tab-9:
|
||||
- 'Alt-9'
|
||||
- ['Ctrl-A', '9']
|
||||
tab-10:
|
||||
- 'Alt-0'
|
||||
- ['Ctrl-A', '0']
|
||||
split-right:
|
||||
- 'Ctrl-Shift-E'
|
||||
split-bottom:
|
||||
- 'Ctrl-Shift-D'
|
||||
split-left: []
|
||||
split-top: []
|
||||
pane-nav-right:
|
||||
- 'Ctrl-Alt-ArrowRight'
|
||||
pane-nav-down:
|
||||
- 'Ctrl-Alt-ArrowDown'
|
||||
pane-nav-up:
|
||||
- 'Ctrl-Alt-ArrowUp'
|
||||
pane-nav-left:
|
||||
- 'Ctrl-Alt-ArrowLeft'
|
||||
close-pane: []
|
||||
pluginBlacklist: []
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Directive, AfterViewInit, ElementRef } from '@angular/core'
|
||||
|
||||
/** @hidden */
|
||||
@Directive({
|
||||
selector: '[autofocus]'
|
||||
})
|
||||
|
121
terminus-core/src/hotkeys.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
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: 'pane-nav-up',
|
||||
name: 'Focus the pane above',
|
||||
},
|
||||
{
|
||||
id: 'pane-nav-down',
|
||||
name: 'Focus the pane below',
|
||||
},
|
||||
{
|
||||
id: 'pane-nav-left',
|
||||
name: 'Focus the pane on the left',
|
||||
},
|
||||
{
|
||||
id: 'pane-nav-right',
|
||||
name: 'Focus the pane on the right',
|
||||
},
|
||||
{
|
||||
id: 'close-pane',
|
||||
name: 'Close focused pane',
|
||||
},
|
||||
]
|
||||
|
||||
async provide (): Promise<IHotkeyDescription[]> {
|
||||
return this.hotkeys
|
||||
}
|
||||
}
|
@@ -6,20 +6,6 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
|
||||
import { DndModule } from 'ng2-dnd'
|
||||
|
||||
import { AppService } from './services/app.service'
|
||||
import { ConfigService } from './services/config.service'
|
||||
import { ElectronService } from './services/electron.service'
|
||||
import { HostAppService } from './services/hostApp.service'
|
||||
import { LogService } from './services/log.service'
|
||||
import { HomeBaseService } from './services/homeBase.service'
|
||||
import { HotkeysService, AppHotkeyProvider } from './services/hotkeys.service'
|
||||
import { DockingService } from './services/docking.service'
|
||||
import { ShellIntegrationService } from './services/shellIntegration.service'
|
||||
import { TabRecoveryService } from './services/tabRecovery.service'
|
||||
import { ThemesService } from './services/themes.service'
|
||||
import { TouchbarService } from './services/touchbar.service'
|
||||
import { UpdaterService } from './services/updater.service'
|
||||
|
||||
import { AppRootComponent } from './components/appRoot.component'
|
||||
import { CheckboxComponent } from './components/checkbox.component'
|
||||
import { TabBodyComponent } from './components/tabBody.component'
|
||||
@@ -30,41 +16,39 @@ import { TitleBarComponent } from './components/titleBar.component'
|
||||
import { ToggleComponent } from './components/toggle.component'
|
||||
import { WindowControlsComponent } from './components/windowControls.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 { HotkeyProvider } from './api/hotkeyProvider'
|
||||
import { ConfigProvider } from './api/configProvider'
|
||||
import { Theme } from './api/theme'
|
||||
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
|
||||
import { TabRecoveryProvider } from './api/tabRecovery'
|
||||
|
||||
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
|
||||
import { CoreConfigProvider } from './config'
|
||||
import { AppHotkeyProvider } from './hotkeys'
|
||||
import { TaskCompletionContextMenu, CommonOptionsContextMenu, CloseContextMenu } from './tabContextMenu'
|
||||
|
||||
import 'perfect-scrollbar/css/perfect-scrollbar.css'
|
||||
import 'ng2-dnd/bundles/style.css'
|
||||
|
||||
const PROVIDERS = [
|
||||
AppService,
|
||||
ConfigService,
|
||||
DockingService,
|
||||
ElectronService,
|
||||
HomeBaseService,
|
||||
HostAppService,
|
||||
HotkeysService,
|
||||
LogService,
|
||||
ShellIntegrationService,
|
||||
TabRecoveryService,
|
||||
ThemesService,
|
||||
TouchbarService,
|
||||
UpdaterService,
|
||||
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
|
||||
{ provide: Theme, useClass: StandardTheme, multi: true },
|
||||
{ provide: Theme, useClass: StandardCompactTheme, multi: true },
|
||||
{ provide: Theme, useClass: PaperTheme, multi: true },
|
||||
{ provide: ConfigProvider, useClass: CoreConfigProvider, multi: true },
|
||||
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true }}
|
||||
{ provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true },
|
||||
{ provide: TabContextMenuItemProvider, useClass: CloseContextMenu, multi: true },
|
||||
{ provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true },
|
||||
{ provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true },
|
||||
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } }
|
||||
]
|
||||
|
||||
/** @hidden */
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -86,10 +70,13 @@ const PROVIDERS = [
|
||||
RenameTabModalComponent,
|
||||
SafeModeModalComponent,
|
||||
AutofocusDirective,
|
||||
SplitTabComponent,
|
||||
SplitTabSpannerComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
RenameTabModalComponent,
|
||||
SafeModeModalComponent,
|
||||
SplitTabComponent,
|
||||
],
|
||||
exports: [
|
||||
CheckboxComponent,
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { Observable, Subject, AsyncSubject } from 'rxjs'
|
||||
import { takeUntil } from 'rxjs/operators'
|
||||
import { Injectable, ComponentFactoryResolver, Injector } from '@angular/core'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { Logger, LogService } from './log.service'
|
||||
import { SplitTabComponent } from '../components/splitTab.component'
|
||||
import { ConfigService } from './config.service'
|
||||
import { HostAppService } from './hostApp.service'
|
||||
|
||||
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
|
||||
import { TabRecoveryService } from './tabRecovery.service'
|
||||
import { TabsService, TabComponentType } from './tabs.service'
|
||||
|
||||
class CompletionObserver {
|
||||
get done$ (): Observable<void> { return this.done }
|
||||
@@ -35,12 +35,14 @@ class CompletionObserver {
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AppService {
|
||||
tabs: BaseTabComponent[] = []
|
||||
activeTab: BaseTabComponent
|
||||
lastTabIndex = 0
|
||||
logger: Logger
|
||||
|
||||
get activeTab (): BaseTabComponent { return this._activeTab }
|
||||
|
||||
private lastTabIndex = 0
|
||||
private _activeTab: BaseTabComponent
|
||||
|
||||
private activeTabChange = new Subject<BaseTabComponent>()
|
||||
private tabsChanged = new Subject<void>()
|
||||
@@ -54,62 +56,105 @@ export class AppService {
|
||||
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
|
||||
get tabsChanged$ (): Observable<void> { return this.tabsChanged }
|
||||
get tabClosed$ (): Observable<BaseTabComponent> { return this.tabClosed }
|
||||
|
||||
/** Fires once when the app is ready */
|
||||
get ready$ (): Observable<void> { return this.ready }
|
||||
|
||||
/** @hidden */
|
||||
constructor (
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private config: ConfigService,
|
||||
private hostApp: HostAppService,
|
||||
private injector: Injector,
|
||||
log: LogService,
|
||||
private tabRecovery: TabRecoveryService,
|
||||
private tabsService: TabsService,
|
||||
) {
|
||||
this.logger = log.create('app')
|
||||
this.tabRecovery.recoverTabs().then(tabs => {
|
||||
for (let tab of tabs) {
|
||||
this.openNewTabRaw(tab.type, tab.options)
|
||||
}
|
||||
|
||||
this.hostApp.windowCloseRequest$.subscribe(() => this.closeWindow())
|
||||
this.tabsChanged$.subscribe(() => {
|
||||
tabRecovery.saveTabs(this.tabs)
|
||||
})
|
||||
setInterval(() => {
|
||||
tabRecovery.saveTabs(this.tabs)
|
||||
}, 30000)
|
||||
})
|
||||
}
|
||||
|
||||
openNewTab (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 || {})
|
||||
|
||||
private addTabRaw (tab: BaseTabComponent) {
|
||||
this.tabs.push(tab)
|
||||
this.selectTab(tab)
|
||||
this.tabsChanged.next()
|
||||
this.tabOpened.next(tab)
|
||||
|
||||
tab.recoveryStateChangedHint$.subscribe(() => {
|
||||
this.tabRecovery.saveTabs(this.tabs)
|
||||
})
|
||||
|
||||
tab.titleChange$.subscribe(title => {
|
||||
if (tab === this.activeTab) {
|
||||
if (tab === this._activeTab) {
|
||||
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
|
||||
}
|
||||
|
||||
selectTab (tab: BaseTabComponent) {
|
||||
if (this.activeTab === tab) {
|
||||
this.activeTab.emitFocused()
|
||||
if (this._activeTab === tab) {
|
||||
this._activeTab.emitFocused()
|
||||
return
|
||||
}
|
||||
if (this.tabs.includes(this.activeTab)) {
|
||||
this.lastTabIndex = this.tabs.indexOf(this.activeTab)
|
||||
if (this.tabs.includes(this._activeTab)) {
|
||||
this.lastTabIndex = this.tabs.indexOf(this._activeTab)
|
||||
} else {
|
||||
this.lastTabIndex = null
|
||||
}
|
||||
if (this.activeTab) {
|
||||
this.activeTab.clearActivity()
|
||||
this.activeTab.emitBlurred()
|
||||
if (this._activeTab) {
|
||||
this._activeTab.clearActivity()
|
||||
this._activeTab.emitBlurred()
|
||||
}
|
||||
this.activeTab = tab
|
||||
this._activeTab = tab
|
||||
this.activeTabChange.next(tab)
|
||||
if (this.activeTab) {
|
||||
this.activeTab.emitFocused()
|
||||
this.hostApp.setTitle(this.activeTab.title)
|
||||
if (this._activeTab) {
|
||||
setImmediate(() => {
|
||||
this._activeTab.emitFocused()
|
||||
})
|
||||
this.hostApp.setTitle(this._activeTab.title)
|
||||
}
|
||||
}
|
||||
|
||||
/** Switches between the current tab and the previously active one */
|
||||
toggleLastTab () {
|
||||
if (!this.lastTabIndex || this.lastTabIndex >= this.tabs.length) {
|
||||
this.lastTabIndex = 0
|
||||
@@ -119,7 +164,7 @@ export class AppService {
|
||||
|
||||
nextTab () {
|
||||
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) {
|
||||
this.selectTab(this.tabs[tabIndex + 1])
|
||||
} else if (this.config.store.appearance.cycleTabs) {
|
||||
@@ -130,7 +175,7 @@ export class AppService {
|
||||
|
||||
previousTab () {
|
||||
if (this.tabs.length > 1) {
|
||||
let tabIndex = this.tabs.indexOf(this.activeTab)
|
||||
let tabIndex = this.tabs.indexOf(this._activeTab)
|
||||
if (tabIndex > 0) {
|
||||
this.selectTab(this.tabs[tabIndex - 1])
|
||||
} else if (this.config.store.appearance.cycleTabs) {
|
||||
@@ -139,6 +184,7 @@ export class AppService {
|
||||
}
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
emitTabsChanged () {
|
||||
this.tabsChanged.next()
|
||||
}
|
||||
@@ -150,17 +196,17 @@ export class AppService {
|
||||
if (checkCanClose && !await tab.canClose()) {
|
||||
return
|
||||
}
|
||||
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
||||
this.tabs = this.tabs.filter((x) => x !== tab)
|
||||
tab.destroy()
|
||||
if (tab === this.activeTab) {
|
||||
this.selectTab(this.tabs[newIndex])
|
||||
}
|
||||
this.tabsChanged.next()
|
||||
this.tabClosed.next(tab)
|
||||
}
|
||||
|
||||
async closeWindow () {
|
||||
async duplicateTab (tab: BaseTabComponent) {
|
||||
let dup = await this.tabsService.duplicate(tab)
|
||||
if (dup) {
|
||||
this.addTabRaw(dup)
|
||||
}
|
||||
}
|
||||
|
||||
async closeAllTabs () {
|
||||
for (let tab of this.tabs) {
|
||||
if (!await tab.canClose()) {
|
||||
return
|
||||
@@ -169,15 +215,19 @@ export class AppService {
|
||||
for (let tab of this.tabs) {
|
||||
tab.destroy()
|
||||
}
|
||||
this.hostApp.closeWindow()
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
emitReady () {
|
||||
this.ready.next(null)
|
||||
this.ready.complete()
|
||||
this.hostApp.emitReady()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that fires once
|
||||
* the tab's internal "process" (see [[BaseTabProcess]]) completes
|
||||
*/
|
||||
observeTabCompletion (tab: BaseTabComponent): Observable<void> {
|
||||
if (!this.completionObservers.has(tab)) {
|
||||
let observer = new CompletionObserver(tab)
|
||||
|
@@ -18,6 +18,7 @@ function isNonStructuralObjectMember (v) {
|
||||
return v instanceof Object && !(v instanceof Array) && v.__nonStructural
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
export class ConfigProxy {
|
||||
constructor (real: any, defaults: any) {
|
||||
for (let key in defaults) {
|
||||
@@ -56,7 +57,7 @@ export class ConfigProxy {
|
||||
return real[key]
|
||||
} else {
|
||||
if (isNonStructuralObjectMember(defaults[key])) {
|
||||
real[key] = {...defaults[key]}
|
||||
real[key] = { ...defaults[key] }
|
||||
delete real[key].__nonStructural
|
||||
return real[key]
|
||||
} else {
|
||||
@@ -74,18 +75,31 @@ export class ConfigProxy {
|
||||
setValue (key: string, value: any) { } // tslint:disable-line
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ConfigService {
|
||||
/**
|
||||
* Contains the actual config values
|
||||
*/
|
||||
store: any
|
||||
|
||||
/**
|
||||
* Whether an app restart is required due to recent changes
|
||||
*/
|
||||
restartRequested: boolean
|
||||
|
||||
/**
|
||||
* Full config file path
|
||||
*/
|
||||
path: string
|
||||
|
||||
private changed = new Subject<void>()
|
||||
private _store: any
|
||||
private path: string
|
||||
private defaults: any
|
||||
private servicesCache: { [id: string]: Function[] } = null
|
||||
|
||||
get changed$ (): Observable<void> { return this.changed }
|
||||
|
||||
/** @hidden */
|
||||
constructor (
|
||||
electron: ElectronService,
|
||||
private hostApp: HostAppService,
|
||||
@@ -129,10 +143,16 @@ export class ConfigService {
|
||||
this.hostApp.broadcastConfigChange()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads config YAML as string
|
||||
*/
|
||||
readRaw (): string {
|
||||
return yaml.safeDump(this._store)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes config YAML as string
|
||||
*/
|
||||
writeRaw (data: string): void {
|
||||
this._store = yaml.safeLoad(data)
|
||||
this.save()
|
||||
@@ -140,7 +160,7 @@ export class ConfigService {
|
||||
this.emitChange()
|
||||
}
|
||||
|
||||
emitChange (): void {
|
||||
private emitChange (): void {
|
||||
this.changed.next()
|
||||
}
|
||||
|
||||
@@ -148,6 +168,12 @@ export class ConfigService {
|
||||
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[] {
|
||||
if (!this.servicesCache) {
|
||||
this.servicesCache = {}
|
||||
|
@@ -8,8 +8,9 @@ export interface IScreen {
|
||||
name: string
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DockingService {
|
||||
/** @hidden */
|
||||
constructor (
|
||||
private electron: ElectronService,
|
||||
private config: ConfigService,
|
||||
@@ -20,21 +21,23 @@ export class DockingService {
|
||||
}
|
||||
|
||||
dock () {
|
||||
let dockSide = this.config.store.appearance.dock
|
||||
|
||||
if (dockSide === 'off') {
|
||||
this.hostApp.setAlwaysOnTop(false)
|
||||
return
|
||||
}
|
||||
|
||||
let display = this.electron.screen.getAllDisplays()
|
||||
.filter((x) => x.id === this.config.store.appearance.dockScreen)[0]
|
||||
if (!display) {
|
||||
display = this.getCurrentScreen()
|
||||
}
|
||||
|
||||
let dockSide = this.config.store.appearance.dock
|
||||
let newBounds: Bounds = { x: 0, y: 0, width: 0, height: 0 }
|
||||
let fill = this.config.store.appearance.dockFill
|
||||
let [minWidth, minHeight] = this.hostApp.getWindow().getMinimumSize()
|
||||
|
||||
if (dockSide === 'off') {
|
||||
this.hostApp.setAlwaysOnTop(false)
|
||||
return
|
||||
}
|
||||
if (dockSide === 'left' || dockSide === 'right') {
|
||||
newBounds.width = Math.max(minWidth, Math.round(fill * display.bounds.width))
|
||||
newBounds.height = display.bounds.height
|
||||
@@ -76,7 +79,7 @@ export class DockingService {
|
||||
})
|
||||
}
|
||||
|
||||
repositionWindow () {
|
||||
private repositionWindow () {
|
||||
let [x, y] = this.hostApp.getWindow().getPosition()
|
||||
for (let screen of this.electron.screen.getAllDisplays()) {
|
||||
let bounds = screen.bounds
|
||||
|
@@ -1,7 +1,12 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TouchBar, BrowserWindow, Menu, MenuItem } from 'electron'
|
||||
|
||||
@Injectable()
|
||||
export interface MessageBoxResponse {
|
||||
response: number
|
||||
checkboxChecked?: boolean
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ElectronService {
|
||||
app: any
|
||||
ipcRenderer: any
|
||||
@@ -19,6 +24,7 @@ export class ElectronService {
|
||||
MenuItem: typeof MenuItem
|
||||
private electron: any
|
||||
|
||||
/** @hidden */
|
||||
constructor () {
|
||||
this.electron = require('electron')
|
||||
this.remote = this.electron.remote
|
||||
@@ -37,21 +43,23 @@ export class ElectronService {
|
||||
this.MenuItem = this.remote.MenuItem
|
||||
}
|
||||
|
||||
remoteRequire (name: string): any {
|
||||
return this.remote.require(name)
|
||||
}
|
||||
|
||||
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}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes OS focus from Terminus' window
|
||||
*/
|
||||
loseFocus () {
|
||||
if (process.platform === 'darwin') {
|
||||
this.remote.Menu.sendActionToFirstResponder('hide:')
|
||||
}
|
||||
}
|
||||
|
||||
showMessageBox (
|
||||
browserWindow: Electron.BrowserWindow,
|
||||
options: Electron.MessageBoxOptions
|
||||
): Promise<MessageBoxResponse> {
|
||||
return new Promise(resolve => {
|
||||
this.dialog.showMessageBox(browserWindow, options, (response, checkboxChecked) => {
|
||||
resolve({ response, checkboxChecked })
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -5,10 +5,11 @@ import { ConfigService } from './config.service'
|
||||
import ua = require('universal-analytics')
|
||||
import uuidv4 = require('uuid/v4')
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HomeBaseService {
|
||||
appVersion: string
|
||||
|
||||
/** @hidden */
|
||||
constructor (
|
||||
private electron: ElectronService,
|
||||
private config: ConfigService,
|
||||
@@ -33,7 +34,7 @@ export class HomeBaseService {
|
||||
linux: 'OS: Linux',
|
||||
}[os.platform()]
|
||||
let plugins = (window as any).installedPlugins.filter(x => !x.isBuiltin).map(x => x.name)
|
||||
body += `Plugins: ${plugins.join(', ')}\n\n`
|
||||
body += `Plugins: ${plugins.join(', ') || 'none'}\n\n`
|
||||
this.electron.shell.openExternal(`https://github.com/eugeny/terminus/issues/new?body=${encodeURIComponent(body)}&labels=${label}`)
|
||||
}
|
||||
|
||||
|
@@ -16,42 +16,88 @@ export interface Bounds {
|
||||
height: number
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
/**
|
||||
* Provides interaction with the main process
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HostAppService {
|
||||
platform: Platform
|
||||
nodePlatform: string
|
||||
|
||||
/**
|
||||
* Fired once the window is visible
|
||||
*/
|
||||
shown = new EventEmitter<any>()
|
||||
isFullScreen = false
|
||||
|
||||
private preferencesMenu = new Subject<void>()
|
||||
private secondInstance = new Subject<void>()
|
||||
private cliOpenDirectory = new Subject<string>()
|
||||
private cliRunCommand = new Subject<string[]>()
|
||||
private cliPaste = new Subject<string>()
|
||||
private cliOpenProfile = new Subject<string>()
|
||||
private configChangeBroadcast = new Subject<void>()
|
||||
private windowCloseRequest = new Subject<void>()
|
||||
private windowMoved = new Subject<void>()
|
||||
private displayMetricsChanged = new Subject<void>()
|
||||
private logger: Logger
|
||||
private windowId: number
|
||||
|
||||
/**
|
||||
* Fired when Preferences is selected in the macOS menu
|
||||
*/
|
||||
get preferencesMenu$ (): Observable<void> { return this.preferencesMenu }
|
||||
|
||||
/**
|
||||
* Fired when a second instance of Terminus is launched
|
||||
*/
|
||||
get secondInstance$ (): Observable<void> { return this.secondInstance }
|
||||
|
||||
/**
|
||||
* Fired for the `terminus open` CLI command
|
||||
*/
|
||||
get cliOpenDirectory$ (): Observable<string> { return this.cliOpenDirectory }
|
||||
|
||||
/**
|
||||
* Fired for the `terminus run` CLI command
|
||||
*/
|
||||
get cliRunCommand$ (): Observable<string[]> { return this.cliRunCommand }
|
||||
|
||||
/**
|
||||
* Fired for the `terminus paste` CLI command
|
||||
*/
|
||||
get cliPaste$ (): Observable<string> { return this.cliPaste }
|
||||
|
||||
/**
|
||||
* Fired for the `terminus profile` CLI command
|
||||
*/
|
||||
get cliOpenProfile$ (): Observable<string> { return this.cliOpenProfile }
|
||||
|
||||
/**
|
||||
* Fired when another window modified the config file
|
||||
*/
|
||||
get configChangeBroadcast$ (): Observable<void> { return this.configChangeBroadcast }
|
||||
|
||||
/**
|
||||
* Fired when the window close button is pressed
|
||||
*/
|
||||
get windowCloseRequest$ (): Observable<void> { return this.windowCloseRequest }
|
||||
|
||||
get windowMoved$ (): Observable<void> { return this.windowMoved }
|
||||
|
||||
get displayMetricsChanged$ (): Observable<void> { return this.displayMetricsChanged }
|
||||
|
||||
/** @hidden */
|
||||
constructor (
|
||||
private zone: NgZone,
|
||||
private electron: ElectronService,
|
||||
log: LogService,
|
||||
) {
|
||||
this.logger = log.create('hostApp')
|
||||
this.nodePlatform = require('os').platform()
|
||||
this.platform = {
|
||||
win32: Platform.Windows,
|
||||
darwin: Platform.macOS,
|
||||
linux: Platform.Linux
|
||||
}[this.nodePlatform]
|
||||
}[process.platform]
|
||||
|
||||
this.windowId = parseInt(location.search.substring(1))
|
||||
this.logger.info('Window ID:', this.windowId)
|
||||
@@ -78,6 +124,14 @@ export class HostAppService {
|
||||
this.zone.run(() => this.windowCloseRequest.next())
|
||||
})
|
||||
|
||||
electron.ipcRenderer.on('host:window-moved', () => {
|
||||
this.zone.run(() => this.windowMoved.next())
|
||||
})
|
||||
|
||||
electron.ipcRenderer.on('host:display-metrics-changed', () => {
|
||||
this.zone.run(() => this.displayMetricsChanged.next())
|
||||
})
|
||||
|
||||
electron.ipcRenderer.on('host:second-instance', (_$event, argv: any, cwd: string) => this.zone.run(() => {
|
||||
this.logger.info('Second instance', argv)
|
||||
const op = argv._[0]
|
||||
@@ -91,6 +145,8 @@ export class HostAppService {
|
||||
text = shellEscape([text])
|
||||
}
|
||||
this.cliPaste.next(text)
|
||||
} else if (op === 'profile') {
|
||||
this.cliOpenProfile.next(argv.profileName)
|
||||
} else {
|
||||
this.secondInstance.next()
|
||||
}
|
||||
@@ -101,6 +157,9 @@ export class HostAppService {
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current remote [[BrowserWindow]]
|
||||
*/
|
||||
getWindow () {
|
||||
return this.electron.BrowserWindow.fromId(this.windowId)
|
||||
}
|
||||
@@ -109,18 +168,6 @@ export class HostAppService {
|
||||
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 () {
|
||||
let window = this.getWindow()
|
||||
window.setFullScreen(!this.isFullScreen)
|
||||
@@ -158,6 +205,11 @@ export class HostAppService {
|
||||
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) {
|
||||
document.body.classList.toggle('vibrant', enable)
|
||||
if (this.platform === Platform.macOS) {
|
||||
@@ -180,6 +232,9 @@ export class HostAppService {
|
||||
this.electron.Menu.buildFromTemplate(menuDefinition).popup({})
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies other windows of config file changes
|
||||
*/
|
||||
broadcastConfigChange () {
|
||||
this.electron.ipcRenderer.send('app:config-change')
|
||||
}
|
||||
|
@@ -5,19 +5,19 @@ import { ConfigService } from '../services/config.service'
|
||||
import { ElectronService } from '../services/electron.service'
|
||||
|
||||
export interface PartialHotkeyMatch {
|
||||
id: string,
|
||||
strokes: string[],
|
||||
matchedLength: number,
|
||||
id: string
|
||||
strokes: string[]
|
||||
matchedLength: number
|
||||
}
|
||||
|
||||
const KEY_TIMEOUT = 2000
|
||||
|
||||
interface EventBufferEntry {
|
||||
event: NativeKeyEvent,
|
||||
time: number,
|
||||
event: NativeKeyEvent
|
||||
time: number
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HotkeysService {
|
||||
key = new EventEmitter<NativeKeyEvent>()
|
||||
matchedHotkey = new EventEmitter<string>()
|
||||
@@ -26,6 +26,7 @@ export class HotkeysService {
|
||||
private disabledLevel = 0
|
||||
private hotkeyDescriptions: IHotkeyDescription[] = []
|
||||
|
||||
/** @hidden */
|
||||
constructor (
|
||||
private zone: NgZone,
|
||||
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) {
|
||||
nativeEvent.event = name
|
||||
this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() })
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the buffer for new complete keystrokes
|
||||
*/
|
||||
processKeystrokes () {
|
||||
if (this.isEnabled()) {
|
||||
this.zone.run(() => {
|
||||
@@ -80,13 +90,13 @@ export class HotkeysService {
|
||||
}
|
||||
|
||||
getCurrentKeystrokes (): string[] {
|
||||
this.currentKeystrokes = this.currentKeystrokes.filter((x) => performance.now() - x.time < KEY_TIMEOUT )
|
||||
return stringifyKeySequence(this.currentKeystrokes.map((x) => x.event))
|
||||
this.currentKeystrokes = this.currentKeystrokes.filter(x => performance.now() - x.time < KEY_TIMEOUT)
|
||||
return stringifyKeySequence(this.currentKeystrokes.map(x => x.event))
|
||||
}
|
||||
|
||||
registerGlobalHotkey () {
|
||||
private registerGlobalHotkey () {
|
||||
this.electron.globalShortcut.unregisterAll()
|
||||
let value = this.config.store.hotkeys['toggle-window']
|
||||
let value = this.config.store.hotkeys['toggle-window'] || []
|
||||
if (typeof value === 'string') {
|
||||
value = [value]
|
||||
}
|
||||
@@ -103,11 +113,11 @@ export class HotkeysService {
|
||||
})
|
||||
}
|
||||
|
||||
getHotkeysConfig () {
|
||||
private getHotkeysConfig () {
|
||||
return this.getHotkeysConfigRecursive(this.config.store.hotkeys)
|
||||
}
|
||||
|
||||
getHotkeysConfigRecursive (branch) {
|
||||
private getHotkeysConfigRecursive (branch) {
|
||||
let keys = {}
|
||||
for (let key in branch) {
|
||||
let value = branch[key]
|
||||
@@ -120,14 +130,16 @@ export class HotkeysService {
|
||||
if (typeof value === 'string') {
|
||||
value = [value]
|
||||
}
|
||||
value = value.map((item) => (typeof item === 'string') ? [item] : item)
|
||||
keys[key] = value
|
||||
if (value) {
|
||||
value = value.map((item) => (typeof item === 'string') ? [item] : item)
|
||||
keys[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
getCurrentFullyMatchedHotkey (): string {
|
||||
private getCurrentFullyMatchedHotkey (): string {
|
||||
let currentStrokes = this.getCurrentKeystrokes()
|
||||
let config = this.getHotkeysConfig()
|
||||
for (let id in config) {
|
||||
@@ -197,81 +209,3 @@ export class HotkeysService {
|
||||
).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: '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]
|
||||
|
||||
export interface NativeKeyEvent {
|
||||
event?: string,
|
||||
altKey: boolean,
|
||||
ctrlKey: boolean,
|
||||
metaKey: boolean,
|
||||
shiftKey: boolean,
|
||||
key: string,
|
||||
keyCode: string,
|
||||
event?: string
|
||||
altKey: boolean
|
||||
ctrlKey: boolean
|
||||
metaKey: boolean
|
||||
shiftKey: boolean
|
||||
key: string
|
||||
keyCode: string
|
||||
}
|
||||
|
||||
export function stringifyKeySequence (events: NativeKeyEvent[]): string[] {
|
||||
|
@@ -23,7 +23,7 @@ const initializeWinston = (electron: ElectronService) => {
|
||||
colorize: false
|
||||
}),
|
||||
new winston.transports.Console({
|
||||
level: 'info',
|
||||
level: 'debug',
|
||||
handleExceptions: false,
|
||||
json: false,
|
||||
colorize: true
|
||||
@@ -39,7 +39,7 @@ export class Logger {
|
||||
private name: string,
|
||||
) {}
|
||||
|
||||
doLog (level: string, ...args: any[]) {
|
||||
private doLog (level: string, ...args: any[]) {
|
||||
console[level](`%c[${this.name}]`, 'color: #aaa', ...args)
|
||||
if (this.winstonLogger) {
|
||||
this.winstonLogger[level](...args)
|
||||
@@ -53,10 +53,11 @@ export class Logger {
|
||||
log (...args: any[]) { this.doLog('log', ...args) }
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class LogService {
|
||||
private log: any
|
||||
|
||||
/** @hidden */
|
||||
constructor (electron: ElectronService) {
|
||||
this.log = initializeWinston(electron)
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'mz/fs'
|
||||
import { Registry } from 'rage-edit-tmp'
|
||||
import { Registry } from 'rage-edit'
|
||||
import { exec } from 'mz/child_process'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ElectronService } from './electron.service'
|
||||
import { HostAppService, Platform } from './hostApp.service'
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ShellIntegrationService {
|
||||
private automatorWorkflows = ['Open Terminus here.workflow', 'Paste path into Terminus.workflow']
|
||||
private automatorWorkflowsLocation: string
|
||||
@@ -37,7 +37,7 @@ export class ShellIntegrationService {
|
||||
this.updatePaths()
|
||||
}
|
||||
|
||||
async updatePaths (): Promise<void> {
|
||||
private async updatePaths (): Promise<void> {
|
||||
// Update paths in case of an update
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
if (await this.isInstalled()) {
|
||||
@@ -48,9 +48,9 @@ export class ShellIntegrationService {
|
||||
|
||||
async isInstalled (): Promise<boolean> {
|
||||
if (this.hostApp.platform === Platform.macOS) {
|
||||
return await fs.exists(path.join(this.automatorWorkflowsDestination, this.automatorWorkflows[0]))
|
||||
return fs.exists(path.join(this.automatorWorkflowsDestination, this.automatorWorkflows[0]))
|
||||
} else if (this.hostApp.platform === Platform.Windows) {
|
||||
return await Registry.has(this.registryKeys[0].path)
|
||||
return Registry.has(this.registryKeys[0].path)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@@ -2,52 +2,57 @@ import { Injectable, Inject } from '@angular/core'
|
||||
import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { Logger, LogService } from '../services/log.service'
|
||||
import { AppService } from '../services/app.service'
|
||||
import { ConfigService } from '../services/config.service'
|
||||
|
||||
@Injectable()
|
||||
/** @hidden */
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TabRecoveryService {
|
||||
logger: Logger
|
||||
|
||||
constructor (
|
||||
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[],
|
||||
private app: AppService,
|
||||
private config: ConfigService,
|
||||
log: LogService
|
||||
) {
|
||||
this.logger = log.create('tabRecovery')
|
||||
app.tabsChanged$.subscribe(() => {
|
||||
this.saveTabs(app.tabs)
|
||||
})
|
||||
}
|
||||
|
||||
saveTabs (tabs: BaseTabComponent[]) {
|
||||
async saveTabs (tabs: BaseTabComponent[]) {
|
||||
window.localStorage.tabsRecovery = JSON.stringify(
|
||||
tabs
|
||||
.map((tab) => tab.getRecoveryToken())
|
||||
.filter((token) => !!token)
|
||||
await Promise.all(
|
||||
tabs
|
||||
.map(tab => tab.getRecoveryToken())
|
||||
.filter(token => !!token)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async recoverTabs (): Promise<void> {
|
||||
async recoverTab (token: any): Promise<RecoveredTab> {
|
||||
for (let provider of this.config.enabledServices(this.tabRecoveryProviders)) {
|
||||
try {
|
||||
let tab = await provider.recover(token)
|
||||
if (tab) {
|
||||
return tab
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn('Tab recovery crashed:', token, provider, error)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async recoverTabs (): Promise<RecoveredTab[]> {
|
||||
if (window.localStorage.tabsRecovery) {
|
||||
let tabs: RecoveredTab[] = []
|
||||
for (let token of JSON.parse(window.localStorage.tabsRecovery)) {
|
||||
for (let provider of this.config.enabledServices(this.tabRecoveryProviders)) {
|
||||
try {
|
||||
let tab = await provider.recover(token)
|
||||
if (tab) {
|
||||
tabs.push(tab)
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn('Tab recovery crashed:', token, provider, error)
|
||||
}
|
||||
let tab = await this.recoverTab(token)
|
||||
if (tab) {
|
||||
tabs.push(tab)
|
||||
}
|
||||
}
|
||||
tabs.forEach(tab => {
|
||||
this.app.openNewTab(tab.type, tab.options)
|
||||
})
|
||||
return tabs
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
|
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
|
||||
}
|
||||
}
|