Compare commits

...

79 Commits

Author SHA1 Message Date
Eugene Pankov
524550f6e3 made context menu extensible 2018-12-24 19:41:27 +01:00
Eugene Pankov
fe31131fc1 typo fix 2018-12-24 18:40:29 +01:00
Eugene Pankov
a7c1fe3425 Experimental UAC start-as-admin wrapper (fixes #511) 2018-12-24 18:11:26 +01:00
Eugene Pankov
d7b305bf29 fixes in profile editor 2018-12-24 17:22:27 +01:00
Eugene Pankov
0bd0c850da fixed profile duplication 2018-12-24 11:32:04 +01:00
Eugene Pankov
88bb40f94b offer shell selection in the terminal context menu 2018-12-23 21:03:09 +01:00
Eugene Pankov
120e2a2cd5 fixed --login flag for older shells 2018-12-23 21:02:18 +01:00
Eugene Pankov
cbb6821814 don't set an empty jumplist 2018-12-23 20:56:39 +01:00
Eugene Pankov
75bf374a8f build fix 2018-12-23 20:56:30 +01:00
Eugene Pankov
bf995981d3 use yarn on appveyor & autoinstall plugin deps 2018-12-23 20:03:29 +01:00
Eugene Pankov
a6fdabcd2f removed debug logging 2018-12-22 11:15:50 +01:00
Eugene Pankov
0e6886d00a fixed args field focus 2018-12-22 11:15:40 +01:00
Eugene Pankov
459d6aadd9 fixed beam cursor on xterm (fixes #582) 2018-12-22 09:39:17 +01:00
Eugene Pankov
21d533c7cf attempt to detect CWD on classic windows shells 2018-12-22 01:36:05 +01:00
Eugene Pankov
211566488d removed default ctrl-a hotkeys (fixes #578) 2018-12-21 23:49:30 +01:00
Eugene Pankov
282aab2e55 fixed alt-v passing in hterm (fixes #560) 2018-12-21 23:48:44 +01:00
Eugene Pankov
6f41865474 lint 2018-12-21 23:18:22 +01:00
Eugene Pankov
e4bcfd8f39 bumped node-pty 2018-12-21 23:11:53 +01:00
Eugene Pankov
504cfcf8ff build fix (fixes 579) 2018-12-21 23:06:24 +01:00
Eugene Pankov
6e13914712 fixed nightly builds (fixes #579) 2018-12-21 21:52:12 +01:00
Eugene Pankov
9aaf670092 fontawesome 5 2018-12-21 21:37:34 +01:00
Eugene Pankov
c204f6d5a4 use providedIn 2018-12-21 21:21:33 +01:00
Eugene Pankov
91bba042b5 made conpty optional 2018-12-21 21:05:59 +01:00
Eugene Pankov
2ca6135c06 ui tweaks 2018-12-21 20:43:11 +01:00
Eugene Pankov
9ef3cbc177 profile args editor 2018-12-21 20:06:03 +01:00
Eugene Pankov
8a3906687a Merge branch 'master' into persistence 2018-12-21 20:04:49 +01:00
Eugene
3192a14c9d Merge pull request #568 from ehwarren/feature/rename-tab-qol
Feature/rename tab qol
2018-12-19 10:17:08 +01:00
Austin Warren
b510a86f4d Change rename hotkey to ⌘-R 2018-12-18 17:20:08 -08:00
Austin Warren
fcf14eaa8b Move focus to RenameTabModalComponent onInit 2018-12-18 17:19:41 -08:00
Eugene Pankov
137dd0bbe8 profile editor, env vars editor, creating profiles from shell list 2018-12-18 15:08:23 +01:00
Eugene Pankov
4b5b75a57a ui 2018-12-17 20:41:08 +01:00
Eugene Pankov
68c497e5fc windows jumplist integration 2018-12-16 23:20:35 +01:00
Eugene Pankov
1da7c85973 cli option to launch a specific profile 2018-12-16 23:13:14 +01:00
Eugene Pankov
fe75aab724 show profiles in macos dock item menu 2018-12-16 23:02:17 +01:00
Eugene Pankov
85bcac1fb7 profile settings 2018-12-16 17:41:30 +01:00
Eugene Pankov
72287cc7cb profile settings tab 2018-12-16 17:09:35 +01:00
Eugene Pankov
1f1d212c1d build fix 2018-12-16 15:57:08 +01:00
Eugene Pankov
cded1284de simpler tab recovery system 2018-12-16 15:42:04 +01:00
Eugene Pankov
df97e7ebb5 updated wsl truecolor warning 2018-12-15 23:51:03 +01:00
Eugene Pankov
d80c9a27d3 pulled in the freshest node-pty (fixes #23) 2018-12-15 23:44:20 +01:00
Eugene Pankov
3469ec9b6b fixed blur (fixes #556) 2018-12-15 15:59:16 +01:00
Eugene Pankov
d4db8f4b18 build script fixes 2018-12-15 15:49:06 +01:00
Eugene Pankov
384744ec44 only enable agent forwarding on windows when pageant is running (fixes #496) 2018-12-15 15:19:47 +01:00
Eugene Pankov
76633db25e migrate back to stock rage-edit 2018-12-15 14:27:12 +01:00
Austin Warren
6b823d0fa0 Fixed build errors 2018-12-14 14:50:38 -08:00
Austin Warren
798dda5236 Added rename-tab hotkey 2018-12-14 14:50:16 -08:00
Austin Warren
2b90a17d5e Added hotkey rename, right click rename, and auto select text in rename modal 2018-12-14 14:49:20 -08:00
Eugene Pankov
6387539980 bumped angular 2018-12-13 18:01:44 +01:00
Eugene Pankov
cb17fd0866 fixed #564 2018-12-13 18:00:59 +01:00
Eugene Pankov
17bac5a904 build fix 2018-12-10 17:18:23 +01:00
Eugene Pankov
c34123ffe3 build fix 2018-12-10 16:31:45 +01:00
Eugene Pankov
c755885bbb build fix 2018-12-10 15:06:15 +01:00
Eugene Pankov
f49e3f0664 Revert "bumped webpack"
This reverts commit c58c629d0e.
2018-12-10 14:54:46 +01:00
Eugene Pankov
7852ac2071 potential fix for xterm double-paste (#468) 2018-12-10 14:23:08 +01:00
Eugene Pankov
60358e7ac4 xterm copy-on-select (fixes #400) 2018-12-10 13:08:57 +01:00
Eugene Pankov
f32bdbdeac make scroll-on-input behaviour configurable (fixes #543) 2018-12-10 11:57:13 +01:00
Eugene Pankov
c58c629d0e bumped webpack 2018-12-10 11:56:07 +01:00
Eugene Pankov
a091f46100 fixed settings sidebar offset (fixes #549) 2018-12-10 11:27:32 +01:00
Eugene Pankov
76e8652492 hotkey fixes 2018-12-07 15:12:37 +01:00
Eugene Pankov
2606b910f1 nicer scrollbars (fixes #440) 2018-12-07 14:54:56 +01:00
Eugene Pankov
9440d687d3 don't crash if no global spawn hotkey is assigned (#540) 2018-12-07 14:50:26 +01:00
Eugene Pankov
216f5c2213 . 2018-12-07 14:50:16 +01:00
Eugene
dbadef1c4e Merge pull request #541 from scott-kirk/patch-1
Typo in README
2018-12-06 23:55:30 +01:00
Scott Kirkpatrick
1baf80cfe6 Typo in README 2018-12-06 15:16:01 -05:00
Eugene Pankov
2e50bfccf4 Merge branch 'master' of github.com:Eugeny/terminus 2018-12-05 13:57:02 +01:00
Eugene Pankov
07a3d88397 updated icon 2018-12-05 01:34:11 +01:00
Eugene
6e5ce8e0b1 Merge pull request #536 from Drachenkaetzchen/wsl-color-warning
Inform users about 16 color limit with WSL
2018-12-05 01:33:36 +01:00
Eugene Pankov
c1d1ddd3b7 Merge branch 'master' of github.com:Eugeny/terminus 2018-12-05 01:31:03 +01:00
Eugene
bd5f274cf3 Merge pull request #537 from Drachenkaetzchen/bugfix-wheelevent-ts32
Fix for MouseWheelEvent deprecation in TypeScript 3.2
2018-12-05 01:30:19 +01:00
Eugene Pankov
38045165d4 avoid double squirrel check 2018-12-05 01:28:51 +01:00
Eugene
9e7721d2a9 Merge pull request #535 from Drachenkaetzchen/bugfix-534
Bugfix for issue #534: Catch any errors occurring during checkForUpda…
2018-12-05 01:27:29 +01:00
Felicia Hummel
1d593e0495 Inform users about 16 color limit with WSL
This patch adds a warning that when using WSL we're limited to 16 colors. It took me about 4 hours debugging why it didn't work, and this patch hopefully prevents others from wasting 4 hours.
2018-12-05 00:38:19 +01:00
Felicia Hummel
9b263c7237 Fix for MouseWheelEvent deprecation in TypeScript 3.2
MouseWheelEvent is deprecated and was removed with TypeScript 3.2, however, MouseWheelEvent is still aliased to WheelEvent. For more info see https://github.com/Microsoft/TSJS-lib-generator/pull/579

This PR fixes the build with TypeScript 3.2 by checking the object properties.
2018-12-05 00:33:10 +01:00
Felicia Hummel
ca05c1b819 Merge 2018-12-05 00:27:42 +01:00
Eugene Pankov
2107ed7ab7 force focus on new terminal tabs (fixes #533) 2018-12-05 00:06:01 +01:00
Felicia Hummel
9fd69f668a Bugfix for issue #534: Catch any errors occurring during checkForUpdates() to allow App to start up even if Squirrel is not available 2018-12-04 23:58:18 +01:00
Eugene Pankov
8800614bff tab colors 2018-12-02 16:41:17 +01:00
Eugene Pankov
867ddb4809 bumped electron 2018-12-02 15:28:25 +01:00
Eugene Pankov
5cf31d3351 fall back to github updates if squirrel is unavailable 2018-12-02 15:28:18 +01:00
121 changed files with 2069 additions and 1495 deletions

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ node_modules
build/files.wxs build/files.wxs
dist dist
*/dist
*.xcworkspacedata *.xcworkspacedata
*.xcuserstate *.xcuserstate

View File

@@ -14,8 +14,7 @@ cache:
- app/node_modules - app/node_modules
before_install: before_install:
- yarn install - yarn
- scripts/install-deps.js
script: script:
- scripts/build-native.js - scripts/build-native.js

View File

@@ -37,7 +37,7 @@ 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 * [shell-selector](https://github.com/Eugeny/terminus-shell-selector) - a quick shell selector pane
* [title-control](https://github.com/kbjr/terminus-title-control) - allows modifying the title of the terminal tabs by providing a prefix, suffix, and/or strings to be removed * [title-control](https://github.com/kbjr/terminus-title-control) - allows modifying the title of the terminal tabs by providing a prefix, suffix, and/or strings to be removed
* [scrollbar](https://github.com/kbjr/terminus-scrollbar) - adds a scrollbar to terminal tabs * [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
--- ---

View File

@@ -1,91 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) --> <!-- 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"
<svg viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024 1024;" xml:space="preserve">
xmlns:dc="http://purl.org/dc/elements/1.1/" <style type="text/css">
xmlns:cc="http://creativecommons.org/ns#" .st0{fill:url(#SVGID_1_);}
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" .st1{opacity:0.16;fill:url(#SVGID_2_);}
xmlns:svg="http://www.w3.org/2000/svg" .st2{fill:url(#SVGID_3_);}
xmlns="http://www.w3.org/2000/svg" .st3{opacity:0.16;fill:url(#SVGID_4_);}
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" .st4{fill:url(#SVGID_5_);}
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" .st5{opacity:0.15;fill:url(#SVGID_6_);}
width="150mm" .st6{fill:url(#SVGID_7_);}
height="150mm" </style>
viewBox="0 0 150 150" <g>
version="1.1" <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="260.9675" y1="871.1813" x2="919.1845" y2="491.1596">
id="svg8" <stop offset="0" style="stop-color:#669ABD"/>
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)" <stop offset="1" style="stop-color:#77DBDB"/>
sodipodi:docname="logo.svg" </linearGradient>
inkscape:export-filename="/home/eugene/Work/term/build/icons/512x512.png" <polygon class="st0" points="297.54,934.52 882.6,596.72 882.61,427.82 297.54,765.65 "/>
inkscape:export-xdpi="86.699997" <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="553.5051" y1="617.8278" x2="626.647" y2="744.5132">
inkscape:export-ydpi="86.699997"> <stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
<defs <stop offset="1" style="stop-color:#000000"/>
id="defs2" /> </linearGradient>
<sodipodi:namedview <polygon class="st1" points="297.54,934.52 882.6,596.72 882.61,427.82 297.54,765.65 "/>
id="base" </g>
pagecolor="#ffffff" <g>
bordercolor="#666666" <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="114.6631" y1="744.5275" x2="334.0905" y2="871.2141">
borderopacity="1.0" <stop offset="0" style="stop-color:#6A8FAD"/>
inkscape:pageopacity="0.0" <stop offset="1" style="stop-color:#669ABD"/>
inkscape:pageshadow="2" </linearGradient>
inkscape:zoom="0.49497475" <polygon class="st2" points="151.23,681.18 151.22,850.09 297.54,934.52 297.54,765.65 "/>
inkscape:cx="85.897128" <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="260.9478" y1="744.5281" x2="187.8059" y2="871.2135">
inkscape:cy="375.72042" <stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
inkscape:document-units="mm" <stop offset="1" style="stop-color:#000000"/>
inkscape:current-layer="layer1" </linearGradient>
showgrid="false" <polygon class="st3" points="151.23,681.18 151.22,850.09 297.54,934.52 297.54,765.65 "/>
inkscape:snap-bbox="true" </g>
inkscape:window-width="1366" <g>
inkscape:window-height="692" <linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="114.663" y1="237.793" x2="553.5026" y2="491.1571">
inkscape:window-x="0" <stop offset="0" style="stop-color:#6A8FAD"/>
inkscape:window-y="0" <stop offset="1" style="stop-color:#669ABD"/>
inkscape:window-maximized="1" </linearGradient>
fit-margin-top="0" <polygon class="st4" points="151.23,174.45 151.21,343.36 443.79,512.27 590.08,427.81 "/>
fit-margin-left="0" <linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="370.6562" y1="301.1281" x2="297.5094" y2="427.8221">
fit-margin-right="0" <stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
fit-margin-bottom="0" <stop offset="1" style="stop-color:#000000"/>
inkscape:snap-intersection-paths="true" </linearGradient>
inkscape:object-paths="true" /> <polygon class="st5" points="151.23,174.45 151.21,343.36 443.79,512.27 590.08,427.81 "/>
<metadata </g>
id="metadata5"> <linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="78.0912" y1="554.4979" x2="736.3375" y2="174.4593">
<rdf:RDF> <stop offset="0" style="stop-color:#CCECFF"/>
<cc:Work <stop offset="1" style="stop-color:#9FECED"/>
rdf:about=""> </linearGradient>
<dc:format>image/svg+xml</dc:format> <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 "/>
<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>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -13,6 +13,9 @@ export function parseArgs (argv, cwd) {
.command('run [command...]', 'run a command in the terminal', { .command('run [command...]', 'run a command in the terminal', {
command: { type: 'string' }, 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 => { .command('paste [text]', 'paste stdin into the active tab', yargs => {
return yargs.option('escape', { return yargs.option('escape', {
alias: 'e', alias: 'e',

View File

@@ -13,13 +13,13 @@
"watch": "webpack --progress --color --watch" "watch": "webpack --progress --color --watch"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "7.0.0", "@angular/animations": "7.2.0-beta.1",
"@angular/common": "7.0.0", "@angular/common": "7.2.0-beta.1",
"@angular/compiler": "7.0.0", "@angular/compiler": "7.2.0-beta.1",
"@angular/core": "7.0.0", "@angular/core": "7.2.0-beta.1",
"@angular/forms": "7.0.0", "@angular/forms": "7.2.0-beta.1",
"@angular/platform-browser": "7.0.0", "@angular/platform-browser": "7.2.0-beta.1",
"@angular/platform-browser-dynamic": "7.0.0", "@angular/platform-browser-dynamic": "7.2.0-beta.1",
"@ng-bootstrap/ng-bootstrap": "^3.3.1", "@ng-bootstrap/ng-bootstrap": "^3.3.1",
"devtron": "1.4.0", "devtron": "1.4.0",
"electron-config": "0.2.1", "electron-config": "0.2.1",

View File

@@ -1,7 +1,9 @@
import '../lib/lru' import '../lib/lru'
import 'source-sans-pro' import 'source-sans-pro'
import 'source-code-pro/source-code-pro.css' 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 'ngx-toastr/toastr.css'
import './preload.scss' import './preload.scss'

View File

@@ -34,7 +34,7 @@ async function bootstrap (plugins: IPluginInfo[], safeMode = false): Promise<NgM
}) })
let module = getRootModule(pluginsModules) let module = getRootModule(pluginsModules)
window['rootModule'] = module window['rootModule'] = module
return await platformBrowserDynamic().bootstrapModule(module) return platformBrowserDynamic().bootstrapModule(module)
} }
findPlugins().then(async plugins => { findPlugins().then(async plugins => {

View File

@@ -118,7 +118,7 @@ export async function findPlugins (): Promise<IPluginInfo[]> {
} }
try { 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'))) { if (!info.keywords || !(info.keywords.includes('terminus-plugin') || info.keywords.includes('terminus-builtin-plugin'))) {
continue continue
} }

View File

@@ -2,52 +2,52 @@
# yarn lockfile v1 # yarn lockfile v1
"@angular/animations@7.0.0": "@angular/animations@7.2.0-beta.1":
version "7.0.0" version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.0.0.tgz#5c9e1683063c29df10253b7dc5bb9b13694ee396" resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.2.0-beta.1.tgz#c288de4a89b0197ba53a8411173ec4085fbbd9c9"
integrity sha512-IYdryQXdYfPvhJpExLSAr0o9rlUeyVS++a6h/sjqN1dkUt/yJBHLRreuHx8Udvlj2nH70raHJgevk8FwhAkTdA== integrity sha512-82u9L2poaREjkTJlYEKdOO1B1LaVBwqJBZvIXU04+21WQBJoi050sxUl6lmjVVs5rRc0e/y2gifyrb42pUEntA==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@angular/common@7.0.0": "@angular/common@7.2.0-beta.1":
version "7.0.0" version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.0.0.tgz#29206614d2b8dc79e5207b7dc6f9fc559e9a24f2" resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.2.0-beta.1.tgz#be9d14f239b7a390fc056e3bb540da181631fb1a"
integrity sha512-jp6MA6EOq/a1m+F0c1aZC345pAYYYFpN1m7GMM91JlqkjzJMhyYVk+Bod9xQOEWadcpY+RFudG+jRsPCMO8bvQ== integrity sha512-+aYfO20nrqurOLxM0w/UJkOHjGzNFOc2j52ggyj1vr62nTk+W63j4P8tcUsW6iHFCsOF5auSkclKUbNIliMf0A==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@angular/compiler@7.0.0": "@angular/compiler@7.2.0-beta.1":
version "7.0.0" version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.0.0.tgz#f953a213a01e4736e94fe1a370b07e13e2393b71" resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.2.0-beta.1.tgz#355a10a80615afdd1d6a5ed682222e9e57e65fd7"
integrity sha512-4fkohfGyG1BEpeYenOartuJmduyZ/R3XQx46hDDiR/9A8/Go4qLGkgr9Bd/JL/gPIR1XAHH9D5ii2sh+28ZEmA== integrity sha512-KxI93dLm1SPNbazfG41QcmxVS7T9VmQ8wzhMHOVJo4DP77g2E5xUB5nOInMCMI43lbZEIckBxo/ci4jwiiq8uA==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@angular/core@7.0.0": "@angular/core@7.2.0-beta.1":
version "7.0.0" version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.0.0.tgz#01e9db9074a1db1c47a32f745b787d1c86f5d61a" resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.2.0-beta.1.tgz#32bef8f3d424333791d0e0bd4a3f3afee9b1349d"
integrity sha512-DjVyWNGBWKEeBvxeXy8FGBNlnr/W/tNygOZEd6/uCktcXTG4DNyNQrWuNZUKEpr7RuIT3YVMj+UNwgTq0jB/9g== integrity sha512-gJzQAauezAMU8vEEMh1PrXv52fA9ceUXac/tJ8KIi08zEjyIRXLvKggWr7YXAbt5LwgKsn27JwecqeS5h4K/BQ==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@angular/forms@7.0.0": "@angular/forms@7.2.0-beta.1":
version "7.0.0" version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.0.0.tgz#672c306b13e94a20b72c096214642a326c43699a" resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.2.0-beta.1.tgz#399ea4585502027d53396eac935f8f20bd5ad2bf"
integrity sha512-rTg1UHq9gHR6zY3Kkip1KCm/YTck/rlR8CvVFIMwF0bdQxUCT51SXVn58nXts9yDaieABcGaQHNkQn1mARslgw== integrity sha512-1NQ2hw8Y4hjgr5qXoOvQJRjmQno/fhQUuIRx0SC7hYySur1E9vcI8rZDVDB+LwiGexALh8H70zwJ64lNxzwpvA==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@angular/platform-browser-dynamic@7.0.0": "@angular/platform-browser-dynamic@7.2.0-beta.1":
version "7.0.0" version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.0.0.tgz#2b2a50b5a8176bee257f90ee47b1d873502f7182" resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.0-beta.1.tgz#4a95ee6d53fb02e529f1f1842ee8839a25ec4790"
integrity sha512-lH2KuH+Em1y/mTOE6yTJmsOxYkMbYKzKLP9gYzc9vZu3er1df6Jx6jxefeBmAr9v+kNCLnpnHWHz2y4GzAesJA== integrity sha512-wnf6WSfh9AsBzI8I//eNolmQ2rwMIk/KIuGmTEOAuAxRMLgqzZUA3PjX2XAE6oUUowqy1MET1UFiqjDf/NZcNQ==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@angular/platform-browser@7.0.0": "@angular/platform-browser@7.2.0-beta.1":
version "7.0.0" version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.0.0.tgz#8c13a6380cf465b3628e5b576a1313e9b4976093" resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.2.0-beta.1.tgz#20c272ebfcccb3151baed7ceddbafdb359f54cd8"
integrity sha512-XyvL30d6meJ+SXlOmdR+sxoLdSvkQdmVNvpdvUzAHC/EqwA/byg4V3bTe5lpZmypclgFCjkGoTsz6uOnnwlQhw== integrity sha512-sCMzCFCdE4Dq9foXf2PpWPegtVfirHhg+DQzoiwFDYj5i+QB9nWY7BOLlrCPAWpd8opUxCsaLrzXbfgM40FAGA==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"

View File

@@ -7,18 +7,18 @@ environment:
nodejs_version: "10" nodejs_version: "10"
cache: cache:
- '%USERPROFILE%\.electron' - "%USERPROFILE%\\.electron"
- "%LOCALAPPDATA%\\Yarn"
version: "{build}" version: "{build}"
install: install:
- ps: Install-Product node $env:nodejs_version $env:platform - ps: Install-Product node $env:nodejs_version $env:platform
- npm install - yarn
- node scripts/install-deps.js
- node scripts/build-native.js - node scripts/build-native.js
build_script: build_script:
- npm run build - yarn run build
- node scripts/prepackage-plugins.js - node scripts/prepackage-plugins.js
- node scripts/build-windows.js - node scripts/build-windows.js

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 644 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -1,124 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) --> <!-- 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"
<svg viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024 1024;" xml:space="preserve">
xmlns:dc="http://purl.org/dc/elements/1.1/" <style type="text/css">
xmlns:cc="http://creativecommons.org/ns#" .st0{opacity:0.8;fill:#00232E;}
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" .st1{fill:url(#SVGID_1_);}
xmlns:svg="http://www.w3.org/2000/svg" .st2{opacity:0.16;fill:url(#SVGID_2_);}
xmlns="http://www.w3.org/2000/svg" .st3{fill:url(#SVGID_3_);}
xmlns:xlink="http://www.w3.org/1999/xlink" .st4{opacity:0.16;fill:url(#SVGID_4_);}
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" .st5{fill:url(#SVGID_5_);}
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" .st6{opacity:0.15;fill:url(#SVGID_6_);}
width="150mm" .st7{fill:url(#SVGID_7_);}
height="150mm" </style>
viewBox="0 0 150 150" <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 "/>
version="1.1" <g>
id="svg8" <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="439.0065" y1="603.0394" x2="627.9464" y2="493.9549">
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)" <stop offset="0" style="stop-color:#669ABD"/>
sodipodi:docname="icon.svg" <stop offset="1" style="stop-color:#77DBDB"/>
inkscape:export-filename="/home/eugene/Work/term/build/icons/512x512.png" </linearGradient>
inkscape:export-xdpi="86.699997" <polygon class="st1" points="449.5,621.22 617.45,524.25 617.45,475.77 449.5,572.75 "/>
inkscape:export-ydpi="86.699997"> <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="522.9788" y1="530.3148" x2="543.9741" y2="566.6795">
<defs <stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
id="defs2"> <stop offset="1" style="stop-color:#000000"/>
<linearGradient </linearGradient>
inkscape:collect="always" <polygon class="st2" points="449.5,621.22 617.45,524.25 617.45,475.77 449.5,572.75 "/>
id="linearGradient4649"> </g>
<stop <g>
style="stop-color:#000316;stop-opacity:1" <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="397.0101" y1="566.6837" x2="459.9963" y2="603.0487">
offset="0" <stop offset="0" style="stop-color:#6A8FAD"/>
id="stop4645" /> <stop offset="1" style="stop-color:#669ABD"/>
<stop </linearGradient>
style="stop-color:#190065;stop-opacity:1" <polygon class="st3" points="407.51,548.5 407.5,596.99 449.5,621.22 449.5,572.75 "/>
offset="1" <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="439.0009" y1="566.6838" x2="418.0056" y2="603.0486">
id="stop4647" /> <stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
</linearGradient> <stop offset="1" style="stop-color:#000000"/>
<linearGradient </linearGradient>
inkscape:collect="always" <polygon class="st4" points="407.51,548.5 407.5,596.99 449.5,621.22 449.5,572.75 "/>
xlink:href="#linearGradient4649" </g>
id="linearGradient4651" <g>
x1="89.26284" <linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="397.0101" y1="421.2265" x2="522.9781" y2="493.9542">
y1="85.146751" <stop offset="0" style="stop-color:#6A8FAD"/>
x2="89.26284" <stop offset="1" style="stop-color:#669ABD"/>
y2="229.47229" </linearGradient>
gradientUnits="userSpaceOnUse" <polygon class="st5" points="407.51,403.04 407.5,451.53 491.49,500.01 533.48,475.77 "/>
gradientTransform="matrix(0.82182032,0,0,0.82182032,15.208802,28.029361)" /> <linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="470.4924" y1="439.4067" x2="449.4958" y2="475.774">
</defs> <stop offset="0.5588" style="stop-color:#000000;stop-opacity:0"/>
<sodipodi:namedview <stop offset="1" style="stop-color:#000000"/>
id="base" </linearGradient>
pagecolor="#ffffff" <polygon class="st6" points="407.51,403.04 407.5,451.53 491.49,500.01 533.48,475.77 "/>
bordercolor="#666666" </g>
borderopacity="1.0" <linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="386.5123" y1="512.136" x2="575.4605" y2="403.0467">
inkscape:pageopacity="0.0" <stop offset="0" style="stop-color:#CCECFF"/>
inkscape:pageshadow="2" <stop offset="1" style="stop-color:#9FECED"/>
inkscape:zoom="0.49497475" </linearGradient>
inkscape:cx="85.897128" <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 "/>
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>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -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
extras/UAC.exe Normal file

Binary file not shown.

View File

@@ -1,6 +1,7 @@
{ {
"name": "term", "name": "term",
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-free": "^5.6.3",
"@types/electron-config": "^0.2.1", "@types/electron-config": "^0.2.1",
"@types/electron-debug": "^1.1.0", "@types/electron-debug": "^1.1.0",
"@types/fs-promise": "1.0.1", "@types/fs-promise": "1.0.1",
@@ -13,18 +14,15 @@
"core-js": "2.4.1", "core-js": "2.4.1",
"cross-env": "4.0.0", "cross-env": "4.0.0",
"css-loader": "0.28.0", "css-loader": "0.28.0",
"electron": "3.0.8", "electron": "4.0.0-beta.7",
"electron-builder": "^20.27.1", "electron-builder": "^20.38.2",
"electron-builder-squirrel-windows": "^20.28.3", "electron-builder-squirrel-windows": "^20.28.3",
"electron-installer-snap": "^3.0.0", "electron-installer-snap": "^3.0.0",
"electron-rebuild": "^1.8.2", "electron-rebuild": "^1.8.2",
"file-loader": "^1.1.11", "file-loader": "^1.1.11",
"font-awesome": "4.7.0",
"graceful-fs": "^4.1.11", "graceful-fs": "^4.1.11",
"html-loader": "0.4.4", "html-loader": "0.4.4",
"json-loader": "0.5.4", "json-loader": "0.5.4",
"less": "2.7.1",
"less-loader": "2.2.3",
"node-abi": "^2.4.4", "node-abi": "^2.4.4",
"node-gyp": "^3.6.2", "node-gyp": "^3.6.2",
"node-sass": "^4.5.3", "node-sass": "^4.5.3",
@@ -44,13 +42,13 @@
"style-loader": "0.13.1", "style-loader": "0.13.1",
"svg-inline-loader": "^0.8.0", "svg-inline-loader": "^0.8.0",
"to-string-loader": "1.1.5", "to-string-loader": "1.1.5",
"tslint": "5.1.0", "tslint": "^5.12.0",
"tslint-config-standard": "5.0.2", "tslint-config-standard": "^8.0.1",
"tslint-eslint-rules": "4.0.0", "tslint-eslint-rules": "^5.4.0",
"typescript": "^3.1.3", "typescript": "^3.1.3",
"url-loader": "^1.1.1", "url-loader": "^1.1.1",
"val-loader": "0.5.0", "val-loader": "0.5.0",
"webpack": "^4.22.0", "webpack": "^4.27.1",
"webpack-cli": "^3.1.2", "webpack-cli": "^3.1.2",
"yaml-loader": "0.4.0", "yaml-loader": "0.4.0",
"yarn": "^1.10.1" "yarn": "^1.10.1"
@@ -128,7 +126,7 @@
"start": "cross-env DEV=1 electron app --debug", "start": "cross-env DEV=1 electron app --debug",
"prod": "cross-env DEV=1 electron app", "prod": "cross-env DEV=1 electron app",
"lint": "tslint -c tslint.json -t stylish terminus-*/src/**/*.ts terminus-*/src/*.ts app/src/*.ts", "lint": "tslint -c tslint.json -t stylish terminus-*/src/**/*.ts terminus-*/src/*.ts app/src/*.ts",
"postinstall": "install-app-deps" "postinstall": "node ./scripts/install-deps.js"
}, },
"repository": "eugeny/terminus" "repository": "eugeny/terminus"
} }

View File

@@ -4,24 +4,18 @@ const path = require('path')
const vars = require('./vars') const vars = require('./vars')
lifecycles = [] lifecycles = []
lifecycles.push(rebuild({ for (let dir of ['app', 'terminus-ssh', 'terminus-terminal']) {
buildPath: path.resolve(__dirname, '../app'), lifecycles.push([rebuild({
electronVersion: vars.electronVersion, buildPath: path.resolve(__dirname, '../' + dir),
force: true, electronVersion: vars.electronVersion,
}).lifecycle) force: true,
lifecycles.push(rebuild({ }).lifecycle, dir])
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 lc of lifecycles) { console.info('Building against Electron', vars.electronVersion)
for (let [lc, dir] of lifecycles) {
lc.on('module-found', name => { lc.on('module-found', name => {
console.info('Rebuilding', name) console.info('Rebuilding', dir + '/' + name)
}) })
} }

View File

@@ -8,7 +8,6 @@ const localBinPath = path.resolve(__dirname, '../node_modules/.bin');
const npx = `${localBinPath}/npx`; const npx = `${localBinPath}/npx`;
log.info('deps', 'app') log.info('deps', 'app')
sh.exec(`${npx} yarn install`)
sh.cd('app') sh.cd('app')
sh.exec(`${npx} yarn install`) sh.exec(`${npx} yarn install`)

View File

@@ -3,10 +3,11 @@ const fs = require('fs')
const childProcess = require('child_process') const childProcess = require('child_process')
const appInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../app/package.json'))) 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 = 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 = [ exports.builtinPlugins = [
'terminus-core', 'terminus-core',
@@ -20,4 +21,4 @@ exports.bundledModules = [
'@angular', '@angular',
'@ng-bootstrap', '@ng-bootstrap',
] ]
exports.electronVersion = pkgInfo.devDependencies.electron exports.electronVersion = electronInfo.version

View File

@@ -8,8 +8,8 @@
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",
"scripts": { "scripts": {
"build": "rm -rf dist && webpack --progress --color --display-modules", "build": "webpack --progress --color --display-modules",
"watch": "rm -rf dist && webpack --progress --color --watch" "watch": "webpack --progress --color --watch"
}, },
"files": [ "files": [
"dist" "dist"
@@ -21,12 +21,13 @@
"@types/node": "^7.0.37", "@types/node": "^7.0.37",
"@types/webpack-env": "^1.13.0", "@types/webpack-env": "^1.13.0",
"@types/winston": "^2.3.6", "@types/winston": "^2.3.6",
"axios": "^0.18.0",
"bootstrap": "^4.1.3", "bootstrap": "^4.1.3",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"electron-updater": "^2.8.9", "electron-updater": "^2.8.9",
"ng2-dnd": "^5.0.2", "ng2-dnd": "^5.0.2",
"ngx-perfect-scrollbar": "^6.0.0", "ngx-perfect-scrollbar": "^6.0.0",
"rage-edit-tmp": "^1.1.0", "rage-edit": "^1.2.0",
"shell-escape": "^0.2.0", "shell-escape": "^0.2.0",
"universal-analytics": "^0.4.17" "universal-analytics": "^0.4.17"
}, },
@@ -44,6 +45,5 @@
"deepmerge": "^1.5.0", "deepmerge": "^1.5.0",
"js-yaml": "^3.9.0", "js-yaml": "^3.9.0",
"winston": "^2.4.0" "winston": "^2.4.0"
}, }
"false": {}
} }

View File

@@ -6,5 +6,5 @@ export interface RecoveredTab {
} }
export abstract class TabRecoveryProvider { export abstract class TabRecoveryProvider {
abstract async recover (recoveryToken: any): Promise<RecoveredTab|null> abstract async recover (recoveryToken: any): Promise<RecoveredTab | null>
} }

View File

@@ -132,8 +132,8 @@ export class AppRootComponent {
ngbModal.open(SafeModeModalComponent) ngbModal.open(SafeModeModalComponent)
} }
this.updater.check().then(() => { this.updater.check().then(available => {
this.updatesAvailable = true this.updatesAvailable = available
}) })
this.touchbar.update() this.touchbar.update()

View File

@@ -13,6 +13,7 @@ export abstract class BaseTabComponent {
hasFocus = false hasFocus = false
hasActivity = false hasActivity = false
hostView: ViewRef hostView: ViewRef
color: string = null
protected titleChange = new Subject<string>() protected titleChange = new Subject<string>()
protected focused = new Subject<void>() protected focused = new Subject<void>()
protected blurred = new Subject<void>() protected blurred = new Subject<void>()
@@ -68,7 +69,7 @@ export abstract class BaseTabComponent {
this.activity.next(false) this.activity.next(false)
} }
getRecoveryToken (): any { async getRecoveryToken (): Promise<any> {
return null return null
} }

View File

@@ -1,4 +1,4 @@
.icon(tabindex='0', [class.active]='model', (keyup.space)='click()') .icon(tabindex='0', [class.active]='model', (keyup.space)='click()')
i.fa.fa-square-o.off i.fas.fa-square.off
i.fa.fa-check-square.on i.fas.fa-check-square.on
.text {{text}} .text {{text}}

View File

@@ -16,6 +16,7 @@ export class RenameTabModalComponent {
ngOnInit () { ngOnInit () {
setTimeout(() => { setTimeout(() => {
this.input.nativeElement.focus() this.input.nativeElement.focus()
this.input.nativeElement.select()
}, 250) }, 250)
} }

View File

@@ -14,10 +14,10 @@ div
footer.d-flex.align-items-center footer.d-flex.align-items-center
.btn-group.mr-auto .btn-group.mr-auto
button.btn.btn-secondary((click)='homeBase.openGitHub()') button.btn.btn-secondary((click)='homeBase.openGitHub()')
i.fa.fa-github i.fab.fa-github
span GitHub span GitHub
button.btn.btn-secondary((click)='homeBase.reportBug()') button.btn.btn-secondary((click)='homeBase.reportBug()')
i.fa.fa-bug i.fas.fa-bug
span Report a problem span Report a problem
.form-control-static.selectable.no-drag Version: {{homeBase.appVersion}} .form-control-static.selectable.no-drag Version: {{homeBase.appVersion}}

View File

@@ -17,7 +17,7 @@ import { BaseTabComponent } from '../components/baseTab.component'
export class TabBodyComponent implements OnChanges { export class TabBodyComponent implements OnChanges {
@Input() @HostBinding('class.active') active: boolean @Input() @HostBinding('class.active') active: boolean
@Input() tab: BaseTabComponent @Input() tab: BaseTabComponent
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef @ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
ngOnChanges (changes) { ngOnChanges (changes) {
if (changes.tab) { if (changes.tab) {

View File

@@ -1,4 +1,7 @@
.progressbar([style.width]='progress + "%"', *ngIf='progress != null') .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}} .name([title]='tab.customTitle || tab.title') {{tab.customTitle || tab.title}}
button((click)='app.closeTab(tab, true)') &times; button((click)='app.closeTab(tab, true)') &times;

View File

@@ -3,10 +3,21 @@ import { SortableComponent } from 'ng2-dnd'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { BaseTabComponent } from './baseTab.component' import { BaseTabComponent } from './baseTab.component'
import { RenameTabModalComponent } from './renameTabModal.component' import { RenameTabModalComponent } from './renameTabModal.component'
import { HotkeysService } from '../services/hotkeys.service'
import { ElectronService } from '../services/electron.service' import { ElectronService } from '../services/electron.service'
import { AppService } from '../services/app.service' import { AppService } from '../services/app.service'
import { HostAppService, Platform } from '../services/hostApp.service' import { HostAppService, Platform } from '../services/hostApp.service'
const COLORS = [
{ name: 'No color', value: null },
{ name: 'Blue', value: '#0275d8' },
{ name: 'Green', value: '#5cb85c' },
{ name: 'Orange', value: '#f0ad4e' },
{ name: 'Purple', value: '#613d7c' },
{ name: 'Red', value: '#d9534f' },
{ name: 'Yellow', value: '#ffd500' },
]
@Component({ @Component({
selector: 'tab-header', selector: 'tab-header',
template: require('./tabHeader.component.pug'), template: require('./tabHeader.component.pug'),
@@ -28,8 +39,17 @@ export class TabHeaderComponent {
private zone: NgZone, private zone: NgZone,
private hostApp: HostAppService, private hostApp: HostAppService,
private ngbModal: NgbModal, private ngbModal: NgbModal,
private hotkeys: HotkeysService,
private parentDraggable: SortableComponent, private parentDraggable: SortableComponent,
) { } ) {
this.hotkeys.matchedHotkey.subscribe((hotkey) => {
if (this.app.activeTab === this.tab) {
if (hotkey === 'rename-tab') {
this.showRenameTabModal()
}
}
})
}
ngOnInit () { ngOnInit () {
if (this.hostApp.platform === Platform.macOS) { if (this.hostApp.platform === Platform.macOS) {
@@ -40,7 +60,7 @@ export class TabHeaderComponent {
}) })
} }
@HostListener('dblclick') onDoubleClick (): void { showRenameTabModal (): void {
let modal = this.ngbModal.open(RenameTabModalComponent) let modal = this.ngbModal.open(RenameTabModalComponent)
modal.componentInstance.value = this.tab.customTitle || this.tab.title modal.componentInstance.value = this.tab.customTitle || this.tab.title
modal.result.then(result => { modal.result.then(result => {
@@ -49,6 +69,10 @@ export class TabHeaderComponent {
}).catch(() => null) }).catch(() => null)
} }
@HostListener('dblclick') onDoubleClick (): void {
this.showRenameTabModal()
}
@HostListener('auxclick', ['$event']) async onAuxClick ($event: MouseEvent) { @HostListener('auxclick', ['$event']) async onAuxClick ($event: MouseEvent) {
if ($event.which === 2) { if ($event.which === 2) {
this.app.closeTab(this.tab, true) this.app.closeTab(this.tab, true)
@@ -87,8 +111,31 @@ export class TabHeaderComponent {
} }
}) })
}, },
{
label: 'Rename',
click: () => this.zone.run(() => this.showRenameTabModal())
},
{
label: 'Color',
sublabel: COLORS.find(x => x.value === this.tab.color).name,
submenu: COLORS.map(color => ({
label: color.name,
type: 'radio',
checked: this.tab.color === color.value,
click: () => this.zone.run(() => {
this.tab.color = color.value
}),
})),
}
]) ])
if ((this.tab as any).saveAsProfile) {
contextMenu.append(new this.electron.MenuItem({
label: 'Save as a profile',
click: () => this.zone.run(() => (this.tab as any).saveAsProfile())
}))
}
let process = await this.tab.getCurrentProcess() let process = await this.tab.getCurrentProcess()
if (process) { if (process) {
contextMenu.append(new this.electron.MenuItem({ contextMenu.append(new this.electron.MenuItem({

View File

@@ -7,46 +7,33 @@ hotkeys:
- 'F11' - 'F11'
close-tab: close-tab:
- 'Ctrl-Shift-W' - 'Ctrl-Shift-W'
- ['Ctrl-A', 'K'] toggle-last-tab: []
toggle-last-tab: rename-tab:
- ['Ctrl-A', 'A'] - 'Ctrl-Shift-R'
- ['Ctrl-A', 'Ctrl-A']
next-tab: next-tab:
- 'Ctrl-Shift-ArrowRight' - 'Ctrl-Shift-ArrowRight'
- ['Ctrl-A', 'N']
- 'Ctrl-Tab' - 'Ctrl-Tab'
previous-tab: previous-tab:
- 'Ctrl-Shift-ArrowLeft' - 'Ctrl-Shift-ArrowLeft'
- ['Ctrl-A', 'P']
- 'Ctrl-Shift-Tab' - 'Ctrl-Shift-Tab'
tab-1: tab-1:
- 'Alt-1' - 'Alt-1'
- ['Ctrl-A', '1']
tab-2: tab-2:
- 'Alt-2' - 'Alt-2'
- ['Ctrl-A', '2']
tab-3: tab-3:
- 'Alt-3' - 'Alt-3'
- ['Ctrl-A', '3']
tab-4: tab-4:
- 'Alt-4' - 'Alt-4'
- ['Ctrl-A', '4']
tab-5: tab-5:
- 'Alt-5' - 'Alt-5'
- ['Ctrl-A', '5']
tab-6: tab-6:
- 'Alt-6' - 'Alt-6'
- ['Ctrl-A', '6']
tab-7: tab-7:
- 'Alt-7' - 'Alt-7'
- ['Ctrl-A', '7']
tab-8: tab-8:
- 'Alt-8' - 'Alt-8'
- ['Ctrl-A', '8']
tab-9: tab-9:
- 'Alt-9' - 'Alt-9'
- ['Ctrl-A', '9']
tab-10: tab-10:
- 'Alt-0' - 'Alt-0'
- ['Ctrl-A', '0']
pluginBlacklist: ['ssh'] pluginBlacklist: ['ssh']

View File

@@ -8,6 +8,8 @@ hotkeys:
close-tab: close-tab:
- '⌘-W' - '⌘-W'
toggle-last-tab: [] toggle-last-tab: []
rename-tab:
- '⌘-R'
next-tab: next-tab:
- 'Ctrl-Tab' - 'Ctrl-Tab'
previous-tab: previous-tab:

View File

@@ -7,46 +7,33 @@ hotkeys:
- 'F11' - 'F11'
close-tab: close-tab:
- 'Ctrl-Shift-W' - 'Ctrl-Shift-W'
- ['Ctrl-A', 'K'] toggle-last-tab: []
toggle-last-tab: rename-tab:
- ['Ctrl-A', 'A'] - 'Ctrl-Shift-R'
- ['Ctrl-A', 'Ctrl-A']
next-tab: next-tab:
- 'Ctrl-Shift-ArrowRight' - 'Ctrl-Shift-ArrowRight'
- ['Ctrl-A', 'N']
- 'Ctrl-Tab' - 'Ctrl-Tab'
previous-tab: previous-tab:
- 'Ctrl-Shift-ArrowLeft' - 'Ctrl-Shift-ArrowLeft'
- ['Ctrl-A', 'P']
- 'Ctrl-Shift-Tab' - 'Ctrl-Shift-Tab'
tab-1: tab-1:
- 'Alt-1' - 'Alt-1'
- ['Ctrl-A', '1']
tab-2: tab-2:
- 'Alt-2' - 'Alt-2'
- ['Ctrl-A', '2']
tab-3: tab-3:
- 'Alt-3' - 'Alt-3'
- ['Ctrl-A', '3']
tab-4: tab-4:
- 'Alt-4' - 'Alt-4'
- ['Ctrl-A', '4']
tab-5: tab-5:
- 'Alt-5' - 'Alt-5'
- ['Ctrl-A', '5']
tab-6: tab-6:
- 'Alt-6' - 'Alt-6'
- ['Ctrl-A', '6']
tab-7: tab-7:
- 'Alt-7' - 'Alt-7'
- ['Ctrl-A', '7']
tab-8: tab-8:
- 'Alt-8' - 'Alt-8'
- ['Ctrl-A', '8']
tab-9: tab-9:
- 'Alt-9' - 'Alt-9'
- ['Ctrl-A', '9']
tab-10: tab-10:
- 'Alt-0' - 'Alt-0'
- ['Ctrl-A', '0']
pluginBlacklist: [] pluginBlacklist: []

View File

@@ -6,19 +6,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar' import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
import { DndModule } from 'ng2-dnd' import { DndModule } from 'ng2-dnd'
import { AppService } from './services/app.service' import { AppHotkeyProvider } from './services/hotkeys.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 { AppRootComponent } from './components/appRoot.component'
import { CheckboxComponent } from './components/checkbox.component' import { CheckboxComponent } from './components/checkbox.component'
@@ -44,25 +32,12 @@ import 'perfect-scrollbar/css/perfect-scrollbar.css'
import 'ng2-dnd/bundles/style.css' import 'ng2-dnd/bundles/style.css'
const PROVIDERS = [ const PROVIDERS = [
AppService,
ConfigService,
DockingService,
ElectronService,
HomeBaseService,
HostAppService,
HotkeysService,
LogService,
ShellIntegrationService,
TabRecoveryService,
ThemesService,
TouchbarService,
UpdaterService,
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true }, { provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
{ provide: Theme, useClass: StandardTheme, multi: true }, { provide: Theme, useClass: StandardTheme, multi: true },
{ provide: Theme, useClass: StandardCompactTheme, multi: true }, { provide: Theme, useClass: StandardCompactTheme, multi: true },
{ provide: Theme, useClass: PaperTheme, multi: true }, { provide: Theme, useClass: PaperTheme, multi: true },
{ provide: ConfigProvider, useClass: CoreConfigProvider, multi: true }, { provide: ConfigProvider, useClass: CoreConfigProvider, multi: true },
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true }} { provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } }
] ]
@NgModule({ @NgModule({

View File

@@ -35,7 +35,7 @@ class CompletionObserver {
} }
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class AppService { export class AppService {
tabs: BaseTabComponent[] = [] tabs: BaseTabComponent[] = []
activeTab: BaseTabComponent activeTab: BaseTabComponent

View File

@@ -56,7 +56,7 @@ export class ConfigProxy {
return real[key] return real[key]
} else { } else {
if (isNonStructuralObjectMember(defaults[key])) { if (isNonStructuralObjectMember(defaults[key])) {
real[key] = {...defaults[key]} real[key] = { ...defaults[key] }
delete real[key].__nonStructural delete real[key].__nonStructural
return real[key] return real[key]
} else { } else {
@@ -74,7 +74,7 @@ export class ConfigProxy {
setValue (key: string, value: any) { } // tslint:disable-line setValue (key: string, value: any) { } // tslint:disable-line
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class ConfigService { export class ConfigService {
store: any store: any
restartRequested: boolean restartRequested: boolean

View File

@@ -8,7 +8,7 @@ export interface IScreen {
name: string name: string
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class DockingService { export class DockingService {
constructor ( constructor (
private electron: ElectronService, private electron: ElectronService,

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { TouchBar, BrowserWindow, Menu, MenuItem } from 'electron' import { TouchBar, BrowserWindow, Menu, MenuItem } from 'electron'
@Injectable() @Injectable({ providedIn: 'root' })
export class ElectronService { export class ElectronService {
app: any app: any
ipcRenderer: any ipcRenderer: any

View File

@@ -5,7 +5,7 @@ import { ConfigService } from './config.service'
import ua = require('universal-analytics') import ua = require('universal-analytics')
import uuidv4 = require('uuid/v4') import uuidv4 = require('uuid/v4')
@Injectable() @Injectable({ providedIn: 'root' })
export class HomeBaseService { export class HomeBaseService {
appVersion: string appVersion: string
@@ -33,7 +33,7 @@ export class HomeBaseService {
linux: 'OS: Linux', linux: 'OS: Linux',
}[os.platform()] }[os.platform()]
let plugins = (window as any).installedPlugins.filter(x => !x.isBuiltin).map(x => x.name) 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}`) this.electron.shell.openExternal(`https://github.com/eugeny/terminus/issues/new?body=${encodeURIComponent(body)}&labels=${label}`)
} }

View File

@@ -16,7 +16,7 @@ export interface Bounds {
height: number height: number
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class HostAppService { export class HostAppService {
platform: Platform platform: Platform
nodePlatform: string nodePlatform: string
@@ -27,6 +27,7 @@ export class HostAppService {
private cliOpenDirectory = new Subject<string>() private cliOpenDirectory = new Subject<string>()
private cliRunCommand = new Subject<string[]>() private cliRunCommand = new Subject<string[]>()
private cliPaste = new Subject<string>() private cliPaste = new Subject<string>()
private cliOpenProfile = new Subject<string>()
private configChangeBroadcast = new Subject<void>() private configChangeBroadcast = new Subject<void>()
private windowCloseRequest = new Subject<void>() private windowCloseRequest = new Subject<void>()
private logger: Logger private logger: Logger
@@ -37,6 +38,7 @@ export class HostAppService {
get cliOpenDirectory$ (): Observable<string> { return this.cliOpenDirectory } get cliOpenDirectory$ (): Observable<string> { return this.cliOpenDirectory }
get cliRunCommand$ (): Observable<string[]> { return this.cliRunCommand } get cliRunCommand$ (): Observable<string[]> { return this.cliRunCommand }
get cliPaste$ (): Observable<string> { return this.cliPaste } get cliPaste$ (): Observable<string> { return this.cliPaste }
get cliOpenProfile$ (): Observable<string> { return this.cliOpenProfile }
get configChangeBroadcast$ (): Observable<void> { return this.configChangeBroadcast } get configChangeBroadcast$ (): Observable<void> { return this.configChangeBroadcast }
get windowCloseRequest$ (): Observable<void> { return this.windowCloseRequest } get windowCloseRequest$ (): Observable<void> { return this.windowCloseRequest }
@@ -91,6 +93,8 @@ export class HostAppService {
text = shellEscape([text]) text = shellEscape([text])
} }
this.cliPaste.next(text) this.cliPaste.next(text)
} else if (op === 'profile') {
this.cliOpenProfile.next(argv.profileName)
} else { } else {
this.secondInstance.next() this.secondInstance.next()
} }

View File

@@ -17,7 +17,7 @@ interface EventBufferEntry {
time: number, time: number,
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class HotkeysService { export class HotkeysService {
key = new EventEmitter<NativeKeyEvent>() key = new EventEmitter<NativeKeyEvent>()
matchedHotkey = new EventEmitter<string>() matchedHotkey = new EventEmitter<string>()
@@ -80,13 +80,13 @@ export class HotkeysService {
} }
getCurrentKeystrokes (): string[] { getCurrentKeystrokes (): string[] {
this.currentKeystrokes = this.currentKeystrokes.filter((x) => performance.now() - x.time < KEY_TIMEOUT ) this.currentKeystrokes = this.currentKeystrokes.filter(x => performance.now() - x.time < KEY_TIMEOUT)
return stringifyKeySequence(this.currentKeystrokes.map((x) => x.event)) return stringifyKeySequence(this.currentKeystrokes.map(x => x.event))
} }
registerGlobalHotkey () { registerGlobalHotkey () {
this.electron.globalShortcut.unregisterAll() this.electron.globalShortcut.unregisterAll()
let value = this.config.store.hotkeys['toggle-window'] let value = this.config.store.hotkeys['toggle-window'] || []
if (typeof value === 'string') { if (typeof value === 'string') {
value = [value] value = [value]
} }
@@ -120,8 +120,10 @@ export class HotkeysService {
if (typeof value === 'string') { if (typeof value === 'string') {
value = [value] value = [value]
} }
value = value.map((item) => (typeof item === 'string') ? [item] : item) if (value) {
keys[key] = value value = value.map((item) => (typeof item === 'string') ? [item] : item)
keys[key] = value
}
} }
} }
return keys return keys
@@ -213,6 +215,10 @@ export class AppHotkeyProvider extends HotkeyProvider {
id: 'toggle-fullscreen', id: 'toggle-fullscreen',
name: 'Toggle fullscreen mode', name: 'Toggle fullscreen mode',
}, },
{
id: 'rename-tab',
name: 'Rename Tab',
},
{ {
id: 'close-tab', id: 'close-tab',
name: 'Close tab', name: 'Close tab',

View File

@@ -53,7 +53,7 @@ export class Logger {
log (...args: any[]) { this.doLog('log', ...args) } log (...args: any[]) { this.doLog('log', ...args) }
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class LogService { export class LogService {
private log: any private log: any

View File

@@ -1,12 +1,12 @@
import * as path from 'path' import * as path from 'path'
import * as fs from 'mz/fs' import * as fs from 'mz/fs'
import { Registry } from 'rage-edit-tmp' import { Registry } from 'rage-edit'
import { exec } from 'mz/child_process' import { exec } from 'mz/child_process'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { ElectronService } from './electron.service' import { ElectronService } from './electron.service'
import { HostAppService, Platform } from './hostApp.service' import { HostAppService, Platform } from './hostApp.service'
@Injectable() @Injectable({ providedIn: 'root' })
export class ShellIntegrationService { export class ShellIntegrationService {
private automatorWorkflows = ['Open Terminus here.workflow', 'Paste path into Terminus.workflow'] private automatorWorkflows = ['Open Terminus here.workflow', 'Paste path into Terminus.workflow']
private automatorWorkflowsLocation: string private automatorWorkflowsLocation: string
@@ -48,9 +48,9 @@ export class ShellIntegrationService {
async isInstalled (): Promise<boolean> { async isInstalled (): Promise<boolean> {
if (this.hostApp.platform === Platform.macOS) { 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) { } else if (this.hostApp.platform === Platform.Windows) {
return await Registry.has(this.registryKeys[0].path) return Registry.has(this.registryKeys[0].path)
} }
return true return true
} }

View File

@@ -5,7 +5,7 @@ import { Logger, LogService } from '../services/log.service'
import { AppService } from '../services/app.service' import { AppService } from '../services/app.service'
import { ConfigService } from '../services/config.service' import { ConfigService } from '../services/config.service'
@Injectable() @Injectable({ providedIn: 'root' })
export class TabRecoveryService { export class TabRecoveryService {
logger: Logger logger: Logger
@@ -19,13 +19,18 @@ export class TabRecoveryService {
app.tabsChanged$.subscribe(() => { app.tabsChanged$.subscribe(() => {
this.saveTabs(app.tabs) this.saveTabs(app.tabs)
}) })
setInterval(() => {
this.saveTabs(app.tabs)
}, 30000)
} }
saveTabs (tabs: BaseTabComponent[]) { async saveTabs (tabs: BaseTabComponent[]) {
window.localStorage.tabsRecovery = JSON.stringify( window.localStorage.tabsRecovery = JSON.stringify(
tabs await Promise.all(
.map((tab) => tab.getRecoveryToken()) tabs
.filter((token) => !!token) .map(tab => tab.getRecoveryToken())
.filter(token => !!token)
)
) )
} }

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@angular/core'
import { ConfigService } from '../services/config.service' import { ConfigService } from '../services/config.service'
import { Theme } from '../api/theme' import { Theme } from '../api/theme'
@Injectable() @Injectable({ providedIn: 'root' })
export class ThemesService { export class ThemesService {
private styleElement: HTMLElement = null private styleElement: HTMLElement = null

View File

@@ -6,7 +6,7 @@ import { ElectronService } from './electron.service'
import { HostAppService } from './hostApp.service' import { HostAppService } from './hostApp.service'
import { IToolbarButton, ToolbarButtonProvider } from '../api' import { IToolbarButton, ToolbarButtonProvider } from '../api'
@Injectable() @Injectable({ providedIn: 'root' })
export class TouchbarService { export class TouchbarService {
private tabsSegmentedControl: TouchBarSegmentedControl private tabsSegmentedControl: TouchBarSegmentedControl
private tabSegments: SegmentedControlSegment[] = [] private tabSegments: SegmentedControlSegment[] = []
@@ -49,8 +49,8 @@ export class TouchbarService {
let touchBar = new this.electron.TouchBar({ let touchBar = new this.electron.TouchBar({
items: [ items: [
this.tabsSegmentedControl, this.tabsSegmentedControl,
new this.electron.TouchBar.TouchBarSpacer({size: 'flexible'}), new this.electron.TouchBar.TouchBarSpacer({ size: 'flexible' }),
new this.electron.TouchBar.TouchBarSpacer({size: 'small'}), new this.electron.TouchBar.TouchBarSpacer({ size: 'small' }),
...buttons.map(button => this.getButton(button)) ...buttons.map(button => this.getButton(button))
] ]
}) })

View File

@@ -1,19 +1,30 @@
import axios from 'axios'
import * as os from 'os' import * as os from 'os'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Logger, LogService } from './log.service' import { Logger, LogService } from './log.service'
import { ElectronService } from './electron.service' import { ElectronService } from './electron.service'
@Injectable() const UPDATES_URL = 'https://api.github.com/repos/eugeny/terminus/releases/latest'
@Injectable({ providedIn: 'root' })
export class UpdaterService { export class UpdaterService {
private logger: Logger private logger: Logger
private downloaded: Promise<void> private downloaded: Promise<boolean>
private isSquirrel = true
private updateURL: string
constructor ( constructor (
log: LogService, log: LogService,
private electron: ElectronService, private electron: ElectronService,
) { ) {
this.logger = log.create('updater') this.logger = log.create('updater')
electron.autoUpdater.setFeedURL(`https://terminus-updates.herokuapp.com/update/${os.platform()}/${electron.app.getVersion()}`)
try {
electron.autoUpdater.setFeedURL(`https://terminus-updates.herokuapp.com/update/${os.platform()}/${electron.app.getVersion()}`)
} catch (e) {
this.isSquirrel = false
this.logger.info('Squirrel updater unavailable, falling back')
}
this.electron.autoUpdater.on('update-available', () => { this.electron.autoUpdater.on('update-available', () => {
this.logger.info('Update available') this.logger.info('Update available')
@@ -22,20 +33,45 @@ export class UpdaterService {
this.logger.info('No updates') this.logger.info('No updates')
}) })
this.downloaded = new Promise<void>(resolve => { this.downloaded = new Promise<boolean>(resolve => {
this.electron.autoUpdater.once('update-downloaded', resolve) this.electron.autoUpdater.once('update-downloaded', () => resolve(true))
}) })
this.logger.debug('Checking for updates') this.logger.debug('Checking for updates')
this.electron.autoUpdater.checkForUpdates()
if (this.isSquirrel) {
try {
this.electron.autoUpdater.checkForUpdates()
} catch (e) {
this.isSquirrel = false
this.logger.info('Squirrel updater unavailable, falling back')
}
}
} }
check (): Promise<void> { async check (): Promise<boolean> {
if (!this.isSquirrel) {
this.logger.debug('Checking for updates')
let response = await axios.get(UPDATES_URL)
let data = response.data
let version = data.tag_name.substring(1)
if (this.electron.app.getVersion() !== version) {
this.logger.info('Update available')
this.updateURL = data.html_url
return true
}
this.logger.info('No updates')
return false
}
return this.downloaded return this.downloaded
} }
async update () { async update () {
await this.downloaded if (!this.isSquirrel) {
this.electron.autoUpdater.quitAndInstall() this.electron.shell.openExternal(this.updateURL)
} else {
await this.downloaded
this.electron.autoUpdater.quitAndInstall()
}
} }
} }

View File

@@ -51,6 +51,7 @@ $input-disabled-bg: #333;
$input-color: $body-color; $input-color: $body-color;
$input-color-placeholder: #333; $input-color-placeholder: #333;
$input-border-color: #344; $input-border-color: #344;
$input-border-width: 0;
//$input-box-shadow: inset 0 1px 1px rgba($black,.075); //$input-box-shadow: inset 0 1px 1px rgba($black,.075);
$input-border-radius: 0; $input-border-radius: 0;
$custom-select-border-radius: 0; $custom-select-border-radius: 0;
@@ -70,7 +71,7 @@ $popover-bg: $body-bg;
$dropdown-bg: $body-bg; $dropdown-bg: $body-bg;
$dropdown-link-color: $body-color; $dropdown-link-color: $body-color;
$dropdown-link-hover-color: #333; $dropdown-link-hover-color: white;
$dropdown-link-hover-bg: $body-bg2; $dropdown-link-hover-bg: $body-bg2;
//$dropdown-link-active-color: $component-active-color; //$dropdown-link-active-color: $component-active-color;
//$dropdown-link-active-bg: $component-active-bg; //$dropdown-link-active-bg: $component-active-bg;
@@ -346,6 +347,15 @@ ngb-tabset .tab-content {
} }
} }
.list-group.list-group-flush .list-group-item:not(.list-group-item-action) {
background: transparent;
border-color: rgba(0, 0, 0, 0.2);
&:not(:last-child) {
border-bottom: none;
}
}
select.form-control { select.form-control {
-webkit-appearance: none; -webkit-appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='24' height='24' viewBox='0 0 24 24'><path fill='#444' d='M7.406 7.828l4.594 4.594 4.594-4.594 1.406 1.406-6 6-6-6z'></path></svg>"); background-image: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='24' height='24' viewBox='0 0 24 24'><path fill='#444' d='M7.406 7.828l4.594 4.594 4.594-4.594 1.406 1.406-6 6-6-6z'></path></svg>");
@@ -362,7 +372,31 @@ toggle.active .body .toggle {
background: $blue; background: $blue;
} }
.modal .modal-footer {
background: rgba(0, 0, 0, .25);
.btn {
font-weight: bold;
padding: 0.375rem 1.5rem;
}
}
.list-group-item svg { .list-group-item svg {
fill: white; fill: white;
fill-opacity: 0.75; fill-opacity: 0.75;
} }
*::-webkit-scrollbar {
background: rgba(0, 0, 0, .125);
width: 10px;
margin: 5px;
}
*::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, .25);
}
*::-webkit-scrollbar-corner,
*::-webkit-resizer {
opacity: 0;
}

View File

@@ -73,6 +73,14 @@ aws4@^1.6.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
axios@^0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102"
integrity sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=
dependencies:
follow-redirects "^1.3.0"
is-buffer "^1.1.5"
bcrypt-pbkdf@^1.0.0: bcrypt-pbkdf@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
@@ -162,7 +170,7 @@ dashdash@^1.12.0:
dependencies: dependencies:
assert-plus "^1.0.0" assert-plus "^1.0.0"
debug@^3.0.0: debug@=3.1.0, debug@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
@@ -261,6 +269,13 @@ fast-json-stable-stringify@^2.0.0:
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
follow-redirects@^1.3.0:
version "1.5.10"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
dependencies:
debug "=3.1.0"
forever-agent@~0.6.1: forever-agent@~0.6.1:
version "0.6.1" version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
@@ -341,6 +356,11 @@ http-signature@~1.2.0:
jsprim "^1.2.2" jsprim "^1.2.2"
sshpk "^1.7.0" sshpk "^1.7.0"
is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-typedarray@~1.0.0: is-typedarray@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -466,10 +486,10 @@ qs@~6.5.1:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
rage-edit-tmp@^1.1.0: rage-edit@^1.2.0:
version "1.1.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/rage-edit-tmp/-/rage-edit-tmp-1.1.0.tgz#fc5d76716d2fe2cf97dcafbf3e26753e3a08e3b2" resolved "https://registry.yarnpkg.com/rage-edit/-/rage-edit-1.2.0.tgz#991860a60fef934d8a6d0f057e55786b02f94a2b"
integrity sha512-lR97QHY5WSf9orInMJhPqUbenkdiy7QbXUoRMI+wBZGyAPkxNwgo7h6ojq634QrBf/kQo3mVXYjuD3ZYraNaZQ== integrity sha512-0RspBRc2s6We4g7hRCvT5mu7YPEnfjvQK8Tt354a2uUNJCMC7MKLvo/1mLvHUCQ/zbP6siQyp5VRZN7UCpMFZg==
request@2.86.0: request@2.86.0:
version "2.86.0" version "2.86.0"

View File

@@ -2,57 +2,61 @@
strong Error in {{erroredPlugin}}: strong Error in {{erroredPlugin}}:
pre {{errorMessage}} pre {{errorMessage}}
button.btn.btn-outline-info.btn-sm.pull-right((click)='openPluginsFolder()')
i.fa.fa-folder
span Plugins folder
h3.mb-1 Installed .d-flex
h3.mb-1 Installed
button.btn.btn-outline-info.btn-sm.ml-auto((click)='openPluginsFolder()')
i.fas.fa-folder
span Plugins folder
.mb-3.d-flex.w-100.align-items-center(*ngFor='let plugin of pluginManager.installedPlugins|orderBy:"name"') .list-group.list-group-flush.mt-2
button.btn.btn-outline-danger.active.mr-2( .list-group-item.d-flex.align-items-center(*ngFor='let plugin of pluginManager.installedPlugins|orderBy:"name"')
*ngIf='config.store.pluginBlacklist.includes(plugin.name)', .mr-auto.d-flex.flex-column
(click)='enablePlugin(plugin)' div
) strong {{plugin.name}}
i.fa.fa-fw.fa-pause small.text-muted.ml-1(*ngIf='!plugin.isBuiltin') {{plugin.version}} / {{plugin.author}}
button.btn.btn-outline-secondary.mr-2( small.text-warning.ml-1(*ngIf='config.store.pluginBlacklist.includes(plugin.name)') Disabled
*ngIf='!config.store.pluginBlacklist.includes(plugin.name)', a.text-muted.mb-0((click)='showPluginInfo(plugin)')
(click)='disablePlugin(plugin)' small {{plugin.description}}
)
i.fa.fa-fw.fa-check
.mr-auto.d-flex.flex-column button.btn.btn-primary.ml-2(
div *ngIf='npmInstalled && knownUpgrades[plugin.name]',
strong {{plugin.name}} (click)='upgradePlugin(plugin)',
small.text-muted.ml-1 {{plugin.version}} / {{plugin.author}} [disabled]='busy[plugin.name] != undefined'
a.text-muted.mb-0((click)='showPluginInfo(plugin)') )
small {{plugin.description}} i.fas.fa-fw.fa-arrow-up(*ngIf='busy[plugin.name] != BusyState.Installing')
i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Installing')
span Upgrade ({{knownUpgrades[plugin.name].version}})
button.btn.btn-primary.ml-2( button.btn.btn-primary.ml-2(
*ngIf='npmInstalled && knownUpgrades[plugin.name]', *ngIf='config.store.pluginBlacklist.includes(plugin.name)',
(click)='upgradePlugin(plugin)', (click)='enablePlugin(plugin)'
[disabled]='busy[plugin.name] != undefined' )
) i.fas.fa-fw.fa-play
i.fa.fa-fw.fa-arrow-up(*ngIf='busy[plugin.name] != BusyState.Installing')
i.fa.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Installing')
span Upgrade ({{knownUpgrades[plugin.name].version}})
button.btn.btn-outline-danger.ml-2( button.btn.btn-secondary.ml-2(
(click)='uninstallPlugin(plugin)', *ngIf='!config.store.pluginBlacklist.includes(plugin.name)',
*ngIf='!plugin.isBuiltin && npmInstalled', (click)='disablePlugin(plugin)'
[disabled]='busy[plugin.name] != undefined' )
) i.fas.fa-fw.fa-pause
i.fa.fa-fw.fa-trash-o(*ngIf='busy[plugin.name] != BusyState.Uninstalling')
i.fa.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Uninstalling') button.btn.btn-danger.ml-2(
(click)='uninstallPlugin(plugin)',
*ngIf='!plugin.isBuiltin && npmInstalled',
[disabled]='busy[plugin.name] != undefined'
)
i.fas.fa-fw.fa-trash(*ngIf='busy[plugin.name] != BusyState.Uninstalling')
i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Uninstalling')
.text-center.mt-5(*ngIf='npmMissing') .text-center.mt-5(*ngIf='npmMissing')
h4 npm not installed h4 npm not installed
p.mb-2 npm is required to install Terminus plugins. p.mb-2 npm is required to install Terminus plugins.
.btn-group .btn-group
button.btn.btn-outline-primary((click)='downloadNPM()') button.btn.btn-outline-primary((click)='downloadNPM()')
i.fa.fa-download i.fas.fa-download
span Get npm span Get npm
button.btn.btn-outline-info((click)='checkNPM()') button.btn.btn-outline-info((click)='checkNPM()')
i.fa.fa-refresh i.fas.fa-refresh
span Try again span Try again
div(*ngIf='npmInstalled') div(*ngIf='npmInstalled')
@@ -61,8 +65,8 @@ div(*ngIf='npmInstalled')
.input-group.mb-3 .input-group.mb-3
.input-group-prepend .input-group-prepend
.input-group-text .input-group-text
i.fa.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='!availablePluginsReady') i.fas.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='!availablePluginsReady')
i.fa.fa-fw.fa-search(*ngIf='availablePluginsReady') i.fas.fa-fw.fa-search(*ngIf='availablePluginsReady')
input.form-control( input.form-control(
type='text', type='text',
[(ngModel)]='_1', [(ngModel)]='_1',
@@ -71,19 +75,19 @@ div(*ngIf='npmInstalled')
) )
.mb-4(*ngIf='availablePlugins$') .list-group.list-group-flush.mb-4(*ngIf='availablePlugins$')
ng-container(*ngFor='let plugin of (availablePlugins$|async|orderBy:"name")') ng-container(*ngFor='let plugin of (availablePlugins$|async|orderBy:"name")')
.d-flex.w-100.align-items-center.mb-3(*ngIf='!isAlreadyInstalled(plugin)') .list-group-item.d-flex.align-items-center(*ngIf='!isAlreadyInstalled(plugin)')
button.btn.btn-primary.mr-2( button.btn.btn-primary.mr-3(
(click)='installPlugin(plugin)', (click)='installPlugin(plugin)',
[disabled]='busy[plugin.name] != undefined' [disabled]='busy[plugin.name] != undefined'
) )
i.fa.fa-fw.fa-download(*ngIf='busy[plugin.name] != BusyState.Installing') i.fas.fa-fw.fa-download(*ngIf='busy[plugin.name] != BusyState.Installing')
i.fa.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Installing') i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Installing')
div((click)='showPluginInfo(plugin)') div((click)='showPluginInfo(plugin)')
div div
strong {{plugin.name}} strong {{plugin.name}}
small.text-muted.ml-1 {{plugin.version}} / {{plugin.author}} small.text-muted.ml-1 {{plugin.version}} / {{plugin.author}}
i.fa.fa-check.text-success.ml-1(*ngIf='plugin.isOfficial', title='Official') i.fas.fa-check.text-success.ml-1(*ngIf='plugin.isOfficial', title='Official')
small.text-muted {{plugin.description}} small.text-muted {{plugin.description}}

View File

@@ -22,7 +22,6 @@ import { PluginsSettingsTabProvider } from './settings'
providers: [ providers: [
{ provide: SettingsTabProvider, useClass: PluginsSettingsTabProvider, multi: true }, { provide: SettingsTabProvider, useClass: PluginsSettingsTabProvider, multi: true },
{ provide: ConfigProvider, useClass: PluginsConfigProvider, multi: true }, { provide: ConfigProvider, useClass: PluginsConfigProvider, multi: true },
PluginManagerService,
], ],
entryComponents: [ entryComponents: [
PluginsSettingsTabComponent, PluginsSettingsTabComponent,

View File

@@ -23,7 +23,7 @@ export interface IPluginInfo {
path?: string path?: string
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class PluginManagerService { export class PluginManagerService {
logger: Logger logger: Logger
builtinPluginsPath: string = (window as any).builtinPluginsPath builtinPluginsPath: string = (window as any).builtinPluginsPath

View File

@@ -6,7 +6,7 @@
"terminus-builtin-plugin" "terminus-builtin-plugin"
], ],
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/src/index.d.ts",
"scripts": { "scripts": {
"build": "webpack --progress --color --display-modules", "build": "webpack --progress --color --display-modules",
"watch": "webpack --progress --color --watch" "watch": "webpack --progress --color --watch"
@@ -30,6 +30,5 @@
"@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.22", "@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.22",
"terminus-core": "*", "terminus-core": "*",
"rxjs": "5.3.0" "rxjs": "5.3.0"
}, }
"false": {}
} }

View File

@@ -12,11 +12,11 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
.text-muted.mr-auto {{homeBase.appVersion}} .text-muted.mr-auto {{homeBase.appVersion}}
button.btn.btn-secondary.mr-3((click)='homeBase.openGitHub()') button.btn.btn-secondary.mr-3((click)='homeBase.openGitHub()')
i.fa.fa-github i.fab.fa-github
span GitHub span GitHub
button.btn.btn-secondary((click)='homeBase.reportBug()') button.btn.btn-secondary((click)='homeBase.reportBug()')
i.fa.fa-bug i.fas.fa-bug
span Report a problem span Report a problem
.form-line(*ngIf='!isShellIntegrationInstalled') .form-line(*ngIf='!isShellIntegrationInstalled')
@@ -24,7 +24,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
.title Shell integration .title Shell integration
.description Allows quickly opening a terminal in the selected folder .description Allows quickly opening a terminal in the selected folder
button.btn.btn-primary((click)='installShellIntegration()') button.btn.btn-primary((click)='installShellIntegration()')
i.fa.fa-check i.fas.fa-check
span Install span Install
.form-line .form-line
@@ -225,7 +225,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
.title Debugging .title Debugging
button.btn.btn-secondary((click)='hostApp.openDevTools()') button.btn.btn-secondary((click)='hostApp.openDevTools()')
i.fa.fa-bug i.fas.fa-bug
span Open DevTools span Open DevTools
.form-line .form-line
@@ -254,7 +254,7 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
.input-group.mb-4 .input-group.mb-4
.input-group-prepend .input-group-prepend
.input-group-text .input-group-text
i.fa.fa-fw.fa-search i.fas.fa-fw.fa-search
input.form-control(type='search', placeholder='Search hotkeys', [(ngModel)]='hotkeyFilter') input.form-control(type='search', placeholder='Search hotkeys', [(ngModel)]='hotkeyFilter')
.form-group .form-group
@@ -298,8 +298,8 @@ ngb-tabset.vertical(type='pills', [activeId]='activeTab')
) )
.mt-3 .mt-3
button.btn.btn-primary((click)='saveConfigFile()', *ngIf='isConfigFileValid()') button.btn.btn-primary((click)='saveConfigFile()', *ngIf='isConfigFileValid()')
i.fa.fa-check.mr-2 i.fas.fa-check.mr-2
| Save and apply | Save and apply
button.btn.btn-primary(disabled, *ngIf='!isConfigFileValid()') button.btn.btn-primary(disabled, *ngIf='!isConfigFileValid()')
i.fa.fa-warning.mr-2 i.fas.fa-exclamation-triangle.mr-2
| Invalid syntax | Invalid syntax

View File

@@ -1,14 +1,13 @@
import * as yaml from 'js-yaml' import * as yaml from 'js-yaml'
import * as os from 'os' import * as os from 'os'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { Component, Inject, Input } from '@angular/core' import { Component, Inject, Input, HostBinding } from '@angular/core'
import { HotkeysService } from 'terminus-core'
import { import {
ElectronService, ElectronService,
DockingService, DockingService,
ConfigService, ConfigService,
IHotkeyDescription, IHotkeyDescription,
HotkeyProvider, HotkeysService,
BaseTabComponent, BaseTabComponent,
Theme, Theme,
HostAppService, HostAppService,
@@ -37,6 +36,7 @@ export class SettingsTabComponent extends BaseTabComponent {
configFile: string configFile: string
isShellIntegrationInstalled = false isShellIntegrationInstalled = false
isFluentVibrancySupported = false isFluentVibrancySupported = false
@HostBinding('class.pad-window-controls') padWindowControls = false
private configSubscription: Subscription private configSubscription: Subscription
constructor ( constructor (
@@ -47,7 +47,6 @@ export class SettingsTabComponent extends BaseTabComponent {
public homeBase: HomeBaseService, public homeBase: HomeBaseService,
public shellIntegration: ShellIntegrationService, public shellIntegration: ShellIntegrationService,
hotkeys: HotkeysService, hotkeys: HotkeysService,
@Inject(HotkeyProvider) hotkeyProviders: HotkeyProvider[],
@Inject(SettingsTabProvider) public settingsProviders: SettingsTabProvider[], @Inject(SettingsTabProvider) public settingsProviders: SettingsTabProvider[],
@Inject(Theme) public themes: Theme[], @Inject(Theme) public themes: Theme[],
) { ) {
@@ -58,16 +57,21 @@ export class SettingsTabComponent extends BaseTabComponent {
this.themes = config.enabledServices(this.themes) this.themes = config.enabledServices(this.themes)
this.configDefaults = yaml.safeDump(config.getDefaults()) this.configDefaults = yaml.safeDump(config.getDefaults())
this.configFile = config.readRaw()
this.configSubscription = config.changed$.subscribe(() => { const onConfigChange = () => {
this.configFile = config.readRaw() this.configFile = config.readRaw()
}) this.padWindowControls = hostApp.platform === Platform.macOS
&& config.store.appearance.tabsLocation === 'bottom'
}
this.configSubscription = config.changed$.subscribe(onConfigChange)
onConfigChange()
hotkeys.getHotkeyDescriptions().then(descriptions => { hotkeys.getHotkeyDescriptions().then(descriptions => {
this.hotkeyDescriptions = descriptions this.hotkeyDescriptions = descriptions
}) })
this.isFluentVibrancySupported = process.platform === 'win32' this.isFluentVibrancySupported = hostApp.platform === Platform.Windows
&& parseFloat(os.release()) >= 10 && parseFloat(os.release()) >= 10
&& parseInt(os.release().split('.')[2]) >= 17063 && parseInt(os.release().split('.')[2]) >= 17063
} }
@@ -76,7 +80,7 @@ export class SettingsTabComponent extends BaseTabComponent {
this.isShellIntegrationInstalled = await this.shellIntegration.isInstalled() this.isShellIntegrationInstalled = await this.shellIntegration.isInstalled()
} }
getRecoveryToken (): any { async getRecoveryToken (): Promise<any> {
return { type: 'app:settings' } return { type: 'app:settings' }
} }

View File

@@ -21,3 +21,7 @@
:host /deep/ ngb-tabset > .tab-content > .tab-pane { :host /deep/ ngb-tabset > .tab-content > .tab-pane {
width: 100%; width: 100%;
} }
:host.pad-window-controls /deep/ ngb-tabset > .nav {
padding-top: 40px;
}

View File

@@ -7,7 +7,7 @@ import { SettingsTabProvider } from '../api'
}) })
export class SettingsTabBodyComponent { export class SettingsTabBodyComponent {
@Input() provider: SettingsTabProvider @Input() provider: SettingsTabProvider
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef @ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
component: ComponentRef<Component> component: ComponentRef<Component>
constructor (private componentFactoryResolver: ComponentFactoryResolver) { } constructor (private componentFactoryResolver: ComponentFactoryResolver) { }

View File

@@ -4,8 +4,7 @@ import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgPipesModule } from 'ngx-pipes' import { NgPipesModule } from 'ngx-pipes'
import { ToolbarButtonProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider } from 'terminus-core' import TerminusCorePlugin, { ToolbarButtonProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider } from 'terminus-core'
import TerminusCorePlugin from 'terminus-core'
import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component' import { HotkeyInputModalComponent } from './components/hotkeyInputModal.component'
import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component' import { MultiHotkeyInputComponent } from './components/multiHotkeyInput.component'

View File

@@ -6,6 +6,7 @@ module.exports = {
entry: 'src/index.ts', entry: 'src/index.ts',
devtool: 'source-map', devtool: 'source-map',
context: __dirname, context: __dirname,
mode: 'development',
output: { output: {
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
filename: 'index.js', filename: 'index.js',

View File

@@ -36,7 +36,8 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"wincredmgr": "^2.0.0", "wincredmgr": "^2.0.0",
"xkeychain": "^0.0.6" "xkeychain": "^0.0.6",
"windows-process-tree": "^0.2.3"
}, },
"dependencies": { "dependencies": {
"ssh2": "^0.5.5" "ssh2": "^0.5.5"

View File

@@ -56,7 +56,7 @@
) )
.input-group-btn .input-group-btn
button.btn.btn-secondary((click)='selectPrivateKey()') button.btn.btn-secondary((click)='selectPrivateKey()')
i.fa.fa-folder-open i.fas.fa-folder-open
ngb-tab(id='advanced') ngb-tab(id='advanced')
ng-template(ngbTabTitle) ng-template(ngbTabTitle)
@@ -119,11 +119,11 @@
td td
.input-group.flex-nowrap .input-group.flex-nowrap
button.btn.btn-outline-info.ml-0((click)='moveScriptUp(script)') button.btn.btn-outline-info.ml-0((click)='moveScriptUp(script)')
i.fa.fa-arrow-up i.fas.fa-arrow-up
button.btn.btn-outline-info.ml-0((click)='moveScriptDown(script)') button.btn.btn-outline-info.ml-0((click)='moveScriptDown(script)')
i.fa.fa-arrow-down i.fas.fa-arrow-down
button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)') button.btn.btn-outline-danger.ml-0((click)='deleteScript(script)')
i.fa.fa-trash-o i.fas.fa-trash
tr tr
td td
input.form-control( input.form-control(
@@ -148,9 +148,9 @@
td td
.input-group.flex-nowrap .input-group.flex-nowrap
button.btn.btn-outline-info.ml-0((click)='addScript()') button.btn.btn-outline-info.ml-0((click)='addScript()')
i.fa.fa-check i.fas.fa-check
button.btn.btn-outline-danger.ml-0((click)='clearScript()') button.btn.btn-outline-danger.ml-0((click)='clearScript()')
i.fa.fa-trash-o i.fas.fa-trash
.modal-footer .modal-footer
button.btn.btn-outline-primary((click)='save()') Save button.btn.btn-outline-primary((click)='save()') Save

View File

@@ -76,7 +76,7 @@ export class EditConnectionModalComponent {
if (!this.connection.scripts) { if (!this.connection.scripts) {
this.connection.scripts = [] this.connection.scripts = []
} }
this.connection.scripts.push({...this.newScript}) this.connection.scripts.push({ ...this.newScript })
this.clearScript() this.clearScript()
} }

View File

@@ -10,10 +10,10 @@
.list-group.mt-3(*ngIf='lastConnection') .list-group.mt-3(*ngIf='lastConnection')
a.list-group-item.list-group-item-action.d-flex.align-items-center((click)='connect(lastConnection)') a.list-group-item.list-group-item-action.d-flex.align-items-center((click)='connect(lastConnection)')
i.fa.fa-fw.fa-history i.fas.fa-fw.fa-history
.mr-auto {{lastConnection.name}} .mr-auto {{lastConnection.name}}
button.btn.btn-outline-danger.btn-sm((click)='clearLastConnection(); $event.stopPropagation()') button.btn.btn-outline-danger.btn-sm((click)='clearLastConnection(); $event.stopPropagation()')
i.fa.fa-trash-o i.fas.fa-trash
.list-group.mt-3.connections-list(*ngIf='childGroups.length') .list-group.mt-3.connections-list(*ngIf='childGroups.length')
ng-container(*ngFor='let group of childGroups') ng-container(*ngFor='let group of childGroups')
@@ -27,4 +27,6 @@
.list-group-item.list-group-item-action.pl-5.d-flex.align-items-center( .list-group-item.list-group-item-action.pl-5.d-flex.align-items-center(
*ngFor='let connection of group.connections', *ngFor='let connection of group.connections',
(click)='connect(connection)' (click)='connect(connection)'
) {{connection.name}} )
.mr-2 {{connection.name}}
.text-muted {{connection.host}}

View File

@@ -1,25 +1,28 @@
h3 Connections h3 Connections
.list-group.mt-3.mb-3 .list-group.list-group-flush.mt-3.mb-3
ng-container(*ngFor='let group of childGroups') ng-container(*ngFor='let group of childGroups')
.list-group-item.list-group-item-action.d-flex.align-items-center((click)='groupCollapsed[group.name] = !groupCollapsed[group.name]') .list-group-item.list-group-item-action.d-flex.align-items-center(
(click)='groupCollapsed[group.name] = !groupCollapsed[group.name]'
)
.fa.fa-fw.fa-chevron-right(*ngIf='groupCollapsed[group.name]') .fa.fa-fw.fa-chevron-right(*ngIf='groupCollapsed[group.name]')
.fa.fa-fw.fa-chevron-down(*ngIf='!groupCollapsed[group.name]') .fa.fa-fw.fa-chevron-down(*ngIf='!groupCollapsed[group.name]')
span.ml-3.mr-auto {{group.name || "Ungrouped"}} span.ml-3.mr-auto {{group.name || "Ungrouped"}}
button.btn.btn-outline-info.ml-2((click)='editGroup(group)') button.btn.btn-outline-info.ml-2((click)='editGroup(group)')
i.fa.fa-pencil i.fas.fa-edit
button.btn.btn-outline-danger.ml-1((click)='deleteGroup(group)') button.btn.btn-outline-danger.ml-1((click)='deleteGroup(group)')
i.fa.fa-trash-o i.fas.fa-trash
ng-container(*ngIf='!groupCollapsed[group.name]') ng-container(*ngIf='!groupCollapsed[group.name]')
.list-group-item.pl-5.d-flex.align-items-center(*ngFor='let connection of group.connections') .list-group-item.list-group-item-action.pl-5.d-flex.align-items-center(
*ngFor='let connection of group.connections',
(click)='editConnection(connection)'
)
.mr-auto .mr-auto
div {{connection.name}} div {{connection.name}}
.text-muted {{connection.host}} .text-muted {{connection.host}}
button.btn.btn-outline-info.ml-2((click)='editConnection(connection)') button.btn.btn-outline-danger.ml-1((click)='$event.stopPropagation(); deleteConnection(connection)')
i.fa.fa-pencil i.fas.fa-trash
button.btn.btn-outline-danger.ml-1((click)='deleteConnection(connection)')
i.fa.fa-trash-o
button.btn.btn-outline-primary((click)='createConnection()') button.btn.btn-primary((click)='createConnection()')
div.fa.fa-fw.fa-globe i.fas.fa-fw.fa-plus
span.ml-2 Add connection span.ml-2 Add connection

View File

@@ -3,16 +3,13 @@ import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { ToastrModule } from 'ngx-toastr' import { ToastrModule } from 'ngx-toastr'
import { ToolbarButtonProvider, ConfigProvider } from 'terminus-core' import TerminusCoreModule, { ToolbarButtonProvider, ConfigProvider } from 'terminus-core'
import TerminusCoreModule from 'terminus-core'
import { SettingsTabProvider } from 'terminus-settings' import { SettingsTabProvider } from 'terminus-settings'
import { EditConnectionModalComponent } from './components/editConnectionModal.component' import { EditConnectionModalComponent } from './components/editConnectionModal.component'
import { SSHModalComponent } from './components/sshModal.component' import { SSHModalComponent } from './components/sshModal.component'
import { PromptModalComponent } from './components/promptModal.component' import { PromptModalComponent } from './components/promptModal.component'
import { SSHSettingsTabComponent } from './components/sshSettingsTab.component' import { SSHSettingsTabComponent } from './components/sshSettingsTab.component'
import { SSHService } from './services/ssh.service'
import { PasswordStorageService } from './services/passwordStorage.service'
import { ButtonProvider } from './buttonProvider' import { ButtonProvider } from './buttonProvider'
import { SSHConfigProvider } from './config' import { SSHConfigProvider } from './config'
@@ -27,8 +24,6 @@ import { SSHSettingsTabProvider } from './settings'
TerminusCoreModule, TerminusCoreModule,
], ],
providers: [ providers: [
PasswordStorageService,
SSHService,
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true }, { provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
{ provide: ConfigProvider, useClass: SSHConfigProvider, multi: true }, { provide: ConfigProvider, useClass: SSHConfigProvider, multi: true },
{ provide: SettingsTabProvider, useClass: SSHSettingsTabProvider, multi: true }, { provide: SettingsTabProvider, useClass: SSHSettingsTabProvider, multi: true },

View File

@@ -13,7 +13,7 @@ try {
} }
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class PasswordStorageService { export class PasswordStorageService {
constructor ( constructor (
private zone: NgZone, private zone: NgZone,

View File

@@ -11,7 +11,13 @@ import { PromptModalComponent } from '../components/promptModal.component'
import { PasswordStorageService } from './passwordStorage.service' import { PasswordStorageService } from './passwordStorage.service'
const { SSH2Stream } = require('ssh2-streams') const { SSH2Stream } = require('ssh2-streams')
@Injectable() let windowsProcessTree
try {
windowsProcessTree = require('windows-process-tree/build/Release/windows_process_tree.node')
} catch (e) {
} // tslint:disable-line
@Injectable({ providedIn: 'root' })
export class SSHService { export class SSHService {
private logger: Logger private logger: Logger
@@ -67,7 +73,7 @@ export class SSHService {
let ssh = new Client() let ssh = new Client()
let connected = false let connected = false
let savedPassword: string = null let savedPassword: string = null
await new Promise((resolve, reject) => { await new Promise(async (resolve, reject) => {
ssh.on('ready', () => { ssh.on('ready', () => {
connected = true connected = true
if (savedPassword) { if (savedPassword) {
@@ -99,7 +105,14 @@ export class SSHService {
let agent: string = null let agent: string = null
if (this.hostApp.platform === Platform.Windows) { if (this.hostApp.platform === Platform.Windows) {
agent = 'pageant' let pageantRunning = new Promise<boolean>(resolve => {
windowsProcessTree.getProcessList(list => {
resolve(list.some(x => x.name === 'pageant.exe'))
}, 0)
})
if (await pageantRunning) {
agent = 'pageant'
}
} else { } else {
agent = process.env.SSH_AUTH_SOCK agent = process.env.SSH_AUTH_SOCK
} }

View File

@@ -48,6 +48,7 @@ module.exports = {
'wincredmgr', 'wincredmgr',
'path', 'path',
'ngx-toastr', 'ngx-toastr',
'windows-process-tree/build/Release/windows_process_tree.node',
/^rxjs/, /^rxjs/,
/^@angular/, /^@angular/,
/^@ng-bootstrap/, /^@ng-bootstrap/,

View File

@@ -1507,6 +1507,11 @@ ms@2.0.0:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
nan@^2.10.0:
version "2.11.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766"
integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==
nan@^2.3.0: nan@^2.3.0:
version "2.8.0" version "2.8.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
@@ -2520,6 +2525,13 @@ window-size@0.1.0:
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0= integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=
windows-process-tree@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.2.3.tgz#6b781f0a320e8a0d6434c9399add4389c709cf6e"
integrity sha512-SzPJSubVVsToz1g5lr2P+4mQT70gvJ9u/nlnpfkOeQcAhOuhKz5DiO1TARgR0OnVsv21LPzxbA2m/4JQkGh1wA==
dependencies:
nan "^2.10.0"
wordwrap@0.0.2: wordwrap@0.0.2:
version "0.0.2" version "0.0.2"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"

View File

@@ -6,7 +6,7 @@
"terminus-builtin-plugin" "terminus-builtin-plugin"
], ],
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/src/index.d.ts",
"scripts": { "scripts": {
"build": "webpack --progress --color --display-modules", "build": "webpack --progress --color --display-modules",
"watch": "webpack --progress --color --watch" "watch": "webpack --progress --color --watch"
@@ -17,6 +17,7 @@
"author": "Eugene Pankov", "author": "Eugene Pankov",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@terminus-term/xterm": "3.8.4",
"@types/deep-equal": "^1.0.0", "@types/deep-equal": "^1.0.0",
"@types/mz": "0.0.31", "@types/mz": "0.0.31",
"@types/node": "7.0.12", "@types/node": "7.0.12",
@@ -24,9 +25,8 @@
"dataurl": "0.1.0", "dataurl": "0.1.0",
"deep-equal": "1.0.1", "deep-equal": "1.0.1",
"file-loader": "^0.11.2", "file-loader": "^0.11.2",
"rage-edit-tmp": "^1.1.0", "rage-edit": "1.2.0",
"uuid": "^3.3.2", "uuid": "^3.3.2",
"xterm": "^3.8.0",
"xterm-addon-ligatures-tmp": "^0.1.0-beta-1" "xterm-addon-ligatures-tmp": "^0.1.0-beta-1"
}, },
"peerDependencies": { "peerDependencies": {
@@ -40,12 +40,10 @@
"terminus-settings": "*" "terminus-settings": "*"
}, },
"dependencies": { "dependencies": {
"@types/async-lock": "0.0.19", "@terminus-term/node-pty": "0.8.0-1",
"async-lock": "^1.0.0",
"font-manager": "0.3.0", "font-manager": "0.3.0",
"hterm-umdjs": "1.4.1", "hterm-umdjs": "1.4.1",
"mz": "^2.6.0", "mz": "^2.6.0",
"node-pty-tmp": "0.7.2",
"ps-node": "^0.1.6", "ps-node": "^0.1.6",
"runes": "^0.4.2" "runes": "^0.4.2"
}, },

View File

@@ -1,4 +1,3 @@
import { Observable } from 'rxjs'
import { TerminalTabComponent } from './components/terminalTab.component' import { TerminalTabComponent } from './components/terminalTab.component'
export abstract class TerminalDecorator { export abstract class TerminalDecorator {
@@ -15,25 +14,19 @@ export interface ResizeEvent {
export interface SessionOptions { export interface SessionOptions {
name?: string name?: string
command?: string command: string
args?: string[] args: string[]
cwd?: string cwd?: string
env?: any env?: any
width?: number width?: number
height?: number height?: number
recoveryId?: string
recoveredTruePID$?: Observable<number>
pauseAfterExit?: boolean pauseAfterExit?: boolean
runAsAdministrator?: boolean
} }
export abstract class SessionPersistenceProvider { export interface Profile {
abstract id: string name: string,
abstract displayName: string sessionOptions: SessionOptions,
abstract isAvailable (): boolean
abstract async attachSession (recoveryId: any): Promise<SessionOptions>
abstract async startSession (options: SessionOptions): Promise<any>
abstract async terminateSession (recoveryId: string): Promise<void>
} }
export interface ITerminalColorScheme { export interface ITerminalColorScheme {
@@ -48,6 +41,12 @@ export abstract class TerminalColorSchemeProvider {
abstract async getSchemes (): Promise<ITerminalColorScheme[]> abstract async getSchemes (): Promise<ITerminalColorScheme[]>
} }
export abstract class TerminalContextMenuItemProvider {
weight: number
abstract async getItems (tab: TerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]>
}
export interface IShell { export interface IShell {
id: string id: string
name?: string name?: string

View File

@@ -57,7 +57,7 @@ h3.mb-3 Appearance
(click)='deleteScheme(config.store.terminal.colorScheme)', (click)='deleteScheme(config.store.terminal.colorScheme)',
*ngIf='isCustomScheme(config.store.terminal.colorScheme)' *ngIf='isCustomScheme(config.store.terminal.colorScheme)'
) )
i.fa.fa-trash-o i.fas.fa-trash
.form-group(*ngIf='editingColorScheme') .form-group(*ngIf='editingColorScheme')
label Editing label Editing

View File

@@ -0,0 +1,58 @@
.modal-body
.form-group
label Name
input.form-control(
type='text',
autofocus,
[(ngModel)]='profile.name',
)
.form-group
label Command
input.form-control(
type='text',
[(ngModel)]='profile.sessionOptions.command',
)
.form-group
label Arguments
.input-group(
*ngFor='let arg of profile.sessionOptions.args; index as i; trackBy: trackByIndex',
)
input.form-control(
type='text',
[(ngModel)]='profile.sessionOptions.args[i]',
)
.input-group-btn
button.btn.btn-secondary((click)='profile.sessionOptions.args.splice(i, 1)')
i.fas.fa-trash
.mt-2
button.btn.btn-secondary((click)='profile.sessionOptions.args.push("")')
i.fas.fa-plus.mr-2
| Add
.form-line(*ngIf='uac.isAvailable')
.header
.title Run as administrator
toggle(
[(ngModel)]='profile.sessionOptions.runAsAdministrator',
)
.form-group
label Working directory
input.form-control(
type='text',
[(ngModel)]='profile.sessionOptions.cwd',
)
.form-group
label Environment
environment-editor(
type='text',
[(model)]='profile.sessionOptions.env',
)
.modal-footer
button.btn.btn-outline-primary((click)='save()') Save
button.btn.btn-outline-danger((click)='cancel()') Cancel

View File

@@ -0,0 +1,34 @@
import { Component } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { UACService } from '../services/uac.service'
import { Profile } from '../api'
@Component({
template: require('./editProfileModal.component.pug'),
})
export class EditProfileModalComponent {
profile: Profile
constructor (
public uac: UACService,
private modalInstance: NgbActiveModal,
) {
}
ngOnInit () {
this.profile.sessionOptions.env = this.profile.sessionOptions.env || {}
this.profile.sessionOptions.args = this.profile.sessionOptions.args || []
}
save () {
this.modalInstance.close(this.profile)
}
cancel () {
this.modalInstance.dismiss()
}
trackByIndex (index) {
return index
}
}

View File

@@ -0,0 +1,12 @@
.mb-2.d-flex.align-items-center(*ngFor='let pair of vars')
.input-group.w-50
input.form-control([(ngModel)]='pair.key', (blur)='emitUpdate()', placeholder='Variable name')
.input-group-append
.input-group-text =
input.form-control.w-50.mr-1([(ngModel)]='pair.value', (blur)='emitUpdate()', placeholder='Value')
button.btn.btn-secondary((click)='removeEnvironmentVar(pair.key)')
i.fas.fa-trash
button.btn.btn-secondary((click)='addEnvironmentVar()')
i.fas.fa-plus.mr-2
span Add

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,45 @@
import { Component, Output, Input } from '@angular/core'
import { Subject } from 'rxjs'
@Component({
selector: 'environment-editor',
template: require('./environmentEditor.component.pug'),
styles: [require('./environmentEditor.component.scss')],
})
export class EnvironmentEditorComponent {
@Output() modelChange = new Subject<any>()
vars: {key: string, value: string}[] = []
private cachedModel: any
@Input() get model (): any {
return this.cachedModel
}
set model (value) {
this.vars = Object.entries(value).map(([k, v]) => ({ key: k, value: v as string }))
this.cachedModel = this.getModel()
}
getModel () {
let model = {}
for (let pair of this.vars) {
model[pair.key] = pair.value
}
return model
}
emitUpdate () {
this.cachedModel = this.getModel()
this.modelChange.next(this.cachedModel)
}
addEnvironmentVar () {
this.vars.push({ key: '', value: '' })
}
removeEnvironmentVar (key: string) {
this.vars = this.vars.filter(x => x.key !== key)
this.emitUpdate()
}
}

View File

@@ -14,6 +14,20 @@ h3.mb-3 Shell
[ngValue]='shell.id' [ngValue]='shell.id'
) {{shell.name}} ) {{shell.name}}
.form-line(*ngIf='isConPTYAvailable')
.header
.title Use ConPTY
.description Enables the experimental Windows ConPTY API
toggle(
[(ngModel)]='config.store.terminal.useConPTY',
(ngModelChange)='config.save()'
)
.alert.alert-info.d-flex.align-items-center(*ngIf='config.store.terminal.shell.startsWith("wsl") && (config.store.terminal.frontend != "hterm" || !config.store.terminal.useConPTY)')
.mr-auto WSL terminal only supports TrueColor with ConPTY and the hterm frontend
.form-line(*ngIf='config.store.terminal.shell == "custom"') .form-line(*ngIf='config.store.terminal.shell == "custom"')
.header .header
.title Custom shell .title Custom shell
@@ -24,20 +38,6 @@ h3.mb-3 Shell
(ngModelChange)='config.save()', (ngModelChange)='config.save()',
) )
.form-line(*ngIf='persistenceProviders.length > 0')
.header
.title Session persistence
.description Restores tabs when Terminus is restarted
select.form-control(
[(ngModel)]='config.store.terminal.persistence',
(ngModelChange)='config.save()',
)
option([ngValue]='null') Off
option(
*ngFor='let provider of persistenceProviders',
[ngValue]='provider.id'
) {{provider.displayName}}
.form-line .form-line
.header .header
.title Working directory .title Working directory
@@ -50,20 +50,31 @@ h3.mb-3 Shell
) )
.input-group-btn .input-group-btn
button.btn.btn-secondary((click)='pickWorkingDirectory()') button.btn.btn-secondary((click)='pickWorkingDirectory()')
i.fa.fa-folder-open i.fas.fa-folder-open
.form-line .form-line.align-items-start
.header .header
.title Environment .title Environment
.description Inject additional environment variables .description Inject additional environment variables
div environment-editor([(model)]='this.config.store.terminal.environment')
.mb-2.d-flex.align-items-center(*ngFor='let pair of environmentVars')
input.form-control.w-50([(ngModel)]='pair.key', (blur)='saveEnvironment()', placeholder='Variable name')
input.form-control.w-50.mr-1([(ngModel)]='pair.value', (blur)='saveEnvironment()', placeholder='Value')
button.btn.btn-secondary((click)='removeEnvironmentVar(pair.key)')
i.fa.fa-trash-o
button.btn.btn-secondary((click)='addEnvironmentVar()') h3.mt-3 Saved Profiles
i.fa.fa-plus.mr-2
span Add .list-group.list-group-flush.mt-3.mb-3
.list-group-item.list-group-item-action.d-flex.align-items-center(
*ngFor='let profile of profiles',
(click)='editProfile(profile)',
)
.mr-auto
div {{profile.name}}
.text-muted {{profile.sessionOptions.command}}
button.btn.btn-outline-danger.ml-1((click)='$event.stopPropagation(); deleteProfile(profile)')
i.fas.fa-trash
div(ngbDropdown, placement='top-left')
button.btn.btn-primary(ngbDropdownToggle)
i.fas.fa-fw.fa-plus
| New profile
div(ngbDropdownMenu)
button.dropdown-item(*ngFor='let shell of shells', (click)='newProfile(shell)') {{shell.name}}

View File

@@ -1,39 +1,50 @@
import { Component, Inject } from '@angular/core' import { Component } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { ConfigService, ElectronService } from 'terminus-core' import { ConfigService, ElectronService, HostAppService, Platform } from 'terminus-core'
import { IShell, ShellProvider, SessionPersistenceProvider } from '../api' import { EditProfileModalComponent } from './editProfileModal.component'
import { IShell, Profile } from '../api'
import { TerminalService } from '../services/terminal.service'
import { UACService } from '../services/uac.service'
@Component({ @Component({
template: require('./shellSettingsTab.component.pug'), template: require('./shellSettingsTab.component.pug'),
}) })
export class ShellSettingsTabComponent { export class ShellSettingsTabComponent {
shells: IShell[] = [] shells: IShell[] = []
persistenceProviders: SessionPersistenceProvider[] profiles: Profile[] = []
Platform = Platform
environmentVars: {key: string, value: string}[] = [] isConPTYAvailable: boolean
private configSubscription: Subscription private configSubscription: Subscription
constructor ( constructor (
uac: UACService,
public config: ConfigService, public config: ConfigService,
public hostApp: HostAppService,
private electron: ElectronService, private electron: ElectronService,
@Inject(ShellProvider) private shellProviders: ShellProvider[], private terminalService: TerminalService,
@Inject(SessionPersistenceProvider) persistenceProviders: SessionPersistenceProvider[], private ngbModal: NgbModal,
) { ) {
this.persistenceProviders = this.config.enabledServices(persistenceProviders).filter(x => x.isAvailable())
config.store.terminal.environment = config.store.terminal.environment || {} config.store.terminal.environment = config.store.terminal.environment || {}
this.reloadEnvironment() this.configSubscription = this.config.changed$.subscribe(() => {
this.configSubscription = config.changed$.subscribe(() => this.reloadEnvironment()) this.reload()
})
this.reload()
this.isConPTYAvailable = uac.isAvailable
} }
async ngOnInit () { async ngOnInit () {
this.shells = (await Promise.all(this.config.enabledServices(this.shellProviders).map(x => x.provide()))).reduce((a, b) => a.concat(b)) this.shells = await this.terminalService.shells$.toPromise()
} }
ngOnDestroy () { ngOnDestroy () {
this.configSubscription.unsubscribe() this.configSubscription.unsubscribe()
} }
reload () {
this.profiles = this.config.store.terminal.profiles
}
pickWorkingDirectory () { pickWorkingDirectory () {
let shell = this.shells.find(x => x.id === this.config.store.terminal.shell) let shell = this.shells.find(x => x.id === this.config.store.terminal.shell)
console.log(shell) console.log(shell)
@@ -46,23 +57,28 @@ export class ShellSettingsTabComponent {
} }
} }
reloadEnvironment () { newProfile (shell: IShell) {
this.environmentVars = Object.entries(this.config.store.terminal.environment).map(([k, v]) => ({ key: k, value: v as string })) let profile: Profile = {
} name: shell.name,
sessionOptions: this.terminalService.optionsFromShell(shell),
saveEnvironment () {
this.config.store.terminal.environment = {}
for (let pair of this.environmentVars) {
this.config.store.terminal.environment[pair.key] = pair.value
} }
this.profiles.push(profile)
this.config.store.terminal.profiles = this.profiles
this.config.save()
} }
addEnvironmentVar () { editProfile (profile: Profile) {
this.environmentVars.push({ key: '', value: '' }) let modal = this.ngbModal.open(EditProfileModalComponent)
modal.componentInstance.profile = Object.assign({}, profile)
modal.result.then(result => {
Object.assign(profile, result)
this.config.save()
})
} }
removeEnvironmentVar (key: string) { deleteProfile (profile: Profile) {
this.environmentVars = this.environmentVars.filter(x => x.key !== key) this.profiles = this.profiles.filter(x => x !== profile)
this.saveEnvironment() this.config.store.terminal.profiles = this.profiles
this.config.save()
} }
} }

View File

@@ -83,6 +83,15 @@ h3.mb-3 Terminal
(ngModelChange)='config.save()', (ngModelChange)='config.save()',
) )
.form-line
.header
.title Scroll on input
.description Scrolls the terminal to the bottom on user input
toggle(
[(ngModel)]='config.store.terminal.scrollOnInput',
(ngModelChange)='config.save()',
)
.form-line .form-line
.header .header
.title Use Alt key as the Meta key .title Use Alt key as the Meta key

View File

@@ -4,12 +4,10 @@ import { ToastrService } from 'ngx-toastr'
import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core' import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core'
import { AppService, ConfigService, BaseTabComponent, BaseTabProcess, ElectronService, HostAppService, HotkeysService, Platform } from 'terminus-core' import { AppService, ConfigService, BaseTabComponent, BaseTabProcess, ElectronService, HostAppService, HotkeysService, Platform } from 'terminus-core'
import { IShell } from '../api'
import { Session, SessionsService } from '../services/sessions.service' import { Session, SessionsService } from '../services/sessions.service'
import { TerminalService } from '../services/terminal.service'
import { TerminalFrontendService } from '../services/terminalFrontend.service' import { TerminalFrontendService } from '../services/terminalFrontend.service'
import { TerminalDecorator, ResizeEvent, SessionOptions } from '../api' import { TerminalDecorator, ResizeEvent, SessionOptions, TerminalContextMenuItemProvider } from '../api'
import { Frontend } from '../frontends/frontend' import { Frontend } from '../frontends/frontend'
@Component({ @Component({
@@ -33,10 +31,8 @@ export class TerminalTabComponent extends BaseTabComponent {
sessionCloseSubscription: Subscription sessionCloseSubscription: Subscription
hotkeysSubscription: Subscription hotkeysSubscription: Subscription
htermVisible = false htermVisible = false
shell: IShell
private output = new Subject<string>() private output = new Subject<string>()
private bellPlayer: HTMLAudioElement private bellPlayer: HTMLAudioElement
private contextMenu: any
private termContainerSubscriptions: Subscription[] = [] private termContainerSubscriptions: Subscription[] = []
get input$ (): Observable<string> { return this.frontend.input$ } get input$ (): Observable<string> { return this.frontend.input$ }
@@ -51,17 +47,17 @@ export class TerminalTabComponent extends BaseTabComponent {
private hotkeys: HotkeysService, private hotkeys: HotkeysService,
private sessions: SessionsService, private sessions: SessionsService,
private electron: ElectronService, private electron: ElectronService,
private terminalService: TerminalService,
private terminalContainersService: TerminalFrontendService, private terminalContainersService: TerminalFrontendService,
public config: ConfigService, public config: ConfigService,
private toastr: ToastrService, private toastr: ToastrService,
@Optional() @Inject(TerminalDecorator) private decorators: TerminalDecorator[], @Optional() @Inject(TerminalDecorator) private decorators: TerminalDecorator[],
@Optional() @Inject(TerminalContextMenuItemProvider) private contextMenuProviders: TerminalContextMenuItemProvider[],
) { ) {
super() super()
this.decorators = this.decorators || [] this.decorators = this.decorators || []
this.setTitle('Terminal') this.setTitle('Terminal')
this.session = new Session() this.session = new Session(this.config)
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => { this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
if (!this.hasFocus) { if (!this.hasFocus) {
@@ -118,6 +114,8 @@ export class TerminalTabComponent extends BaseTabComponent {
}) })
this.bellPlayer = document.createElement('audio') this.bellPlayer = document.createElement('audio')
this.bellPlayer.src = require<string>('../bell.ogg') this.bellPlayer.src = require<string>('../bell.ogg')
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
} }
initializeSession (columns: number, rows: number) { initializeSession (columns: number, rows: number) {
@@ -143,10 +141,14 @@ export class TerminalTabComponent extends BaseTabComponent {
}) })
} }
getRecoveryToken (): any { async getRecoveryToken (): Promise<any> {
let cwd = this.session ? await this.session.getWorkingDirectory() : null
return { return {
type: 'app:terminal', type: 'app:terminal-tab',
recoveryId: this.sessionOptions.recoveryId, sessionOptions: {
...this.sessionOptions,
cwd: cwd || this.sessionOptions.cwd,
},
} }
} }
@@ -162,7 +164,7 @@ export class TerminalTabComponent extends BaseTabComponent {
this.htermVisible = true this.htermVisible = true
}) })
this.frontend.resize$.pipe(first()).subscribe(async ({columns, rows}) => { this.frontend.resize$.pipe(first()).subscribe(async ({ columns, rows }) => {
if (!this.session.open) { if (!this.session.open) {
this.initializeSession(columns, rows) this.initializeSession(columns, rows)
} }
@@ -199,35 +201,17 @@ export class TerminalTabComponent extends BaseTabComponent {
} }
}) })
this.contextMenu = [ this.frontend.focus()
{ }
label: 'New terminal',
click: () => { async buildContextMenu (): Promise<Electron.MenuItemConstructorOptions[]> {
this.zone.run(() => { let items: Electron.MenuItemConstructorOptions[] = []
this.terminalService.openTab(this.shell) for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this)))) {
}) items = items.concat(section)
} items.push({ type: 'separator' })
}, }
{ items.splice(items.length - 1, 1)
label: 'Copy', return items
click: () => {
this.zone.run(() => {
setTimeout(() => {
this.frontend.copySelection()
this.toastr.info('Copied')
})
})
}
},
{
label: 'Paste',
click: () => {
this.zone.run(() => {
this.paste()
})
}
},
]
} }
detachTermContainerHandlers () { detachTermContainerHandlers () {
@@ -245,11 +229,11 @@ export class TerminalTabComponent extends BaseTabComponent {
this.focused$.subscribe(() => this.frontend.enableResizing = true), this.focused$.subscribe(() => this.frontend.enableResizing = true),
this.blurred$.subscribe(() => this.frontend.enableResizing = false), this.blurred$.subscribe(() => this.frontend.enableResizing = false),
this.frontend.mouseEvent$.subscribe(event => { this.frontend.mouseEvent$.subscribe(async event => {
if (event.type === 'mousedown') { if (event.type === 'mousedown') {
if (event.which === 3) { if (event.which === 3) {
if (this.config.store.terminal.rightClick === 'menu') { if (this.config.store.terminal.rightClick === 'menu') {
this.hostApp.popupContextMenu(this.contextMenu) this.hostApp.popupContextMenu(await this.buildContextMenu())
} else if (this.config.store.terminal.rightClick === 'paste') { } else if (this.config.store.terminal.rightClick === 'paste') {
this.paste() this.paste()
} }
@@ -259,15 +243,23 @@ export class TerminalTabComponent extends BaseTabComponent {
} }
} }
if (event.type === 'mousewheel') { if (event.type === 'mousewheel') {
let wheelDeltaY = 0
if ('wheelDeltaY' in event) {
wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY']
} else {
wheelDeltaY = (event as MouseWheelEvent)['deltaY']
}
if (event.ctrlKey || event.metaKey) { if (event.ctrlKey || event.metaKey) {
if ((event as MouseWheelEvent).wheelDeltaY > 0) {
if (wheelDeltaY > 0) {
this.zoomIn() this.zoomIn()
} else { } else {
this.zoomOut() this.zoomOut()
} }
} else if (event.altKey) { } else if (event.altKey) {
event.preventDefault() event.preventDefault()
let delta = Math.round((event as MouseWheelEvent).wheelDeltaY / 50) let delta = Math.round(wheelDeltaY / 50)
this.sendInput(((delta > 0) ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta))) this.sendInput(((delta > 0) ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
} }
} }
@@ -277,7 +269,7 @@ export class TerminalTabComponent extends BaseTabComponent {
this.sendInput(data) this.sendInput(data)
}), }),
this.frontend.resize$.subscribe(({columns, rows}) => { this.frontend.resize$.subscribe(({ columns, rows }) => {
console.log(`Resizing to ${columns}x${rows}`) console.log(`Resizing to ${columns}x${rows}`)
this.zone.run(() => { this.zone.run(() => {
if (this.session.open) { if (this.session.open) {
@@ -290,7 +282,9 @@ export class TerminalTabComponent extends BaseTabComponent {
sendInput (data: string) { sendInput (data: string) {
this.session.write(data) this.session.write(data)
this.frontend.scrollToBottom() if (this.config.store.terminal.scrollOnInput) {
this.frontend.scrollToBottom()
}
} }
write (data: string) { write (data: string) {
@@ -384,4 +378,20 @@ export class TerminalTabComponent extends BaseTabComponent {
} }
return confirm(`"${children[0].command}" is still running. Close?`) return confirm(`"${children[0].command}" is still running. Close?`)
} }
async saveAsProfile () {
let profile = {
sessionOptions: {
...this.sessionOptions,
cwd: (await this.session.getWorkingDirectory()) || this.sessionOptions.cwd,
},
name: this.sessionOptions.command,
}
this.config.store.terminal.profiles = [
...this.config.store.terminal.profiles,
profile,
]
this.config.save()
this.toastr.info('Saved')
}
} }

View File

@@ -21,6 +21,7 @@ export class TerminalConfigProvider extends ConfigProvider {
customShell: '', customShell: '',
rightClick: 'menu', rightClick: 'menu',
copyOnSelect: false, copyOnSelect: false,
scrollOnInput: true,
workingDirectory: '', workingDirectory: '',
altIsMeta: false, altIsMeta: false,
colorScheme: { colorScheme: {
@@ -50,6 +51,8 @@ export class TerminalConfigProvider extends ConfigProvider {
}, },
customColorSchemes: [], customColorSchemes: [],
environment: {}, environment: {},
profiles: [],
useConPTY: true,
}, },
} }
@@ -58,7 +61,6 @@ export class TerminalConfigProvider extends ConfigProvider {
terminal: { terminal: {
font: 'Menlo', font: 'Menlo',
shell: 'default', shell: 'default',
persistence: 'screen',
}, },
hotkeys: { hotkeys: {
'ctrl-c': ['Ctrl-C'], 'ctrl-c': ['Ctrl-C'],
@@ -82,8 +84,6 @@ export class TerminalConfigProvider extends ConfigProvider {
'⌘-0', '⌘-0',
], ],
'new-tab': [ 'new-tab': [
['Ctrl-A', 'C'],
['Ctrl-A', 'Ctrl-C'],
'⌘-T', '⌘-T',
'⌘-N', '⌘-N',
], ],
@@ -99,7 +99,6 @@ export class TerminalConfigProvider extends ConfigProvider {
terminal: { terminal: {
font: 'Consolas', font: 'Consolas',
shell: 'clink', shell: 'clink',
persistence: null,
rightClick: 'paste', rightClick: 'paste',
copyOnSelect: true, copyOnSelect: true,
}, },
@@ -126,8 +125,6 @@ export class TerminalConfigProvider extends ConfigProvider {
'Ctrl-0', 'Ctrl-0',
], ],
'new-tab': [ 'new-tab': [
['Ctrl-A', 'C'],
['Ctrl-A', 'Ctrl-C'],
'Ctrl-Shift-T', 'Ctrl-Shift-T',
], ],
'home': ['Home'], 'home': ['Home'],
@@ -142,7 +139,6 @@ export class TerminalConfigProvider extends ConfigProvider {
terminal: { terminal: {
font: 'Liberation Mono', font: 'Liberation Mono',
shell: 'default', shell: 'default',
persistence: 'tmux',
}, },
hotkeys: { hotkeys: {
'ctrl-c': ['Ctrl-C'], 'ctrl-c': ['Ctrl-C'],
@@ -167,8 +163,6 @@ export class TerminalConfigProvider extends ConfigProvider {
'Ctrl-0', 'Ctrl-0',
], ],
'new-tab': [ 'new-tab': [
['Ctrl-A', 'C'],
['Ctrl-A', 'Ctrl-C'],
'Ctrl-Shift-T', 'Ctrl-Shift-T',
], ],
'home': ['Home'], 'home': ['Home'],

View File

@@ -0,0 +1,108 @@
import { NgZone, Injectable } from '@angular/core'
import { ToastrService } from 'ngx-toastr'
import { ConfigService } from 'terminus-core'
import { UACService } from './services/uac.service'
import { TerminalService } from './services/terminal.service'
import { TerminalTabComponent } from './components/terminalTab.component'
import { TerminalContextMenuItemProvider } from './api'
@Injectable()
export class NewTabContextMenu extends TerminalContextMenuItemProvider {
weight = 0
constructor (
public config: ConfigService,
private zone: NgZone,
private terminalService: TerminalService,
private uac: UACService,
) {
super()
}
async getItems (tab: TerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
let shells = await this.terminalService.shells$.toPromise()
let items: Electron.MenuItemConstructorOptions[] = [
{
label: 'New terminal',
click: () => this.zone.run(() => {
this.terminalService.openTabWithOptions(tab.sessionOptions)
})
},
{
label: 'New with shell',
submenu: shells.map(shell => ({
label: shell.name,
click: () => this.zone.run(async () => {
this.terminalService.openTab(shell, await tab.session.getWorkingDirectory())
}),
})),
},
]
if (this.uac.isAvailable) {
items.push({
label: 'New as admin',
submenu: shells.map(shell => ({
label: shell.name,
click: () => this.zone.run(async () => {
let options = this.terminalService.optionsFromShell(shell)
options.runAsAdministrator = true
this.terminalService.openTabWithOptions(options)
}),
})),
})
}
items = items.concat([
{
label: 'New with profile',
submenu: this.config.store.terminal.profiles.length ? this.config.store.terminal.profiles.map(profile => ({
label: profile.name,
click: () => this.zone.run(() => {
this.terminalService.openTabWithOptions(profile.sessionOptions)
}),
})) : [{
label: 'No profiles saved',
enabled: false,
}],
},
])
return items
}
}
@Injectable()
export class CopyPasteContextMenu extends TerminalContextMenuItemProvider {
weight = 1
constructor (
private zone: NgZone,
private toastr: ToastrService,
) {
super()
}
async getItems (tab: TerminalTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
return [
{
label: 'Copy',
click: () => {
this.zone.run(() => {
setTimeout(() => {
tab.frontend.copySelection()
this.toastr.info('Copied')
})
})
}
},
{
label: 'Paste',
click: () => {
this.zone.run(() => tab.paste())
}
},
]
}
}

View File

@@ -66,12 +66,15 @@ export class HTermFrontend extends Frontend {
preferenceManager.set('ctrl-plus-minus-zero-zoom', false) preferenceManager.set('ctrl-plus-minus-zero-zoom', false)
preferenceManager.set('scrollbar-visible', process.platform === 'darwin') preferenceManager.set('scrollbar-visible', process.platform === 'darwin')
preferenceManager.set('copy-on-select', config.terminal.copyOnSelect) preferenceManager.set('copy-on-select', config.terminal.copyOnSelect)
preferenceManager.set('pass-meta-v', false)
preferenceManager.set('alt-is-meta', config.terminal.altIsMeta) preferenceManager.set('alt-is-meta', config.terminal.altIsMeta)
preferenceManager.set('alt-sends-what', 'browser-key') preferenceManager.set('alt-sends-what', 'browser-key')
preferenceManager.set('alt-gr-mode', 'ctrl-alt') preferenceManager.set('alt-gr-mode', 'ctrl-alt')
preferenceManager.set('pass-alt-number', true) preferenceManager.set('pass-alt-number', true)
preferenceManager.set('cursor-blink', config.terminal.cursorBlink) preferenceManager.set('cursor-blink', config.terminal.cursorBlink)
preferenceManager.set('clear-selection-after-copy', true) preferenceManager.set('clear-selection-after-copy', true)
preferenceManager.set('scroll-on-output', false)
preferenceManager.set('scroll-on-keystroke', config.terminal.scrollOnInput)
if (config.terminal.colorScheme.foreground) { if (config.terminal.colorScheme.foreground) {
preferenceManager.set('foreground-color', config.terminal.colorScheme.foreground) preferenceManager.set('foreground-color', config.terminal.colorScheme.foreground)
@@ -151,7 +154,14 @@ export class HTermFrontend extends Frontend {
} }
private setFontSize () { private setFontSize () {
preferenceManager.set('font-size', this.configuredFontSize * Math.pow(1.1, this.zoom)) let size = this.configuredFontSize * Math.pow(1.1, this.zoom)
preferenceManager.set('font-size', size)
if (this.term) {
setTimeout(() => {
this.term.scrollPort_.characterSize = this.term.scrollPort_.measureCharacterSize()
this.term.setFontSize(size)
})
}
} }
private init () { private init () {

View File

@@ -1,8 +1,8 @@
import { Frontend } from './frontend' import { Frontend } from './frontend'
import { Terminal, ITheme } from 'xterm' import { Terminal, ITheme } from '@terminus-term/xterm'
import * as fit from 'xterm/lib/addons/fit/fit' import * as fit from '@terminus-term/xterm/src/addons/fit/fit'
import * as ligatures from 'xterm-addon-ligatures-tmp' import * as ligatures from 'xterm-addon-ligatures-tmp'
import 'xterm/dist/xterm.css' import '@terminus-term/xterm/lib/xterm.css'
import './xterm.css' import './xterm.css'
import deepEqual = require('deep-equal') import deepEqual = require('deep-equal')
@@ -16,6 +16,7 @@ export class XTermFrontend extends Frontend {
private zoom = 0 private zoom = 0
private resizeHandler: any private resizeHandler: any
private configuredTheme: ITheme = {} private configuredTheme: ITheme = {}
private copyOnSelect = false
constructor () { constructor () {
super() super()
@@ -33,6 +34,11 @@ export class XTermFrontend extends Frontend {
this.xterm.on('title', title => { this.xterm.on('title', title => {
this.title.next(title) this.title.next(title)
}) })
this.xterm.on('selection', () => {
if (this.copyOnSelect) {
this.copySelection()
}
})
} }
attach (host: HTMLElement): void { attach (host: HTMLElement): void {
@@ -94,13 +100,15 @@ export class XTermFrontend extends Frontend {
this.xterm.setOption('bellStyle', config.terminal.bell) this.xterm.setOption('bellStyle', config.terminal.bell)
this.xterm.setOption('cursorStyle', { this.xterm.setOption('cursorStyle', {
beam: 'bar' beam: 'bar'
}[config.terminal.cuxrsor] || config.terminal.cursor) }[config.terminal.cursor] || config.terminal.cursor)
this.xterm.setOption('cursorBlink', config.terminal.cursorBlink) this.xterm.setOption('cursorBlink', config.terminal.cursorBlink)
this.xterm.setOption('macOptionIsMeta', config.terminal.altIsMeta) this.xterm.setOption('macOptionIsMeta', config.terminal.altIsMeta)
// this.xterm.setOption('colors', ) // this.xterm.setOption('colors', )
this.configuredFontSize = config.terminal.fontSize this.configuredFontSize = config.terminal.fontSize
this.setFontSize() this.setFontSize()
this.copyOnSelect = config.terminal.copyOnSelect
let theme: ITheme = { let theme: ITheme = {
foreground: config.terminal.colorScheme.foreground, foreground: config.terminal.colorScheme.foreground,
background: (config.terminal.background === 'colorScheme') ? config.terminal.colorScheme.background : 'transparent', background: (config.terminal.background === 'colorScheme') ? config.terminal.colorScheme.background : 'transparent',

View File

@@ -52,7 +52,6 @@ hterm.hterm.Terminal.prototype.applyCursorShape = function () {
[hterm.hterm.Terminal.cursorShape.BEAM, false], [hterm.hterm.Terminal.cursorShape.BEAM, false],
] ]
let modeNumber = this.cursorMode || 1 let modeNumber = this.cursorMode || 1
console.log('mode', modeNumber)
if (modeNumber >= modes.length) { if (modeNumber >= modes.length) {
console.warn('Unknown cursor style: ' + modeNumber) console.warn('Unknown cursor style: ' + modeNumber)
return return

View File

@@ -5,32 +5,32 @@ import { BrowserModule } from '@angular/platform-browser'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { ToastrModule } from 'ngx-toastr' import { ToastrModule } from 'ngx-toastr'
import TerminusCorePlugin from 'terminus-core'
import { HostAppService } from 'terminus-core'
import { ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, AppService, ConfigService } from 'terminus-core' import TerminusCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, AppService, ConfigService } from 'terminus-core'
import { SettingsTabProvider } from 'terminus-settings' import { SettingsTabProvider } from 'terminus-settings'
import { AppearanceSettingsTabComponent } from './components/appearanceSettingsTab.component' import { AppearanceSettingsTabComponent } from './components/appearanceSettingsTab.component'
import { ShellSettingsTabComponent } from './components/shellSettingsTab.component'
import { TerminalTabComponent } from './components/terminalTab.component' import { TerminalTabComponent } from './components/terminalTab.component'
import { ShellSettingsTabComponent } from './components/shellSettingsTab.component'
import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.component' import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.component'
import { ColorPickerComponent } from './components/colorPicker.component' import { ColorPickerComponent } from './components/colorPicker.component'
import { EditProfileModalComponent } from './components/editProfileModal.component'
import { EnvironmentEditorComponent } from './components/environmentEditor.component'
import { SessionsService, BaseSession } from './services/sessions.service' import { BaseSession } from './services/sessions.service'
import { TerminalFrontendService } from './services/terminalFrontend.service' import { TerminalFrontendService } from './services/terminalFrontend.service'
import { TerminalService } from './services/terminal.service' import { TerminalService } from './services/terminal.service'
import { DockMenuService } from './services/dockMenu.service'
import { ScreenPersistenceProvider } from './persistence/screen'
import { TMuxPersistenceProvider } from './persistence/tmux'
import { ButtonProvider } from './buttonProvider' import { ButtonProvider } from './buttonProvider'
import { RecoveryProvider } from './recoveryProvider' import { RecoveryProvider } from './recoveryProvider'
import { SessionPersistenceProvider, TerminalColorSchemeProvider, TerminalDecorator, ShellProvider } from './api' import { TerminalColorSchemeProvider, TerminalDecorator, ShellProvider, TerminalContextMenuItemProvider } from './api'
import { TerminalSettingsTabProvider, AppearanceSettingsTabProvider, ShellSettingsTabProvider } from './settings' import { TerminalSettingsTabProvider, AppearanceSettingsTabProvider, ShellSettingsTabProvider } from './settings'
import { PathDropDecorator } from './pathDrop' import { PathDropDecorator } from './pathDrop'
import { TerminalConfigProvider } from './config' import { TerminalConfigProvider } from './config'
import { TerminalHotkeyProvider } from './hotkeys' import { TerminalHotkeyProvider } from './hotkeys'
import { HyperColorSchemes } from './colorSchemes' import { HyperColorSchemes } from './colorSchemes'
import { NewTabContextMenu, CopyPasteContextMenu } from './contextMenu'
import { CmderShellProvider } from './shells/cmder' import { CmderShellProvider } from './shells/cmder'
import { CustomShellProvider } from './shells/custom' import { CustomShellProvider } from './shells/custom'
@@ -56,10 +56,6 @@ import { hterm } from './hterm'
TerminusCorePlugin, TerminusCorePlugin,
], ],
providers: [ providers: [
SessionsService,
TerminalFrontendService,
TerminalService,
{ provide: SettingsTabProvider, useClass: AppearanceSettingsTabProvider, multi: true }, { provide: SettingsTabProvider, useClass: AppearanceSettingsTabProvider, multi: true },
{ provide: SettingsTabProvider, useClass: ShellSettingsTabProvider, multi: true }, { provide: SettingsTabProvider, useClass: ShellSettingsTabProvider, multi: true },
{ provide: SettingsTabProvider, useClass: TerminalSettingsTabProvider, multi: true }, { provide: SettingsTabProvider, useClass: TerminalSettingsTabProvider, multi: true },
@@ -71,9 +67,6 @@ import { hterm } from './hterm'
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true }, { provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
{ provide: TerminalDecorator, useClass: PathDropDecorator, multi: true }, { provide: TerminalDecorator, useClass: PathDropDecorator, multi: true },
{ provide: SessionPersistenceProvider, useClass: ScreenPersistenceProvider, multi: true },
{ provide: SessionPersistenceProvider, useClass: TMuxPersistenceProvider, multi: true },
{ provide: ShellProvider, useClass: WindowsDefaultShellProvider, multi: true }, { provide: ShellProvider, useClass: WindowsDefaultShellProvider, multi: true },
{ provide: ShellProvider, useClass: MacOSDefaultShellProvider, multi: true }, { provide: ShellProvider, useClass: MacOSDefaultShellProvider, multi: true },
{ provide: ShellProvider, useClass: LinuxDefaultShellProvider, multi: true }, { provide: ShellProvider, useClass: LinuxDefaultShellProvider, multi: true },
@@ -87,6 +80,9 @@ import { hterm } from './hterm'
{ provide: ShellProvider, useClass: PowerShellCoreShellProvider, multi: true }, { provide: ShellProvider, useClass: PowerShellCoreShellProvider, multi: true },
{ provide: ShellProvider, useClass: WSLShellProvider, multi: true }, { provide: ShellProvider, useClass: WSLShellProvider, multi: true },
{ provide: TerminalContextMenuItemProvider, useClass: NewTabContextMenu, multi: true },
{ provide: TerminalContextMenuItemProvider, useClass: CopyPasteContextMenu, multi: true },
// For WindowsDefaultShellProvider // For WindowsDefaultShellProvider
PowerShellCoreShellProvider, PowerShellCoreShellProvider,
WSLShellProvider, WSLShellProvider,
@@ -97,6 +93,7 @@ import { hterm } from './hterm'
AppearanceSettingsTabComponent, AppearanceSettingsTabComponent,
ShellSettingsTabComponent, ShellSettingsTabComponent,
TerminalSettingsTabComponent, TerminalSettingsTabComponent,
EditProfileModalComponent,
], ],
declarations: [ declarations: [
ColorPickerComponent, ColorPickerComponent,
@@ -104,6 +101,12 @@ import { hterm } from './hterm'
AppearanceSettingsTabComponent, AppearanceSettingsTabComponent,
ShellSettingsTabComponent, ShellSettingsTabComponent,
TerminalSettingsTabComponent, TerminalSettingsTabComponent,
EditProfileModalComponent,
EnvironmentEditorComponent,
],
exports: [
ColorPickerComponent,
EnvironmentEditorComponent,
], ],
}) })
export default class TerminalModule { export default class TerminalModule {
@@ -113,6 +116,7 @@ export default class TerminalModule {
hotkeys: HotkeysService, hotkeys: HotkeysService,
terminal: TerminalService, terminal: TerminalService,
hostApp: HostAppService, hostApp: HostAppService,
dockMenu: DockMenuService,
) { ) {
let events = [ let events = [
{ {
@@ -159,6 +163,7 @@ export default class TerminalModule {
} }
} }
}) })
hostApp.cliOpenDirectory$.subscribe(async directory => { hostApp.cliOpenDirectory$.subscribe(async directory => {
if (await fs.exists(directory)) { if (await fs.exists(directory)) {
if ((await fs.stat(directory)).isDirectory()) { if ((await fs.stat(directory)).isDirectory()) {
@@ -167,6 +172,7 @@ export default class TerminalModule {
} }
} }
}) })
hostApp.cliRunCommand$.subscribe(async command => { hostApp.cliRunCommand$.subscribe(async command => {
terminal.openTab({ terminal.openTab({
id: '', id: '',
@@ -175,12 +181,25 @@ export default class TerminalModule {
}, null, true) }, null, true)
hostApp.bringToFront() hostApp.bringToFront()
}) })
hostApp.cliPaste$.subscribe(text => { hostApp.cliPaste$.subscribe(text => {
if (app.activeTab instanceof TerminalTabComponent && app.activeTab.session) { if (app.activeTab instanceof TerminalTabComponent && app.activeTab.session) {
(app.activeTab as TerminalTabComponent).sendInput(text) (app.activeTab as TerminalTabComponent).sendInput(text)
hostApp.bringToFront() hostApp.bringToFront()
} }
}) })
hostApp.cliOpenProfile$.subscribe(async profileName => {
let profile = config.store.terminal.profiles.find(x => x.name === profileName)
if (!profile) {
console.error('Requested profile', profileName, 'not found')
return
}
terminal.openTabWithOptions(profile.sessionOptions)
hostApp.bringToFront()
})
dockMenu.update()
} }
} }

View File

@@ -1,142 +0,0 @@
import * as fs from 'mz/fs'
import * as path from 'path'
import { exec, spawn } from 'mz/child_process'
import { exec as execAsync, execFileSync } from 'child_process'
import { AsyncSubject } from 'rxjs'
import { Injectable } from '@angular/core'
import { Logger, LogService, ElectronService } from 'terminus-core'
import { SessionOptions, SessionPersistenceProvider } from '../api'
declare function delay (ms: number): Promise<void>
interface IChildProcess {
pid: number
ppid: number
command: string
}
async function listProcesses (): Promise<IChildProcess[]> {
return (await exec(`ps -A -o pid,ppid,command`))[0].toString()
.split('\n')
.slice(1)
.map(line => line.split(' ').filter(x => x).slice(0, 3))
.map(([pid, ppid, command]) => {
return {
pid: parseInt(pid), ppid: parseInt(ppid), command
}
})
}
@Injectable()
export class ScreenPersistenceProvider extends SessionPersistenceProvider {
id = 'screen'
displayName = 'GNU Screen'
private logger: Logger
constructor (
log: LogService,
private electron: ElectronService,
) {
super()
this.logger = log.create('main')
}
isAvailable () {
try {
execFileSync('sh', ['-c', 'which screen'])
return true
} catch (_) {
return false
}
}
async attachSession (recoveryId: any): Promise<SessionOptions> {
let lines = await new Promise<string[]>(resolve => {
execAsync('screen -list', (_err, stdout) => {
// returns an error code on macOS
resolve(stdout.split('\n'))
})
})
let screenPID = lines
.filter(line => line.indexOf('.' + recoveryId) !== -1)
.map(line => parseInt(line.trim().split('.')[0]))[0]
if (!screenPID) {
return null
}
let truePID$ = new AsyncSubject<number>()
this.extractShellPID(screenPID).then(pid => {
truePID$.next(pid)
truePID$.complete()
})
return {
recoveryId,
recoveredTruePID$: truePID$.asObservable(),
command: 'screen',
args: ['-d', '-r', recoveryId, '-c', await this.prepareConfig()],
}
}
async extractShellPID (screenPID: number): Promise<number> {
let processes = await listProcesses()
let child = processes.find(x => x.ppid === screenPID)
if (!child) {
throw new Error(`Could not find any children of the screen process (PID ${screenPID})!`)
}
if (child.command === 'login') {
await delay(1000)
child = processes.find(x => x.ppid === child.pid)
}
return child.pid
}
async startSession (options: SessionOptions): Promise<any> {
let recoveryId = `term-tab-${Date.now()}`
let args = ['-d', '-m', '-c', await this.prepareConfig(), '-U', '-S', recoveryId, '-T', 'xterm-256color', '--', '-' + options.command].concat(options.args || [])
this.logger.debug('Spawning screen with', args.join(' '))
await spawn('screen', args, {
cwd: options.cwd,
env: options.env || process.env,
})
return recoveryId
}
async terminateSession (recoveryId: string): Promise<void> {
try {
await exec(`screen -S ${recoveryId} -X quit`)
} catch (_) {
// screen has already quit
}
}
private async prepareConfig (): Promise<string> {
let configPath = path.join(this.electron.app.getPath('userData'), 'screen-config.tmp')
await fs.writeFile(configPath, `
escape ^^^
vbell off
deflogin on
defflow off
term xterm-color
bindkey "^[OH" beginning-of-line
bindkey "^[OF" end-of-line
bindkey "^[[H" beginning-of-line
bindkey "^[[F" end-of-line
bindkey "\\027[?1049h" stuff ----alternate enter-----
bindkey "\\027[?1049l" stuff ----alternate leave-----
termcapinfo xterm* 'hs:ts=\\E]0;:fs=\\007:ds=\\E]0;\\007'
defhstatus "^Et"
hardstatus off
altscreen on
defutf8 on
defencoding utf8
`, 'utf-8')
return configPath
}
}

View File

@@ -1,248 +0,0 @@
import { Injectable } from '@angular/core'
import { execFileSync } from 'child_process'
import AsyncLock = require('async-lock')
import { ConnectableObservable, AsyncSubject, Subject } from 'rxjs'
import { first, publish } from 'rxjs/operators'
import * as childProcess from 'child_process'
import { Logger } from 'terminus-core'
import { SessionOptions, SessionPersistenceProvider } from '../api'
declare function delay (ms: number): Promise<void>
const TMUX_CONFIG = `
set -g status off
set -g focus-events on
set -g bell-action any
set -g bell-on-alert on
set -g visual-bell off
set -g set-titles on
set -g set-titles-string "#W"
set -g window-status-format '#I:#(pwd="#{pane_current_path}"; echo \${pwd####*/})#F'
set -g window-status-current-format '#I:#(pwd="#{pane_current_path}"; echo \${pwd####*/})#F'
set-option -g prefix C-^
set-option -g status-interval 1
`
export class TMuxBlock {
time: number
number: number
error: boolean
lines: string[]
constructor (line: string) {
this.time = parseInt(line.split(' ')[1])
this.number = parseInt(line.split(' ')[2])
this.lines = []
}
}
export class TMuxMessage {
type: string
content: string
constructor (line: string) {
this.type = line.substring(0, line.indexOf(' '))
this.content = line.substring(line.indexOf(' ') + 1)
}
}
export class TMuxCommandProcess {
private process: childProcess.ChildProcess
private rawOutput$ = new Subject<string>()
private line$ = new Subject<string>()
private message$ = new Subject<string>()
private block$ = new Subject<TMuxBlock>()
private response$: ConnectableObservable<TMuxBlock>
private lock = new AsyncLock({ timeout: 1000 })
private logger = new Logger(null, 'tmuxProcess')
constructor () {
this.process = childProcess.spawn('tmux', ['-C', '-f', '/dev/null', '-L', 'terminus', 'new-session', '-A', '-D', '-s', 'control'])
this.logger.log('started')
this.process.stdout.on('data', data => {
// console.debug('tmux says:', data.toString())
this.rawOutput$.next(data.toString())
})
let rawBuffer = ''
this.rawOutput$.subscribe(raw => {
rawBuffer += raw
if (rawBuffer.includes('\n')) {
let lines = rawBuffer.split('\n')
rawBuffer = lines.pop()
lines.forEach(line => this.line$.next(line))
}
})
let currentBlock = null
this.line$.subscribe(line => {
if (currentBlock) {
if (line.startsWith('%end ')) {
let block = currentBlock
currentBlock = null
setImmediate(() => {
this.block$.next(block)
})
} else if (line.startsWith('%error ')) {
let block = currentBlock
block.error = true
currentBlock = null
setImmediate(() => {
this.block$.next(block)
})
} else {
currentBlock.lines.push(line)
}
} else {
if (line.startsWith('%begin ')) {
currentBlock = new TMuxBlock(line)
} else {
this.message$.next(line)
}
}
})
this.response$ = this.block$.asObservable().pipe(publish()) as ConnectableObservable<TMuxBlock>
this.response$.connect()
this.block$.subscribe(block => {
this.logger.debug('block:', block)
})
this.message$.subscribe(message => {
this.logger.debug('message:', message)
})
}
command (command: string): Promise<TMuxBlock> {
return this.lock.acquire('key', () => {
let p = this.response$.pipe(first()).toPromise()
this.logger.debug('command:', command)
this.process.stdin.write(command + '\n')
return p
}).then(response => {
if (response.error) {
throw response
}
return response
}) as Promise<TMuxBlock>
}
destroy () {
this.rawOutput$.complete()
this.line$.complete()
this.block$.complete()
this.message$.complete()
this.process.kill('SIGTERM')
}
}
export class TMux {
private process: TMuxCommandProcess
private ready: Promise<void>
private logger = new Logger(null, 'tmux')
constructor () {
this.process = new TMuxCommandProcess()
this.ready = (async () => {
for (let line of TMUX_CONFIG.split('\n')) {
if (line) {
try {
await this.process.command(line)
} catch (e) {
this.logger.warn('Skipping failing config line:', line)
}
}
}
// Tmux sometimes sends a stray response block at start
await delay(500)
})()
}
async create (id: string, options: SessionOptions): Promise<void> {
await this.ready
let args = [options.command].concat(options.args.slice(1))
let cmd = args.map(x => `"${x.replace('"', '\\"')}"`).join(' ')
await this.process.command(
`new-session -s "${id}" -d`
+ (options.cwd ? ` -c '${options.cwd.replace("'", "\\'")}'` : '')
+ ` '${cmd}'`
)
}
async list (): Promise<string[]> {
await this.ready
let block = await this.process.command('list-sessions -F "#{session_name}"')
return block.lines
}
async getPID (id: string): Promise<number|null> {
await this.ready
let response = await this.process.command(`list-panes -t ${id} -F "#{pane_pid}"`)
if (response.lines.length === 0) {
return null
} else {
return parseInt(response.lines[0])
}
}
async terminate (id: string): Promise<void> {
await this.ready
this.process.command(`kill-session -t ${id}`).catch(() => {
console.debug('Session already killed')
})
}
}
@Injectable()
export class TMuxPersistenceProvider extends SessionPersistenceProvider {
id = 'tmux'
displayName = 'Tmux'
private tmux: TMux
constructor () {
super()
if (this.isAvailable()) {
this.tmux = new TMux()
}
}
isAvailable (): boolean {
try {
execFileSync('tmux', ['-V'])
return true
} catch (_) {
return false
}
}
async attachSession (recoveryId: any): Promise<SessionOptions> {
let sessions = await this.tmux.list()
if (!sessions.includes(recoveryId)) {
return null
}
let truePID$ = new AsyncSubject<number>()
this.tmux.getPID(recoveryId).then(pid => {
truePID$.next(pid)
truePID$.complete()
})
return {
command: 'tmux',
args: ['-L', 'terminus', 'attach-session', '-d', '-t', recoveryId, ';', 'refresh-client'],
recoveredTruePID$: truePID$.asObservable(),
recoveryId,
}
}
async startSession (options: SessionOptions): Promise<any> {
// TODO env
let recoveryId = Date.now().toString()
await this.tmux.create(recoveryId, options)
return recoveryId
}
async terminateSession (recoveryId: string): Promise<void> {
await this.tmux.terminate(recoveryId)
}
}

View File

@@ -2,25 +2,20 @@ import { Injectable } from '@angular/core'
import { TabRecoveryProvider, RecoveredTab } from 'terminus-core' import { TabRecoveryProvider, RecoveredTab } from 'terminus-core'
import { TerminalTabComponent } from './components/terminalTab.component' import { TerminalTabComponent } from './components/terminalTab.component'
import { SessionsService } from './services/sessions.service'
@Injectable() @Injectable()
export class RecoveryProvider extends TabRecoveryProvider { export class RecoveryProvider extends TabRecoveryProvider {
constructor ( constructor (
private sessions: SessionsService, // private sessions: SessionsService,
) { ) {
super() super()
} }
async recover (recoveryToken: any): Promise<RecoveredTab> { async recover (recoveryToken: any): Promise<RecoveredTab> {
if (recoveryToken.type === 'app:terminal') { if (recoveryToken.type === 'app:terminal-tab') {
let sessionOptions = await this.sessions.recover(recoveryToken.recoveryId)
if (!sessionOptions) {
return null
}
return { return {
type: TerminalTabComponent, type: TerminalTabComponent,
options: { sessionOptions }, options: { sessionOptions: recoveryToken.sessionOptions },
} }
} }
return null return null

View File

@@ -0,0 +1,45 @@
import { NgZone, Injectable } from '@angular/core'
import { ElectronService, ConfigService, HostAppService, Platform } from 'terminus-core'
import { TerminalService } from './terminal.service'
@Injectable({ providedIn: 'root' })
export class DockMenuService {
appVersion: string
constructor (
private electron: ElectronService,
private config: ConfigService,
private hostApp: HostAppService,
private zone: NgZone,
private terminalService: TerminalService,
) {
config.changed$.subscribe(() => this.update())
}
update () {
if (this.hostApp.platform === Platform.Windows) {
this.electron.app.setJumpList(this.config.store.terminal.profiles.length ? [{
type: 'custom',
name: 'Profiles',
items: this.config.store.terminal.profiles.map(profile => ({
type: 'task',
program: process.execPath,
args: `profile "${profile.name}"`,
title: profile.name,
iconPath: process.execPath,
iconIndex: 0,
}))
}] : null)
}
if (this.hostApp.platform === Platform.macOS) {
this.electron.app.dock.setMenu(this.electron.Menu.buildFromTemplate(
this.config.store.terminal.profiles.map(profile => ({
label: profile.name,
click: () => this.zone.run(() => {
this.terminalService.openTabWithOptions(profile.sessionOptions)
}),
}))
))
}
}
}

View File

@@ -3,11 +3,10 @@ let nodePTY
import * as fs from 'mz/fs' import * as fs from 'mz/fs'
import { Observable, Subject } from 'rxjs' import { Observable, Subject } from 'rxjs'
import { first } from 'rxjs/operators' import { first } from 'rxjs/operators'
import { Injectable, Inject } from '@angular/core' import { Injectable } from '@angular/core'
import { Logger, LogService, ConfigService } from 'terminus-core' import { Logger, LogService, ConfigService } from 'terminus-core'
import { exec } from 'mz/child_process' import { exec } from 'mz/child_process'
import { SessionOptions } from '../api'
import { SessionOptions, SessionPersistenceProvider } from '../api'
let macOSNativeProcessList let macOSNativeProcessList
try { try {
@@ -26,10 +25,11 @@ export interface IChildProcess {
command: string command: string
} }
const windowsDirectoryRegex = /([a-zA-Z]:[^\:\[\]\?\"\<\>\|]+)/mi // tslint:disable-line
export abstract class BaseSession { export abstract class BaseSession {
open: boolean open: boolean
name: string name: string
recoveryId: string
truePID: number truePID: number
protected output = new Subject<string>() protected output = new Subject<string>()
protected closed = new Subject<void>() protected closed = new Subject<void>()
@@ -77,15 +77,20 @@ export abstract class BaseSession {
export class Session extends BaseSession { export class Session extends BaseSession {
private pty: any private pty: any
private pauseAfterExit = false private pauseAfterExit = false
private guessedCWD: string
constructor (private config: ConfigService) {
super()
}
start (options: SessionOptions) { start (options: SessionOptions) {
this.name = options.name this.name = options.name
this.recoveryId = options.recoveryId
let env = { let env = {
...process.env, ...process.env,
TERM: 'xterm-256color', TERM: 'xterm-256color',
...options.env, ...options.env,
...this.config.store.terminal.environment || {},
} }
if (process.platform === 'darwin' && !process.env.LC_ALL) { if (process.platform === 'darwin' && !process.env.LC_ALL) {
@@ -105,15 +110,12 @@ export class Session extends BaseSession {
rows: options.height || 30, rows: options.height || 30,
cwd: options.cwd || process.env.HOME, cwd: options.cwd || process.env.HOME,
env: env, env: env,
experimentalUseConpty: this.config.store.terminal.useConPTY,
}) })
if (options.recoveredTruePID$) { this.guessedCWD = options.cwd || process.env.HOME
options.recoveredTruePID$.subscribe(pid => {
this.truePID = pid this.truePID = (this.pty as any).pid
})
} else {
this.truePID = (this.pty as any).pid
}
setTimeout(async () => { setTimeout(async () => {
// Retrieve any possible single children now that shell has fully started // Retrieve any possible single children now that shell has fully started
@@ -128,6 +130,9 @@ export class Session extends BaseSession {
this.pty.on('data-buffered', data => { this.pty.on('data-buffered', data => {
this.emitOutput(data) this.emitOutput(data)
if (process.platform === 'win32') {
this.guessWindowsCWD(data)
}
}) })
this.pty.on('exit', () => { this.pty.on('exit', () => {
@@ -184,7 +189,7 @@ export class Session extends BaseSession {
})) }))
} }
if (process.platform === 'win32') { if (process.platform === 'win32') {
return await new Promise<IChildProcess[]>(resolve => { return new Promise<IChildProcess[]>(resolve => {
windowsProcessTree.getProcessTree(this.truePID, tree => { windowsProcessTree.getProcessTree(this.truePID, tree => {
resolve(tree ? tree.children.map(child => ({ resolve(tree ? tree.children.map(child => ({
pid: child.pid, pid: child.pid,
@@ -244,65 +249,44 @@ export class Session extends BaseSession {
} }
} }
if (process.platform === 'linux') { if (process.platform === 'linux') {
return await fs.readlink(`/proc/${this.truePID}/cwd`) return fs.readlink(`/proc/${this.truePID}/cwd`)
}
if (process.platform === 'win32') {
return this.guessedCWD
} }
return null return null
} }
private guessWindowsCWD (data: string) {
let match = windowsDirectoryRegex.exec(data)
if (match) {
this.guessedCWD = match[0]
}
}
} }
@Injectable() @Injectable({ providedIn: 'root' })
export class SessionsService { export class SessionsService {
sessions: {[id: string]: BaseSession} = {} sessions: {[id: string]: BaseSession} = {}
logger: Logger logger: Logger
private lastID = 0 private lastID = 0
constructor ( constructor (
@Inject(SessionPersistenceProvider) private persistenceProviders: SessionPersistenceProvider[],
private config: ConfigService,
log: LogService, log: LogService,
) { ) {
nodePTY = require('node-pty-tmp') nodePTY = require('@terminus-term/node-pty')
nodePTY = require('../bufferizedPTY')(nodePTY) nodePTY = require('../bufferizedPTY')(nodePTY)
this.logger = log.create('sessions') this.logger = log.create('sessions')
this.persistenceProviders = this.config.enabledServices(this.persistenceProviders).filter(x => x.isAvailable())
}
async prepareNewSession (options: SessionOptions): Promise<SessionOptions> {
let persistence = this.getPersistence()
if (persistence) {
let recoveryId = await persistence.startSession(options)
options = await persistence.attachSession(recoveryId)
}
return options
} }
addSession (session: BaseSession, options: SessionOptions) { addSession (session: BaseSession, options: SessionOptions) {
this.lastID++ this.lastID++
options.name = `session-${this.lastID}` options.name = `session-${this.lastID}`
session.start(options) session.start(options)
let persistence = this.getPersistence()
session.destroyed$.pipe(first()).subscribe(() => { session.destroyed$.pipe(first()).subscribe(() => {
delete this.sessions[session.name] delete this.sessions[session.name]
if (persistence) {
persistence.terminateSession(session.recoveryId)
}
}) })
this.sessions[session.name] = session this.sessions[session.name] = session
return session return session
} }
async recover (recoveryId: string): Promise<SessionOptions> {
let persistence = this.getPersistence()
if (persistence) {
return await persistence.attachSession(recoveryId)
}
return null
}
private getPersistence (): SessionPersistenceProvider {
if (!this.config.store.terminal.persistence) {
return null
}
return this.persistenceProviders.find(x => x.id === this.config.store.terminal.persistence) || null
}
} }

View File

@@ -1,11 +1,11 @@
import { Observable, AsyncSubject } from 'rxjs' import { Observable, AsyncSubject } from 'rxjs'
import { Injectable, Inject } from '@angular/core' import { Injectable, Inject } from '@angular/core'
import { AppService, Logger, LogService, ConfigService } from 'terminus-core' import { AppService, Logger, LogService, ConfigService } from 'terminus-core'
import { IShell, ShellProvider } from '../api' import { IShell, ShellProvider, SessionOptions } from '../api'
import { SessionsService } from './sessions.service'
import { TerminalTabComponent } from '../components/terminalTab.component' import { TerminalTabComponent } from '../components/terminalTab.component'
import { UACService } from './uac.service'
@Injectable() @Injectable({ providedIn: 'root' })
export class TerminalService { export class TerminalService {
private shells = new AsyncSubject<IShell[]>() private shells = new AsyncSubject<IShell[]>()
private logger: Logger private logger: Logger
@@ -14,8 +14,8 @@ export class TerminalService {
constructor ( constructor (
private app: AppService, private app: AppService,
private sessions: SessionsService,
private config: ConfigService, private config: ConfigService,
private uac: UACService,
@Inject(ShellProvider) private shellProviders: ShellProvider[], @Inject(ShellProvider) private shellProviders: ShellProvider[],
log: LogService, log: LogService,
) { ) {
@@ -52,22 +52,34 @@ export class TerminalService {
let shells = await this.shells$.toPromise() let shells = await this.shells$.toPromise()
shell = shells.find(x => x.id === this.config.store.terminal.shell) || shells[0] shell = shells.find(x => x.id === this.config.store.terminal.shell) || shells[0]
} }
let env: any = Object.assign({}, process.env, shell.env || {}, this.config.store.terminal.environment || {})
this.logger.log(`Starting shell ${shell.name}`, shell) this.logger.log(`Starting shell ${shell.name}`, shell)
let sessionOptions = await this.sessions.prepareNewSession({ let sessionOptions = {
...this.optionsFromShell(shell),
pauseAfterExit: pause,
cwd,
}
return this.openTabWithOptions(sessionOptions)
}
optionsFromShell (shell: IShell): SessionOptions {
return {
command: shell.command, command: shell.command,
args: shell.args || [], args: shell.args || [],
cwd, env: shell.env,
env, }
pauseAfterExit: pause, }
})
openTabWithOptions (sessionOptions: SessionOptions): TerminalTabComponent {
if (sessionOptions.runAsAdministrator && this.uac.isAvailable) {
sessionOptions = this.uac.patchSessionOptionsForUAC(sessionOptions)
}
this.logger.log('Using session options:', sessionOptions) this.logger.log('Using session options:', sessionOptions)
return this.app.openNewTab( return this.app.openNewTab(
TerminalTabComponent, TerminalTabComponent,
{ sessionOptions, shell } { sessionOptions }
) as TerminalTabComponent ) as TerminalTabComponent
} }
} }

View File

@@ -5,7 +5,7 @@ import { HTermFrontend } from '../frontends/htermFrontend'
import { XTermFrontend } from '../frontends/xtermFrontend' import { XTermFrontend } from '../frontends/xtermFrontend'
import { BaseSession } from '../services/sessions.service' import { BaseSession } from '../services/sessions.service'
@Injectable() @Injectable({ providedIn: 'root' })
export class TerminalFrontendService { export class TerminalFrontendService {
private containers = new WeakMap<BaseSession, Frontend>() private containers = new WeakMap<BaseSession, Frontend>()

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