Merge branch 'master' of https://github.com/Eugeny/terminus
20
README.md
@ -1,7 +1,16 @@
|
||||
# Terminus α
|
||||
*A terminal for a more modern age*
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/Eugeny/terminus/master/build/icons/128x128.png">
|
||||
<h1>Terminus α</h1>
|
||||
<p>
|
||||
<i>A terminal for a more modern age</i>
|
||||
</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
[![Build Status](https://travis-ci.org/Eugeny/terminus.svg?branch=master)](https://travis-ci.org/Eugeny/terminus) [![Build status](https://ci.appveyor.com/api/projects/status/wnnq4hm5mbd9rgoy?svg=true)](https://ci.appveyor.com/project/Eugeny/terminus) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/Eugeny/terminus/master/LICENSE) [![Downloads](https://img.shields.io/badge/downloads-latest_release-brightgreen.svg)](https://github.com/Eugeny/terminus/releases/latest)
|
||||
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FEugeny%2Fterminus.svg?type=shield)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FEugeny%2Fterminus?ref=badge_shield)
|
||||
|
||||
----
|
||||
|
||||
@ -13,12 +22,12 @@
|
||||
* Theming and color schemes
|
||||
* Configurable hotkey schemes
|
||||
* **GNU Screen** style hotkeys available by default
|
||||
* Default Linux style hotkeys for Copy(`Ctrl`+`Shift`+`C`), and Paste(`Ctrl`+`Shift`+`V`)
|
||||
* Full Unicode support including double-width characters
|
||||
* Doesn't choke on fast-flowing outputs
|
||||
* Tab persistence on macOS and Linux
|
||||
* Proper shell-like experience on Windows including tab completion (thanks, Clink!)
|
||||
* CMD, PowerShell, Cygwin, Git-Bash and Bash on Windows support
|
||||
* Default Linux style hotkeys for copy (`Ctrl`+`Shift`+`C`) and paste (`Ctrl`+`Shift`+`V`)
|
||||
|
||||
---
|
||||
|
||||
@ -28,6 +37,7 @@ Plugins can be installed directly from the Settings view inside Terminus.
|
||||
|
||||
* [clickable-links](https://github.com/Eugeny/terminus-clickable-links) - makes paths and URLs in the terminal clickable
|
||||
* [theme-hype](https://github.com/Eugeny/terminus-theme-hype) - a Hyper inspired theme
|
||||
* [shell-selector](https://github.com/Eugeny/terminus-shell-selector) - a quick shell selector pane
|
||||
|
||||
---
|
||||
|
||||
@ -36,3 +46,7 @@ Plugins can be installed directly from the Settings view inside Terminus.
|
||||
Pull requests and plugins are welcome! Publish your plugin on NPM with a `terminus-plugin` keyword to make them appear in the Plugin Manager.
|
||||
|
||||
See [HACKING.md](https://github.com/Eugeny/terminus/blob/master/HACKING.md) for a very brief plugin development tutorial!
|
||||
|
||||
|
||||
## License
|
||||
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FEugeny%2Fterminus.svg?type=large)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FEugeny%2Fterminus?ref=badge_large)
|
@ -13,22 +13,9 @@ html
|
||||
app-root
|
||||
.preload-logo
|
||||
div
|
||||
.terminus-logo.animated
|
||||
.part(style='transform: rotateZ(0deg)')
|
||||
div
|
||||
.part(style='transform: rotateZ(51deg)')
|
||||
div
|
||||
.part(style='transform: rotateZ(102deg)')
|
||||
div
|
||||
.part(style='transform: rotateZ(154deg)')
|
||||
div
|
||||
.part(style='transform: rotateZ(205deg)')
|
||||
div
|
||||
.part(style='transform: rotateZ(257deg)')
|
||||
div
|
||||
.part(style='transform: rotateZ(308deg)')
|
||||
div
|
||||
.terminus-logo
|
||||
h1.terminus-title Terminus
|
||||
sup α
|
||||
.progress
|
||||
.bar(style='width: 0%')
|
||||
|
||||
|
17
app/main.js
@ -30,28 +30,18 @@ if (!process.env.TERMINUS_PLUGINS) {
|
||||
}
|
||||
|
||||
setupWindowManagement = () => {
|
||||
let windowCloseable
|
||||
|
||||
app.window.on('show', () => {
|
||||
app.window.webContents.send('host:window-shown')
|
||||
})
|
||||
|
||||
app.window.on('close', (e) => {
|
||||
windowConfig.set('windowBoundaries', app.window.getBounds())
|
||||
if (!windowCloseable) {
|
||||
app.window.minimize()
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
app.window.on('closed', () => {
|
||||
app.window = null
|
||||
})
|
||||
|
||||
electron.ipcMain.on('window-closeable', (event, flag) => {
|
||||
windowCloseable = flag
|
||||
})
|
||||
|
||||
electron.ipcMain.on('window-focus', () => {
|
||||
app.window.focus()
|
||||
})
|
||||
@ -86,6 +76,8 @@ setupWindowManagement = () => {
|
||||
|
||||
electron.ipcMain.on('window-set-bounds', (event, bounds) => {
|
||||
let actualBounds = app.window.getBounds()
|
||||
actualBounds.width -= bounds.x - actualBounds.x
|
||||
actualBounds.height -= bounds.y - actualBounds.y
|
||||
actualBounds.x = bounds.x
|
||||
actualBounds.y = bounds.y
|
||||
app.window.setBounds(actualBounds)
|
||||
@ -102,8 +94,6 @@ setupWindowManagement = () => {
|
||||
electron.ipcMain.on('window-set-always-on-top', (event, flag) => {
|
||||
app.window.setAlwaysOnTop(flag)
|
||||
})
|
||||
|
||||
app.on('before-quit', () => windowCloseable = true)
|
||||
}
|
||||
|
||||
|
||||
@ -131,7 +121,7 @@ setupMenu = () => {
|
||||
label: 'Quit',
|
||||
accelerator: 'Cmd+Q',
|
||||
click () {
|
||||
app.window.webContents.send('host:quit-request')
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -202,7 +192,6 @@ start = () => {
|
||||
let options = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
//icon: `${app.getAppPath()}/assets/img/icon.png`,
|
||||
title: 'Terminus',
|
||||
minWidth: 400,
|
||||
minHeight: 300,
|
||||
|
@ -12,14 +12,14 @@
|
||||
"watch": "webpack --progress --color --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "4.0.1",
|
||||
"@angular/common": "4.0.1",
|
||||
"@angular/compiler": "4.0.1",
|
||||
"@angular/core": "4.0.1",
|
||||
"@angular/forms": "4.0.1",
|
||||
"@angular/platform-browser": "4.0.1",
|
||||
"@angular/platform-browser-dynamic": "4.0.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.22",
|
||||
"@angular/animations": "4.3.0",
|
||||
"@angular/common": "4.3.0",
|
||||
"@angular/compiler": "4.3.0",
|
||||
"@angular/core": "4.3.0",
|
||||
"@angular/forms": "4.3.0",
|
||||
"@angular/platform-browser": "4.3.0",
|
||||
"@angular/platform-browser-dynamic": "4.3.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.28",
|
||||
"devtron": "1.4.0",
|
||||
"electron-config": "0.2.1",
|
||||
"electron-debug": "^1.0.1",
|
||||
@ -29,7 +29,7 @@
|
||||
"mz": "^2.6.0",
|
||||
"path": "0.12.7",
|
||||
"rxjs": "5.3.0",
|
||||
"zone.js": "0.8.4"
|
||||
"zone.js": "0.8.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mz": "0.0.31"
|
||||
|
@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
export async function getRootModule (plugins: any[]): Promise<any> {
|
||||
export function getRootModule (plugins: any[]) {
|
||||
let imports = [
|
||||
BrowserModule,
|
||||
...(plugins.map(x => x.default.forRoot ? x.default.forRoot() : x.default)),
|
||||
|
@ -31,3 +31,7 @@ process.on('uncaughtException', (err) => {
|
||||
Raven.captureException(err)
|
||||
console.error(err)
|
||||
})
|
||||
|
||||
const childProcess = require('child_process')
|
||||
childProcess.spawn = require('electron').remote.require('child_process').spawn
|
||||
childProcess.exec = require('electron').remote.require('child_process').exec
|
||||
|
@ -6,13 +6,13 @@ import 'rxjs'
|
||||
// Always land on the start view
|
||||
location.hash = ''
|
||||
|
||||
import { enableProdMode } from '@angular/core'
|
||||
import { enableProdMode, NgModuleRef } from '@angular/core'
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
||||
|
||||
import { getRootModule } from './app.module'
|
||||
import { findPlugins, loadPlugins } from './plugins'
|
||||
import { findPlugins, loadPlugins, IPluginInfo } from './plugins'
|
||||
|
||||
if (process.platform == 'win32') {
|
||||
if (process.platform === 'win32') {
|
||||
process.env.HOME = process.env.HOMEDRIVE + process.env.HOMEPATH
|
||||
}
|
||||
|
||||
@ -22,10 +22,29 @@ if (require('electron-is-dev')) {
|
||||
enableProdMode()
|
||||
}
|
||||
|
||||
findPlugins().then(async plugins => {
|
||||
async function bootstrap (plugins: IPluginInfo[], safeMode = false): Promise<NgModuleRef<any>> {
|
||||
if (safeMode) {
|
||||
plugins = plugins.filter(x => x.isBuiltin)
|
||||
}
|
||||
let pluginsModules = await loadPlugins(plugins, (current, total) => {
|
||||
(document.querySelector('.progress .bar') as HTMLElement).style.width = 100 * current / total + '%'
|
||||
})
|
||||
let module = await getRootModule(pluginsModules)
|
||||
platformBrowserDynamic().bootstrapModule(module)
|
||||
let module = getRootModule(pluginsModules)
|
||||
return await platformBrowserDynamic().bootstrapModule(module)
|
||||
}
|
||||
|
||||
findPlugins().then(async plugins => {
|
||||
console.log('Starting with plugins:', plugins)
|
||||
try {
|
||||
await bootstrap(plugins)
|
||||
} catch (error) {
|
||||
console.error('Angular bootstrapping error:', error)
|
||||
console.warn('Trying safe mode')
|
||||
window['safeModeReason'] = error
|
||||
try {
|
||||
await bootstrap(plugins, true)
|
||||
} catch (error) {
|
||||
console.error('Bootstrap failed:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
89
app/src/logo.svg
Normal file
@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="150mm"
|
||||
height="150mm"
|
||||
viewBox="0 0 150 150"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.1 r15371"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:export-filename="/home/eugene/Work/term/build/icons/16x16.png"
|
||||
inkscape:export-xdpi="2.7093334"
|
||||
inkscape:export-ydpi="2.7093334">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.49497475"
|
||||
inkscape:cx="134.39743"
|
||||
inkscape:cy="340.43068"
|
||||
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" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-10.356544,-82.309525)">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path138"
|
||||
style="opacity:0.9;fill:#ccccff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 39.305965,108.47713 60.922105,35.13225 0.0945,21.68327 -61.016595,-37.11662 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path116"
|
||||
style="opacity:0.9;fill:#6666cc;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 136.19445,144.4429 0.0455,20.67266 -78.028381,44.11611 -0.0031,-19.78119 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path118"
|
||||
style="opacity:0.9;fill:#ccccff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 39.471179,178.6501 18.737341,10.818 0.0031,19.78099 -18.740409,-10.88245 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="opacity:0.9;fill:#b4e2ff;fill-rule:evenodd;stroke:none;stroke-width:1.00546169px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 56.43263,98.242186 -17.391087,10.041014 61.186527,35.32618 -61.020778,35.23005 18.839694,10.87703 61.020784,-35.23005 17.39108,-10.04102 z"
|
||||
id="path134"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
@ -20,7 +20,7 @@ if (process.env.DEV) {
|
||||
nodeModule.globalPaths.unshift(path.dirname(require('electron').remote.app.getAppPath()))
|
||||
}
|
||||
|
||||
const builtinPluginsPath = path.join((process as any).resourcesPath, 'builtin-plugins')
|
||||
const builtinPluginsPath = process.env.DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
|
||||
|
||||
const userPluginsPath = path.join(
|
||||
require('electron').remote.app.getPath('appData'),
|
||||
@ -108,8 +108,9 @@ export async function findPlugins (): Promise<IPluginInfo[]> {
|
||||
continue
|
||||
}
|
||||
|
||||
if (foundPlugins.some(x => x.name === pluginName)) {
|
||||
console.info(`Plugin ${pluginName} already exists`)
|
||||
if (foundPlugins.some(x => x.name === pluginName.substring('terminus-'.length))) {
|
||||
console.info(`Plugin ${pluginName} already exists, overriding`)
|
||||
foundPlugins = foundPlugins.filter(x => x.name !== pluginName.substring('terminus-'.length))
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -1,6 +1,3 @@
|
||||
$color: rgba(66, 142, 173, 0.75);
|
||||
|
||||
|
||||
.preload-logo {
|
||||
-webkit-app-region: drag;
|
||||
position: fixed;
|
||||
@ -24,7 +21,7 @@ $color: rgba(66, 142, 173, 0.75);
|
||||
|
||||
.bar {
|
||||
transition: 1s ease-out width;
|
||||
background: $color;
|
||||
background: #a1c5e4;
|
||||
height: 3px;
|
||||
}
|
||||
}
|
||||
@ -42,63 +39,22 @@ $color: rgba(66, 142, 173, 0.75);
|
||||
.terminus-logo {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
background: url('./logo.svg');
|
||||
background-repeat: none;
|
||||
background-size: contain;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
transform: rotateZ(-14.5deg);
|
||||
|
||||
.part {
|
||||
position: absolute;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
top: 33px;
|
||||
left: 24px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: $color;
|
||||
transform: rotateX(52deg) rotateY(-42deg);
|
||||
animation: terminusLogoPartOnce ease-out 1s;
|
||||
}
|
||||
}
|
||||
|
||||
&.animated .part div {
|
||||
animation: terminusLogoPart infinite ease-out 2s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.terminus-title {
|
||||
color: $color;
|
||||
color: #a1c5e4;
|
||||
font-family: 'Source Sans Pro';
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
font-size: 42px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
@keyframes terminusLogoPart {
|
||||
0% {
|
||||
transform: rotateX(90deg) rotateY(-90deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotateX(52deg) rotateY(-42deg);
|
||||
}
|
||||
75% {
|
||||
transform: rotateX(52deg) rotateY(-42deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(-90deg) rotateY(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes terminusLogoPartOnce {
|
||||
0% {
|
||||
transform: rotateX(90deg) rotateY(-90deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(52deg) rotateY(-42deg);
|
||||
sup {
|
||||
color: #842fe0;
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ module.exports = {
|
||||
'@angular/forms': 'commonjs @angular/forms',
|
||||
'@angular/common': 'commonjs @angular/common',
|
||||
'@ng-bootstrap/ng-bootstrap': 'commonjs @ng-bootstrap/ng-bootstrap',
|
||||
'child_process': 'commonjs child_process',
|
||||
'electron': 'commonjs electron',
|
||||
'electron-is-dev': 'commonjs electron-is-dev',
|
||||
'module': 'commonjs module',
|
||||
|
@ -2,37 +2,51 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@angular/animations@4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-4.0.1.tgz#154420c8ee5c22fbaf1434b6d156150cf5218da6"
|
||||
"@angular/animations@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-4.3.0.tgz#56f34b84649379202ac359929b82eb0b915e9c72"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/common@4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/common/-/common-4.0.1.tgz#df488eada842b2d841ded750712292b18387b5b0"
|
||||
"@angular/common@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/common/-/common-4.3.0.tgz#13a54a6929dd52f9729b16ae446fad58fe163053"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/compiler@4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-4.0.1.tgz#15721edb148167a2d83b6f9324817e658eac8280"
|
||||
"@angular/compiler@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-4.3.0.tgz#55503bf27a1f062f71b9495393f3311903a8fc43"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/core@4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/core/-/core-4.0.1.tgz#0b110a001012076ea696460ccd922707bcdf51ba"
|
||||
"@angular/core@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/core/-/core-4.3.0.tgz#bd2249c3de1224a7c6536c4aba728d6565329334"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/forms@4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-4.0.1.tgz#b9ebdbbb8ace0f9a3bf9e53c299eafdfab1d5041"
|
||||
"@angular/forms@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-4.3.0.tgz#7d0c7a854737e9a30a5fd9665f8d4f56a1b91bd8"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/platform-browser-dynamic@4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-4.0.1.tgz#fd5debb2d3f6474350965e71c2674e2170d7cfcb"
|
||||
"@angular/platform-browser-dynamic@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-4.3.0.tgz#551fb18851b27ee8f3e4b0ee25aad10bd7b312e3"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@angular/platform-browser@4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-4.0.1.tgz#4b9efbeb2fbb900de188743b988802d3aa2b33ff"
|
||||
"@angular/platform-browser@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-4.3.0.tgz#02389489185185c3becf06359346100e5479c7e1"
|
||||
dependencies:
|
||||
tslib "^1.7.1"
|
||||
|
||||
"@ng-bootstrap/ng-bootstrap@1.0.0-alpha.22":
|
||||
version "1.0.0-alpha.22"
|
||||
resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-1.0.0-alpha.22.tgz#aaad058cc39293ea6184e4b9b849f298c0b11a86"
|
||||
"@ng-bootstrap/ng-bootstrap@^1.0.0-alpha.28":
|
||||
version "1.0.0-alpha.28"
|
||||
resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-1.0.0-alpha.28.tgz#30a6503bf7f94f9d3187591fb3267b59cc0cdaad"
|
||||
|
||||
"@types/mz@0.0.31":
|
||||
version "0.0.31"
|
||||
@ -41,8 +55,8 @@
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*":
|
||||
version "8.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.7.tgz#fb0ad04b5b6f6eabe0372a32a8f1fbba5c130cae"
|
||||
version "8.0.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.13.tgz#530f0f9254209b0335bf5cc6387822594ef47093"
|
||||
|
||||
accessibility-developer-tools@^2.11.0:
|
||||
version "2.12.0"
|
||||
@ -244,12 +258,16 @@ thenify-all@^1.0.0:
|
||||
dependencies:
|
||||
any-promise "^1.0.0"
|
||||
|
||||
tslib@^1.7.1:
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.7.1.tgz#bc8004164691923a79fe8378bbeb3da2017538ec"
|
||||
|
||||
util@^0.10.3:
|
||||
version "0.10.3"
|
||||
resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
|
||||
dependencies:
|
||||
inherits "2.0.1"
|
||||
|
||||
zone.js@0.8.4:
|
||||
version "0.8.4"
|
||||
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.4.tgz#cc40ae5a1c879601c5ebba2096b5c80f0c4c3602"
|
||||
zone.js@0.8.12:
|
||||
version "0.8.12"
|
||||
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.8.12.tgz#86ff5053c98aec291a0bf4bbac501d694a05cfbb"
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 655 B |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 2.4 KiB |
118
build/icons/icon.svg
Normal file
@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="150mm"
|
||||
height="150mm"
|
||||
viewBox="0 0 150 150"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.1 r15371"
|
||||
sodipodi:docname="icon.svg">
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4649">
|
||||
<stop
|
||||
style="stop-color:#000916;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop4645" />
|
||||
<stop
|
||||
style="stop-color:#004565;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop4647" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4649"
|
||||
id="linearGradient4651"
|
||||
x1="89.26284"
|
||||
y1="85.146751"
|
||||
x2="89.26284"
|
||||
y2="229.47229"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.49497475"
|
||||
inkscape:cx="134.39743"
|
||||
inkscape:cy="340.43068"
|
||||
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" />
|
||||
<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="150"
|
||||
height="150"
|
||||
x="10.356544"
|
||||
y="82.309525"
|
||||
style="fill:url(#linearGradient4651);fill-opacity:1;stroke-width:0.26458332"
|
||||
rx="10"
|
||||
ry="10" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path138"
|
||||
style="opacity:0.9;fill:#ccccff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 39.305965,108.47713 60.922105,35.13225 0.0945,21.68327 -61.016595,-37.11662 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path116"
|
||||
style="opacity:0.9;fill:#6666cc;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 136.19445,144.4429 0.0455,20.67266 -78.028381,44.11611 -0.0031,-19.78119 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path118"
|
||||
style="opacity:0.9;fill:#ccccff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 39.471179,178.6501 18.737341,10.818 0.0031,19.78099 -18.740409,-10.88245 z"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
<path
|
||||
style="opacity:0.9;fill:#b4e2ff;fill-rule:evenodd;stroke:none;stroke-width:1.00546169px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 56.43263,98.242186 -17.391087,10.041014 61.186527,35.32618 -61.020778,35.23005 18.839694,10.87703 61.020784,-35.23005 17.39108,-10.04102 z"
|
||||
id="path134"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 361 KiB |
BIN
docs/linux.png
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 138 KiB |
@ -74,7 +74,8 @@
|
||||
"libnotify4",
|
||||
"libappindicator1",
|
||||
"libxtst6",
|
||||
"libnss3"
|
||||
"libnss3",
|
||||
"tmux"
|
||||
]
|
||||
},
|
||||
"rpm": {
|
||||
|
@ -5,21 +5,18 @@ const vars = require('./vars')
|
||||
const log = require('npmlog')
|
||||
|
||||
log.info('deps', 'app')
|
||||
sh.exec('npm prune')
|
||||
sh.exec('npm install')
|
||||
sh.exec('npm update --dev')
|
||||
sh.exec('yarn prune')
|
||||
sh.exec('yarn install')
|
||||
|
||||
sh.cd('app')
|
||||
sh.exec('npm prune')
|
||||
sh.exec('npm install')
|
||||
sh.exec('npm update --dev')
|
||||
sh.exec('yarn prune')
|
||||
sh.exec('yarn install')
|
||||
sh.cd('..')
|
||||
|
||||
vars.builtinPlugins.forEach(plugin => {
|
||||
log.info('deps', plugin)
|
||||
sh.cd(plugin)
|
||||
sh.exec('npm prune')
|
||||
sh.exec('npm install')
|
||||
sh.exec('npm update --dev')
|
||||
sh.exec('yarn prune')
|
||||
sh.exec('yarn install')
|
||||
sh.cd('..')
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-community-color-schemes",
|
||||
"version": "1.0.0-alpha.16-8-gfc060ac",
|
||||
"version": "1.0.0-alpha.24",
|
||||
"description": "Community color schemes for Terminus",
|
||||
"keywords": [
|
||||
"terminus-plugin"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-core",
|
||||
"version": "1.0.0-alpha.16-8-gfc060ac",
|
||||
"version": "1.0.0-alpha.24",
|
||||
"description": "Terminus core",
|
||||
"keywords": [
|
||||
"terminus-plugin"
|
||||
@ -17,14 +17,13 @@
|
||||
"author": "Eugene Pankov",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.22",
|
||||
"@types/js-yaml": "^3.5.29",
|
||||
"@types/node": "^7.0.12",
|
||||
"@types/js-yaml": "^3.9.0",
|
||||
"@types/node": "^7.0.37",
|
||||
"@types/webpack-env": "^1.13.0",
|
||||
"bootstrap": "4.0.0-alpha.6",
|
||||
"core-js": "^2.4.1",
|
||||
"ngx-perfect-scrollbar": "4.0.0",
|
||||
"typescript": "^2.4.0"
|
||||
"typescript": "^2.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "4.0.1",
|
||||
@ -37,8 +36,8 @@
|
||||
"zone.js": "0.8.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": "^1.4.4",
|
||||
"js-yaml": "^3.8.4"
|
||||
"deepmerge": "^1.5.0",
|
||||
"js-yaml": "^3.9.0"
|
||||
},
|
||||
"false": {}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
export { BaseTabComponent } from '../components/baseTab.component'
|
||||
export { TabRecoveryProvider } from './tabRecovery'
|
||||
export { TabRecoveryProvider, RecoveredTab } from './tabRecovery'
|
||||
export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
|
||||
export { ConfigProvider } from './configProvider'
|
||||
export { HotkeyProvider, IHotkeyDescription } from './hotkeyProvider'
|
||||
|
@ -1,3 +1,10 @@
|
||||
export abstract class TabRecoveryProvider {
|
||||
abstract async recover (recoveryToken: any): Promise<void>
|
||||
import { TabComponentType } from '../services/app.service'
|
||||
|
||||
export interface RecoveredTab {
|
||||
type: TabComponentType,
|
||||
options?: any,
|
||||
}
|
||||
|
||||
export abstract class TabRecoveryProvider {
|
||||
abstract async recover (recoveryToken: any): Promise<RecoveredTab|null>
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ title-bar(
|
||||
[class.drag-region]='hostApp.platform == Platform.macOS',
|
||||
@animateTab,
|
||||
(click)='app.selectTab(tab)',
|
||||
(closeClicked)='app.closeTab(tab)',
|
||||
(closeClicked)='app.closeTab(tab, true)',
|
||||
)
|
||||
|
||||
.btn-group
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { Component, Inject, Input, HostListener } from '@angular/core'
|
||||
import { trigger, style, animate, transition, state } from '@angular/animations'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
import { ElectronService } from '../services/electron.service'
|
||||
import { HostAppService, Platform } from '../services/hostApp.service'
|
||||
import { HotkeysService } from '../services/hotkeys.service'
|
||||
import { Logger, LogService } from '../services/log.service'
|
||||
import { QuitterService } from '../services/quitter.service'
|
||||
import { ConfigService } from '../services/config.service'
|
||||
import { DockingService } from '../services/docking.service'
|
||||
import { TabRecoveryService } from '../services/tabRecovery.service'
|
||||
import { ThemesService } from '../services/themes.service'
|
||||
|
||||
import { SafeModeModalComponent } from './safeModeModal.component'
|
||||
import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
|
||||
@Component({
|
||||
@ -28,9 +29,16 @@ import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
'flex-basis': '1px',
|
||||
'width': '1px',
|
||||
}),
|
||||
animate('250ms ease-in-out')
|
||||
animate('250ms ease-in-out', style({
|
||||
'flex-basis': '200px',
|
||||
'width': '200px',
|
||||
}))
|
||||
]),
|
||||
transition(':leave', [
|
||||
style({
|
||||
'flex-basis': '200px',
|
||||
'width': '200px',
|
||||
}),
|
||||
animate('250ms ease-in-out', style({
|
||||
'flex-basis': '1px',
|
||||
'width': '1px',
|
||||
@ -56,8 +64,8 @@ export class AppRootComponent {
|
||||
public app: AppService,
|
||||
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
|
||||
log: LogService,
|
||||
ngbModal: NgbModal,
|
||||
_themes: ThemesService,
|
||||
_quitter: QuitterService,
|
||||
) {
|
||||
this.logger = log.create('main')
|
||||
this.logger.info('v', electron.app.getVersion())
|
||||
@ -74,7 +82,7 @@ export class AppRootComponent {
|
||||
}
|
||||
if (this.app.activeTab) {
|
||||
if (hotkey === 'close-tab') {
|
||||
this.app.closeTab(this.app.activeTab)
|
||||
this.app.closeTab(this.app.activeTab, true)
|
||||
}
|
||||
if (hotkey === 'toggle-last-tab') {
|
||||
this.app.toggleLastTab()
|
||||
@ -99,6 +107,10 @@ export class AppRootComponent {
|
||||
this.hotkeys.globalHotkey.subscribe(() => {
|
||||
this.onGlobalHotkey()
|
||||
})
|
||||
|
||||
if (window['safeModeReason']) {
|
||||
ngbModal.open(SafeModeModalComponent)
|
||||
}
|
||||
}
|
||||
|
||||
onGlobalHotkey () {
|
||||
@ -133,16 +145,6 @@ export class AppRootComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private getToolbarButtons (aboveZero: boolean): IToolbarButton[] {
|
||||
let buttons: IToolbarButton[] = []
|
||||
this.toolbarButtonProviders.forEach((provider) => {
|
||||
buttons = buttons.concat(provider.provide())
|
||||
})
|
||||
return buttons
|
||||
.filter((button) => (button.weight > 0) === aboveZero)
|
||||
.sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0))
|
||||
}
|
||||
|
||||
@HostListener('dragover')
|
||||
onDragOver () {
|
||||
return false
|
||||
@ -152,4 +154,14 @@ export class AppRootComponent {
|
||||
onDrop () {
|
||||
return false
|
||||
}
|
||||
|
||||
private getToolbarButtons (aboveZero: boolean): IToolbarButton[] {
|
||||
let buttons: IToolbarButton[] = []
|
||||
this.toolbarButtonProviders.forEach((provider) => {
|
||||
buttons = buttons.concat(provider.provide())
|
||||
})
|
||||
return buttons
|
||||
.filter((button) => (button.weight > 0) === aboveZero)
|
||||
.sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0))
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Subject, BehaviorSubject } from 'rxjs'
|
||||
import { Subject } from 'rxjs'
|
||||
import { ViewRef } from '@angular/core'
|
||||
|
||||
export abstract class BaseTabComponent {
|
||||
private static lastTabID = 0
|
||||
id: number
|
||||
title$ = new BehaviorSubject<string>(null)
|
||||
title: string
|
||||
customTitle: string
|
||||
scrollable: boolean
|
||||
hasActivity = false
|
||||
focused$ = new Subject<void>()
|
||||
@ -30,9 +31,12 @@ export abstract class BaseTabComponent {
|
||||
return null
|
||||
}
|
||||
|
||||
async canClose (): Promise<boolean> {
|
||||
return true
|
||||
}
|
||||
|
||||
destroy (): void {
|
||||
this.focused$.complete()
|
||||
this.blurred$.complete()
|
||||
this.title$.complete()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
.modal-body
|
||||
input.form-control(type='text', [(ngModel)]='value', (keyup.enter)='save()', autofocus)
|
||||
|
||||
.modal-footer
|
||||
button.btn.btn-outline-primary((click)='save()') Save
|
||||
button.btn.btn-outline-secondary((click)='close()') Cancel
|
22
terminus-core/src/components/renameTabModal.component.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
@Component({
|
||||
selector: 'rename-tab-modal',
|
||||
template: require('./renameTabModal.component.pug'),
|
||||
})
|
||||
export class RenameTabModalComponent {
|
||||
@Input() value: string
|
||||
|
||||
constructor (
|
||||
private modalInstance: NgbActiveModal
|
||||
) { }
|
||||
|
||||
save () {
|
||||
this.modalInstance.close(this.value)
|
||||
}
|
||||
|
||||
close () {
|
||||
this.modalInstance.dismiss()
|
||||
}
|
||||
}
|
7
terminus-core/src/components/safeModeModal.component.pug
Normal file
@ -0,0 +1,7 @@
|
||||
.modal-body
|
||||
.alert.alert-danger Terminus could not start with your plugins, so all third party plugins have been disabled in this session. The error was:
|
||||
|
||||
pre {{error}}
|
||||
|
||||
.modal-footer
|
||||
button.btn.btn-outline-primary((click)='close()') Close
|
19
terminus-core/src/components/safeModeModal.component.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
@Component({
|
||||
template: require('./safeModeModal.component.pug'),
|
||||
})
|
||||
export class SafeModeModalComponent {
|
||||
@Input() error: Error
|
||||
|
||||
constructor (
|
||||
public modalInstance: NgbActiveModal,
|
||||
) {
|
||||
this.error = window['safeModeReason']
|
||||
}
|
||||
|
||||
close () {
|
||||
this.modalInstance.dismiss()
|
||||
}
|
||||
}
|
@ -1,28 +1,15 @@
|
||||
div
|
||||
.terminus-logo
|
||||
.part(style='transform: rotateZ(0deg)')
|
||||
div
|
||||
.part(style='transform: rotateZ(51deg)')
|
||||
div
|
||||
.part(style='transform: rotateZ(102deg)')
|
||||
div
|
||||
.part(style='transform: rotateZ(154deg)')
|
||||
div
|
||||
.part(style='transform: rotateZ(205deg)')
|
||||
div
|
||||
.part(style='transform: rotateZ(257deg)')
|
||||
div
|
||||
.part(style='transform: rotateZ(308deg)')
|
||||
div
|
||||
h1.terminus-title Terminus
|
||||
span.text-muted α
|
||||
sup α
|
||||
|
||||
button.btn.btn-primary.btn-lg.btn-block(
|
||||
*ngFor='let button of getButtons()',
|
||||
(click)='button.click()',
|
||||
)
|
||||
i.fa([class]='"fa fa-" + button.icon')
|
||||
span {{button.title}}
|
||||
.list-group
|
||||
a.list-group-item.list-group-item-action(
|
||||
*ngFor='let button of getButtons()',
|
||||
(click)='button.click()',
|
||||
)
|
||||
i([class]='"fa fa-fw fa-" + button.icon')
|
||||
span {{button.title}}
|
||||
|
||||
footer
|
||||
.pull-right
|
||||
|
@ -24,6 +24,6 @@ footer {
|
||||
background: rgba(0,0,0,.5);
|
||||
}
|
||||
|
||||
button {
|
||||
a, button {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
.index {{index + 1}}
|
||||
.name {{tab.title$ | async}}
|
||||
.name([title]='tab.customTitle || tab.title') {{tab.customTitle || tab.title}}
|
||||
button((click)='closeClicked.emit()') ×
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { Component, Input, Output, EventEmitter, HostBinding, HostListener } from '@angular/core'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { RenameTabModalComponent } from './renameTabModal.component'
|
||||
|
||||
@Component({
|
||||
selector: 'tab-header',
|
||||
@ -13,8 +15,20 @@ export class TabHeaderComponent {
|
||||
@Input() tab: BaseTabComponent
|
||||
@Output() closeClicked = new EventEmitter()
|
||||
|
||||
@HostListener('auxclick', ['$event']) onClick ($event: MouseEvent): void {
|
||||
if ($event.which == 2) {
|
||||
constructor (
|
||||
private ngbModal: NgbModal,
|
||||
) { }
|
||||
|
||||
@HostListener('dblclick') onDoubleClick (): void {
|
||||
let modal = this.ngbModal.open(RenameTabModalComponent)
|
||||
modal.componentInstance.value = this.tab.customTitle || this.tab.title
|
||||
modal.result.then(result => {
|
||||
this.tab.customTitle = result
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
||||
@HostListener('auxclick', ['$event']) onAuxClick ($event: MouseEvent): void {
|
||||
if ($event.which === 2) {
|
||||
this.closeClicked.emit()
|
||||
}
|
||||
}
|
||||
|
@ -11,17 +11,18 @@ import { ElectronService } from './services/electron.service'
|
||||
import { HostAppService } from './services/hostApp.service'
|
||||
import { LogService } from './services/log.service'
|
||||
import { HotkeysService, AppHotkeyProvider } from './services/hotkeys.service'
|
||||
import { QuitterService } from './services/quitter.service'
|
||||
import { DockingService } from './services/docking.service'
|
||||
import { TabRecoveryService } from './services/tabRecovery.service'
|
||||
import { ThemesService } from './services/themes.service'
|
||||
|
||||
import { AppRootComponent } from './components/appRoot.component'
|
||||
import { TabBodyComponent } from './components/tabBody.component'
|
||||
import { SafeModeModalComponent } from './components/safeModeModal.component'
|
||||
import { StartPageComponent } from './components/startPage.component'
|
||||
import { TabHeaderComponent } from './components/tabHeader.component'
|
||||
import { TitleBarComponent } from './components/titleBar.component'
|
||||
import { WindowControlsComponent } from './components/windowControls.component'
|
||||
import { RenameTabModalComponent } from './components/renameTabModal.component'
|
||||
|
||||
import { HotkeyProvider } from './api/hotkeyProvider'
|
||||
import { ConfigProvider } from './api/configProvider'
|
||||
@ -42,7 +43,6 @@ const PROVIDERS = [
|
||||
LogService,
|
||||
TabRecoveryService,
|
||||
ThemesService,
|
||||
QuitterService,
|
||||
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
|
||||
{ provide: Theme, useClass: StandardTheme, multi: true },
|
||||
{ provide: ConfigProvider, useClass: CoreConfigProvider, multi: true },
|
||||
@ -65,7 +65,13 @@ const PROVIDERS = [
|
||||
TabHeaderComponent,
|
||||
TitleBarComponent,
|
||||
WindowControlsComponent,
|
||||
RenameTabModalComponent,
|
||||
SafeModeModalComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
RenameTabModalComponent,
|
||||
SafeModeModalComponent,
|
||||
]
|
||||
})
|
||||
export default class AppModule {
|
||||
static forRoot (): ModuleWithProviders {
|
||||
|
@ -82,10 +82,16 @@ export class AppService {
|
||||
}
|
||||
}
|
||||
|
||||
closeTab (tab: BaseTabComponent) {
|
||||
async closeTab (tab: BaseTabComponent, checkCanClose?: boolean): Promise<void> {
|
||||
if (!this.tabs.includes(tab)) {
|
||||
return
|
||||
}
|
||||
if (checkCanClose && !await tab.canClose()) {
|
||||
return
|
||||
}
|
||||
this.tabs = this.tabs.filter((x) => x !== tab)
|
||||
tab.destroy()
|
||||
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
||||
this.tabs = this.tabs.filter((x) => x !== tab)
|
||||
if (tab === this.activeTab) {
|
||||
this.selectTab(this.tabs[newIndex])
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ export class ConfigProxy {
|
||||
{
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
get: () => real[key] || defaults[key],
|
||||
get: () => (real[key] !== undefined) ? real[key] : defaults[key],
|
||||
set: (value) => {
|
||||
real[key] = value
|
||||
}
|
||||
|
@ -40,12 +40,12 @@ export class DockingService {
|
||||
newBounds.height = Math.round(fill * display.bounds.height)
|
||||
}
|
||||
if (dockSide === 'right') {
|
||||
newBounds.x = display.bounds.x + Math.round(display.bounds.width * (1.0 - fill))
|
||||
newBounds.x = display.bounds.x + display.bounds.width - newBounds.width
|
||||
} else {
|
||||
newBounds.x = display.bounds.x
|
||||
}
|
||||
if (dockSide === 'bottom') {
|
||||
newBounds.y = display.bounds.y + Math.round(display.bounds.height * (1.0 - fill))
|
||||
newBounds.y = display.bounds.y + display.bounds.height - newBounds.height
|
||||
} else {
|
||||
newBounds.y = display.bounds.y
|
||||
}
|
||||
|
@ -27,4 +27,8 @@ export class ElectronService {
|
||||
remoteRequire (name: string): any {
|
||||
return this.remote.require(name)
|
||||
}
|
||||
|
||||
remoteRequirePluginModule (plugin: string, module: string, globals: any): any {
|
||||
return this.remoteRequire(globals.require.resolve(`${plugin}/node_modules/${module}`))
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ export interface Bounds {
|
||||
export class HostAppService {
|
||||
platform: Platform
|
||||
nodePlatform: string
|
||||
quitRequested = new EventEmitter<any>()
|
||||
preferencesMenu$ = new Subject<void>()
|
||||
ready = new EventEmitter<any>()
|
||||
shown = new EventEmitter<any>()
|
||||
@ -39,7 +38,6 @@ export class HostAppService {
|
||||
linux: Platform.Linux
|
||||
}[this.nodePlatform]
|
||||
|
||||
electron.ipcRenderer.on('host:quit-request', () => this.zone.run(() => this.quitRequested.emit()))
|
||||
electron.ipcRenderer.on('host:preferences-menu', () => this.zone.run(() => this.preferencesMenu$.next()))
|
||||
|
||||
electron.ipcRenderer.on('uncaughtException', ($event, err) => {
|
||||
@ -79,10 +77,6 @@ export class HostAppService {
|
||||
this.getWindow().webContents.openDevTools()
|
||||
}
|
||||
|
||||
setCloseable (flag: boolean) {
|
||||
this.electron.ipcRenderer.send('window-set-closeable', flag)
|
||||
}
|
||||
|
||||
focusWindow () {
|
||||
this.electron.ipcRenderer.send('window-focus')
|
||||
}
|
||||
|
@ -5,14 +5,15 @@ export class Logger {
|
||||
private name: string,
|
||||
) {}
|
||||
|
||||
log (level: string, ...args: any[]) {
|
||||
doLog (level: string, ...args: any[]) {
|
||||
console[level](`%c[${this.name}]`, 'color: #aaa', ...args)
|
||||
}
|
||||
|
||||
debug (...args: any[]) { this.log('debug', ...args) }
|
||||
info (...args: any[]) { this.log('info', ...args) }
|
||||
warn (...args: any[]) { this.log('warn', ...args) }
|
||||
error (...args: any[]) { this.log('error', ...args) }
|
||||
debug (...args: any[]) { this.doLog('debug', ...args) }
|
||||
info (...args: any[]) { this.doLog('info', ...args) }
|
||||
warn (...args: any[]) { this.doLog('warn', ...args) }
|
||||
error (...args: any[]) { this.doLog('error', ...args) }
|
||||
log (...args: any[]) { this.doLog('log', ...args) }
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
|
@ -1,18 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HostAppService } from '../services/hostApp.service'
|
||||
|
||||
@Injectable()
|
||||
export class QuitterService {
|
||||
constructor (
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
hostApp.quitRequested.subscribe(() => {
|
||||
this.quit()
|
||||
})
|
||||
}
|
||||
|
||||
quit () {
|
||||
this.hostApp.setCloseable(true)
|
||||
this.hostApp.quit()
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
import { TabRecoveryProvider } from '../api/tabRecovery'
|
||||
import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { Logger, LogService } from '../services/log.service'
|
||||
import { AppService } from '../services/app.service'
|
||||
@ -10,7 +10,7 @@ export class TabRecoveryService {
|
||||
|
||||
constructor (
|
||||
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[],
|
||||
app: AppService,
|
||||
private app: AppService,
|
||||
log: LogService
|
||||
) {
|
||||
this.logger = log.create('tabRecovery')
|
||||
@ -29,15 +29,22 @@ export class TabRecoveryService {
|
||||
|
||||
async recoverTabs (): Promise<void> {
|
||||
if (window.localStorage.tabsRecovery) {
|
||||
let tabs: RecoveredTab[] = []
|
||||
for (let token of JSON.parse(window.localStorage.tabsRecovery)) {
|
||||
for (let provider of this.tabRecoveryProviders) {
|
||||
try {
|
||||
await provider.recover(token)
|
||||
let tab = await provider.recover(token)
|
||||
if (tab) {
|
||||
tabs.push(tab)
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn('Tab recovery crashed:', token, provider, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
tabs.forEach(tab => {
|
||||
this.app.openNewTab(tab.type, tab.options)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,6 @@ export class ThemesService {
|
||||
this.applyCurrentTheme()
|
||||
config.changed$.subscribe(() => {
|
||||
this.applyCurrentTheme()
|
||||
document.querySelector('style#custom-css').innerHTML = config.store.appearance.css
|
||||
})
|
||||
}
|
||||
|
||||
@ -32,6 +31,7 @@ export class ThemesService {
|
||||
document.querySelector('head').appendChild(this.styleElement)
|
||||
}
|
||||
this.styleElement.textContent = theme.css
|
||||
document.querySelector('style#custom-css').innerHTML = this.config.store.appearance.css
|
||||
}
|
||||
|
||||
applyCurrentTheme (): void {
|
||||
|
@ -23,6 +23,7 @@ $body-color: #aaa;
|
||||
$font-family-sans-serif: "Source Sans Pro";
|
||||
$font-size-base: 14rem / 16;
|
||||
|
||||
$btn-border-radius: 0;
|
||||
$btn-secondary-color: #ccc;
|
||||
$btn-secondary-bg: #222;
|
||||
$btn-secondary-border: #444;
|
||||
@ -70,7 +71,18 @@ $dropdown-link-disabled-color: #333;
|
||||
$dropdown-header-color: #333;
|
||||
|
||||
$list-group-color: $body-color;
|
||||
$list-group-bg: $body-bg2;
|
||||
$list-group-bg: rgba(255,255,255,.05);
|
||||
$list-group-border-color: rgba(255,255,255,.1);
|
||||
$list-group-hover-bg: rgba(255,255,255,.1);
|
||||
$list-group-link-active-bg: rgba(255,255,255,.2);
|
||||
|
||||
$pre-bg: $dropdown-bg;
|
||||
$pre-color: $dropdown-link-color;
|
||||
|
||||
$alert-danger-bg: $body-bg2;
|
||||
$alert-danger-text: $red;
|
||||
$alert-danger-border: $red;
|
||||
|
||||
|
||||
@import '~bootstrap/scss/bootstrap.scss';
|
||||
|
||||
@ -270,12 +282,6 @@ hotkey-input-modal {
|
||||
}
|
||||
}
|
||||
|
||||
start-page {
|
||||
.terminus-title {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
@ -313,3 +319,11 @@ ngb-tabset .tab-content {
|
||||
.input-group > select.form-control {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
transition: 0.25s background;
|
||||
|
||||
i + * {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-plugin-manager",
|
||||
"version": "1.0.0-alpha.16-8-gfc060ac",
|
||||
"version": "1.0.0-alpha.24",
|
||||
"description": "Terminus' plugin manager",
|
||||
"keywords": [
|
||||
"terminus-plugin"
|
||||
@ -19,10 +19,10 @@
|
||||
"devDependencies": {
|
||||
"@types/mz": "0.0.31",
|
||||
"@types/node": "7.0.12",
|
||||
"@types/semver": "^5.3.31",
|
||||
"@types/semver": "^5.3.32",
|
||||
"@types/webpack-env": "1.13.0",
|
||||
"ngx-pipes": "^1.6.1",
|
||||
"css-loader": "^0.28.0",
|
||||
"ngx-pipes": "^1.6.1",
|
||||
"semver": "^5.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-settings",
|
||||
"version": "1.0.0-alpha.16-8-gfc060ac",
|
||||
"version": "1.0.0-alpha.24",
|
||||
"description": "Terminus terminal settings page",
|
||||
"keywords": [
|
||||
"terminus-plugin"
|
||||
|
@ -2,9 +2,9 @@ button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(clic
|
||||
|
||||
ngb-tabset.vertical(type='tabs')
|
||||
ngb-tab
|
||||
template(ngbTabTitle)
|
||||
ng-template(ngbTabTitle)
|
||||
| Application
|
||||
template(ngbTabContent)
|
||||
ng-template(ngbTabContent)
|
||||
.row
|
||||
.col.col-lg-6
|
||||
.form-group
|
||||
@ -153,9 +153,9 @@ ngb-tabset.vertical(type='tabs')
|
||||
)
|
||||
|
||||
ngb-tab
|
||||
template(ngbTabTitle)
|
||||
ng-template(ngbTabTitle)
|
||||
| Hotkeys
|
||||
template(ngbTabContent)
|
||||
ng-template(ngbTabContent)
|
||||
input.form-control(type='search', placeholder='Search hotkeys', [(ngModel)]='hotkeyFilter')
|
||||
.form-group
|
||||
table.hotkeys-table
|
||||
@ -173,7 +173,7 @@ ngb-tabset.vertical(type='tabs')
|
||||
)
|
||||
|
||||
ngb-tab(*ngFor='let provider of settingsProviders')
|
||||
template(ngbTabTitle)
|
||||
ng-template(ngbTabTitle)
|
||||
| {{provider.title}}
|
||||
template(ngbTabContent)
|
||||
ng-template(ngbTabContent)
|
||||
settings-tab-body([provider]='provider')
|
||||
|
@ -27,7 +27,7 @@ export class SettingsTabComponent extends BaseTabComponent {
|
||||
) {
|
||||
super()
|
||||
this.hotkeyDescriptions = hotkeyProviders.map(x => x.hotkeys).reduce((a, b) => a.concat(b))
|
||||
this.title$.next('Settings')
|
||||
this.title = 'Settings'
|
||||
this.scrollable = true
|
||||
this.screens = this.docking.getScreens()
|
||||
}
|
||||
|
@ -1,19 +1,14 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TabRecoveryProvider, AppService } from 'terminus-core'
|
||||
import { TabRecoveryProvider, RecoveredTab } from 'terminus-core'
|
||||
|
||||
import { SettingsTabComponent } from './components/settingsTab.component'
|
||||
|
||||
@Injectable()
|
||||
export class RecoveryProvider extends TabRecoveryProvider {
|
||||
constructor (
|
||||
private app: AppService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async recover (recoveryToken: any): Promise<void> {
|
||||
async recover (recoveryToken: any): Promise<RecoveredTab> {
|
||||
if (recoveryToken.type === 'app:settings') {
|
||||
this.app.openNewTab(SettingsTabComponent)
|
||||
return { type: SettingsTabComponent }
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-terminal",
|
||||
"version": "1.0.0-alpha.16-8-gfc060ac",
|
||||
"version": "1.0.0-alpha.24",
|
||||
"description": "Terminus' terminal emulation core",
|
||||
"keywords": [
|
||||
"terminus-plugin"
|
||||
@ -23,7 +23,8 @@
|
||||
"@types/webpack-env": "1.13.0",
|
||||
"@types/winreg": "^1.2.30",
|
||||
"dataurl": "0.1.0",
|
||||
"deep-equal": "1.0.1"
|
||||
"deep-equal": "1.0.1",
|
||||
"file-loader": "^0.11.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "4.0.1",
|
||||
@ -31,15 +32,19 @@
|
||||
"@angular/forms": "4.0.1",
|
||||
"@angular/platform-browser": "4.0.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.22",
|
||||
"rxjs": "5.3.0",
|
||||
"terminus-core": "*",
|
||||
"terminus-settings": "*",
|
||||
"rxjs": "5.3.0"
|
||||
"terminus-settings": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/async-lock": "0.0.19",
|
||||
"async-lock": "^1.0.0",
|
||||
"font-manager": "0.2.2",
|
||||
"hterm-umdjs": "1.2.0",
|
||||
"hterm-umdjs": "1.1.3",
|
||||
"mz": "^2.6.0",
|
||||
"node-pty": "0.6.8",
|
||||
"ps-node": "^0.1.6",
|
||||
"runes": "^0.4.2",
|
||||
"winreg": "^1.2.3"
|
||||
},
|
||||
"false": {}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
export { TerminalTabComponent }
|
||||
export { IChildProcess } from './services/sessions.service'
|
||||
|
||||
export abstract class TerminalDecorator {
|
||||
// tslint:disable-next-line no-empty
|
||||
@ -27,6 +28,10 @@ export interface SessionOptions {
|
||||
}
|
||||
|
||||
export abstract class SessionPersistenceProvider {
|
||||
abstract id: string
|
||||
abstract displayName: string
|
||||
|
||||
abstract isAvailable (): boolean
|
||||
abstract async attachSession (recoveryId: any): Promise<SessionOptions>
|
||||
abstract async startSession (options: SessionOptions): Promise<any>
|
||||
abstract async terminateSession (recoveryId: string): Promise<void>
|
||||
@ -43,3 +48,15 @@ export interface ITerminalColorScheme {
|
||||
export abstract class TerminalColorSchemeProvider {
|
||||
abstract async getSchemes (): Promise<ITerminalColorScheme[]>
|
||||
}
|
||||
|
||||
export interface IShell {
|
||||
id: string
|
||||
name: string
|
||||
command: string
|
||||
args?: string[]
|
||||
env?: any
|
||||
}
|
||||
|
||||
export abstract class ShellProvider {
|
||||
abstract async provide (): Promise<IShell[]>
|
||||
}
|
||||
|
BIN
terminus-terminal/src/bell.ogg
Normal file
@ -1,24 +1,32 @@
|
||||
import { AsyncSubject } from 'rxjs'
|
||||
import * as fs from 'mz/fs'
|
||||
import * as path from 'path'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HotkeysService, ToolbarButtonProvider, IToolbarButton, AppService, ConfigService, HostAppService, Platform, ElectronService } from 'terminus-core'
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
import { HotkeysService, ToolbarButtonProvider, IToolbarButton, ConfigService, HostAppService, ElectronService, Logger, LogService } from 'terminus-core'
|
||||
|
||||
import { SessionsService } from './services/sessions.service'
|
||||
import { ShellsService } from './services/shells.service'
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
import { IShell, ShellProvider } from './api'
|
||||
import { TerminalService } from './services/terminal.service'
|
||||
|
||||
@Injectable()
|
||||
export class ButtonProvider extends ToolbarButtonProvider {
|
||||
private shells$ = new AsyncSubject<IShell[]>()
|
||||
private logger: Logger
|
||||
|
||||
constructor (
|
||||
private app: AppService,
|
||||
private sessions: SessionsService,
|
||||
private terminal: TerminalService,
|
||||
private config: ConfigService,
|
||||
private shells: ShellsService,
|
||||
private hostApp: HostAppService,
|
||||
log: LogService,
|
||||
hostApp: HostAppService,
|
||||
@Inject(ShellProvider) shellProviders: ShellProvider[],
|
||||
electron: ElectronService,
|
||||
hotkeys: HotkeysService,
|
||||
) {
|
||||
super()
|
||||
this.logger = log.create('newTerminalButton')
|
||||
Promise.all(shellProviders.map(x => x.provide())).then(shellLists => {
|
||||
this.shells$.next(shellLists.reduce((a, b) => a.concat(b)))
|
||||
this.shells$.complete()
|
||||
})
|
||||
hotkeys.matchedHotkey.subscribe(async (hotkey) => {
|
||||
if (hotkey === 'new-tab') {
|
||||
this.openNewTab()
|
||||
@ -35,7 +43,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
||||
if (!electron.remote.process.env.DEV) {
|
||||
setImmediate(async () => {
|
||||
let argv: string[] = electron.remote.process.argv
|
||||
for (let arg of argv.slice(1)) {
|
||||
for (let arg of argv.slice(1).concat([electron.remote.process.argv0])) {
|
||||
if (await fs.exists(arg)) {
|
||||
if ((await fs.stat(arg)).isDirectory()) {
|
||||
this.openNewTab(arg)
|
||||
@ -47,31 +55,9 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
||||
}
|
||||
|
||||
async openNewTab (cwd?: string): Promise<void> {
|
||||
if (!cwd && this.app.activeTab instanceof TerminalTabComponent) {
|
||||
cwd = await this.app.activeTab.session.getWorkingDirectory()
|
||||
}
|
||||
let command = this.config.store.terminal.shell
|
||||
let env: any = {}
|
||||
let args: string[] = []
|
||||
if (command === '~clink~') {
|
||||
({ command, args } = this.shells.getClinkOptions())
|
||||
}
|
||||
if (command === '~default-shell~') {
|
||||
command = await this.shells.getDefaultShell()
|
||||
}
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
env.TERM = 'cygwin'
|
||||
}
|
||||
let sessionOptions = await this.sessions.prepareNewSession({
|
||||
command,
|
||||
args,
|
||||
cwd,
|
||||
env,
|
||||
})
|
||||
this.app.openNewTab(
|
||||
TerminalTabComponent,
|
||||
{ sessionOptions }
|
||||
)
|
||||
let shells = await this.shells$.first().toPromise()
|
||||
let shell = shells.find(x => x.id === this.config.store.terminal.shell) || shells[0]
|
||||
this.terminal.openTab(shell, cwd)
|
||||
}
|
||||
|
||||
provide (): IToolbarButton[] {
|
||||
|
@ -174,26 +174,53 @@
|
||||
[title]='idx',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Terminal background
|
||||
br
|
||||
div(
|
||||
'[(ngModel)]'='config.store.terminal.background',
|
||||
(ngModelChange)='config.save()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary
|
||||
input(
|
||||
type='radio',
|
||||
[value]='"theme"'
|
||||
)
|
||||
| From theme
|
||||
label.btn.btn-secondary
|
||||
input(
|
||||
type='radio',
|
||||
[value]='"colorScheme"'
|
||||
)
|
||||
| From colors
|
||||
.d-flex
|
||||
.form-group.mr-3
|
||||
label Terminal background
|
||||
br
|
||||
div(
|
||||
'[(ngModel)]'='config.store.terminal.background',
|
||||
(ngModelChange)='config.save()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary
|
||||
input(
|
||||
type='radio',
|
||||
[value]='"theme"'
|
||||
)
|
||||
| From theme
|
||||
label.btn.btn-secondary
|
||||
input(
|
||||
type='radio',
|
||||
[value]='"colorScheme"'
|
||||
)
|
||||
| From colors
|
||||
.form-group
|
||||
label Cursor shape
|
||||
br
|
||||
div(
|
||||
[(ngModel)]='config.store.terminal.cursor',
|
||||
(ngModelChange)='config.save()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary
|
||||
input(
|
||||
type='radio',
|
||||
[value]='"block"'
|
||||
)
|
||||
| █
|
||||
label.btn.btn-secondary
|
||||
input(
|
||||
type='radio',
|
||||
[value]='"beam"'
|
||||
)
|
||||
| |
|
||||
label.btn.btn-secondary
|
||||
input(
|
||||
type='radio',
|
||||
[value]='"underline"'
|
||||
)
|
||||
| ▁
|
||||
|
||||
.form-group
|
||||
label Shell
|
||||
@ -203,7 +230,7 @@
|
||||
)
|
||||
option(
|
||||
*ngFor='let shell of shells',
|
||||
[ngValue]='shell.command'
|
||||
[ngValue]='shell.id'
|
||||
) {{shell.name}}
|
||||
|
||||
.form-group
|
||||
@ -232,3 +259,15 @@
|
||||
[value]='"audible"'
|
||||
)
|
||||
| Audible
|
||||
|
||||
.form-group
|
||||
label Session persistence
|
||||
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}}
|
||||
|
@ -1,23 +1,11 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import * as fs from 'mz/fs'
|
||||
import * as path from 'path'
|
||||
import { exec } from 'mz/child_process'
|
||||
const equal = require('deep-equal')
|
||||
const fontManager = require('font-manager')
|
||||
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { ConfigService, HostAppService, Platform } from 'terminus-core'
|
||||
import { TerminalColorSchemeProvider, ITerminalColorScheme } from '../api'
|
||||
|
||||
let Registry = null
|
||||
try {
|
||||
Registry = require('winreg')
|
||||
} catch (_) { } // tslint:disable-line no-empty
|
||||
|
||||
interface IShell {
|
||||
name: string
|
||||
command: string
|
||||
}
|
||||
import { TerminalColorSchemeProvider, ITerminalColorScheme, IShell, ShellProvider, SessionPersistenceProvider } from '../api'
|
||||
|
||||
@Component({
|
||||
template: require('./terminalSettingsTab.component.pug'),
|
||||
@ -26,6 +14,7 @@ interface IShell {
|
||||
export class TerminalSettingsTabComponent {
|
||||
fonts: string[] = []
|
||||
shells: IShell[] = []
|
||||
persistenceProviders: SessionPersistenceProvider[]
|
||||
colorSchemes: ITerminalColorScheme[] = []
|
||||
equalComparator = equal
|
||||
editingColorScheme: ITerminalColorScheme
|
||||
@ -34,8 +23,12 @@ export class TerminalSettingsTabComponent {
|
||||
constructor (
|
||||
public config: ConfigService,
|
||||
private hostApp: HostAppService,
|
||||
@Inject(ShellProvider) private shellProviders: ShellProvider[],
|
||||
@Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[],
|
||||
) { }
|
||||
@Inject(SessionPersistenceProvider) persistenceProviders: SessionPersistenceProvider[],
|
||||
) {
|
||||
this.persistenceProviders = persistenceProviders.filter(x => x.isAvailable())
|
||||
}
|
||||
|
||||
async ngOnInit () {
|
||||
if (this.hostApp.platform === Platform.Windows || this.hostApp.platform === Platform.macOS) {
|
||||
@ -53,71 +46,8 @@ export class TerminalSettingsTabComponent {
|
||||
this.fonts.sort()
|
||||
})
|
||||
}
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
this.shells = [
|
||||
{ name: 'CMD (clink)', command: '~clink~' },
|
||||
{ name: 'CMD (stock)', command: 'cmd.exe' },
|
||||
{ name: 'PowerShell', command: 'powershell.exe' },
|
||||
]
|
||||
|
||||
// Detect whether BoW is installed
|
||||
const wslPath = `${process.env.windir}\\system32\\bash.exe`
|
||||
if (await fs.exists(wslPath)) {
|
||||
this.shells.push({ name: 'Bash on Windows', command: wslPath })
|
||||
}
|
||||
|
||||
// Detect Cygwin
|
||||
let cygwinPath = await new Promise<string>(resolve => {
|
||||
let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\Cygwin\\setup', arch: 'x64' })
|
||||
reg.get('rootdir', (err, item) => {
|
||||
if (err) {
|
||||
return resolve(null)
|
||||
}
|
||||
resolve(item.value)
|
||||
})
|
||||
})
|
||||
if (cygwinPath) {
|
||||
this.shells.push({ name: 'Cygwin', command: path.join(cygwinPath, 'bin', 'bash.exe') })
|
||||
}
|
||||
|
||||
// Detect 32-bit Cygwin
|
||||
let cygwin32Path = await new Promise<string>(resolve => {
|
||||
let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\Cygwin\\setup', arch: 'x86' })
|
||||
reg.get('rootdir', (err, item) => {
|
||||
if (err) {
|
||||
return resolve(null)
|
||||
}
|
||||
resolve(item.value)
|
||||
})
|
||||
})
|
||||
if (cygwin32Path) {
|
||||
this.shells.push({ name: 'Cygwin (32 bit)', command: path.join(cygwin32Path, 'bin', 'bash.exe') })
|
||||
}
|
||||
|
||||
// Detect Git-Bash
|
||||
let gitBashPath = await new Promise<string>(resolve => {
|
||||
let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\GitForWindows' })
|
||||
reg.get('InstallPath', (err, item) => {
|
||||
if (err) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
resolve(item.value)
|
||||
})
|
||||
})
|
||||
if (gitBashPath) {
|
||||
this.shells.push({ name: 'Git-Bash', command: path.join(gitBashPath, 'bin', 'bash.exe') })
|
||||
}
|
||||
}
|
||||
if (this.hostApp.platform === Platform.Linux || this.hostApp.platform === Platform.macOS) {
|
||||
this.shells = [{ name: 'Default shell', command: '~default-shell~' }]
|
||||
this.shells = this.shells.concat((await fs.readFile('/etc/shells', { encoding: 'utf-8' }))
|
||||
.split('\n')
|
||||
.map(x => x.trim())
|
||||
.filter(x => x && !x.startsWith('#'))
|
||||
.map(x => ({ name: x, command: x })))
|
||||
}
|
||||
this.colorSchemes = (await Promise.all(this.colorSchemeProviders.map(x => x.getSchemes()))).reduce((a, b) => a.concat(b))
|
||||
this.shells = (await Promise.all(this.shellProviders.map(x => x.provide()))).reduce((a, b) => a.concat(b))
|
||||
}
|
||||
|
||||
fontAutocomplete = (text$: Observable<string>) => {
|
||||
|
@ -9,7 +9,7 @@
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
margin: 15px;
|
||||
transition: opacity ease-out 0.1s;
|
||||
transition: opacity ease-out 0.25s;
|
||||
opacity: 0;
|
||||
|
||||
div[style]:last-child {
|
||||
|
@ -1,4 +1,3 @@
|
||||
const dataurl = require('dataurl')
|
||||
import { BehaviorSubject, Subject, Subscription } from 'rxjs'
|
||||
import 'rxjs/add/operator/bufferTime'
|
||||
import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core'
|
||||
@ -21,7 +20,6 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
@ViewChild('content') content
|
||||
@HostBinding('style.background-color') backgroundColor: string
|
||||
hterm: any
|
||||
configSubscription: Subscription
|
||||
sessionCloseSubscription: Subscription
|
||||
hotkeysSubscription: Subscription
|
||||
bell$ = new Subject()
|
||||
@ -33,6 +31,7 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
alternateScreenActive$ = new BehaviorSubject(false)
|
||||
mouseEvent$ = new Subject<Event>()
|
||||
htermVisible = false
|
||||
private bellPlayer: HTMLAudioElement
|
||||
private io: any
|
||||
|
||||
constructor (
|
||||
@ -47,14 +46,14 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
) {
|
||||
super()
|
||||
this.decorators = this.decorators || []
|
||||
this.title$.next('Terminal')
|
||||
this.configSubscription = config.changed$.subscribe(() => {
|
||||
this.configure()
|
||||
})
|
||||
this.title = 'Terminal'
|
||||
this.resize$.first().subscribe(async (resizeEvent) => {
|
||||
this.session = this.sessions.addSession(
|
||||
Object.assign({}, this.sessionOptions, resizeEvent)
|
||||
)
|
||||
setTimeout(() => {
|
||||
this.session.resize(resizeEvent.width, resizeEvent.height)
|
||||
}, 1000)
|
||||
// this.session.output$.bufferTime(10).subscribe((datas) => {
|
||||
this.session.output$.subscribe(data => {
|
||||
// let data = datas.join('')
|
||||
@ -88,6 +87,8 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
this.resetZoom()
|
||||
}
|
||||
})
|
||||
this.bellPlayer = document.createElement('audio')
|
||||
this.bellPlayer.src = require<string>('../bell.ogg')
|
||||
}
|
||||
|
||||
getRecoveryToken (): any {
|
||||
@ -99,6 +100,7 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
|
||||
ngOnInit () {
|
||||
this.focused$.subscribe(() => {
|
||||
this.configure()
|
||||
setTimeout(() => {
|
||||
this.hterm.scrollPort_.resize()
|
||||
this.hterm.scrollPort_.focus()
|
||||
@ -129,13 +131,15 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
}, 1000)
|
||||
|
||||
this.bell$.subscribe(() => {
|
||||
if (this.config.store.terminal.bell !== 'off') {
|
||||
let bg = preferenceManager.get('background-color')
|
||||
if (this.config.store.terminal.bell === 'visual') {
|
||||
preferenceManager.set('background-color', 'rgba(128,128,128,.25)')
|
||||
setTimeout(() => {
|
||||
preferenceManager.set('background-color', bg)
|
||||
this.configure()
|
||||
}, 125)
|
||||
}
|
||||
if (this.config.store.terminal.bell === 'audible') {
|
||||
this.bellPlayer.play()
|
||||
}
|
||||
// TODO audible
|
||||
})
|
||||
}
|
||||
@ -143,7 +147,7 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
attachHTermHandlers (hterm: any) {
|
||||
hterm.setWindowTitle = (title) => {
|
||||
this.zone.run(() => {
|
||||
this.title$.next(title)
|
||||
this.title = title
|
||||
})
|
||||
}
|
||||
|
||||
@ -155,6 +159,8 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
|
||||
hterm.primaryScreen_.syncSelectionCaret = () => null
|
||||
hterm.alternateScreen_.syncSelectionCaret = () => null
|
||||
hterm.primaryScreen_.terminal = hterm
|
||||
hterm.alternateScreen_.terminal = hterm
|
||||
|
||||
const _onPaste = hterm.scrollPort_.onPaste_.bind(hterm.scrollPort_)
|
||||
hterm.scrollPort_.onPaste_ = (event) => {
|
||||
@ -208,6 +214,13 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
const _measureCharacterSize = hterm.scrollPort_.measureCharacterSize.bind(hterm.scrollPort_)
|
||||
hterm.scrollPort_.measureCharacterSize = () => {
|
||||
let size = _measureCharacterSize()
|
||||
size.height += this.config.store.terminal.linePadding
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
attachIOHandlers (io: any) {
|
||||
@ -244,10 +257,10 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
|
||||
async configure (): Promise<void> {
|
||||
let config = this.config.store
|
||||
preferenceManager.set('font-family', config.terminal.font)
|
||||
preferenceManager.set('font-family', `"${config.terminal.font}", "monospace-fallback", monospace`)
|
||||
this.setFontSize()
|
||||
preferenceManager.set('enable-bold', true)
|
||||
preferenceManager.set('audible-bell-sound', '')
|
||||
// preferenceManager.set('audible-bell-sound', '')
|
||||
preferenceManager.set('desktop-notification-bell', config.terminal.bell === 'notification')
|
||||
preferenceManager.set('enable-clipboard-notice', false)
|
||||
preferenceManager.set('receive-encoding', 'raw')
|
||||
@ -294,13 +307,15 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
}
|
||||
`
|
||||
}
|
||||
preferenceManager.set('user-css', dataurl.convert({
|
||||
data: css,
|
||||
mimetype: 'text/css',
|
||||
charset: 'utf8',
|
||||
}))
|
||||
|
||||
css += config.appearance.css
|
||||
this.hterm.setCSS(css)
|
||||
this.hterm.setBracketedPaste(config.terminal.bracketedPaste)
|
||||
this.hterm.defaultCursorShape = {
|
||||
block: hterm.hterm.Terminal.cursorShape.BLOCK,
|
||||
underline: hterm.hterm.Terminal.cursorShape.UNDERLINE,
|
||||
beam: hterm.hterm.Terminal.cursorShape.BEAM,
|
||||
}[config.terminal.cursor]
|
||||
this.hterm.applyCursorShape()
|
||||
}
|
||||
|
||||
zoomIn () {
|
||||
@ -322,7 +337,6 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
this.decorators.forEach(decorator => {
|
||||
decorator.detach(this)
|
||||
})
|
||||
this.configSubscription.unsubscribe()
|
||||
this.hotkeysSubscription.unsubscribe()
|
||||
if (this.sessionCloseSubscription) {
|
||||
this.sessionCloseSubscription.unsubscribe()
|
||||
@ -343,6 +357,17 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
}
|
||||
}
|
||||
|
||||
async canClose (): Promise<boolean> {
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
return true
|
||||
}
|
||||
let children = await this.session.getChildProcesses()
|
||||
if (children.length === 0) {
|
||||
return true
|
||||
}
|
||||
return confirm(`"${children[0].command}" is still running. Close?`)
|
||||
}
|
||||
|
||||
private setFontSize () {
|
||||
preferenceManager.set('font-size', this.config.store.terminal.fontSize * Math.pow(1.1, this.zoom))
|
||||
}
|
||||
|
@ -4,10 +4,12 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
defaults = {
|
||||
terminal: {
|
||||
fontSize: 14,
|
||||
linePadding: 0,
|
||||
bell: 'off',
|
||||
bracketedPaste: false,
|
||||
background: 'theme',
|
||||
ligatures: false,
|
||||
cursor: 'block',
|
||||
colorScheme: {
|
||||
__nonStructural: true,
|
||||
name: 'Material',
|
||||
@ -41,7 +43,8 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
[Platform.macOS]: {
|
||||
terminal: {
|
||||
font: 'Menlo',
|
||||
shell: '~default-shell~',
|
||||
shell: 'default',
|
||||
persistence: 'screen',
|
||||
},
|
||||
hotkeys: {
|
||||
'copy': [
|
||||
@ -72,7 +75,8 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
[Platform.Windows]: {
|
||||
terminal: {
|
||||
font: 'Consolas',
|
||||
shell: '~clink~',
|
||||
shell: 'clink',
|
||||
persistence: null,
|
||||
},
|
||||
hotkeys: {
|
||||
'copy': [
|
||||
@ -102,7 +106,8 @@ export class TerminalConfigProvider extends ConfigProvider {
|
||||
[Platform.Linux]: {
|
||||
terminal: {
|
||||
font: 'Liberation Mono',
|
||||
shell: '~default-shell~',
|
||||
shell: 'default',
|
||||
persistence: 'tmux',
|
||||
},
|
||||
hotkeys: {
|
||||
'copy': [
|
||||
|
BIN
terminus-terminal/src/fonts/Meslo.otf
Normal file
@ -22,6 +22,16 @@ preferenceManager.set('color-palette-overrides', {
|
||||
|
||||
hterm.hterm.Terminal.prototype.showOverlay = () => null
|
||||
|
||||
hterm.hterm.Terminal.prototype.setCSS = function (css) {
|
||||
const doc = this.scrollPort_.document_
|
||||
if (!doc.querySelector('#user-css')) {
|
||||
const node = doc.createElement('style')
|
||||
node.id = 'user-css'
|
||||
doc.head.appendChild(node)
|
||||
}
|
||||
doc.querySelector('#user-css').innerText = css
|
||||
}
|
||||
|
||||
const oldCharWidthDisregardAmbiguous = hterm.lib.wc.charWidthDisregardAmbiguous
|
||||
hterm.lib.wc.charWidthDisregardAmbiguous = codepoint => {
|
||||
if ((codepoint >= 0x1f300 && codepoint <= 0x1f64f) ||
|
||||
@ -30,3 +40,38 @@ hterm.lib.wc.charWidthDisregardAmbiguous = codepoint => {
|
||||
}
|
||||
return oldCharWidthDisregardAmbiguous(codepoint)
|
||||
}
|
||||
|
||||
hterm.hterm.Terminal.prototype.applyCursorShape = function () {
|
||||
let modes = [
|
||||
[hterm.hterm.Terminal.cursorShape.BLOCK, true],
|
||||
[this.defaultCursorShape || hterm.hterm.Terminal.cursorShape.BLOCK, false],
|
||||
[hterm.hterm.Terminal.cursorShape.BLOCK, false],
|
||||
[hterm.hterm.Terminal.cursorShape.UNDERLINE, true],
|
||||
[hterm.hterm.Terminal.cursorShape.UNDERLINE, false],
|
||||
[hterm.hterm.Terminal.cursorShape.BEAM, true],
|
||||
[hterm.hterm.Terminal.cursorShape.BEAM, false],
|
||||
]
|
||||
let modeNumber = this.cursorMode || 1
|
||||
console.log('mode', modeNumber)
|
||||
if (modeNumber >= modes.length) {
|
||||
console.warn('Unknown cursor style: ' + modeNumber)
|
||||
return
|
||||
}
|
||||
this.setCursorShape(modes[modeNumber][0])
|
||||
this.setCursorBlink(modes[modeNumber][1])
|
||||
}
|
||||
|
||||
hterm.hterm.VT.CSI[' q'] = function (parseState) {
|
||||
const arg = parseState.args[0]
|
||||
this.terminal.cursorMode = arg
|
||||
this.terminal.applyCursorShape()
|
||||
}
|
||||
|
||||
const _collapseToEnd = Selection.prototype.collapseToEnd
|
||||
Selection.prototype.collapseToEnd = function () {
|
||||
try {
|
||||
_collapseToEnd.apply(this)
|
||||
} catch (err) {
|
||||
// tslint-disable-line
|
||||
}
|
||||
}
|
||||
|
@ -9,3 +9,13 @@ a:hover {
|
||||
x-screen {
|
||||
transition: 0.125s ease background;
|
||||
}
|
||||
|
||||
x-row > span {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "monospace-fallback";
|
||||
src: url(fonts/Meslo.otf) format("opentype");
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { BrowserModule } from '@angular/platform-browser'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
import { HostAppService, Platform, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider } from 'terminus-core'
|
||||
import { ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider } from 'terminus-core'
|
||||
import { SettingsTabProvider } from 'terminus-settings'
|
||||
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
@ -11,17 +11,28 @@ import { TerminalSettingsTabComponent } from './components/terminalSettingsTab.c
|
||||
import { ColorPickerComponent } from './components/colorPicker.component'
|
||||
|
||||
import { SessionsService } from './services/sessions.service'
|
||||
import { ShellsService } from './services/shells.service'
|
||||
import { TerminalService } from './services/terminal.service'
|
||||
|
||||
import { ScreenPersistenceProvider } from './persistenceProviders'
|
||||
import { ScreenPersistenceProvider } from './persistence/screen'
|
||||
import { TMuxPersistenceProvider } from './persistence/tmux'
|
||||
import { ButtonProvider } from './buttonProvider'
|
||||
import { RecoveryProvider } from './recoveryProvider'
|
||||
import { SessionPersistenceProvider, TerminalColorSchemeProvider, TerminalDecorator } from './api'
|
||||
import { SessionPersistenceProvider, TerminalColorSchemeProvider, TerminalDecorator, ShellProvider } from './api'
|
||||
import { TerminalSettingsTabProvider } from './settings'
|
||||
import { PathDropDecorator } from './pathDrop'
|
||||
import { TerminalConfigProvider } from './config'
|
||||
import { TerminalHotkeyProvider } from './hotkeys'
|
||||
import { HyperColorSchemes } from './colorSchemes'
|
||||
|
||||
import { Cygwin32ShellProvider } from './shells/cygwin32'
|
||||
import { Cygwin64ShellProvider } from './shells/cygwin64'
|
||||
import { GitBashShellProvider } from './shells/gitBash'
|
||||
import { LinuxDefaultShellProvider } from './shells/linuxDefault'
|
||||
import { MacOSDefaultShellProvider } from './shells/macDefault'
|
||||
import { POSIXShellsProvider } from './shells/posix'
|
||||
import { WindowsStockShellsProvider } from './shells/windowsStock'
|
||||
import { WSLShellProvider } from './shells/wsl'
|
||||
|
||||
import { hterm } from './hterm'
|
||||
|
||||
@NgModule({
|
||||
@ -32,26 +43,27 @@ import { hterm } from './hterm'
|
||||
],
|
||||
providers: [
|
||||
SessionsService,
|
||||
ShellsService,
|
||||
ScreenPersistenceProvider,
|
||||
TerminalService,
|
||||
|
||||
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
|
||||
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
|
||||
{
|
||||
provide: SessionPersistenceProvider,
|
||||
useFactory: (hostApp: HostAppService, screen: ScreenPersistenceProvider) => {
|
||||
if (hostApp.platform === Platform.Windows) {
|
||||
return null
|
||||
} else {
|
||||
return screen
|
||||
}
|
||||
},
|
||||
deps: [HostAppService, ScreenPersistenceProvider],
|
||||
},
|
||||
{ provide: SettingsTabProvider, useClass: TerminalSettingsTabProvider, multi: true },
|
||||
{ provide: ConfigProvider, useClass: TerminalConfigProvider, multi: true },
|
||||
{ provide: HotkeyProvider, useClass: TerminalHotkeyProvider, multi: true },
|
||||
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
|
||||
{ provide: TerminalDecorator, useClass: PathDropDecorator, multi: true },
|
||||
|
||||
{ provide: SessionPersistenceProvider, useClass: ScreenPersistenceProvider, multi: true },
|
||||
{ provide: SessionPersistenceProvider, useClass: TMuxPersistenceProvider, multi: true },
|
||||
|
||||
{ provide: ShellProvider, useClass: WindowsStockShellsProvider, multi: true },
|
||||
{ provide: ShellProvider, useClass: MacOSDefaultShellProvider, multi: true },
|
||||
{ provide: ShellProvider, useClass: LinuxDefaultShellProvider, multi: true },
|
||||
{ provide: ShellProvider, useClass: Cygwin32ShellProvider, multi: true },
|
||||
{ provide: ShellProvider, useClass: Cygwin64ShellProvider, multi: true },
|
||||
{ provide: ShellProvider, useClass: GitBashShellProvider, multi: true },
|
||||
{ provide: ShellProvider, useClass: POSIXShellsProvider, multi: true },
|
||||
{ provide: ShellProvider, useClass: WSLShellProvider, multi: true },
|
||||
],
|
||||
entryComponents: [
|
||||
TerminalTabComponent,
|
||||
@ -93,3 +105,4 @@ export default class TerminalModule {
|
||||
}
|
||||
|
||||
export * from './api'
|
||||
export { TerminalService }
|
||||
|
@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'
|
||||
import { TerminalDecorator } from './api'
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class PathDropDecorator extends TerminalDecorator {
|
||||
attach (terminal: TerminalTabComponent): void {
|
||||
|
@ -1,11 +1,12 @@
|
||||
import * as fs from 'mz/fs'
|
||||
import * as path from 'path'
|
||||
import { exec, spawn } from 'mz/child_process'
|
||||
import { exec as execCallback } from 'child_process'
|
||||
import { exec as execAsync, execFileSync } from 'child_process'
|
||||
|
||||
import { AsyncSubject } from 'rxjs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Logger, LogService } from 'terminus-core'
|
||||
import { SessionOptions, SessionPersistenceProvider } from './api'
|
||||
import { Logger, LogService, ElectronService } from 'terminus-core'
|
||||
import { SessionOptions, SessionPersistenceProvider } from '../api'
|
||||
|
||||
declare function delay (ms: number): Promise<void>
|
||||
|
||||
@ -29,18 +30,30 @@ async function listProcesses (): Promise<IChildProcess[]> {
|
||||
|
||||
@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 => {
|
||||
execCallback('screen -list', (_err, stdout) => {
|
||||
execAsync('screen -list', (_err, stdout) => {
|
||||
// returns an error code on macOS
|
||||
resolve(stdout.split('\n'))
|
||||
})
|
||||
@ -64,12 +77,13 @@ export class ScreenPersistenceProvider extends SessionPersistenceProvider {
|
||||
recoveryId,
|
||||
recoveredTruePID$: truePID$.asObservable(),
|
||||
command: 'screen',
|
||||
args: ['-r', recoveryId],
|
||||
args: ['-d', '-r', recoveryId, '-c', await this.prepareConfig()],
|
||||
}
|
||||
}
|
||||
|
||||
async extractShellPID (screenPID: number): Promise<number> {
|
||||
let child = (await listProcesses()).find(x => x.ppid === screenPID)
|
||||
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})!`)
|
||||
@ -77,32 +91,15 @@ export class ScreenPersistenceProvider extends SessionPersistenceProvider {
|
||||
|
||||
if (child.command === 'login') {
|
||||
await delay(1000)
|
||||
child = (await listProcesses()).find(x => x.ppid === child.pid)
|
||||
child = processes.find(x => x.ppid === child.pid)
|
||||
}
|
||||
|
||||
return child.pid
|
||||
}
|
||||
|
||||
async startSession (options: SessionOptions): Promise<any> {
|
||||
let configPath = '/tmp/.termScreenConfig'
|
||||
await fs.writeFile(configPath, `
|
||||
escape ^^^
|
||||
vbell on
|
||||
deflogin on
|
||||
term xterm-color
|
||||
bindkey "^[OH" beginning-of-line
|
||||
bindkey "^[OF" 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')
|
||||
let recoveryId = `term-tab-${Date.now()}`
|
||||
let args = ['-d', '-m', '-c', configPath, '-U', '-S', recoveryId, '-T', 'xterm-256color', '--', '-' + options.command].concat(options.args || [])
|
||||
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,
|
||||
@ -118,4 +115,28 @@ export class ScreenPersistenceProvider extends SessionPersistenceProvider {
|
||||
// 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
|
||||
}
|
||||
}
|
226
terminus-terminal/src/persistence/tmux.ts
Normal file
@ -0,0 +1,226 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { execFileSync } from 'child_process'
|
||||
import * as AsyncLock from 'async-lock'
|
||||
import { ConnectableObservable, AsyncSubject, Subject } from 'rxjs'
|
||||
import * as childProcess from 'child_process'
|
||||
import { SessionOptions, SessionPersistenceProvider } from '../api'
|
||||
|
||||
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 })
|
||||
|
||||
constructor () {
|
||||
this.process = childProcess.spawn('tmux', ['-C', '-f', '/dev/null', '-L', 'terminus', 'new-session', '-A', '-D', '-s', 'control'])
|
||||
console.log('[tmux] 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$.skip(1).publish()
|
||||
this.response$.connect()
|
||||
|
||||
this.block$.subscribe(block => {
|
||||
console.debug('[tmux] block:', block)
|
||||
})
|
||||
|
||||
this.message$.subscribe(message => {
|
||||
console.debug('[tmux] message:', message)
|
||||
})
|
||||
}
|
||||
|
||||
command (command: string): Promise<TMuxBlock> {
|
||||
return this.lock.acquire('key', () => {
|
||||
let p = this.response$.take(1).toPromise()
|
||||
console.debug('[tmux] 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
|
||||
|
||||
constructor () {
|
||||
this.process = new TMuxCommandProcess()
|
||||
TMUX_CONFIG.split('\n').filter(x => x).forEach(async (line) => {
|
||||
await this.process.command(line)
|
||||
})
|
||||
}
|
||||
|
||||
async create (id: string, options: SessionOptions): Promise<void> {
|
||||
let args = [options.command].concat(options.args)
|
||||
let cmd = args.map(x => `"${x.replace('"', '\\"')}"`)
|
||||
await this.process.command(
|
||||
`new-session -s "${id}" -d`
|
||||
+ (options.cwd ? ` -c '${options.cwd.replace("'", "\\'")}'` : '')
|
||||
+ ` '${cmd}'`
|
||||
)
|
||||
}
|
||||
|
||||
async list (): Promise<string[]> {
|
||||
let block = await this.process.command('list-sessions -F "#{session_name}"')
|
||||
return block.lines
|
||||
}
|
||||
|
||||
async getPID (id: string): Promise<number|null> {
|
||||
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> {
|
||||
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)
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TabRecoveryProvider, AppService } from 'terminus-core'
|
||||
import { TabRecoveryProvider, RecoveredTab } from 'terminus-core'
|
||||
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
import { SessionsService } from './services/sessions.service'
|
||||
@ -8,18 +8,21 @@ import { SessionsService } from './services/sessions.service'
|
||||
export class RecoveryProvider extends TabRecoveryProvider {
|
||||
constructor (
|
||||
private sessions: SessionsService,
|
||||
private app: AppService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async recover (recoveryToken: any): Promise<void> {
|
||||
async recover (recoveryToken: any): Promise<RecoveredTab> {
|
||||
if (recoveryToken.type === 'app:terminal') {
|
||||
let sessionOptions = await this.sessions.recover(recoveryToken.recoveryId)
|
||||
if (!sessionOptions) {
|
||||
return
|
||||
return null
|
||||
}
|
||||
return {
|
||||
type: TerminalTabComponent,
|
||||
options: { sessionOptions },
|
||||
}
|
||||
this.app.openNewTab(TerminalTabComponent, { sessionOptions })
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,20 @@
|
||||
import * as nodePTY from 'node-pty'
|
||||
const psNode = require('ps-node')
|
||||
// import * as nodePTY from 'node-pty'
|
||||
let nodePTY
|
||||
import * as fs from 'mz/fs'
|
||||
import { Subject } from 'rxjs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Logger, LogService } from 'terminus-core'
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
import { Logger, LogService, ElectronService, ConfigService } from 'terminus-core'
|
||||
import { exec } from 'mz/child_process'
|
||||
|
||||
import { SessionOptions, SessionPersistenceProvider } from '../api'
|
||||
|
||||
export interface IChildProcess {
|
||||
pid: number
|
||||
ppid: number
|
||||
command: string
|
||||
}
|
||||
|
||||
export class Session {
|
||||
open: boolean
|
||||
name: string
|
||||
@ -101,6 +109,20 @@ export class Session {
|
||||
this.pty.kill(signal)
|
||||
}
|
||||
|
||||
async getChildProcesses (): Promise<IChildProcess[]> {
|
||||
if (!this.truePID) {
|
||||
return []
|
||||
}
|
||||
return new Promise<IChildProcess[]>((resolve, reject) => {
|
||||
psNode.lookup({ ppid: this.truePID }, (err, processes) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(processes as IChildProcess[])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async gracefullyKillProcess (): Promise<void> {
|
||||
if (process.platform === 'win32') {
|
||||
this.kill()
|
||||
@ -156,16 +178,21 @@ export class SessionsService {
|
||||
private lastID = 0
|
||||
|
||||
constructor (
|
||||
private persistence: SessionPersistenceProvider,
|
||||
@Inject(SessionPersistenceProvider) private persistenceProviders: SessionPersistenceProvider[],
|
||||
private config: ConfigService,
|
||||
electron: ElectronService,
|
||||
log: LogService,
|
||||
) {
|
||||
nodePTY = electron.remoteRequirePluginModule('terminus-terminal', 'node-pty', global as any)
|
||||
this.logger = log.create('sessions')
|
||||
this.persistenceProviders = this.persistenceProviders.filter(x => x.isAvailable())
|
||||
}
|
||||
|
||||
async prepareNewSession (options: SessionOptions): Promise<SessionOptions> {
|
||||
if (this.persistence) {
|
||||
let recoveryId = await this.persistence.startSession(options)
|
||||
options = await this.persistence.attachSession(recoveryId)
|
||||
let persistence = this.getPersistence()
|
||||
if (persistence) {
|
||||
let recoveryId = await persistence.startSession(options)
|
||||
options = await persistence.attachSession(recoveryId)
|
||||
}
|
||||
return options
|
||||
}
|
||||
@ -174,10 +201,11 @@ export class SessionsService {
|
||||
this.lastID++
|
||||
options.name = `session-${this.lastID}`
|
||||
let session = new Session(options)
|
||||
let persistence = this.getPersistence()
|
||||
session.destroyed$.first().subscribe(() => {
|
||||
delete this.sessions[session.name]
|
||||
if (this.persistence) {
|
||||
this.persistence.terminateSession(session.recoveryId)
|
||||
if (persistence) {
|
||||
persistence.terminateSession(session.recoveryId)
|
||||
}
|
||||
})
|
||||
this.sessions[session.name] = session
|
||||
@ -185,9 +213,17 @@ export class SessionsService {
|
||||
}
|
||||
|
||||
async recover (recoveryId: string): Promise<SessionOptions> {
|
||||
if (!this.persistence) {
|
||||
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 await this.persistence.attachSession(recoveryId)
|
||||
return this.persistenceProviders.find(x => x.id === this.config.store.terminal.persistence) || null
|
||||
}
|
||||
}
|
||||
|
@ -1,58 +0,0 @@
|
||||
import * as path from 'path'
|
||||
import { exec } from 'mz/child_process'
|
||||
import * as fs from 'mz/fs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ElectronService, HostAppService, Platform, Logger, LogService } from 'terminus-core'
|
||||
|
||||
@Injectable()
|
||||
export class ShellsService {
|
||||
private logger: Logger
|
||||
|
||||
constructor (
|
||||
log: LogService,
|
||||
private electron: ElectronService,
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
this.logger = log.create('shells')
|
||||
}
|
||||
|
||||
getClinkOptions (): { command, args } {
|
||||
return {
|
||||
command: 'cmd.exe',
|
||||
args: [
|
||||
'/k',
|
||||
path.join(
|
||||
path.dirname(this.electron.app.getPath('exe')),
|
||||
'resources',
|
||||
'clink',
|
||||
`clink_${process.arch}.exe`,
|
||||
),
|
||||
'inject',
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async getDefaultShell (): Promise<string> {
|
||||
if (this.hostApp.platform === Platform.macOS) {
|
||||
return this.getDefaultMacOSShell()
|
||||
} else {
|
||||
return this.getDefaultLinuxShell()
|
||||
}
|
||||
}
|
||||
|
||||
async getDefaultMacOSShell (): Promise<string> {
|
||||
let shellEntry = (await exec(`dscl . -read /Users/${process.env.LOGNAME} UserShell`))[0].toString()
|
||||
return shellEntry.split(' ')[1].trim()
|
||||
}
|
||||
|
||||
async getDefaultLinuxShell (): Promise<string> {
|
||||
let line = (await fs.readFile('/etc/passwd', { encoding: 'utf-8' }))
|
||||
.split('\n').find(x => x.startsWith(process.env.LOGNAME + ':'))
|
||||
if (!line) {
|
||||
this.logger.warn('Could not detect user shell')
|
||||
return '/bin/sh'
|
||||
} else {
|
||||
return line.split(':')[6]
|
||||
}
|
||||
}
|
||||
}
|
40
terminus-terminal/src/services/terminal.service.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AppService, Logger, LogService } from 'terminus-core'
|
||||
import { IShell } from '../api'
|
||||
import { SessionsService } from './sessions.service'
|
||||
import { TerminalTabComponent } from '../components/terminalTab.component'
|
||||
|
||||
@Injectable()
|
||||
export class TerminalService {
|
||||
private logger: Logger
|
||||
|
||||
constructor (
|
||||
private app: AppService,
|
||||
private sessions: SessionsService,
|
||||
log: LogService,
|
||||
) {
|
||||
this.logger = log.create('terminal')
|
||||
}
|
||||
|
||||
async openTab (shell: IShell, cwd?: string): Promise<TerminalTabComponent> {
|
||||
if (!cwd && this.app.activeTab instanceof TerminalTabComponent) {
|
||||
cwd = await this.app.activeTab.session.getWorkingDirectory()
|
||||
}
|
||||
let env: any = Object.assign({}, process.env, shell.env || {})
|
||||
|
||||
this.logger.log(`Starting shell ${shell.name}`, shell)
|
||||
let sessionOptions = await this.sessions.prepareNewSession({
|
||||
command: shell.command,
|
||||
args: shell.args || [],
|
||||
cwd,
|
||||
env,
|
||||
})
|
||||
|
||||
this.logger.log('Using session options:', sessionOptions)
|
||||
|
||||
return this.app.openNewTab(
|
||||
TerminalTabComponent,
|
||||
{ sessionOptions }
|
||||
) as TerminalTabComponent
|
||||
}
|
||||
}
|
48
terminus-terminal/src/shells/cygwin32.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import * as path from 'path'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HostAppService, Platform } from 'terminus-core'
|
||||
|
||||
import { ShellProvider, IShell } from '../api'
|
||||
|
||||
let Registry = null
|
||||
try {
|
||||
Registry = require('winreg')
|
||||
} catch (_) { } // tslint:disable-line no-empty
|
||||
|
||||
@Injectable()
|
||||
export class Cygwin32ShellProvider extends ShellProvider {
|
||||
constructor (
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async provide (): Promise<IShell[]> {
|
||||
if (this.hostApp.platform !== Platform.Windows) {
|
||||
return []
|
||||
}
|
||||
|
||||
let cygwinPath = await new Promise<string>(resolve => {
|
||||
let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\Cygwin\\setup', arch: 'x86' })
|
||||
reg.get('rootdir', (err, item) => {
|
||||
if (err || !item) {
|
||||
return resolve(null)
|
||||
}
|
||||
resolve(item.value)
|
||||
})
|
||||
})
|
||||
|
||||
if (!cygwinPath) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{
|
||||
id: 'cygwin32',
|
||||
name: 'Cygwin (32 bit)',
|
||||
command: path.join(cygwinPath, 'bin', 'bash.exe'),
|
||||
env: {
|
||||
TERM: 'cygwin',
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
48
terminus-terminal/src/shells/cygwin64.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import * as path from 'path'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HostAppService, Platform } from 'terminus-core'
|
||||
|
||||
import { ShellProvider, IShell } from '../api'
|
||||
|
||||
let Registry = null
|
||||
try {
|
||||
Registry = require('winreg')
|
||||
} catch (_) { } // tslint:disable-line no-empty
|
||||
|
||||
@Injectable()
|
||||
export class Cygwin64ShellProvider extends ShellProvider {
|
||||
constructor (
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async provide (): Promise<IShell[]> {
|
||||
if (this.hostApp.platform !== Platform.Windows) {
|
||||
return []
|
||||
}
|
||||
|
||||
let cygwinPath = await new Promise<string>(resolve => {
|
||||
let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\Cygwin\\setup', arch: 'x64' })
|
||||
reg.get('rootdir', (err, item) => {
|
||||
if (err || !item) {
|
||||
return resolve(null)
|
||||
}
|
||||
resolve(item.value)
|
||||
})
|
||||
})
|
||||
|
||||
if (!cygwinPath) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{
|
||||
id: 'cygwin64',
|
||||
name: 'Cygwin',
|
||||
command: path.join(cygwinPath, 'bin', 'bash.exe'),
|
||||
env: {
|
||||
TERM: 'cygwin',
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
63
terminus-terminal/src/shells/gitBash.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import * as path from 'path'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HostAppService, Platform } from 'terminus-core'
|
||||
|
||||
import { ShellProvider, IShell } from '../api'
|
||||
|
||||
let Registry = null
|
||||
try {
|
||||
Registry = require('winreg')
|
||||
} catch (_) { } // tslint:disable-line no-empty
|
||||
|
||||
@Injectable()
|
||||
export class GitBashShellProvider extends ShellProvider {
|
||||
constructor (
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async provide (): Promise<IShell[]> {
|
||||
if (this.hostApp.platform !== Platform.Windows) {
|
||||
return []
|
||||
}
|
||||
|
||||
let gitBashPath = await new Promise<string>(resolve => {
|
||||
let reg = new Registry({ hive: Registry.HKLM, key: '\\Software\\GitForWindows' })
|
||||
reg.get('InstallPath', (err, item) => {
|
||||
if (err || !item) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
resolve(item.value)
|
||||
})
|
||||
})
|
||||
|
||||
if (!gitBashPath) {
|
||||
gitBashPath = await new Promise<string>(resolve => {
|
||||
let reg = new Registry({ hive: Registry.HKCU, key: '\\Software\\GitForWindows' })
|
||||
reg.get('InstallPath', (err, item) => {
|
||||
if (err || !item) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
resolve(item.value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (!gitBashPath) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{
|
||||
id: 'git-bash',
|
||||
name: 'Git-Bash',
|
||||
command: path.join(gitBashPath, 'bin', 'bash.exe'),
|
||||
args: [ '--login', '-i' ],
|
||||
env: {
|
||||
TERM: 'cygwin',
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
40
terminus-terminal/src/shells/linuxDefault.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import * as fs from 'mz/fs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HostAppService, Platform, LogService, Logger } from 'terminus-core'
|
||||
|
||||
import { ShellProvider, IShell } from '../api'
|
||||
|
||||
@Injectable()
|
||||
export class LinuxDefaultShellProvider extends ShellProvider {
|
||||
private logger: Logger
|
||||
|
||||
constructor (
|
||||
private hostApp: HostAppService,
|
||||
log: LogService,
|
||||
) {
|
||||
super()
|
||||
this.logger = log.create('linuxDefaultShell')
|
||||
}
|
||||
|
||||
async provide (): Promise<IShell[]> {
|
||||
if (this.hostApp.platform !== Platform.Linux) {
|
||||
return []
|
||||
}
|
||||
let line = (await fs.readFile('/etc/passwd', { encoding: 'utf-8' }))
|
||||
.split('\n').find(x => x.startsWith(process.env.LOGNAME + ':'))
|
||||
if (!line) {
|
||||
this.logger.warn('Could not detect user shell')
|
||||
return [{
|
||||
id: 'default',
|
||||
name: 'User default',
|
||||
command: '/bin/sh'
|
||||
}]
|
||||
} else {
|
||||
return [{
|
||||
id: 'default',
|
||||
name: 'User default',
|
||||
command: line.split(':')[6]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
26
terminus-terminal/src/shells/macDefault.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { exec } from 'mz/child_process'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HostAppService, Platform } from 'terminus-core'
|
||||
|
||||
import { ShellProvider, IShell } from '../api'
|
||||
|
||||
@Injectable()
|
||||
export class MacOSDefaultShellProvider extends ShellProvider {
|
||||
constructor (
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async provide (): Promise<IShell[]> {
|
||||
if (this.hostApp.platform !== Platform.macOS) {
|
||||
return []
|
||||
}
|
||||
let shellEntry = (await exec(`dscl . -read /Users/${process.env.LOGNAME} UserShell`))[0].toString()
|
||||
return [{
|
||||
id: 'default',
|
||||
name: 'User default',
|
||||
command: shellEntry.split(' ')[1].trim()
|
||||
}]
|
||||
}
|
||||
}
|
29
terminus-terminal/src/shells/posix.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import * as fs from 'mz/fs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HostAppService, Platform } from 'terminus-core'
|
||||
|
||||
import { ShellProvider, IShell } from '../api'
|
||||
|
||||
@Injectable()
|
||||
export class POSIXShellsProvider extends ShellProvider {
|
||||
constructor (
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async provide (): Promise<IShell[]> {
|
||||
if (this.hostApp.platform === Platform.Windows) {
|
||||
return []
|
||||
}
|
||||
return (await fs.readFile('/etc/shells', { encoding: 'utf-8' }))
|
||||
.split('\n')
|
||||
.map(x => x.trim())
|
||||
.filter(x => x && !x.startsWith('#'))
|
||||
.map(x => ({
|
||||
id: x,
|
||||
name: x,
|
||||
command: x,
|
||||
}))
|
||||
}
|
||||
}
|
40
terminus-terminal/src/shells/windowsStock.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import * as path from 'path'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HostAppService, Platform, ElectronService } from 'terminus-core'
|
||||
|
||||
import { ShellProvider, IShell } from '../api'
|
||||
|
||||
@Injectable()
|
||||
export class WindowsStockShellsProvider extends ShellProvider {
|
||||
constructor (
|
||||
private hostApp: HostAppService,
|
||||
private electron: ElectronService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async provide (): Promise<IShell[]> {
|
||||
if (this.hostApp.platform !== Platform.Windows) {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: 'clink',
|
||||
name: 'CMD (clink)',
|
||||
command: 'cmd.exe',
|
||||
args: [
|
||||
'/k',
|
||||
path.join(
|
||||
path.dirname(this.electron.app.getPath('exe')),
|
||||
'resources',
|
||||
'clink',
|
||||
`clink_${process.arch}.exe`,
|
||||
),
|
||||
'inject',
|
||||
]
|
||||
},
|
||||
{ id: 'cmd', name: 'CMD (stock)', command: 'cmd.exe' },
|
||||
{ id: 'powershell', name: 'PowerShell', command: 'powershell.exe' },
|
||||
]
|
||||
}
|
||||
}
|
31
terminus-terminal/src/shells/wsl.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import * as fs from 'mz/fs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HostAppService, Platform } from 'terminus-core'
|
||||
|
||||
import { ShellProvider, IShell } from '../api'
|
||||
|
||||
@Injectable()
|
||||
export class WSLShellProvider extends ShellProvider {
|
||||
constructor (
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async provide (): Promise<IShell[]> {
|
||||
if (this.hostApp.platform !== Platform.Windows) {
|
||||
return []
|
||||
}
|
||||
|
||||
const wslPath = `${process.env.windir}\\system32\\bash.exe`
|
||||
if (!await fs.exists(wslPath)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{
|
||||
id: 'wsl',
|
||||
name: 'Bash on Windows',
|
||||
command: wslPath
|
||||
}]
|
||||
}
|
||||
}
|
@ -3,6 +3,10 @@
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"declarationDir": "dist"
|
||||
"declarationDir": "dist",
|
||||
"paths": {
|
||||
"terminus-*": ["terminus-*"],
|
||||
"*": ["app/node_modules/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,9 +34,17 @@ module.exports = {
|
||||
{ test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
|
||||
{ test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] },
|
||||
{ test: /\.css$/, use: ['to-string-loader', 'css-loader'] },
|
||||
{
|
||||
test: /\.(ttf|eot|otf|woff|woff2|ogg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
limit: 999999999999,
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
externals: [
|
||||
'electron',
|
||||
'fs',
|
||||
'font-manager',
|
||||
'path',
|
||||
|
135
terminus-terminal/yarn.lock
Normal file
@ -0,0 +1,135 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/deep-equal@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.0.tgz#9ebeaa73d1fc4791f038a5f1440e0449ea968495"
|
||||
|
||||
"@types/mz@0.0.31":
|
||||
version "0.0.31"
|
||||
resolved "https://registry.yarnpkg.com/@types/mz/-/mz-0.0.31.tgz#a4d80c082fefe71e40a7c0f07d1e6555bbbc7b52"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*", "@types/node@7.0.12":
|
||||
version "7.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.12.tgz#ae5f67a19c15f752148004db07cbbb372e69efc9"
|
||||
|
||||
"@types/webpack-env@1.13.0":
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.0.tgz#3044381647e11ee973c5af2e925323930f691d80"
|
||||
|
||||
"@types/winreg@^1.2.30":
|
||||
version "1.2.30"
|
||||
resolved "https://registry.yarnpkg.com/@types/winreg/-/winreg-1.2.30.tgz#91d6710e536d345b9c9b017c574cf6a8da64c518"
|
||||
|
||||
any-promise@^1.0.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
|
||||
|
||||
big.js@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978"
|
||||
|
||||
connected-domain@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/connected-domain/-/connected-domain-1.0.0.tgz#bfe77238c74be453a79f0cb6058deeb4f2358e93"
|
||||
|
||||
dataurl@0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/dataurl/-/dataurl-0.1.0.tgz#1f4734feddec05ffe445747978d86759c4b33199"
|
||||
|
||||
deep-equal@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
|
||||
|
||||
emojis-list@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||
|
||||
file-loader@^0.11.2:
|
||||
version "0.11.2"
|
||||
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.11.2.tgz#4ff1df28af38719a6098093b88c82c71d1794a34"
|
||||
dependencies:
|
||||
loader-utils "^1.0.2"
|
||||
|
||||
font-manager@0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/font-manager/-/font-manager-0.2.2.tgz#18a1c5b6ec7f91e22a17c71cbbaa0ea4e68e3a44"
|
||||
dependencies:
|
||||
nan "~2.2.0"
|
||||
|
||||
hterm-umdjs@1.1.3:
|
||||
version "1.1.3+1.58.sha.15ed490"
|
||||
resolved "https://registry.yarnpkg.com/hterm-umdjs/-/hterm-umdjs-1.1.3.tgz#8b57bcaded5ba9541d6c8e32a82b34abb93e885e"
|
||||
|
||||
json5@^0.5.0:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
|
||||
|
||||
loader-utils@^1.0.2:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
|
||||
dependencies:
|
||||
big.js "^3.1.3"
|
||||
emojis-list "^2.0.0"
|
||||
json5 "^0.5.0"
|
||||
|
||||
mz@^2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mz/-/mz-2.6.0.tgz#c8b8521d958df0a4f2768025db69c719ee4ef1ce"
|
||||
dependencies:
|
||||
any-promise "^1.0.0"
|
||||
object-assign "^4.0.1"
|
||||
thenify-all "^1.0.0"
|
||||
|
||||
nan@2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.0.tgz#aa8f1e34531d807e9e27755b234b4a6ec0c152a8"
|
||||
|
||||
nan@~2.2.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.2.1.tgz#d68693f6b34bb41d66bc68b3a4f9defc79d7149b"
|
||||
|
||||
node-pty@0.6.8:
|
||||
version "0.6.8"
|
||||
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.6.8.tgz#a7b145397bef23a719128a75b20d4821726dfe90"
|
||||
dependencies:
|
||||
nan "2.5.0"
|
||||
|
||||
object-assign@^4.0.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
|
||||
ps-node@^0.1.6:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/ps-node/-/ps-node-0.1.6.tgz#9af67a99d7b1d0132e51a503099d38a8d2ace2c3"
|
||||
dependencies:
|
||||
table-parser "^0.1.3"
|
||||
|
||||
runes@^0.4.2:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/runes/-/runes-0.4.2.tgz#1ddc1ea41de769cb32fc068a64fbbc45cd21052e"
|
||||
|
||||
table-parser@^0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/table-parser/-/table-parser-0.1.3.tgz#0441cfce16a59481684c27d1b5a67ff15a43c7b0"
|
||||
dependencies:
|
||||
connected-domain "^1.0.0"
|
||||
|
||||
thenify-all@^1.0.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
|
||||
dependencies:
|
||||
thenify ">= 3.1.0 < 4"
|
||||
|
||||
"thenify@>= 3.1.0 < 4":
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839"
|
||||
dependencies:
|
||||
any-promise "^1.0.0"
|
||||
|
||||
winreg@^1.2.3:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b"
|