Compare commits

...

116 Commits

Author SHA1 Message Date
Eugene Pankov
d42e070e6c use new style xterm events 2019-04-17 22:03:20 +02:00
Eugene Pankov
c5958bc9a0 bumped xterm for true color support (#23) 2019-04-17 21:57:52 +02:00
Eugene Pankov
9dc2337787 hotkey naming 2019-04-16 15:42:11 +02:00
Eugene
1a38cc30a8 Merge pull request #766 from Mezner/master
Create close pane hotkey.
2019-04-16 15:39:22 +02:00
Russell Myers
4949f14184 Create close pane hotkey. 2019-04-15 21:16:54 -04:00
Eugene
11902020a5 Merge pull request #743 from mischah/relaxed-theme
Add Relaxed color scheme
2019-03-30 08:30:05 -07:00
Michael Kühnel
3f96c21f33 Add Relaxed color scheme 2019-03-30 16:22:20 +01:00
Eugene
9e81f0aa0e Update README.md 2019-03-19 13:26:06 +01:00
Eugene
1cce23cef5 Update README.md 2019-03-19 13:02:33 +01:00
Eugene Pankov
b7a56adb60 smaller icons for the compact theme 2019-03-18 22:34:46 +01:00
Eugene Pankov
eb02752cbf use dynamic xterm atlas for faster startup 2019-03-18 22:23:44 +01:00
Eugene Pankov
3a6eb8cb2f bumped plugin versions 2019-03-18 22:02:23 +01:00
Eugene Pankov
a7d62b0234 fixed home/end keys with conpty (fixes #587) 2019-03-18 22:00:43 +01:00
Eugene
b9cbe4f12d Update snapcraft.yaml 2019-03-13 12:45:41 +01:00
Eugene
5f74b35ba9 Update snapcraft.yaml 2019-03-13 12:39:14 +01:00
Eugene
affb439ab2 Update snapcraft.yaml 2019-03-13 12:33:48 +01:00
Eugene Pankov
afdb06df40 fixed npm require path (fixes #714) 2019-03-11 14:01:51 +01:00
Eugene Pankov
2227f2cff3 bumped node-pty 2019-03-08 13:11:01 +01:00
Eugene Pankov
1a46f0ec3c fixed cwd detection for split tabs 2019-03-08 12:35:14 +01:00
Eugene Pankov
e246e22bfd set TERM_PROGRAM (fixes #702) 2019-03-08 12:27:57 +01:00
Eugene Pankov
89e4a80a37 fixed xterm hotkeys - fixed #696 2019-03-08 12:24:42 +01:00
Eugene
3a522f4f73 Update README.md 2019-03-08 08:32:01 +01:00
Eugene Pankov
a01238d34c travis 2019-03-07 23:17:44 +01:00
Eugene Pankov
75fea5e390 Merge branch 'master' of github.com:Eugeny/terminus 2019-03-07 22:38:26 +01:00
Eugene Pankov
af792399da travis 2019-03-07 22:38:05 +01:00
Eugene
7d86a7e4cc Update .travis.yml 2019-03-07 21:31:49 +01:00
Eugene Pankov
cf6a7d10b3 travis 2019-03-07 19:27:38 +01:00
Eugene Pankov
4bc03319e7 travis 2019-03-07 18:15:52 +01:00
Eugene Pankov
8cf7851801 autogen docs 2019-03-07 18:04:03 +01:00
Eugene Pankov
c70e6fde35 prep other plugins for typedoc 2019-03-07 02:05:26 +01:00
Eugene Pankov
2ed35cb400 prepared terminus-core for typedocs 2019-03-07 01:51:15 +01:00
Eugene Pankov
cf5e31be79 angular bump 2019-03-06 23:46:08 +01:00
Eugene Pankov
2c4c094941 splits API 2019-03-06 23:46:04 +01:00
Eugene Pankov
d25751abe7 handle split pane being closed 2019-03-04 21:26:28 +01:00
Eugene Pankov
bcc4a262e2 build fix 2019-03-04 21:03:36 +01:00
Eugene Pankov
27fb611166 splitting tabs (fixes #49) 2019-03-04 20:39:05 +01:00
Eugene Pankov
7279ba13ac split tab recovery (#49) 2019-03-03 23:51:25 +01:00
Eugene Pankov
70b463b086 splits WIP (#49) 2019-03-03 22:56:58 +01:00
Eugene Pankov
ef4e1e5a0d registry fixes 2019-03-02 17:44:02 +01:00
Eugene Pankov
04d621d62b use native registry implementation 2019-03-02 15:56:58 +01:00
Eugene Pankov
dc813b8edb windows fix 2019-03-02 15:34:18 +01:00
Eugene Pankov
426bedd7b7 restored readme images 2019-03-01 18:10:55 +01:00
Eugene Pankov
34b7c80243 website moved to gh-pages branch 2019-02-24 15:07:14 +01:00
Eugene Pankov
33281b5caf skip unnecessary ops 2019-02-24 14:39:05 +01:00
Eugene Pankov
e6e88272c2 force yarn relink on travis 2019-02-20 10:51:32 +01:00
Eugene Pankov
05bb07e37d bumped electron 2019-02-20 01:57:38 +01:00
Eugene Pankov
61726cad36 parse iTerm OSC 1337 cwd reports 2019-02-20 01:07:05 +01:00
Eugene Pankov
77c253594f don't offer separate wsl distros until Win 10 17763 (fixes #642) 2019-02-20 00:04:06 +01:00
Eugene
478d715d10 Gitter link 2019-02-19 00:26:31 +01:00
Eugene Pankov
1675312f75 added hotkeys to open specific profiles 2019-02-17 13:12:05 +01:00
Eugene Pankov
128aa618f0 avoid importing npm until required 2019-02-17 13:02:56 +01:00
Eugene Pankov
6b56155ca5 fixed #517 2019-02-17 12:32:37 +01:00
Eugene Pankov
3ee1f1b023 Update readme.png 2019-02-13 23:12:47 +01:00
Eugene Pankov
db54f4af13 a button to show config file location 2019-02-13 22:58:47 +01:00
Eugene Pankov
59d1a2fc23 bundle NPM with Terminus (fixes #584, fixes #10) 2019-02-13 21:35:37 +01:00
Eugene Pankov
808e7f4699 build fix 2019-02-12 13:02:59 +01:00
Eugene Pankov
e338bc417a only configure tabs if they're focused (fixes #659) 2019-02-12 12:44:23 +01:00
Eugene Pankov
3039a65757 fixed conpty detection (fixes #653) 2019-02-12 11:19:17 +01:00
Eugene Pankov
714f181be5 try to force terminus onto the low-power gpu 2019-02-11 15:48:16 +01:00
Eugene Pankov
329d0448d3 reconfigure terminals on DPI change (fixes #576) 2019-02-10 00:23:49 +01:00
Eugene Pankov
100436f511 set xterm as default frontend (fixes #542) 2019-02-09 22:40:31 +01:00
Eugene Pankov
22d3e35723 ignore events on destroyed windows 2019-02-09 22:38:45 +01:00
Eugene Pankov
9cdcc8d8e5 fixed #649 2019-02-09 22:10:42 +01:00
Eugene Pankov
168e6f17dc allow selecting ssh ciphers (fixes #645) 2019-02-09 18:52:09 +01:00
Eugene Pankov
a2c636fdbf console logging 2019-02-09 17:44:23 +01:00
Eugene Pankov
413ca70729 Warn when enabling ConPTY on older insider builds (fixes #609, fixes #594) 2019-02-09 17:44:17 +01:00
Eugene Pankov
6f99e6c14b Merge branch 'master' of github.com:Eugeny/terminus 2019-01-30 19:46:25 +01:00
Eugene Pankov
e65811786d bumped node-ssh2 (fixes #605) 2019-01-30 18:11:49 +01:00
Eugene
aecd381b25 Merge pull request #633 from sylveon/master
Update windows-swca dependency
2019-01-30 16:48:11 +01:00
Eugene Pankov
89465f57d5 bumped node-pty 2019-01-30 13:02:35 +01:00
Charles Milette
3bf0ac43ef Update yarn lockfile 2019-01-29 15:37:40 -05:00
Charles Milette
a66dd43e1e Update windows-swca dependency 2019-01-29 15:33:50 -05:00
Eugene Pankov
dd4566cf02 #618 fixes 2019-01-27 23:58:55 +01:00
Eugene Pankov
f2be34d137 limit max font size (fixes #618) 2019-01-27 23:40:33 +01:00
Eugene Pankov
e28c619bdc force en-us locale (#618) 2019-01-27 23:39:05 +01:00
Eugene Pankov
04bf5dbcfb fixed offset with tabs on bottom on macos (fixes #629) 2019-01-27 23:12:46 +01:00
Eugene Pankov
a2128ca1f2 use ssh connection name for the tab's title (fixes #621) 2019-01-27 22:56:50 +01:00
Eugene Pankov
bf0d02d1fc tab duplication (fixes #588) 2019-01-27 22:45:08 +01:00
Eugene Pankov
792de65696 properly recover tabs with xterm 2019-01-27 22:01:55 +01:00
Eugene Pankov
fab21f6859 mention save-output plugin 2019-01-27 22:01:46 +01:00
Eugene
b0b01b98be Update README.md 2019-01-21 15:52:55 +01:00
Eugene
24dff4b5b7 Update README.md 2019-01-18 13:56:08 +00:00
Eugene Pankov
78f8f4005e fixed #610 2019-01-16 17:13:34 +00:00
Eugene Pankov
38cfb3f036 middle click to paste (fixes #613) 2019-01-16 16:46:01 +00:00
Eugene Pankov
4e4d8a0e91 bumped node-pty 2019-01-16 16:25:43 +00:00
Eugene Pankov
21cfd14f1c use the upstream xtermjs 2019-01-16 16:16:06 +00:00
Eugene Pankov
a64bbe145c fixed automatic resizing with xterm 2019-01-16 15:23:55 +00:00
Eugene Pankov
6a5dc79c5d bumped plugin versions 2019-01-10 12:44:12 +01:00
Eugene Pankov
b799128427 fixed TerminalContextMenuProvider typing 2019-01-10 12:44:07 +01:00
Eugene Pankov
8b64a819e7 expose DOM element ref from BaseTerminalTab 2019-01-08 16:37:54 +03:00
Eugene Pankov
5b78a5c1ed made tab context menu extensible 2019-01-07 19:30:03 +03:00
Eugene Pankov
91b318853f replace the stock installer gif (fixes #606) 2019-01-07 17:31:16 +03:00
Eugene Pankov
ce3610c2da automatically recover ssh tabs (fixes #583) 2019-01-06 11:54:26 +01:00
Eugene Pankov
d03430fb2e ssh - show connection log while connecting 2019-01-06 11:14:13 +01:00
Eugene Pankov
caacc01aea split common terminal behaviour into BaseTerminalTab 2019-01-05 16:54:22 +01:00
Eugene Pankov
bcb6963c35 show ssh connection errors 2019-01-05 15:19:02 +01:00
Eugene Pankov
deb99b0865 wrap TerminalTab into SSHTab 2019-01-05 15:17:41 +01:00
Eugene Pankov
2101c18657 fixed saving ssh connections (fixes #436) 2019-01-05 15:03:31 +01:00
Eugene Pankov
1a258f32b0 fixed npm detection when fish is the default shell (#584) 2019-01-05 14:53:19 +01:00
Eugene Pankov
3aaf490f57 fixed #597 2019-01-05 14:51:36 +01:00
Eugene Pankov
9faa346699 better messageboxes 2019-01-03 17:20:02 +03:00
Eugene Pankov
d5b6a686f8 added settings tab icons 2019-01-03 17:19:50 +03:00
Eugene Pankov
492d006f64 xterm scrollback fix 2019-01-03 17:07:38 +03:00
Eugene Pankov
d999320c24 bumped plugin versions 2019-01-03 13:08:57 +03:00
Eugene Pankov
5142d12e7e fixed macos zip artifact naming 2019-01-03 13:01:15 +03:00
Eugene Pankov
453c613571 bumped xterm scrollback size (fixes #589) 2019-01-03 12:55:14 +03:00
Eugene Pankov
ccc34ae4d9 bumped angular to rc 2018-12-30 17:53:07 +01:00
Eugene Pankov
4362b5c50b bumped node-gyp 2018-12-30 17:44:52 +01:00
Eugene Pankov
2d6023446c bumped electron-builder 2018-12-30 17:35:09 +01:00
Eugene Pankov
dcd43dc019 fixed the Preferences menu item 2018-12-30 17:32:30 +01:00
Eugene Pankov
d8e70f9693 bumped nodejs on travis 2018-12-30 17:26:01 +01:00
Eugene Pankov
7a26e8bd65 ignore non-existent CWDs (fixes #586) 2018-12-30 15:59:40 +01:00
Eugene Pankov
d56287587c bumped electron to stable 2018-12-30 15:54:17 +01:00
Eugene Pankov
8793613117 potentially fixed #576 2018-12-29 13:27:45 +01:00
Eugene Pankov
92afec75e7 fixed plugin blacklisting 2018-12-29 12:50:14 +01:00
Eugene Pankov
ca71ec24f8 fixed #585 2018-12-29 12:41:32 +01:00
180 changed files with 6216 additions and 1456 deletions

3
.gitignore vendored
View File

@@ -18,3 +18,6 @@ npm-debug.log
builtin-plugins
package-lock.json
yarn-error.log
docs/api
.travis.ssh.key

BIN
.travis.ssh.key.enc Normal file

Binary file not shown.

1
.travis.ssh.key.pub Normal file
View File

@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDDFM4nHSbET5V7EYNgjA8NeVfOxV0wVMdZ2YvsDzD+qPJ4+MYbvsL7ZPaSxQSn7n6ATkLHjKje5RpF/Rl9K3kucGs0P6cqJVeE0qryEteQ3Q+fYAk+bD2J9ZQ/hv/0NtLl8T+7lJUZ3WUxFH73sgph77Sw0z+kMpPaK7U2vqMBQD/7+6iJgya31wP0qW0XKDz1BjKeXgwTg10Pm4vcGsR4c2q7YIzSzBHffcyo0vJyFvOX/ZKHlZRcq/wnQMeOl/hPgf1xCENjQZmFVReQlYSw5cNNDT9HZPKekOAZFFez7/AbPiTIo/bnBYIv0mdUjr3nw8nXF505q8LiD3z/ksaaWDqe9CCLM4W0Bh7/dhP7IGPdfX0fVHLhOnYIOsG21D8rWJjMPkVRSLyEvWNAnVuObJNHoQu8VATnOxfPNnMun72IHyyFWVoADk5JcsMbzcP7gZB+5oJO7U1qpcdndtBOA3ZlF0Uz2jVZnqavoEBWT39tl3vs69hAA3aTPGclg7HMuAJOl4HsKmaUgDxqV2wCX/S4pDqmKMbmumDLX+MM0xl0gXj/zpVJp9BzdnrArkC40ivmC6TSA4wrdN0tNBlqApkH5/jxGWrcu2AXVn9PGF3+QrjW0iu+QMZCaKWDhLIQC835uFwzhnNGlx41B7uxMLuNFxKXdQ3f/cC9QMG8ew== TravisCIDeployKey

View File

@@ -1,28 +1,48 @@
matrix:
include:
- os: linux
env: BUILD_FOR=linux
- os: osx
env: BUILD_FOR=macos
language: node_js
node_js: 8
node_js: 11
cache:
directories:
- node_modules
- app/node_modules
stages:
- Build
- name: Docs
if: branch = master
before_install:
- yarn
jobs:
include:
- stage: 'Build'
os: linux
before_install:
- yarn
- rm app/node_modules/.yarn-integrity || true
- scripts/install-deps.js
script:
- scripts/build-native.js
- yarn run build
- scripts/prepackage-plugins.js
- scripts/build-linux.js
script:
- scripts/build-native.js
- yarn run build
- scripts/prepackage-plugins.js
- scripts/build-$BUILD_FOR.js
- stage: 'Build'
os: osx
before_install:
- rm app/node_modules/.yarn-integrity || true
- yarn
script:
- scripts/build-native.js
- yarn run build
- scripts/prepackage-plugins.js
- scripts/build-macos.js
dist: trusty
- stage: 'Docs'
os: linux
script:
- openssl aes-256-cbc -K $encrypted_4e2fb4889ef8_key -iv $encrypted_4e2fb4889ef8_iv -in .travis.ssh.key.enc -out .travis.ssh.key -d
- eval "$(ssh-agent -s)"
- chmod 600 .travis.ssh.key
- ssh-add .travis.ssh.key
- yarn
- yarn run docs
- rsync -e "ssh -o StrictHostKeyChecking=no" -arv docs/api/ root@ajenti.org:/srv/terminus-docs/
dist: xenial
sudo: false
addons:

View File

@@ -1,30 +1,36 @@
![](https://github.com/Eugeny/terminus/raw/master/docs/readme.png)
<p align="center">
<a href="https://raw.githubusercontent.com/Eugeny/terminus/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg"/></a> <a href="https://travis-ci.org/Eugeny/terminus"><img src="https://travis-ci.org/Eugeny/terminus.svg?branch=master"/></a>
<a href="https://ci.appveyor.com/project/Eugeny/terminus"><img src="https://ci.appveyor.com/api/projects/status/wnnq4hm5mbd9rgoy?svg=true"/></a>
<a href="https://raw.githubusercontent.com/Eugeny/terminus/master/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/eugeny/terminus.svg?label=License&style=flat-square"></a> <a href="https://travis-ci.org/Eugeny/terminus"><img alt="Travis (.org)" src="https://img.shields.io/travis/Eugeny/terminus.svg?label=CI&logo=travis&logoColor=white&style=flat-square"></a>
<a href="https://ci.appveyor.com/project/Eugeny/terminus"><img alt="AppVeyor" src="https://img.shields.io/appveyor/ci/eugeny/terminus.svg?label=CI&logo=appveyor&logoColor=white&style=flat-square"></a>
</p>
<p align="center">
<a href="https://github.com/Eugeny/terminus/releases/latest">Downloads</a> | <a href="https://t.me/joinchat/AAAAAEZuCv2WKKYcfyQ3QA">Community</a> | <a href="https://ci.appveyor.com/project/Eugeny/terminus/build/artifacts">Latest Windows nightly</a>
<a href="https://github.com/Eugeny/terminus/releases/latest"><img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/eugeny/terminus/total.svg?label=DOWNLOAD&logo=github&style=for-the-badge"></a> <a href="https://ci.appveyor.com/project/Eugeny/terminus/build/artifacts"><img src="https://img.shields.io/badge/download-nightly%20build-magenta.svg?logo=appveyor&style=for-the-badge"/></a> <a href="https://gitter.im/terminus-terminal/community"><img alt="Gitter" src="https://img.shields.io/gitter/room/terminus/community.svg?color=blue&logo=gitter&style=for-the-badge"></a>
</p>
----
**Terminus** is a terminal heavily inspired by Hyper. It is, however, designed for people who need to get things done.
**Terminus** is a highly configurable terminal emulator for Windows, macOS and Linux
* Runs on Windows, macOS and Linux
* Theming and color schemes
* Fully configurable shortcuts
* Split panes
* Remembers your tabs
* PowerShell (and PS Core), WSL, Git-Bash, Cygwin, Cmder and CMD support
* Integrated SSH client and connection manager
* Full Unicode support including double-width characters
* Doesn't choke on fast-flowing outputs
* Proper shell-like experience on Windows including tab completion (via Clink)
* PowerShell Core, WSL (Bash on Windows), PowerShell, Git-Bash, Cygwin, Cmder and CMD support
* Tab persistence on macOS and Linux
* Proper shell experience on Windows including tab completion (via Clink)
[![Buy me a coffee](https://github.com/Eugeny/terminus/raw/master/docs/kofi.png)](https://ko-fi.com/eugeny)
---
* **Terminus is** an alternative to Windows' standard terminal (conhost), PowerShell ISE, PuTTY or iTerm
* **Terminus is not** a new shell or a MinGW or Cygwin replacement. Neither is it lightweight - if RAM usage is of importance, consider [Conemu](https://conemu.github.io) or [Alacritty](https://github.com/jwilm/alacritty)
---
@@ -38,6 +44,7 @@ Plugins can be installed directly from the Settings view inside Terminus.
* [title-control](https://github.com/kbjr/terminus-title-control) - allows modifying the title of the terminal tabs by providing a prefix, suffix, and/or strings to be removed
* [scrollbar](https://github.com/kbjr/terminus-scrollbar) - adds a scrollbar to terminal tabs
* [quick-cmds](https://github.com/Domain/terminus-quick-cmds) - quickly send commands to one or all terminal tabs
* [save-output](https://github.com/Eugeny/terminus-save-output) - record terminal output into a file
---
@@ -45,8 +52,7 @@ Plugins can be installed directly from the Settings view inside Terminus.
Pull requests and plugins are welcome!
See [HACKING.md](https://github.com/Eugeny/terminus/blob/master/HACKING.md) for information of how the project is laid out, and a very brief plugin development tutorial.
See [HACKING.md](https://github.com/Eugeny/terminus/blob/master/HACKING.md) and [API docs](http://ajenti.org/terminus-docs/) for information of how the project is laid out, and a very brief plugin development tutorial.
## License
[![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)

View File

@@ -1,4 +1,5 @@
import { app, ipcMain, Menu, Tray, shell } from 'electron'
import * as electron from 'electron'
import { loadConfig } from './config'
import { Window, WindowOptions } from './window'
@@ -18,6 +19,12 @@ export class Application {
}
app.commandLine.appendSwitch('disable-http-cache')
app.commandLine.appendSwitch('force_discrete_gpu', '0')
app.commandLine.appendSwitch('lang', 'EN')
}
init () {
electron.screen.on('display-metrics-changed', () => this.broadcast('host:display-metrics-changed'))
}
async newWindow (options?: WindowOptions): Promise<Window> {
@@ -103,7 +110,7 @@ export class Application {
{
label: 'Preferences',
accelerator: 'Cmd+,',
async click () {
click: async () => {
if (!this.hasWindows()) {
await this.newWindow()
}

View File

@@ -12,7 +12,6 @@ if (!process.env.TERMINUS_PLUGINS) {
const application = new Application()
ipcMain.on('app:new-window', () => {
console.log('new-window')
application.newWindow()
})
@@ -59,5 +58,6 @@ app.on('ready', () => {
}
]))
}
application.init()
application.newWindow({ hidden: argv.hidden })
})

View File

@@ -1,4 +1,5 @@
import { Subject, Observable } from 'rxjs'
import { debounceTime } from 'rxjs/operators'
import { BrowserWindow, app, ipcMain, Rectangle } from 'electron'
import ElectronConfig = require('electron-config')
import * as os from 'os'
@@ -10,7 +11,7 @@ let AccentState: any
let DwmEnableBlurBehindWindow: any
if (process.platform === 'win32') {
SetWindowCompositionAttribute = require('windows-swca').SetWindowCompositionAttribute
AccentState = require('windows-swca').AccentState
AccentState = require('windows-swca').ACCENT_STATE
DwmEnableBlurBehindWindow = require('windows-blurbehind').DwmEnableBlurBehindWindow
}
@@ -43,7 +44,9 @@ export class Window {
title: 'Terminus',
minWidth: 400,
minHeight: 300,
webPreferences: { webSecurity: false },
webPreferences: {
nodeIntegration: true,
},
frame: false,
show: false,
backgroundColor: '#00000000'
@@ -102,16 +105,14 @@ export class Window {
if (process.platform === 'win32') {
if (parseFloat(os.release()) >= 10) {
let attribValue = AccentState.ACCENT_DISABLED
let color = 0x00000000
if (enabled) {
if (parseInt(os.release().split('.')[2]) >= 17063 && type === 'fluent') {
attribValue = AccentState.ACCENT_ENABLE_FLUENT
color = 0x01000000 // using a small alpha because acrylic bugs out at full transparency.
attribValue = AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND
} else {
attribValue = AccentState.ACCENT_ENABLE_BLURBEHIND
}
}
SetWindowCompositionAttribute(this.window, attribValue, color)
SetWindowCompositionAttribute(this.window.getNativeWindowHandle(), attribValue, 0x00000000)
} else {
DwmEnableBlurBehindWindow(this.window, enabled)
}
@@ -143,6 +144,16 @@ export class Window {
this.visible.next(false)
})
let moveSubscription = new Observable<void>(observer => {
this.window.on('move', () => observer.next())
}).pipe(debounceTime(250)).subscribe(() => {
this.window.webContents.send('host:window-moved')
})
this.window.on('closed', () => {
moveSubscription.unsubscribe()
})
this.window.on('enter-full-screen', () => this.window.webContents.send('host:window-enter-full-screen'))
this.window.on('leave-full-screen', () => this.window.webContents.send('host:window-leave-full-screen'))
@@ -173,28 +184,28 @@ export class Window {
})
ipcMain.on('window-focus', event => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.focus()
})
ipcMain.on('window-maximize', event => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.maximize()
})
ipcMain.on('window-unmaximize', event => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.unmaximize()
})
ipcMain.on('window-toggle-maximize', event => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
if (this.window.isMaximized()) {
@@ -205,42 +216,42 @@ export class Window {
})
ipcMain.on('window-minimize', event => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.minimize()
})
ipcMain.on('window-set-bounds', (event, bounds) => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.setBounds(bounds)
})
ipcMain.on('window-set-always-on-top', (event, flag) => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.setAlwaysOnTop(flag)
})
ipcMain.on('window-set-vibrancy', (event, enabled, type) => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.setVibrancy(enabled, type)
})
ipcMain.on('window-set-title', (event, title) => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.window.setTitle(title)
})
ipcMain.on('window-bring-to-front', event => {
if (event.sender !== this.window.webContents) {
if (!this.window || event.sender !== this.window.webContents) {
return
}
if (this.window.isMinimized()) {
@@ -250,7 +261,10 @@ export class Window {
this.window.moveTop()
})
ipcMain.on('window-close', () => {
ipcMain.on('window-close', event => {
if (!this.window || event.sender !== this.window.webContents) {
return
}
this.closing = true
this.window.close()
})

View File

@@ -13,13 +13,13 @@
"watch": "webpack --progress --color --watch"
},
"dependencies": {
"@angular/animations": "7.2.0-beta.1",
"@angular/common": "7.2.0-beta.1",
"@angular/compiler": "7.2.0-beta.1",
"@angular/core": "7.2.0-beta.1",
"@angular/forms": "7.2.0-beta.1",
"@angular/platform-browser": "7.2.0-beta.1",
"@angular/platform-browser-dynamic": "7.2.0-beta.1",
"@angular/animations": "7.2.8",
"@angular/common": "7.2.8",
"@angular/compiler": "7.2.8",
"@angular/core": "7.2.8",
"@angular/forms": "7.2.8",
"@angular/platform-browser": "7.2.8",
"@angular/platform-browser-dynamic": "7.2.8",
"@ng-bootstrap/ng-bootstrap": "^3.3.1",
"devtron": "1.4.0",
"electron-config": "0.2.1",
@@ -36,7 +36,7 @@
},
"optionalDependencies": {
"windows-blurbehind": "^1.0.0",
"windows-swca": "^1.1.1"
"windows-swca": "^2.0.1"
},
"devDependencies": {
"@types/mz": "0.0.31"

View File

@@ -15,6 +15,8 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { getRootModule } from './app.module'
import { findPlugins, loadPlugins, IPluginInfo } from './plugins'
;(process as any).enablePromiseAPI = true
if (process.platform === 'win32') {
process.env.HOME = process.env.HOMEDRIVE + process.env.HOMEPATH
}

View File

@@ -16,11 +16,11 @@ function normalizePath (path: string): string {
nodeRequire.main.paths.map(x => nodeModule.globalPaths.push(normalizePath(x)))
if (process.env.DEV) {
if (process.env.TERMINUS_DEV) {
nodeModule.globalPaths.unshift(path.dirname(require('electron').remote.app.getAppPath()))
}
const builtinPluginsPath = process.env.DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
const builtinPluginsPath = process.env.TERMINUS_DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
const userPluginsPath = path.join(
require('electron').remote.app.getPath('appData'),

View File

@@ -9,7 +9,6 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,

View File

@@ -9,7 +9,7 @@ module.exports = {
preload: path.resolve(__dirname, 'src/entry.preload.ts'),
bundle: path.resolve(__dirname, 'src/entry.ts'),
},
mode: process.env.DEV ? 'development' : 'production',
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
optimization:{
minimize: false,
},

View File

@@ -7,7 +7,7 @@ module.exports = {
entry: {
main: path.resolve(__dirname, 'lib/index.ts'),
},
mode: process.env.DEV ? 'development' : 'production',
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
context: __dirname,
devtool: 'source-map',
output: {

View File

@@ -2,52 +2,52 @@
# yarn lockfile v1
"@angular/animations@7.2.0-beta.1":
version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.2.0-beta.1.tgz#c288de4a89b0197ba53a8411173ec4085fbbd9c9"
integrity sha512-82u9L2poaREjkTJlYEKdOO1B1LaVBwqJBZvIXU04+21WQBJoi050sxUl6lmjVVs5rRc0e/y2gifyrb42pUEntA==
"@angular/animations@7.2.8":
version "7.2.8"
resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.2.8.tgz#0285364c839c660a934ab0f753ec21424bfb292e"
integrity sha512-dJn9koYukyz15TouBc+z5z9fdThDk+bKgdlij25eYSu5Mpmtk04gB4eIMQA97K0UDh1d4YukgSJ5w3ZIk0m8DQ==
dependencies:
tslib "^1.9.0"
"@angular/common@7.2.0-beta.1":
version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.2.0-beta.1.tgz#be9d14f239b7a390fc056e3bb540da181631fb1a"
integrity sha512-+aYfO20nrqurOLxM0w/UJkOHjGzNFOc2j52ggyj1vr62nTk+W63j4P8tcUsW6iHFCsOF5auSkclKUbNIliMf0A==
"@angular/common@7.2.8":
version "7.2.8"
resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.2.8.tgz#660c816b6f08cd2919a6efb7465397e4ff14d265"
integrity sha512-LgOhf68+LPndGZhtnUlGFd2goReXYmHzaFZW8gCEi9aC+H+Io8bjYh0gkH3xDreevEOe3f0z6coXNFLIxSmTuA==
dependencies:
tslib "^1.9.0"
"@angular/compiler@7.2.0-beta.1":
version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.2.0-beta.1.tgz#355a10a80615afdd1d6a5ed682222e9e57e65fd7"
integrity sha512-KxI93dLm1SPNbazfG41QcmxVS7T9VmQ8wzhMHOVJo4DP77g2E5xUB5nOInMCMI43lbZEIckBxo/ci4jwiiq8uA==
"@angular/compiler@7.2.8":
version "7.2.8"
resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.2.8.tgz#9d9c1515e99914399e6915c1c90484b1d255560b"
integrity sha512-PrU97cTsOdofpaDkxK0rWUA/CGd0u6ESOI6XvFVm5xH9zJInsdY8ShSHklnr1JJnss70e1dGKZbZq32OChxWMw==
dependencies:
tslib "^1.9.0"
"@angular/core@7.2.0-beta.1":
version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.2.0-beta.1.tgz#32bef8f3d424333791d0e0bd4a3f3afee9b1349d"
integrity sha512-gJzQAauezAMU8vEEMh1PrXv52fA9ceUXac/tJ8KIi08zEjyIRXLvKggWr7YXAbt5LwgKsn27JwecqeS5h4K/BQ==
"@angular/core@7.2.8":
version "7.2.8"
resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.2.8.tgz#6586d9b6c6321c80119b3f3e2bd0edbb32d0b649"
integrity sha512-QKwug2kWJC00zm2rvmD9mCJzsOkMVhSu8vqPWf83poWTh8+F9aIVWcy29W0VoGpBkSchOnK8hf9DnKVv28j9nw==
dependencies:
tslib "^1.9.0"
"@angular/forms@7.2.0-beta.1":
version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.2.0-beta.1.tgz#399ea4585502027d53396eac935f8f20bd5ad2bf"
integrity sha512-1NQ2hw8Y4hjgr5qXoOvQJRjmQno/fhQUuIRx0SC7hYySur1E9vcI8rZDVDB+LwiGexALh8H70zwJ64lNxzwpvA==
"@angular/forms@7.2.8":
version "7.2.8"
resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.2.8.tgz#adf194088495822d55dcf3e5bf69196dcf19465d"
integrity sha512-lbSX4IHFHz/c4e2RHiPpL8MJlzDkCuQEHnqsujDaV2X9o9fApS6+C1X4x7Z2XDKqonmeX+aHQwv9+SLejX6OyQ==
dependencies:
tslib "^1.9.0"
"@angular/platform-browser-dynamic@7.2.0-beta.1":
version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.0-beta.1.tgz#4a95ee6d53fb02e529f1f1842ee8839a25ec4790"
integrity sha512-wnf6WSfh9AsBzI8I//eNolmQ2rwMIk/KIuGmTEOAuAxRMLgqzZUA3PjX2XAE6oUUowqy1MET1UFiqjDf/NZcNQ==
"@angular/platform-browser-dynamic@7.2.8":
version "7.2.8"
resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.8.tgz#e82768900cedfa75bf453263f931a9f90f7aaab2"
integrity sha512-nOJt28A5pRn4mdL8y98V7bA6OOdMRjsQAcWCr/isGYF0l1yDC0ijUGWkHuRtj3z1/9tmERN0BLXx+xs1h4JhCQ==
dependencies:
tslib "^1.9.0"
"@angular/platform-browser@7.2.0-beta.1":
version "7.2.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.2.0-beta.1.tgz#20c272ebfcccb3151baed7ceddbafdb359f54cd8"
integrity sha512-sCMzCFCdE4Dq9foXf2PpWPegtVfirHhg+DQzoiwFDYj5i+QB9nWY7BOLlrCPAWpd8opUxCsaLrzXbfgM40FAGA==
"@angular/platform-browser@7.2.8":
version "7.2.8"
resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.2.8.tgz#11096727b99bf3d7fd82a00a3a468b933c9713bd"
integrity sha512-SizCRMc7Or27g2CugcqWnaAikRPfgLgRvb9GFFGpcgoq8CRfOVwkyR5dFZuqN39H+uwtwuTMP5OUYhZcrFNKug==
dependencies:
tslib "^1.9.0"
@@ -70,6 +70,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.13.tgz#530f0f9254209b0335bf5cc6387822594ef47093"
integrity sha512-Y3EAG7VA7NVNbZek/fjJtILnmTk/ZfpJuWZGDBqDZ1dVIxgJJJ82fXPW7pKnqyV9CD/9bcPOCi7eErUqGMHOrA==
"@types/node@^10.12.18":
version "10.12.18"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==
accessibility-developer-tools@^2.11.0:
version "2.12.0"
resolved "https://registry.yarnpkg.com/accessibility-developer-tools/-/accessibility-developer-tools-2.12.0.tgz#3da0cce9d6ec6373964b84f35db7cfc3df7ab514"
@@ -620,10 +625,12 @@ windows-blurbehind@^1.0.0:
resolved "https://registry.yarnpkg.com/windows-blurbehind/-/windows-blurbehind-1.0.0.tgz#050efb988704c44335bdc3efefd757f6e463b8ac"
integrity sha512-lO+A7fhTHO7oy9zJM3o1AdzfSQrmtPkdwvleeuww840ghijjEA1f1Zp8bKA3mJu2DFNtVT40fwmqtgsCGat4UA==
windows-swca@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/windows-swca/-/windows-swca-1.1.1.tgz#0b3530278c67d408baac71c3a6aeb16d55318bf8"
integrity sha512-hKmHrNYJD72Kg0u35fjkiFIuMKuC+Tztmf3Obnf4aTkNjstEpbSEspEeSo3ZNixaVCETA1dLbDkVUQVF1QxtWA==
windows-swca@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/windows-swca/-/windows-swca-2.0.1.tgz#25d78ce25251292061494a0ad07c02282b28b4e3"
integrity sha512-flj+HD6RUemZUvKKguLLnMUOYkQSgDu9qrhUIO4cydvtb/x+sxU8XmpZUtugYuydcdikB9zsCOMgKnAqIQ+7nw==
dependencies:
"@types/node" "^10.12.18"
wrap-ansi@^2.0.0:
version "2.1.0"

BIN
build/windows/squirrel.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

1
docs/dist/bundle.js vendored

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

View File

@@ -1,9 +0,0 @@
<!DOCTYPE html><html><head><base href="dist/"><meta name="viewport" content="initial-scale=1, minimal-ui, shrink-to-fit=no"><link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400" rel="stylesheet"><script src="bundle.js"></script><title>Terminus</title></head><body><div class="mt-5 mb-5" id="header"><div class="text-center"><h1>Terminus</h1><div class="subtitle mb-3">A terminal for a more modern age</div><a class="btn btn-lg btn-outline-dark mt-4" href="https://github.com/Eugeny/terminus/releases/latest" target="_blank"><strong>DOWNLOAD</strong></a><a class="btn btn-lg btn-outline-secondary mt-4 ml-3" href="https://github.com/Eugeny/terminus" target="_blank"><strong>GITHUB</strong></a></div></div><div class="background-stripe"><div class="overlay overlay1"></div><div class="overlay overlay2"></div><div class="terminal"></div></div><div class="container mt-5 mb-5"><div class="d-flex flex-wrap flex-md-nowrap"><div class="w-100"><div class="feature">windows</div><div class="feature">linux</div><div class="feature">macos</div><br><div class="feature">powershell</div><div class="feature">wsl</div><div class="feature">cygwin</div><div class="feature">git-bash</div><div class="feature">cmder</div><div class="feature">clink</div></div><div class="w-100"><div class="feature">full unicode</div><div class="feature">global hotkey</div><div class="feature">plugins</div><div class="feature">tab recovery</div><div class="feature">custom css</div><div class="feature">themes</div><div class="feature">font ligatures</div><div class="feature">clickable paths</div><div class="feature">tabs on top/bottom</div><div class="feature">vibrancy</div><div class="feature">bracketed paste</div></div></div></div><div class="container mt-5 mb-5"><div class="text-center mt-5"><div class="mb-4 mt-5"><script type="text/javascript" src="https://ko-fi.com/widgets/widget_2.js"></script><script type="text/javascript">kofiwidget2.init('Buy me a coffee', '#46b798', 'J3J8KWTF')
kofiwidget2.draw()
</script></div><a class="btn btn-lg btn-outline-secondary mt-3" href="/terminus/#header"><strong>BEAM ME UP</strong></a></div></div><div class="background-stripe2"><div class="overlay overlay1"></div></div><script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-3278102-18', 'auto');
ga('send', 'pageview');</script></body></html>

View File

@@ -1 +0,0 @@
import './styles.scss'

View File

@@ -1,75 +0,0 @@
doctype html
html
head
base(href='dist/')
meta(name='viewport', content='initial-scale=1, minimal-ui, shrink-to-fit=no')
link(href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400", rel="stylesheet")
script(src='bundle.js')
title Terminus
body
.mt-5.mb-5#header
.text-center
h1 Terminus
.subtitle.mb-3 A terminal for a more modern age
a.btn.btn-lg.btn-outline-dark.mt-4(href='https://github.com/Eugeny/terminus/releases/latest', target='_blank')
strong DOWNLOAD
a.btn.btn-lg.btn-outline-secondary.mt-4.ml-3(href='https://github.com/Eugeny/terminus', target='_blank')
strong GITHUB
.background-stripe
.overlay.overlay1
.overlay.overlay2
.terminal
.container.mt-5.mb-5
.d-flex.flex-wrap.flex-md-nowrap
.w-100
.feature windows
.feature linux
.feature macos
br
.feature powershell
.feature wsl
.feature cygwin
.feature git-bash
.feature cmder
.feature clink
.w-100
.feature full unicode
.feature global hotkey
.feature plugins
.feature tab recovery
.feature custom css
.feature themes
.feature font ligatures
.feature clickable paths
.feature tabs on top/bottom
.feature vibrancy
.feature bracketed paste
.container.mt-5.mb-5
.text-center.mt-5
.mb-4.mt-5
script(type='text/javascript', src='https://ko-fi.com/widgets/widget_2.js')
script(type='text/javascript').
kofiwidget2.init('Buy me a coffee', '#46b798', 'J3J8KWTF')
kofiwidget2.draw()
a.btn.btn-lg.btn-outline-secondary.mt-3(href='/terminus/#header')
strong BEAM ME UP
.background-stripe2
.overlay.overlay1
script.
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-3278102-18', 'auto');
ga('send', 'pageview');

View File

@@ -1,24 +0,0 @@
{
"name": "docs",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "webpack --progress",
"watch": "webpack --progress --watch"
},
"private": true,
"devDependencies": {
"bootstrap": "^4.1.3",
"css-loader": "^1.0.0",
"file-loader": "^1.1.11",
"node-sass": "^4.9.3",
"pug": "^2.0.3",
"pug-cli": "^1.0.0-alpha6",
"pug-html-loader": "^1.1.5",
"sass-loader": "^7.1.0",
"style-loader": "^0.22.1",
"val-loader": "^1.1.1",
"webpack": "^4.16.5",
"webpack-cli": "^3.1.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -1,141 +0,0 @@
$font-family-sans-serif: "Source Sans Pro";
$border-radius-lg: 0;
$btn-border-width: 3px;
@import "node_modules/bootstrap/scss/bootstrap";
h1 {
font-size: 10vw;
font-weight: 200;
margin: 0;
}
body {
overflow-x: hidden;
}
.subtitle {
font-style: italic;
color: #999;
font-size: 5vw;
font-weight: 300;
}
.background-stripe {
width: 100vw;
background-image: url('./background.jpeg');
background-size: cover;
height: 30vw;
margin: 200px 0 150px;
min-height: 1000px;
position: relative;
.overlay {
position: absolute;
width: 100vw;
width: 1px;
height: 1px;
&.overlay1 {
top: -1px;
left: 0;
border-top: 10vw solid white;
border-right: 100vw solid transparent;
}
&.overlay2 {
bottom: -1px;
right: 0;
border-bottom: 10vw solid white;
border-left: 100vw solid transparent;
}
}
.terminal {
position: absolute;
left: 50%;
top: 5vw;
width: 1304px;
margin-left: -652px;
height: 972px;
border-radius: 9px;
box-shadow: 0 0 100px black;
background: url('./terminal.png');
background-size: cover;
animation: slideIn ease-out 1s;
opacity: .95;
}
@media(max-width: 1500px) {
min-height: 500px;
margin: 200px 0 100px;
.terminal {
width: 652px;
top: -100px;
margin-left: -326px;
height: 486px;
border-radius: 5px;
}
}
@media(max-width: 750px) {
min-height: 250px;
margin: 100px 0 50px;
.terminal {
width: 326px;
top: -50px;
margin-left: -163px;
height: 243px;
border-radius: 3px;
}
}
}
.feature {
font-size: 45px;
line-height: 40px;
opacity: .5;
font-style: italic;
}
@keyframes slideIn {
from {
opacity: 0;
margin-top: 200px;
}
to {
opacity: .95;
margin-top: 0px;
}
}
.background-stripe2 {
width: 100vw;
background-image: url('./background.jpeg');
background-size: cover;
height: 30vw;
margin: 100px 0 0;
position: relative;
.overlay {
position: absolute;
width: 100vw;
width: 1px;
height: 1px;
&.overlay1 {
top: -1px;
right: 0;
border-top: 10vw solid white;
border-left: 100vw solid transparent;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,27 +0,0 @@
const path = require('path')
module.exports = {
entry: {
'index.ignore': 'file-loader?name=../index.html!pug-html-loader!' + path.resolve(__dirname, './index.pug'),
'bundle': path.resolve(__dirname, 'index.js'),
},
context: __dirname,
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{
test: /\.(jpeg|png)?$/,
use: {
loader: 'file-loader',
options: {
name: 'assets/[name].[ext]'
}
}
}
]
},
}

View File

@@ -14,8 +14,8 @@
"core-js": "2.4.1",
"cross-env": "4.0.0",
"css-loader": "0.28.0",
"electron": "4.0.0-beta.7",
"electron-builder": "^20.38.2",
"electron": "4.0.5",
"electron-builder": "^20.38.4",
"electron-builder-squirrel-windows": "^20.28.3",
"electron-installer-snap": "^3.0.0",
"electron-rebuild": "^1.8.2",
@@ -24,7 +24,7 @@
"html-loader": "0.4.4",
"json-loader": "0.5.4",
"node-abi": "^2.4.4",
"node-gyp": "^3.6.2",
"node-gyp": "^3.8.0",
"node-sass": "^4.5.3",
"npmlog": "4.1.0",
"npx": "^10.2.0",
@@ -39,12 +39,13 @@
"shelljs": "0.7.7",
"source-code-pro": "^2.30.1",
"source-sans-pro": "2.0.10",
"style-loader": "0.13.1",
"style-loader": "^0.23.1",
"svg-inline-loader": "^0.8.0",
"to-string-loader": "1.1.5",
"tslint": "^5.12.0",
"tslint-config-standard": "^8.0.1",
"tslint-eslint-rules": "^5.4.0",
"typedoc": "^0.14.2",
"typescript": "^3.1.3",
"url-loader": "^1.1.1",
"val-loader": "0.5.0",
@@ -53,6 +54,9 @@
"yaml-loader": "0.4.0",
"yarn": "^1.10.1"
},
"resolutions": {
"*/node-abi": "^2.5.0"
},
"build": {
"appId": "org.terminus",
"productName": "Terminus",
@@ -74,6 +78,7 @@
},
"squirrelWindows": {
"iconUrl": "https://github.com/Eugeny/terminus/raw/master/build/windows/icon.ico",
"loadingGif": "./build/windows/squirrel.gif",
"artifactName": "terminus-${version}-setup.exe"
},
"portable": {
@@ -82,6 +87,7 @@
"mac": {
"category": "public.app-category.video",
"icon": "./build/mac/icon.icns",
"artifactName": "terminus-${version}-macos.${ext}",
"publish": [
"github"
],
@@ -122,9 +128,10 @@
},
"scripts": {
"build": "webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js",
"watch": "cross-env DEV=1 webpack --progress --color --watch",
"start": "cross-env DEV=1 electron app --debug",
"prod": "cross-env DEV=1 electron app",
"watch": "cross-env TERMINUS_DEV=1 webpack --progress --color --watch",
"start": "cross-env TERMINUS_DEV=1 electron app --debug",
"prod": "cross-env TERMINUS_DEV=1 electron app",
"docs": "typedoc --out docs/api terminus-core/src && typedoc --out docs/api/terminal terminus-terminal/src && typedoc --out docs/api/settings terminus-settings/src",
"lint": "tslint -c tslint.json -t stylish terminus-*/src/**/*.ts terminus-*/src/*.ts app/src/*.ts",
"postinstall": "node ./scripts/install-deps.js"
},

26
snap/snapcraft.yaml Normal file
View File

@@ -0,0 +1,26 @@
name: terminus
version: '1.0.0'
summary: A terminal for a modern age
description: |
Terminus is a terminal heavily inspired by Hyper. It is, however, designed for people who need to get things done.
grade: devel
confinement: devmode
apps:
terminus:
command: opt/terminus/terminus
parts:
app:
plugin: nodejs
source: .
build-packages:
- libfontconfig-dev
override-build: |
yarn
./scripts/build-native.js
yarn run build
./scripts/build-linux.js
mkdir -p $SNAPCRAFT_PART_INSTALL/opt/terminus || true
cp -ar dist/linux-unpacked/* $SNAPCRAFT_PART_INSTALL/opt/terminus/

View File

@@ -1,6 +1,6 @@
{
"name": "terminus-community-color-schemes",
"version": "1.0.0-alpha.55",
"version": "1.0.73-c4-ga7d62b0",
"description": "Community color schemes for Terminus",
"keywords": [
"terminus-builtin-plugin"

View File

@@ -0,0 +1,36 @@
! special
*.foreground: #d8d8d8
*.background: #343a43
*.cursorColor: #d8d8d8
! black
*.color0: #2c3037
*.color8: #626262
! red
*.color1: #bb5653
*.color9: #c35956
! green
*.color2: #909d62
*.color10: #9fab76
! yellow
*.color3: #eac179
*.color11: #ecc179
! blue
*.color4: #698698
*.color12: #7da9c7
! magenta
*.color5: #b06597
*.color13: #ba6ca0
! cyan
*.color6: #c9dfff
*.color14: #abbacf
! white
*.color7: #d8d8d8
*.color15: #f7f7f7

View File

@@ -13,7 +13,7 @@ module.exports = {
libraryTarget: 'umd',
devtoolModuleFilenameTemplate: 'webpack-terminus-community-color-schemes:///[resource-path]',
},
mode: process.env.DEV ? 'development' : 'production',
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
optimization:{
minimize: false,
},

31
terminus-core/README.md Normal file
View File

@@ -0,0 +1,31 @@
Terminus Core Plugin
--------------------
See also: [Settings plugin API](./settings/), [Terminal plugin API](./settings/)
* tabbed interface services
* toolbar UI
* config file management
* hotkeys
* tab recovery
* logging
* theming
Using the API:
```ts
import { AppService, TabContextMenuItemProvider } from 'terminus-core'
```
Exporting your subclasses:
```ts
@NgModule({
...
providers: [
...
{ provide: TabContextMenuItemProvider, useClass: MyContextMenu, multi: true },
...
]
})
```

View File

@@ -1,6 +1,6 @@
{
"name": "terminus-core",
"version": "1.0.0-alpha.55",
"version": "1.0.73-c4-ga7d62b0",
"description": "Terminus core",
"keywords": [
"terminus-builtin-plugin"

View File

@@ -1,4 +1,37 @@
/**
* Extend to add your own config options
*/
export abstract class ConfigProvider {
/**
* Default values, e.g.
*
* ```ts
* defaults = {
* myPlugin: {
* foo: 1
* }
* }
* ```
*/
defaults: any = {}
platformDefaults: any = {}
/**
* [[Platform]] specific defaults, e.g.
*
* ```ts
* platformDefaults = {
* [Platform.Windows]: {
* myPlugin: {
* bar: true
* }
* },
* [Platform.macOS]: {
* myPlugin: {
* bar: false
* }
* },
* }
* ```
*/
platformDefaults: {[platform: string]: any} = {}
}

View File

@@ -1,8 +1,12 @@
export interface IHotkeyDescription {
id: string,
name: string,
id: string
name: string
}
/**
* Extend to provide your own hotkeys. A corresponding [[ConfigProvider]]
* must also provide the `hotkeys.foo` config options with the default values
*/
export abstract class HotkeyProvider {
hotkeys: IHotkeyDescription[] = []

View File

@@ -1,9 +1,11 @@
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
export { SplitTabComponent, SplitContainer } from '../components/splitTab.component'
export { TabRecoveryProvider, RecoveredTab } from './tabRecovery'
export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
export { ConfigProvider } from './configProvider'
export { HotkeyProvider, IHotkeyDescription } from './hotkeyProvider'
export { Theme } from './theme'
export { TabContextMenuItemProvider } from './tabContextMenuProvider'
export { AppService } from '../services/app.service'
export { ConfigService } from '../services/config.service'
@@ -15,3 +17,4 @@ export { HotkeysService } from '../services/hotkeys.service'
export { HostAppService, Platform } from '../services/hostApp.service'
export { ShellIntegrationService } from '../services/shellIntegration.service'
export { ThemesService } from '../services/themes.service'
export { TabsService } from '../services/tabs.service'

View File

@@ -0,0 +1,11 @@
import { BaseTabComponent } from '../components/baseTab.component'
import { TabHeaderComponent } from '../components/tabHeader.component'
/**
* Extend to add items to the tab header's context menu
*/
export abstract class TabContextMenuItemProvider {
weight = 0
abstract async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]>
}

View File

@@ -1,10 +1,38 @@
import { TabComponentType } from '../services/app.service'
import { TabComponentType } from '../services/tabs.service'
export interface RecoveredTab {
type: TabComponentType,
options?: any,
/**
* Component type to be instantiated
*/
type: TabComponentType
/**
* Component instance inputs
*/
options?: any
}
/**
* Extend to enable recovery for your custom tab.
* This works in conjunction with [[getRecoveryToken()]]
*
* Terminus will try to find any [[TabRecoveryProvider]] that is able to process
* the recovery token previously returned by [[getRecoveryToken]].
*
* Recommended token format:
*
* ```json
* {
* type: 'my-tab-type',
* foo: 'bar',
* }
* ```
*/
export abstract class TabRecoveryProvider {
/**
* @param recoveryToken a recovery token found in the saved tabs list
* @returns [[RecoveredTab]] descriptor containing tab type and component inputs
* or `null` if this token is from a different tab type or is not supported
*/
abstract async recover (recoveryToken: any): Promise<RecoveredTab | null>
}

View File

@@ -1,5 +1,13 @@
/**
* Extend to add a custom CSS theme
*/
export abstract class Theme {
name: string
/**
* Complete CSS stylesheet
*/
css: string
terminalBackground: string
}

View File

@@ -1,14 +1,34 @@
import { SafeHtml } from '@angular/platform-browser'
/**
* See [[ToolbarButtonProvider]]
*/
export interface IToolbarButton {
/**
* Raw SVG icon code
*/
icon: SafeHtml
touchBarNSImage?: string
title: string
/**
* Optional Touch Bar icon ID
*/
touchBarNSImage?: string
/**
* Optional Touch Bar button label
*/
touchBarTitle?: string
weight?: number
click: () => void
}
/**
* Extend to add buttons to the toolbar
*/
export abstract class ToolbarButtonProvider {
abstract provide (): IToolbarButton[]
}

View File

@@ -9,7 +9,6 @@ import { HotkeysService } from '../services/hotkeys.service'
import { Logger, LogService } from '../services/log.service'
import { ConfigService } from '../services/config.service'
import { DockingService } from '../services/docking.service'
import { TabRecoveryService } from '../services/tabRecovery.service'
import { ThemesService } from '../services/themes.service'
import { UpdaterService } from '../services/updater.service'
import { TouchbarService } from '../services/touchbar.service'
@@ -18,6 +17,7 @@ import { BaseTabComponent } from './baseTab.component'
import { SafeModeModalComponent } from './safeModeModal.component'
import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
/** @hidden */
@Component({
selector: 'app-root',
template: require('./appRoot.component.pug'),
@@ -69,7 +69,6 @@ export class AppRootComponent {
constructor (
private docking: DockingService,
private electron: ElectronService,
private tabRecovery: TabRecoveryService,
private hotkeys: HotkeysService,
private updater: UpdaterService,
private touchbar: TouchbarService,
@@ -128,6 +127,11 @@ export class AppRootComponent {
this.onGlobalHotkey()
})
this.hostApp.windowCloseRequest$.subscribe(async () => {
await this.app.closeAllTabs()
this.hostApp.closeWindow()
})
if (window['safeModeReason']) {
ngbModal.open(SafeModeModalComponent)
}
@@ -199,9 +203,7 @@ export class AppRootComponent {
}
async ngOnInit () {
await this.tabRecovery.recoverTabs()
this.ready = true
this.tabRecovery.saveTabs(this.app.tabs)
this.app.emitReady()
}

View File

@@ -1,27 +1,58 @@
import { Observable, Subject } from 'rxjs'
import { ViewRef } from '@angular/core'
/**
* Represents an active "process" inside a tab,
* for example, a user process running inside a terminal tab
*/
export interface BaseTabProcess {
name: string
}
/**
* Abstract base class for custom tab components
*/
export abstract class BaseTabComponent {
private static lastTabID = 0
id: number
/**
* Current tab title
*/
title: string
/**
* User-defined title override
*/
customTitle: string
hasFocus = false
/**
* Last tab activity state
*/
hasActivity = false
/**
* ViewRef to the tab DOM element
*/
hostView: ViewRef
/**
* CSS color override for the tab's header
*/
color: string = null
protected titleChange = new Subject<string>()
protected focused = new Subject<void>()
protected blurred = new Subject<void>()
protected progress = new Subject<number>()
protected activity = new Subject<boolean>()
protected destroyed = new Subject<void>()
protected hasFocus = false
/**
* Ping this if your recovery state has been changed and you want
* your tab state to be saved sooner
*/
protected recoveryStateChangedHint = new Subject<void>()
private progressClearTimeout: number
private titleChange = new Subject<string>()
private focused = new Subject<void>()
private blurred = new Subject<void>()
private progress = new Subject<number>()
private activity = new Subject<boolean>()
private destroyed = new Subject<void>()
get focused$ (): Observable<void> { return this.focused }
get blurred$ (): Observable<void> { return this.blurred }
@@ -29,9 +60,9 @@ export abstract class BaseTabComponent {
get progress$ (): Observable<number> { return this.progress }
get activity$ (): Observable<boolean> { return this.activity }
get destroyed$ (): Observable<void> { return this.destroyed }
get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint }
constructor () {
this.id = BaseTabComponent.lastTabID++
this.focused$.subscribe(() => {
this.hasFocus = true
})
@@ -47,6 +78,11 @@ export abstract class BaseTabComponent {
}
}
/**
* Sets visual progressbar on the tab
*
* @param {type} progress: value between 0 and 1, or `null` to remove
*/
setProgress (progress: number) {
this.progress.next(progress)
if (progress) {
@@ -59,24 +95,43 @@ export abstract class BaseTabComponent {
}
}
/**
* Shows the acticity marker on the tab header
*/
displayActivity (): void {
this.hasActivity = true
this.activity.next(true)
}
/**
* Removes the acticity marker from the tab header
*/
clearActivity (): void {
this.hasActivity = false
this.activity.next(false)
}
/**
* Override this and implement a [[TabRecoveryProvider]] to enable recovery
* for your custom tab
*
* @return JSON serializable tab state representation
* for your [[TabRecoveryProvider]] to parse
*/
async getRecoveryToken (): Promise<any> {
return null
}
/**
* Override this to enable task completion notifications for the tab
*/
async getCurrentProcess (): Promise<BaseTabProcess> {
return null
}
/**
* Return false to prevent the tab from being closed
*/
async canClose (): Promise<boolean> {
return true
}
@@ -89,11 +144,15 @@ export abstract class BaseTabComponent {
this.blurred.next()
}
/**
* Called before the tab is closed
*/
destroy (): void {
this.focused.complete()
this.blurred.complete()
this.titleChange.complete()
this.progress.complete()
this.recoveryStateChangedHint.complete()
this.destroyed.next()
this.destroyed.complete()
}

View File

@@ -20,6 +20,10 @@
flex-direction: row;
align-items: center;
.off {
color: rgba(0, 0, 0, .5);
}
.icon {
position: relative;
flex: none;

View File

@@ -1,6 +1,7 @@
import { NgZone, Component, Input, HostBinding, HostListener } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
/** @hidden */
@Component({
selector: 'checkbox',
template: require('./checkbox.component.pug'),

View File

@@ -1,6 +1,7 @@
import { Component, Input, ElementRef, ViewChild } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
/** @hidden */
@Component({
selector: 'rename-tab-modal',
template: require('./renameTabModal.component.pug'),

View File

@@ -1,6 +1,7 @@
import { Component, Input } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
/** @hidden */
@Component({
template: require('./safeModeModal.component.pug'),
})

View File

@@ -0,0 +1,5 @@
:host {
display: block;
position: relative;
flex: auto;
}

View File

@@ -0,0 +1,524 @@
import { Observable, Subject, Subscription } from 'rxjs'
import { Component, Injectable, ViewChild, ViewContainerRef, EmbeddedViewRef, OnInit, OnDestroy } from '@angular/core'
import { BaseTabComponent, BaseTabProcess } from './baseTab.component'
import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery'
import { TabsService } from '../services/tabs.service'
import { HotkeysService } from '../services/hotkeys.service'
import { TabRecoveryService } from '../services/tabRecovery.service'
export declare type SplitOrientation = 'v' | 'h'
export declare type SplitDirection = 'r' | 't' | 'b' | 'l'
/**
* Describes a horizontal or vertical split row or column
*/
export class SplitContainer {
orientation: SplitOrientation = 'h'
/**
* Children could be tabs or other containers
*/
children: (BaseTabComponent | SplitContainer)[] = []
/**
* Relative sizes of children, between 0 and 1. Total sum is 1
*/
ratios: number[] = []
x: number
y: number
w: number
h: number
/**
* @return Flat list of all tabs inside this container
*/
getAllTabs () {
let r = []
for (let child of this.children) {
if (child instanceof SplitContainer) {
r = r.concat(child.getAllTabs())
} else {
r.push(child)
}
}
return r
}
/**
* Remove unnecessarily nested child containers and renormalizes [[ratios]]
*/
normalize () {
for (let i = 0; i < this.children.length; i++) {
let child = this.children[i]
if (child instanceof SplitContainer) {
child.normalize()
if (child.children.length === 0) {
this.children.splice(i, 1)
this.ratios.splice(i, 1)
i--
continue
} else if (child.children.length === 1) {
this.children[i] = child.children[0]
} else if (child.orientation === this.orientation) {
let ratio = this.ratios[i]
this.children.splice(i, 1)
this.ratios.splice(i, 1)
for (let j = 0; j < child.children.length; j++) {
this.children.splice(i, 0, child.children[j])
this.ratios.splice(i, 0, child.ratios[j] * ratio)
i++
}
}
}
}
let s = 0
for (let x of this.ratios) {
s += x
}
this.ratios = this.ratios.map(x => x / s)
}
/**
* Gets the left/top side offset for the given element index (between 0 and 1)
*/
getOffsetRatio (index: number): number {
let s = 0
for (let i = 0; i < index; i++) {
s += this.ratios[i]
}
return s
}
async serialize () {
let children = []
for (let child of this.children) {
if (child instanceof SplitContainer) {
children.push(await child.serialize())
} else {
children.push(await child.getRecoveryToken())
}
}
return {
type: 'app:split-tab',
ratios: this.ratios,
orientation: this.orientation,
children,
}
}
}
/**
* Represents a spanner (draggable border between two split areas)
*/
export interface SplitSpannerInfo {
container: SplitContainer
/**
* Number of the right/bottom split in the container
*/
index: number
}
/**
* Split tab is a tab that contains other tabs and allows further splitting them
* You'll mainly encounter it inside [[AppService]].tabs
*/
@Component({
selector: 'split-tab',
template: `
<ng-container #vc></ng-container>
<split-tab-spanner
*ngFor='let spanner of _spanners'
[container]='spanner.container'
[index]='spanner.index'
(change)='onSpannerAdjusted(spanner)'
></split-tab-spanner>
`,
styles: [require('./splitTab.component.scss')],
})
export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
/** @hidden */
@ViewChild('vc', { read: ViewContainerRef }) viewContainer: ViewContainerRef
/**
* Top-level split container
*/
root: SplitContainer
/** @hidden */
_recoveredState: any
/** @hidden */
_spanners: SplitSpannerInfo[] = []
private focusedTab: BaseTabComponent
private hotkeysSubscription: Subscription
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
private tabAdded = new Subject<BaseTabComponent>()
private tabRemoved = new Subject<BaseTabComponent>()
private splitAdjusted = new Subject<SplitSpannerInfo>()
private focusChanged = new Subject<BaseTabComponent>()
get tabAdded$ (): Observable<BaseTabComponent> { return this.tabAdded }
get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
/**
* Fired when split ratio is changed for a given spanner
*/
get splitAdjusted$ (): Observable<SplitSpannerInfo> { return this.splitAdjusted }
/**
* Fired when a different sub-tab gains focus
*/
get focusChanged$ (): Observable<BaseTabComponent> { return this.focusChanged }
/** @hidden */
constructor (
private hotkeys: HotkeysService,
private tabsService: TabsService,
private tabRecovery: TabRecoveryService,
) {
super()
this.root = new SplitContainer()
this.setTitle('')
this.focused$.subscribe(() => {
this.getAllTabs().forEach(x => x.emitFocused())
this.focus(this.focusedTab)
})
this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred()))
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
if (!this.hasFocus) {
return
}
switch (hotkey) {
case 'split-right':
this.splitTab(this.focusedTab, 'r')
break
case 'split-bottom':
this.splitTab(this.focusedTab, 'b')
break
case 'split-top':
this.splitTab(this.focusedTab, 't')
break
case 'split-left':
this.splitTab(this.focusedTab, 'l')
break
case 'pane-nav-left':
this.navigate('l')
break
case 'pane-nav-right':
this.navigate('r')
break
case 'pane-nav-up':
this.navigate('t')
break
case 'pane-nav-down':
this.navigate('b')
break
case 'close-pane':
this.removeTab(this.focusedTab)
break
}
})
}
/** @hidden */
async ngOnInit () {
if (this._recoveredState) {
await this.recoverContainer(this.root, this._recoveredState)
this.layout()
setImmediate(() => {
this.getAllTabs().forEach(x => x.emitFocused())
this.focusAnyIn(this.root)
})
}
}
/** @hidden */
ngOnDestroy () {
this.hotkeysSubscription.unsubscribe()
}
/** @returns Flat list of all sub-tabs */
getAllTabs () {
return this.root.getAllTabs()
}
getFocusedTab (): BaseTabComponent {
return this.focusedTab
}
focus (tab: BaseTabComponent) {
this.focusedTab = tab
for (let x of this.getAllTabs()) {
if (x !== tab) {
x.emitBlurred()
}
}
if (tab) {
tab.emitFocused()
this.focusChanged.next(tab)
}
this.layout()
}
/**
* Focuses the first available tab inside the given [[SplitContainer]]
*/
focusAnyIn (parent: BaseTabComponent | SplitContainer) {
if (!parent) {
return
}
if (parent instanceof SplitContainer) {
this.focusAnyIn(parent.children[0])
} else {
this.focus(parent)
}
}
/**
* Inserts a new `tab` to the `side` of the `relative` tab
*/
addTab (tab: BaseTabComponent, relative: BaseTabComponent, side: SplitDirection) {
let target = this.getParentOf(relative) || this.root
let insertIndex = target.children.indexOf(relative)
if (
(target.orientation === 'v' && ['l', 'r'].includes(side)) ||
(target.orientation === 'h' && ['t', 'b'].includes(side))
) {
let newContainer = new SplitContainer()
newContainer.orientation = (target.orientation === 'v') ? 'h' : 'v'
newContainer.children = [relative]
newContainer.ratios = [1]
target.children[insertIndex] = newContainer
target = newContainer
insertIndex = 0
}
if (insertIndex === -1) {
insertIndex = 0
} else {
insertIndex += (side === 'l' || side === 't') ? 0 : 1
}
for (let i = 0; i < target.children.length; i++) {
target.ratios[i] *= target.children.length / (target.children.length + 1)
}
target.ratios.splice(insertIndex, 0, 1 / (target.children.length + 1))
target.children.splice(insertIndex, 0, tab)
this.recoveryStateChangedHint.next()
this.attachTabView(tab)
setImmediate(() => {
this.layout()
this.tabAdded.next(tab)
this.focus(tab)
})
}
removeTab (tab: BaseTabComponent) {
let parent = this.getParentOf(tab)
let index = parent.children.indexOf(tab)
parent.ratios.splice(index, 1)
parent.children.splice(index, 1)
this.detachTabView(tab)
this.layout()
this.tabRemoved.next(tab)
if (this.root.children.length === 0) {
this.destroy()
} else {
this.focusAnyIn(parent)
}
}
/**
* Moves focus in the given direction
*/
navigate (dir: SplitDirection) {
let rel: BaseTabComponent | SplitContainer = this.focusedTab
let parent = this.getParentOf(rel)
let orientation = ['l', 'r'].includes(dir) ? 'h' : 'v'
while (parent !== this.root && parent.orientation !== orientation) {
rel = parent
parent = this.getParentOf(rel)
}
if (parent.orientation !== orientation) {
return
}
let index = parent.children.indexOf(rel)
if (['l', 't'].includes(dir)) {
if (index > 0) {
this.focusAnyIn(parent.children[index - 1])
}
} else {
if (index < parent.children.length - 1) {
this.focusAnyIn(parent.children[index + 1])
}
}
}
async splitTab (tab: BaseTabComponent, dir: SplitDirection) {
let newTab = await this.tabsService.duplicate(tab)
this.addTab(newTab, tab, dir)
}
/**
* @returns the immediate parent of `tab`
*/
getParentOf (tab: BaseTabComponent | SplitContainer, root?: SplitContainer): SplitContainer {
root = root || this.root
for (let child of root.children) {
if (child instanceof SplitContainer) {
let r = this.getParentOf(tab, child)
if (r) {
return r
}
}
if (child === tab) {
return root
}
}
return null
}
/** @hidden */
async canClose (): Promise<boolean> {
return !(await Promise.all(this.getAllTabs().map(x => x.canClose()))).some(x => !x)
}
/** @hidden */
async getRecoveryToken (): Promise<any> {
return this.root.serialize()
}
/** @hidden */
async getCurrentProcess (): Promise<BaseTabProcess> {
return (await Promise.all(this.getAllTabs().map(x => x.getCurrentProcess()))).find(x => !!x)
}
/** @hidden */
onSpannerAdjusted (spanner: SplitSpannerInfo) {
this.layout()
this.splitAdjusted.next(spanner)
}
private attachTabView (tab: BaseTabComponent) {
let ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any>
this.viewRefs.set(tab, ref)
ref.rootNodes[0].addEventListener('click', () => this.focus(tab))
tab.titleChange$.subscribe(t => this.setTitle(t))
tab.activity$.subscribe(a => a ? this.displayActivity() : this.clearActivity())
tab.progress$.subscribe(p => this.setProgress(p))
if (tab.title) {
this.setTitle(tab.title)
}
tab.destroyed$.subscribe(() => {
this.removeTab(tab)
})
}
private detachTabView (tab: BaseTabComponent) {
let ref = this.viewRefs.get(tab)
this.viewRefs.delete(tab)
this.viewContainer.remove(this.viewContainer.indexOf(ref))
}
private layout () {
this.root.normalize()
this._spanners = []
this.layoutInternal(this.root, 0, 0, 100, 100)
}
private layoutInternal (root: SplitContainer, x: number, y: number, w: number, h: number) {
let size = (root.orientation === 'v') ? h : w
let sizes = root.ratios.map(x => x * size)
root.x = x
root.y = y
root.w = w
root.h = h
let offset = 0
root.children.forEach((child, i) => {
let childX = (root.orientation === 'v') ? x : (x + offset)
let childY = (root.orientation === 'v') ? (y + offset) : y
let childW = (root.orientation === 'v') ? w : sizes[i]
let childH = (root.orientation === 'v') ? sizes[i] : h
if (child instanceof SplitContainer) {
this.layoutInternal(child, childX, childY, childW, childH)
} else {
let element = this.viewRefs.get(child).rootNodes[0]
element.style.position = 'absolute'
element.style.left = `${childX}%`
element.style.top = `${childY}%`
element.style.width = `${childW}%`
element.style.height = `${childH}%`
element.style.opacity = (child === this.focusedTab) ? 1 : 0.75
}
offset += sizes[i]
if (i !== 0) {
this._spanners.push({
container: root,
index: i,
})
}
})
}
private async recoverContainer (root: SplitContainer, state: any) {
let children: (SplitContainer | BaseTabComponent)[] = []
root.orientation = state.orientation
root.ratios = state.ratios
root.children = children
for (let childState of state.children) {
if (childState.type === 'app:split-tab') {
let child = new SplitContainer()
await this.recoverContainer(child, childState)
children.push(child)
} else {
let recovered = await this.tabRecovery.recoverTab(childState)
if (recovered) {
let tab = this.tabsService.create(recovered.type, recovered.options)
children.push(tab)
this.attachTabView(tab)
} else {
state.ratios.splice(state.children.indexOf(childState), 0)
}
}
}
}
}
/** @hidden */
@Injectable()
export class SplitTabRecoveryProvider extends TabRecoveryProvider {
async recover (recoveryToken: any): Promise<RecoveredTab> {
if (recoveryToken && recoveryToken.type === 'app:split-tab') {
return {
type: SplitTabComponent,
options: { _recoveredState: recoveryToken },
}
}
return null
}
}

View File

@@ -0,0 +1,22 @@
:host {
display: block;
position: absolute;
z-index: 5;
transition: 0.125s background;
&.v {
cursor: ns-resize;
height: 10px;
margin-top: -5px;
}
&.h {
cursor: ew-resize;
width: 10px;
margin-left: -5px;
}
&:hover, &.active {
background: rgba(255, 255, 255, .125);
}
}

View File

@@ -0,0 +1,88 @@
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
import { SplitContainer } from './splitTab.component'
/** @hidden */
@Component({
selector: 'split-tab-spanner',
template: '',
styles: [require('./splitTabSpanner.component.scss')],
})
export class SplitTabSpannerComponent {
@Input() container: SplitContainer
@Input() index: number
@Output() change = new EventEmitter<void>()
@HostBinding('class.active') isActive = false
@HostBinding('class.h') isHorizontal = false
@HostBinding('class.v') isVertical = true
@HostBinding('style.left') cssLeft: string
@HostBinding('style.top') cssTop: string
@HostBinding('style.width') cssWidth: string
@HostBinding('style.height') cssHeight: string
private marginOffset = -5
constructor (private element: ElementRef) { }
ngAfterViewInit () {
this.element.nativeElement.addEventListener('mousedown', e => {
this.isActive = true
let start = this.isVertical ? e.pageY : e.pageX
let current = start
let oldPosition = this.isVertical ? this.element.nativeElement.offsetTop : this.element.nativeElement.offsetLeft
const dragHandler = e => {
current = this.isVertical ? e.pageY : e.pageX
let newPosition = oldPosition + (current - start)
if (this.isVertical) {
this.element.nativeElement.style.top = `${newPosition - this.marginOffset}px`
} else {
this.element.nativeElement.style.left = `${newPosition - this.marginOffset}px`
}
}
const offHandler = () => {
this.isActive = false
document.removeEventListener('mouseup', offHandler)
this.element.nativeElement.parentElement.removeEventListener('mousemove', dragHandler)
let diff = (current - start) / (this.isVertical ? this.element.nativeElement.parentElement.clientHeight : this.element.nativeElement.parentElement.clientWidth)
diff = Math.max(diff, -this.container.ratios[this.index - 1] + 0.1)
diff = Math.min(diff, this.container.ratios[this.index] - 0.1)
this.container.ratios[this.index - 1] += diff
this.container.ratios[this.index] -= diff
this.change.emit()
}
document.addEventListener('mouseup', offHandler)
this.element.nativeElement.parentElement.addEventListener('mousemove', dragHandler)
})
}
ngOnChanges () {
this.isHorizontal = this.container.orientation === 'h'
this.isVertical = this.container.orientation === 'v'
if (this.isVertical) {
this.setDimensions(
this.container.x,
this.container.y + this.container.h * this.container.getOffsetRatio(this.index),
this.container.w,
null
)
} else {
this.setDimensions(
this.container.x + this.container.w * this.container.getOffsetRatio(this.index),
this.container.y,
null,
this.container.h
)
}
}
private setDimensions (x: number, y: number, w: number, h: number) {
this.cssLeft = `${x}%`
this.cssTop = `${y}%`
this.cssWidth = w ? `${w}%` : null
this.cssHeight = h ? `${h}%` : null
}
}

View File

@@ -3,6 +3,7 @@ import { ConfigService } from '../services/config.service'
import { HomeBaseService } from '../services/homeBase.service'
import { IToolbarButton, ToolbarButtonProvider } from '../api'
/** @hidden */
@Component({
selector: 'start-page',
template: require('./startPage.component.pug'),

View File

@@ -1,6 +1,7 @@
import { Component, Input, ViewChild, HostBinding, ViewContainerRef, OnChanges } from '@angular/core'
import { BaseTabComponent } from '../components/baseTab.component'
/** @hidden */
@Component({
selector: 'tab-body',
template: `

View File

@@ -1,6 +1,7 @@
import { Component, Input, HostBinding, HostListener, NgZone, ViewChild, ElementRef } from '@angular/core'
import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef } from '@angular/core'
import { SortableComponent } from 'ng2-dnd'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider'
import { BaseTabComponent } from './baseTab.component'
import { RenameTabModalComponent } from './renameTabModal.component'
import { HotkeysService } from '../services/hotkeys.service'
@@ -8,16 +9,7 @@ import { ElectronService } from '../services/electron.service'
import { AppService } from '../services/app.service'
import { HostAppService, Platform } from '../services/hostApp.service'
const COLORS = [
{ name: 'No color', value: null },
{ name: 'Blue', value: '#0275d8' },
{ name: 'Green', value: '#5cb85c' },
{ name: 'Orange', value: '#f0ad4e' },
{ name: 'Purple', value: '#613d7c' },
{ name: 'Red', value: '#d9534f' },
{ name: 'Yellow', value: '#ffd500' },
]
/** @hidden */
@Component({
selector: 'tab-header',
template: require('./tabHeader.component.pug'),
@@ -31,16 +23,14 @@ export class TabHeaderComponent {
@Input() progress: number
@ViewChild('handle') handle: ElementRef
private completionNotificationEnabled = false
constructor (
public app: AppService,
private electron: ElectronService,
private zone: NgZone,
private hostApp: HostAppService,
private ngbModal: NgbModal,
private hotkeys: HotkeysService,
private parentDraggable: SortableComponent,
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
) {
this.hotkeys.matchedHotkey.subscribe((hotkey) => {
if (this.app.activeTab === this.tab) {
@@ -49,6 +39,7 @@ export class TabHeaderComponent {
}
}
})
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
}
ngOnInit () {
@@ -69,6 +60,15 @@ export class TabHeaderComponent {
}).catch(() => null)
}
async buildContextMenu (): Promise<Electron.MenuItemConstructorOptions[]> {
let items: Electron.MenuItemConstructorOptions[] = []
for (let section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this.tab, this)))) {
items.push({ type: 'separator' })
items = items.concat(section)
}
return items.slice(1)
}
@HostListener('dblclick') onDoubleClick (): void {
this.showRenameTabModal()
}
@@ -80,96 +80,7 @@ export class TabHeaderComponent {
if ($event.which === 3) {
event.preventDefault()
let contextMenu = this.electron.remote.Menu.buildFromTemplate([
{
label: 'Close',
click: () => this.zone.run(() => {
this.app.closeTab(this.tab, true)
})
},
{
label: 'Close other tabs',
click: () => this.zone.run(() => {
for (let tab of this.app.tabs.filter(x => x !== this.tab)) {
this.app.closeTab(tab, true)
}
})
},
{
label: 'Close tabs to the right',
click: () => this.zone.run(() => {
for (let tab of this.app.tabs.slice(this.app.tabs.indexOf(this.tab) + 1)) {
this.app.closeTab(tab, true)
}
})
},
{
label: 'Close tabs to the left',
click: () => this.zone.run(() => {
for (let tab of this.app.tabs.slice(0, this.app.tabs.indexOf(this.tab))) {
this.app.closeTab(tab, true)
}
})
},
{
label: 'Rename',
click: () => this.zone.run(() => this.showRenameTabModal())
},
{
label: 'Color',
sublabel: COLORS.find(x => x.value === this.tab.color).name,
submenu: COLORS.map(color => ({
label: color.name,
type: 'radio',
checked: this.tab.color === color.value,
click: () => this.zone.run(() => {
this.tab.color = color.value
}),
})),
}
])
if ((this.tab as any).saveAsProfile) {
contextMenu.append(new this.electron.MenuItem({
label: 'Save as a profile',
click: () => this.zone.run(() => (this.tab as any).saveAsProfile())
}))
}
let process = await this.tab.getCurrentProcess()
if (process) {
contextMenu.append(new this.electron.MenuItem({
id: 'sep',
type: 'separator',
}))
contextMenu.append(new this.electron.MenuItem({
id: 'process-name',
enabled: false,
label: 'Current process: ' + process.name,
}))
contextMenu.append(new this.electron.MenuItem({
id: 'completion',
label: 'Notify when done',
type: 'checkbox',
checked: this.completionNotificationEnabled,
click: () => this.zone.run(() => {
this.completionNotificationEnabled = !this.completionNotificationEnabled
if (this.completionNotificationEnabled) {
this.app.observeTabCompletion(this.tab).subscribe(() => {
new Notification('Process completed', {
body: process.name,
}).addEventListener('click', () => {
this.app.selectTab(this.tab)
})
this.completionNotificationEnabled = false
})
} else {
this.app.stopObservingTabCompletion(this.tab)
}
})
}))
}
const contextMenu = this.electron.remote.Menu.buildFromTemplate(await this.buildContextMenu())
contextMenu.popup({
x: $event.pageX,

View File

@@ -1,5 +1,6 @@
import { Component } from '@angular/core'
/** @hidden */
@Component({
selector: 'title-bar',
template: require('./titleBar.component.pug'),

View File

@@ -2,6 +2,7 @@ import { Component } from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { CheckboxComponent } from './checkbox.component'
/** @hidden */
@Component({
selector: 'toggle',
template: `

View File

@@ -9,7 +9,7 @@ button.btn.btn-secondary.btn-maximize(
svg(version='1.1', width='10', height='10')
path(d='M 0,0 0,10 10,10 10,0 Z M 1,1 9,1 9,9 1,9 Z')
button.btn.btn-secondary.btn-close(
(click)='app.closeWindow()'
(click)='closeWindow()'
)
svg(version='1.1', width='10', height='10')
path(d='M 0,0 0,0.7 4.3,5 0,9.3 0,10 0.7,10 5,5.7 9.3,10 10,10 10,9.3 5.7,5 10,0.7 10,0 9.3,0 5,4.3 0.7,0 Z')

View File

@@ -2,6 +2,7 @@ import { Component } from '@angular/core'
import { HostAppService } from '../services/hostApp.service'
import { AppService } from '../services/app.service'
/** @hidden */
@Component({
selector: 'window-controls',
template: require('./windowControls.component.pug'),
@@ -9,4 +10,9 @@ import { AppService } from '../services/app.service'
})
export class WindowControlsComponent {
constructor (public hostApp: HostAppService, public app: AppService) { }
async closeWindow () {
await this.app.closeAllTabs()
this.hostApp.closeWindow()
}
}

View File

@@ -1,6 +1,7 @@
import { ConfigProvider } from './api/configProvider'
import { Platform } from './services/hostApp.service'
/** @hidden */
export class CoreConfigProvider extends ConfigProvider {
platformDefaults = {
[Platform.macOS]: require('./configDefaults.macos.yaml'),

View File

@@ -36,4 +36,19 @@ hotkeys:
- 'Alt-9'
tab-10:
- 'Alt-0'
split-right:
- 'Ctrl-Shift-E'
split-bottom:
- 'Ctrl-Shift-D'
split-left: []
split-top: []
pane-nav-right:
- 'Ctrl-Alt-ArrowRight'
pane-nav-down:
- 'Ctrl-Alt-ArrowDown'
pane-nav-up:
- 'Ctrl-Alt-ArrowUp'
pane-nav-left:
- 'Ctrl-Alt-ArrowLeft'
close-pane: []
pluginBlacklist: ['ssh']

View File

@@ -34,4 +34,20 @@ hotkeys:
- '⌘-9'
tab-10:
- '⌘-0'
split-right:
- '⌘-Shift-D'
split-bottom:
- '⌘-D'
split-left: []
split-top: []
pane-nav-right:
- '⌘-⌥-ArrowRight'
pane-nav-down:
- '⌘-⌥-ArrowDown'
pane-nav-up:
- '⌘-⌥-ArrowUp'
pane-nav-left:
- '⌘-⌥-ArrowLeft'
close-pane:
- '⌘-Shift-W'
pluginBlacklist: ['ssh']

View File

@@ -36,4 +36,19 @@ hotkeys:
- 'Alt-9'
tab-10:
- 'Alt-0'
split-right:
- 'Ctrl-Shift-E'
split-bottom:
- 'Ctrl-Shift-D'
split-left: []
split-top: []
pane-nav-right:
- 'Ctrl-Alt-ArrowRight'
pane-nav-down:
- 'Ctrl-Alt-ArrowDown'
pane-nav-up:
- 'Ctrl-Alt-ArrowUp'
pane-nav-left:
- 'Ctrl-Alt-ArrowLeft'
close-pane: []
pluginBlacklist: []

View File

@@ -1,5 +1,6 @@
import { Directive, AfterViewInit, ElementRef } from '@angular/core'
/** @hidden */
@Directive({
selector: '[autofocus]'
})

View File

@@ -0,0 +1,121 @@
import { Injectable } from '@angular/core'
import { IHotkeyDescription, HotkeyProvider } from './api/hotkeyProvider'
/** @hidden */
@Injectable()
export class AppHotkeyProvider extends HotkeyProvider {
hotkeys: IHotkeyDescription[] = [
{
id: 'new-window',
name: 'New window',
},
{
id: 'toggle-window',
name: 'Toggle terminal window',
},
{
id: 'toggle-fullscreen',
name: 'Toggle fullscreen mode',
},
{
id: 'rename-tab',
name: 'Rename Tab',
},
{
id: 'close-tab',
name: 'Close tab',
},
{
id: 'toggle-last-tab',
name: 'Toggle last tab',
},
{
id: 'next-tab',
name: 'Next tab',
},
{
id: 'previous-tab',
name: 'Previous tab',
},
{
id: 'tab-1',
name: 'Tab 1',
},
{
id: 'tab-2',
name: 'Tab 2',
},
{
id: 'tab-3',
name: 'Tab 3',
},
{
id: 'tab-4',
name: 'Tab 4',
},
{
id: 'tab-5',
name: 'Tab 5',
},
{
id: 'tab-6',
name: 'Tab 6',
},
{
id: 'tab-7',
name: 'Tab 7',
},
{
id: 'tab-8',
name: 'Tab 8',
},
{
id: 'tab-9',
name: 'Tab 9',
},
{
id: 'tab-10',
name: 'Tab 10',
},
{
id: 'split-right',
name: 'Split to the right',
},
{
id: 'split-bottom',
name: 'Split to the bottom',
},
{
id: 'split-left',
name: 'Split to the left',
},
{
id: 'split-top',
name: 'Split to the top',
},
{
id: 'pane-nav-up',
name: 'Focus the pane above',
},
{
id: 'pane-nav-down',
name: 'Focus the pane below',
},
{
id: 'pane-nav-left',
name: 'Focus the pane on the left',
},
{
id: 'pane-nav-right',
name: 'Focus the pane on the right',
},
{
id: 'close-pane',
name: 'Close focused pane',
},
]
async provide (): Promise<IHotkeyDescription[]> {
return this.hotkeys
}
}

View File

@@ -6,8 +6,6 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
import { DndModule } from 'ng2-dnd'
import { AppHotkeyProvider } from './services/hotkeys.service'
import { AppRootComponent } from './components/appRoot.component'
import { CheckboxComponent } from './components/checkbox.component'
import { TabBodyComponent } from './components/tabBody.component'
@@ -18,15 +16,21 @@ import { TitleBarComponent } from './components/titleBar.component'
import { ToggleComponent } from './components/toggle.component'
import { WindowControlsComponent } from './components/windowControls.component'
import { RenameTabModalComponent } from './components/renameTabModal.component'
import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component'
import { SplitTabSpannerComponent } from './components/splitTabSpanner.component'
import { AutofocusDirective } from './directives/autofocus.directive'
import { HotkeyProvider } from './api/hotkeyProvider'
import { ConfigProvider } from './api/configProvider'
import { Theme } from './api/theme'
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
import { TabRecoveryProvider } from './api/tabRecovery'
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
import { CoreConfigProvider } from './config'
import { AppHotkeyProvider } from './hotkeys'
import { TaskCompletionContextMenu, CommonOptionsContextMenu, CloseContextMenu } from './tabContextMenu'
import 'perfect-scrollbar/css/perfect-scrollbar.css'
import 'ng2-dnd/bundles/style.css'
@@ -37,9 +41,14 @@ const PROVIDERS = [
{ provide: Theme, useClass: StandardCompactTheme, multi: true },
{ provide: Theme, useClass: PaperTheme, multi: true },
{ provide: ConfigProvider, useClass: CoreConfigProvider, multi: true },
{ provide: TabContextMenuItemProvider, useClass: CommonOptionsContextMenu, multi: true },
{ provide: TabContextMenuItemProvider, useClass: CloseContextMenu, multi: true },
{ provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true },
{ provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true },
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } }
]
/** @hidden */
@NgModule({
imports: [
BrowserModule,
@@ -61,10 +70,13 @@ const PROVIDERS = [
RenameTabModalComponent,
SafeModeModalComponent,
AutofocusDirective,
SplitTabComponent,
SplitTabSpannerComponent,
],
entryComponents: [
RenameTabModalComponent,
SafeModeModalComponent,
SplitTabComponent,
],
exports: [
CheckboxComponent,

View File

@@ -1,12 +1,12 @@
import { Observable, Subject, AsyncSubject } from 'rxjs'
import { takeUntil } from 'rxjs/operators'
import { Injectable, ComponentFactoryResolver, Injector } from '@angular/core'
import { Injectable } from '@angular/core'
import { BaseTabComponent } from '../components/baseTab.component'
import { Logger, LogService } from './log.service'
import { SplitTabComponent } from '../components/splitTab.component'
import { ConfigService } from './config.service'
import { HostAppService } from './hostApp.service'
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
import { TabRecoveryService } from './tabRecovery.service'
import { TabsService, TabComponentType } from './tabs.service'
class CompletionObserver {
get done$ (): Observable<void> { return this.done }
@@ -38,9 +38,11 @@ class CompletionObserver {
@Injectable({ providedIn: 'root' })
export class AppService {
tabs: BaseTabComponent[] = []
activeTab: BaseTabComponent
lastTabIndex = 0
logger: Logger
get activeTab (): BaseTabComponent { return this._activeTab }
private lastTabIndex = 0
private _activeTab: BaseTabComponent
private activeTabChange = new Subject<BaseTabComponent>()
private tabsChanged = new Subject<void>()
@@ -54,62 +56,105 @@ export class AppService {
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
get tabsChanged$ (): Observable<void> { return this.tabsChanged }
get tabClosed$ (): Observable<BaseTabComponent> { return this.tabClosed }
/** Fires once when the app is ready */
get ready$ (): Observable<void> { return this.ready }
/** @hidden */
constructor (
private componentFactoryResolver: ComponentFactoryResolver,
private config: ConfigService,
private hostApp: HostAppService,
private injector: Injector,
log: LogService,
private tabRecovery: TabRecoveryService,
private tabsService: TabsService,
) {
this.logger = log.create('app')
this.tabRecovery.recoverTabs().then(tabs => {
for (let tab of tabs) {
this.openNewTabRaw(tab.type, tab.options)
}
this.hostApp.windowCloseRequest$.subscribe(() => this.closeWindow())
this.tabsChanged$.subscribe(() => {
tabRecovery.saveTabs(this.tabs)
})
setInterval(() => {
tabRecovery.saveTabs(this.tabs)
}, 30000)
})
}
openNewTab (type: TabComponentType, inputs?: any): BaseTabComponent {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(type)
let componentRef = componentFactory.create(this.injector)
let tab = componentRef.instance
tab.hostView = componentRef.hostView
Object.assign(tab, inputs || {})
private addTabRaw (tab: BaseTabComponent) {
this.tabs.push(tab)
this.selectTab(tab)
this.tabsChanged.next()
this.tabOpened.next(tab)
tab.recoveryStateChangedHint$.subscribe(() => {
this.tabRecovery.saveTabs(this.tabs)
})
tab.titleChange$.subscribe(title => {
if (tab === this.activeTab) {
if (tab === this._activeTab) {
this.hostApp.setTitle(title)
}
})
tab.destroyed$.subscribe(() => {
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
this.tabs = this.tabs.filter((x) => x !== tab)
if (tab === this._activeTab) {
this.selectTab(this.tabs[newIndex])
}
this.tabsChanged.next()
this.tabClosed.next(tab)
})
}
/**
* Adds a new tab **without** wrapping it in a SplitTabComponent
* @param inputs Properties to be assigned on the new tab component instance
*/
openNewTabRaw (type: TabComponentType, inputs?: any): BaseTabComponent {
let tab = this.tabsService.create(type, inputs)
this.addTabRaw(tab)
return tab
}
/**
* Adds a new tab while wrapping it in a SplitTabComponent
* @param inputs Properties to be assigned on the new tab component instance
*/
openNewTab (type: TabComponentType, inputs?: any): BaseTabComponent {
let splitTab = this.tabsService.create(SplitTabComponent) as SplitTabComponent
let tab = this.tabsService.create(type, inputs)
splitTab.addTab(tab, null, 'r')
this.addTabRaw(splitTab)
return tab
}
selectTab (tab: BaseTabComponent) {
if (this.activeTab === tab) {
this.activeTab.emitFocused()
if (this._activeTab === tab) {
this._activeTab.emitFocused()
return
}
if (this.tabs.includes(this.activeTab)) {
this.lastTabIndex = this.tabs.indexOf(this.activeTab)
if (this.tabs.includes(this._activeTab)) {
this.lastTabIndex = this.tabs.indexOf(this._activeTab)
} else {
this.lastTabIndex = null
}
if (this.activeTab) {
this.activeTab.clearActivity()
this.activeTab.emitBlurred()
if (this._activeTab) {
this._activeTab.clearActivity()
this._activeTab.emitBlurred()
}
this.activeTab = tab
this._activeTab = tab
this.activeTabChange.next(tab)
if (this.activeTab) {
this.activeTab.emitFocused()
this.hostApp.setTitle(this.activeTab.title)
if (this._activeTab) {
setImmediate(() => {
this._activeTab.emitFocused()
})
this.hostApp.setTitle(this._activeTab.title)
}
}
/** Switches between the current tab and the previously active one */
toggleLastTab () {
if (!this.lastTabIndex || this.lastTabIndex >= this.tabs.length) {
this.lastTabIndex = 0
@@ -119,7 +164,7 @@ export class AppService {
nextTab () {
if (this.tabs.length > 1) {
let tabIndex = this.tabs.indexOf(this.activeTab)
let tabIndex = this.tabs.indexOf(this._activeTab)
if (tabIndex < this.tabs.length - 1) {
this.selectTab(this.tabs[tabIndex + 1])
} else if (this.config.store.appearance.cycleTabs) {
@@ -130,7 +175,7 @@ export class AppService {
previousTab () {
if (this.tabs.length > 1) {
let tabIndex = this.tabs.indexOf(this.activeTab)
let tabIndex = this.tabs.indexOf(this._activeTab)
if (tabIndex > 0) {
this.selectTab(this.tabs[tabIndex - 1])
} else if (this.config.store.appearance.cycleTabs) {
@@ -139,6 +184,7 @@ export class AppService {
}
}
/** @hidden */
emitTabsChanged () {
this.tabsChanged.next()
}
@@ -150,17 +196,17 @@ export class AppService {
if (checkCanClose && !await tab.canClose()) {
return
}
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
this.tabs = this.tabs.filter((x) => x !== tab)
tab.destroy()
if (tab === this.activeTab) {
this.selectTab(this.tabs[newIndex])
}
this.tabsChanged.next()
this.tabClosed.next(tab)
}
async closeWindow () {
async duplicateTab (tab: BaseTabComponent) {
let dup = await this.tabsService.duplicate(tab)
if (dup) {
this.addTabRaw(dup)
}
}
async closeAllTabs () {
for (let tab of this.tabs) {
if (!await tab.canClose()) {
return
@@ -169,15 +215,19 @@ export class AppService {
for (let tab of this.tabs) {
tab.destroy()
}
this.hostApp.closeWindow()
}
/** @hidden */
emitReady () {
this.ready.next(null)
this.ready.complete()
this.hostApp.emitReady()
}
/**
* Returns an observable that fires once
* the tab's internal "process" (see [[BaseTabProcess]]) completes
*/
observeTabCompletion (tab: BaseTabComponent): Observable<void> {
if (!this.completionObservers.has(tab)) {
let observer = new CompletionObserver(tab)

View File

@@ -18,6 +18,7 @@ function isNonStructuralObjectMember (v) {
return v instanceof Object && !(v instanceof Array) && v.__nonStructural
}
/** @hidden */
export class ConfigProxy {
constructor (real: any, defaults: any) {
for (let key in defaults) {
@@ -76,16 +77,29 @@ export class ConfigProxy {
@Injectable({ providedIn: 'root' })
export class ConfigService {
/**
* Contains the actual config values
*/
store: any
/**
* Whether an app restart is required due to recent changes
*/
restartRequested: boolean
/**
* Full config file path
*/
path: string
private changed = new Subject<void>()
private _store: any
private path: string
private defaults: any
private servicesCache: { [id: string]: Function[] } = null
get changed$ (): Observable<void> { return this.changed }
/** @hidden */
constructor (
electron: ElectronService,
private hostApp: HostAppService,
@@ -129,10 +143,16 @@ export class ConfigService {
this.hostApp.broadcastConfigChange()
}
/**
* Reads config YAML as string
*/
readRaw (): string {
return yaml.safeDump(this._store)
}
/**
* Writes config YAML as string
*/
writeRaw (data: string): void {
this._store = yaml.safeLoad(data)
this.save()
@@ -140,7 +160,7 @@ export class ConfigService {
this.emitChange()
}
emitChange (): void {
private emitChange (): void {
this.changed.next()
}
@@ -148,6 +168,12 @@ export class ConfigService {
this.restartRequested = true
}
/**
* Filters a list of Angular services to only include those provided
* by plugins that are enabled
*
* @typeparam T Base provider type
*/
enabledServices<T> (services: T[]): T[] {
if (!this.servicesCache) {
this.servicesCache = {}

View File

@@ -10,6 +10,7 @@ export interface IScreen {
@Injectable({ providedIn: 'root' })
export class DockingService {
/** @hidden */
constructor (
private electron: ElectronService,
private config: ConfigService,
@@ -20,21 +21,23 @@ export class DockingService {
}
dock () {
let dockSide = this.config.store.appearance.dock
if (dockSide === 'off') {
this.hostApp.setAlwaysOnTop(false)
return
}
let display = this.electron.screen.getAllDisplays()
.filter((x) => x.id === this.config.store.appearance.dockScreen)[0]
if (!display) {
display = this.getCurrentScreen()
}
let dockSide = this.config.store.appearance.dock
let newBounds: Bounds = { x: 0, y: 0, width: 0, height: 0 }
let fill = this.config.store.appearance.dockFill
let [minWidth, minHeight] = this.hostApp.getWindow().getMinimumSize()
if (dockSide === 'off') {
this.hostApp.setAlwaysOnTop(false)
return
}
if (dockSide === 'left' || dockSide === 'right') {
newBounds.width = Math.max(minWidth, Math.round(fill * display.bounds.width))
newBounds.height = display.bounds.height
@@ -76,7 +79,7 @@ export class DockingService {
})
}
repositionWindow () {
private repositionWindow () {
let [x, y] = this.hostApp.getWindow().getPosition()
for (let screen of this.electron.screen.getAllDisplays()) {
let bounds = screen.bounds

View File

@@ -1,6 +1,11 @@
import { Injectable } from '@angular/core'
import { TouchBar, BrowserWindow, Menu, MenuItem } from 'electron'
export interface MessageBoxResponse {
response: number
checkboxChecked?: boolean
}
@Injectable({ providedIn: 'root' })
export class ElectronService {
app: any
@@ -19,6 +24,7 @@ export class ElectronService {
MenuItem: typeof MenuItem
private electron: any
/** @hidden */
constructor () {
this.electron = require('electron')
this.remote = this.electron.remote
@@ -37,21 +43,23 @@ export class ElectronService {
this.MenuItem = this.remote.MenuItem
}
remoteRequire (name: string): any {
return this.remote.require(name)
}
remoteRequirePluginModule (plugin: string, module: string, globals: any): any {
return this.remoteRequire(this.remoteResolvePluginModule(plugin, module, globals))
}
remoteResolvePluginModule (plugin: string, module: string, globals: any): any {
return globals.require.resolve(`${plugin}/node_modules/${module}`)
}
/**
* Removes OS focus from Terminus' window
*/
loseFocus () {
if (process.platform === 'darwin') {
this.remote.Menu.sendActionToFirstResponder('hide:')
}
}
showMessageBox (
browserWindow: Electron.BrowserWindow,
options: Electron.MessageBoxOptions
): Promise<MessageBoxResponse> {
return new Promise(resolve => {
this.dialog.showMessageBox(browserWindow, options, (response, checkboxChecked) => {
resolve({ response, checkboxChecked })
})
})
}
}

View File

@@ -9,6 +9,7 @@ import uuidv4 = require('uuid/v4')
export class HomeBaseService {
appVersion: string
/** @hidden */
constructor (
private electron: ElectronService,
private config: ConfigService,

View File

@@ -16,12 +16,19 @@ export interface Bounds {
height: number
}
/**
* Provides interaction with the main process
*/
@Injectable({ providedIn: 'root' })
export class HostAppService {
platform: Platform
nodePlatform: string
/**
* Fired once the window is visible
*/
shown = new EventEmitter<any>()
isFullScreen = false
private preferencesMenu = new Subject<void>()
private secondInstance = new Subject<void>()
private cliOpenDirectory = new Subject<string>()
@@ -30,30 +37,67 @@ export class HostAppService {
private cliOpenProfile = new Subject<string>()
private configChangeBroadcast = new Subject<void>()
private windowCloseRequest = new Subject<void>()
private windowMoved = new Subject<void>()
private displayMetricsChanged = new Subject<void>()
private logger: Logger
private windowId: number
/**
* Fired when Preferences is selected in the macOS menu
*/
get preferencesMenu$ (): Observable<void> { return this.preferencesMenu }
/**
* Fired when a second instance of Terminus is launched
*/
get secondInstance$ (): Observable<void> { return this.secondInstance }
/**
* Fired for the `terminus open` CLI command
*/
get cliOpenDirectory$ (): Observable<string> { return this.cliOpenDirectory }
/**
* Fired for the `terminus run` CLI command
*/
get cliRunCommand$ (): Observable<string[]> { return this.cliRunCommand }
/**
* Fired for the `terminus paste` CLI command
*/
get cliPaste$ (): Observable<string> { return this.cliPaste }
/**
* Fired for the `terminus profile` CLI command
*/
get cliOpenProfile$ (): Observable<string> { return this.cliOpenProfile }
/**
* Fired when another window modified the config file
*/
get configChangeBroadcast$ (): Observable<void> { return this.configChangeBroadcast }
/**
* Fired when the window close button is pressed
*/
get windowCloseRequest$ (): Observable<void> { return this.windowCloseRequest }
get windowMoved$ (): Observable<void> { return this.windowMoved }
get displayMetricsChanged$ (): Observable<void> { return this.displayMetricsChanged }
/** @hidden */
constructor (
private zone: NgZone,
private electron: ElectronService,
log: LogService,
) {
this.logger = log.create('hostApp')
this.nodePlatform = require('os').platform()
this.platform = {
win32: Platform.Windows,
darwin: Platform.macOS,
linux: Platform.Linux
}[this.nodePlatform]
}[process.platform]
this.windowId = parseInt(location.search.substring(1))
this.logger.info('Window ID:', this.windowId)
@@ -80,6 +124,14 @@ export class HostAppService {
this.zone.run(() => this.windowCloseRequest.next())
})
electron.ipcRenderer.on('host:window-moved', () => {
this.zone.run(() => this.windowMoved.next())
})
electron.ipcRenderer.on('host:display-metrics-changed', () => {
this.zone.run(() => this.displayMetricsChanged.next())
})
electron.ipcRenderer.on('host:second-instance', (_$event, argv: any, cwd: string) => this.zone.run(() => {
this.logger.info('Second instance', argv)
const op = argv._[0]
@@ -105,6 +157,9 @@ export class HostAppService {
}))
}
/**
* Returns the current remote [[BrowserWindow]]
*/
getWindow () {
return this.electron.BrowserWindow.fromId(this.windowId)
}
@@ -113,18 +168,6 @@ export class HostAppService {
this.electron.ipcRenderer.send('app:new-window')
}
getShell () {
return this.electron.shell
}
getAppPath () {
return this.electron.app.getAppPath()
}
getPath (type: string) {
return this.electron.app.getPath(type)
}
toggleFullscreen () {
let window = this.getWindow()
window.setFullScreen(!this.isFullScreen)
@@ -162,6 +205,11 @@ export class HostAppService {
this.electron.ipcRenderer.send('window-set-always-on-top', flag)
}
/**
* Sets window vibrancy mode (Windows, macOS)
*
* @param type `null`, or `fluent` when supported (Windowd only)
*/
setVibrancy (enable: boolean, type: string) {
document.body.classList.toggle('vibrant', enable)
if (this.platform === Platform.macOS) {
@@ -184,6 +232,9 @@ export class HostAppService {
this.electron.Menu.buildFromTemplate(menuDefinition).popup({})
}
/**
* Notifies other windows of config file changes
*/
broadcastConfigChange () {
this.electron.ipcRenderer.send('app:config-change')
}

View File

@@ -5,16 +5,16 @@ import { ConfigService } from '../services/config.service'
import { ElectronService } from '../services/electron.service'
export interface PartialHotkeyMatch {
id: string,
strokes: string[],
matchedLength: number,
id: string
strokes: string[]
matchedLength: number
}
const KEY_TIMEOUT = 2000
interface EventBufferEntry {
event: NativeKeyEvent,
time: number,
event: NativeKeyEvent
time: number
}
@Injectable({ providedIn: 'root' })
@@ -26,6 +26,7 @@ export class HotkeysService {
private disabledLevel = 0
private hotkeyDescriptions: IHotkeyDescription[] = []
/** @hidden */
constructor (
private zone: NgZone,
private electron: ElectronService,
@@ -51,11 +52,20 @@ export class HotkeysService {
})
}
/**
* Adds a new key event to the buffer
*
* @param name DOM event name
* @param nativeEvent event object
*/
pushKeystroke (name, nativeEvent) {
nativeEvent.event = name
this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() })
}
/**
* Check the buffer for new complete keystrokes
*/
processKeystrokes () {
if (this.isEnabled()) {
this.zone.run(() => {
@@ -84,7 +94,7 @@ export class HotkeysService {
return stringifyKeySequence(this.currentKeystrokes.map(x => x.event))
}
registerGlobalHotkey () {
private registerGlobalHotkey () {
this.electron.globalShortcut.unregisterAll()
let value = this.config.store.hotkeys['toggle-window'] || []
if (typeof value === 'string') {
@@ -103,11 +113,11 @@ export class HotkeysService {
})
}
getHotkeysConfig () {
private getHotkeysConfig () {
return this.getHotkeysConfigRecursive(this.config.store.hotkeys)
}
getHotkeysConfigRecursive (branch) {
private getHotkeysConfigRecursive (branch) {
let keys = {}
for (let key in branch) {
let value = branch[key]
@@ -129,7 +139,7 @@ export class HotkeysService {
return keys
}
getCurrentFullyMatchedHotkey (): string {
private getCurrentFullyMatchedHotkey (): string {
let currentStrokes = this.getCurrentKeystrokes()
let config = this.getHotkeysConfig()
for (let id in config) {
@@ -199,85 +209,3 @@ export class HotkeysService {
).reduce((a, b) => a.concat(b))
}
}
@Injectable()
export class AppHotkeyProvider extends HotkeyProvider {
hotkeys: IHotkeyDescription[] = [
{
id: 'new-window',
name: 'New window',
},
{
id: 'toggle-window',
name: 'Toggle terminal window',
},
{
id: 'toggle-fullscreen',
name: 'Toggle fullscreen mode',
},
{
id: 'rename-tab',
name: 'Rename Tab',
},
{
id: 'close-tab',
name: 'Close tab',
},
{
id: 'toggle-last-tab',
name: 'Toggle last tab',
},
{
id: 'next-tab',
name: 'Next tab',
},
{
id: 'previous-tab',
name: 'Previous tab',
},
{
id: 'tab-1',
name: 'Tab 1',
},
{
id: 'tab-2',
name: 'Tab 2',
},
{
id: 'tab-3',
name: 'Tab 3',
},
{
id: 'tab-4',
name: 'Tab 4',
},
{
id: 'tab-5',
name: 'Tab 5',
},
{
id: 'tab-6',
name: 'Tab 6',
},
{
id: 'tab-7',
name: 'Tab 7',
},
{
id: 'tab-8',
name: 'Tab 8',
},
{
id: 'tab-9',
name: 'Tab 9',
},
{
id: 'tab-10',
name: 'Tab 10',
},
]
async provide (): Promise<IHotkeyDescription[]> {
return this.hotkeys
}
}

View File

@@ -11,13 +11,13 @@ export const altKeyName = {
}[process.platform]
export interface NativeKeyEvent {
event?: string,
altKey: boolean,
ctrlKey: boolean,
metaKey: boolean,
shiftKey: boolean,
key: string,
keyCode: string,
event?: string
altKey: boolean
ctrlKey: boolean
metaKey: boolean
shiftKey: boolean
key: string
keyCode: string
}
export function stringifyKeySequence (events: NativeKeyEvent[]): string[] {

View File

@@ -23,7 +23,7 @@ const initializeWinston = (electron: ElectronService) => {
colorize: false
}),
new winston.transports.Console({
level: 'info',
level: 'debug',
handleExceptions: false,
json: false,
colorize: true
@@ -39,7 +39,7 @@ export class Logger {
private name: string,
) {}
doLog (level: string, ...args: any[]) {
private doLog (level: string, ...args: any[]) {
console[level](`%c[${this.name}]`, 'color: #aaa', ...args)
if (this.winstonLogger) {
this.winstonLogger[level](...args)
@@ -57,6 +57,7 @@ export class Logger {
export class LogService {
private log: any
/** @hidden */
constructor (electron: ElectronService) {
this.log = initializeWinston(electron)
}

View File

@@ -37,7 +37,7 @@ export class ShellIntegrationService {
this.updatePaths()
}
async updatePaths (): Promise<void> {
private async updatePaths (): Promise<void> {
// Update paths in case of an update
if (this.hostApp.platform === Platform.Windows) {
if (await this.isInstalled()) {

View File

@@ -2,26 +2,19 @@ import { Injectable, Inject } from '@angular/core'
import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery'
import { BaseTabComponent } from '../components/baseTab.component'
import { Logger, LogService } from '../services/log.service'
import { AppService } from '../services/app.service'
import { ConfigService } from '../services/config.service'
/** @hidden */
@Injectable({ providedIn: 'root' })
export class TabRecoveryService {
logger: Logger
constructor (
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[],
private app: AppService,
private config: ConfigService,
log: LogService
) {
this.logger = log.create('tabRecovery')
app.tabsChanged$.subscribe(() => {
this.saveTabs(app.tabs)
})
setInterval(() => {
this.saveTabs(app.tabs)
}, 30000)
}
async saveTabs (tabs: BaseTabComponent[]) {
@@ -34,25 +27,32 @@ export class TabRecoveryService {
)
}
async recoverTabs (): Promise<void> {
async recoverTab (token: any): Promise<RecoveredTab> {
for (let provider of this.config.enabledServices(this.tabRecoveryProviders)) {
try {
let tab = await provider.recover(token)
if (tab) {
return tab
}
} catch (error) {
this.logger.warn('Tab recovery crashed:', token, provider, error)
}
}
return null
}
async recoverTabs (): Promise<RecoveredTab[]> {
if (window.localStorage.tabsRecovery) {
let tabs: RecoveredTab[] = []
for (let token of JSON.parse(window.localStorage.tabsRecovery)) {
for (let provider of this.config.enabledServices(this.tabRecoveryProviders)) {
try {
let tab = await provider.recover(token)
if (tab) {
tabs.push(tab)
}
} catch (error) {
this.logger.warn('Tab recovery crashed:', token, provider, error)
}
let tab = await this.recoverTab(token)
if (tab) {
tabs.push(tab)
}
}
tabs.forEach(tab => {
this.app.openNewTab(tab.type, tab.options)
})
return tabs
}
return []
}
}

View File

@@ -0,0 +1,42 @@
import { Injectable, ComponentFactoryResolver, Injector } from '@angular/core'
import { BaseTabComponent } from '../components/baseTab.component'
import { TabRecoveryService } from './tabRecovery.service'
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
@Injectable({ providedIn: 'root' })
export class TabsService {
/** @hidden */
constructor (
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector,
private tabRecovery: TabRecoveryService,
) { }
/**
* Instantiates a tab component and assigns given inputs
*/
create (type: TabComponentType, inputs?: any): BaseTabComponent {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(type)
let componentRef = componentFactory.create(this.injector)
let tab = componentRef.instance
tab.hostView = componentRef.hostView
Object.assign(tab, inputs || {})
return tab
}
/**
* Duplicates an existing tab instance (using the tab recovery system)
*/
async duplicate (tab: BaseTabComponent): Promise<BaseTabComponent> {
let token = await tab.getRecoveryToken()
if (!token) {
return null
}
let dup = await this.tabRecovery.recoverTab(token)
if (dup) {
return this.create(dup.type, dup.options)
}
return null
}
}

View File

@@ -6,6 +6,7 @@ import { Theme } from '../api/theme'
export class ThemesService {
private styleElement: HTMLElement = null
/** @hidden */
constructor (
private config: ConfigService,
@Inject(Theme) private themes: Theme[],
@@ -34,7 +35,7 @@ export class ThemesService {
document.querySelector('style#custom-css').innerHTML = this.config.store.appearance.css
}
applyCurrentTheme (): void {
private applyCurrentTheme (): void {
this.applyTheme(this.findCurrentTheme())
}
}

View File

@@ -3,9 +3,10 @@ import { TouchBarSegmentedControl, SegmentedControlSegment } from 'electron'
import { AppService } from './app.service'
import { ConfigService } from './config.service'
import { ElectronService } from './electron.service'
import { HostAppService } from './hostApp.service'
import { HostAppService, Platform } from './hostApp.service'
import { IToolbarButton, ToolbarButtonProvider } from '../api'
/** @hidden */
@Injectable({ providedIn: 'root' })
export class TouchbarService {
private tabsSegmentedControl: TouchBarSegmentedControl
@@ -20,6 +21,9 @@ export class TouchbarService {
private electron: ElectronService,
private zone: NgZone,
) {
if (this.hostApp.platform !== Platform.macOS) {
return
}
app.tabsChanged$.subscribe(() => this.update())
app.activeTabChange$.subscribe(() => this.update())
app.tabOpened$.subscribe(tab => {
@@ -31,6 +35,10 @@ export class TouchbarService {
}
update () {
if (this.hostApp.platform !== Platform.macOS) {
return
}
let buttons: IToolbarButton[] = []
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
buttons = buttons.concat(provider.provide())

View File

@@ -6,6 +6,7 @@ import { ElectronService } from './electron.service'
const UPDATES_URL = 'https://api.github.com/repos/eugeny/terminus/releases/latest'
/** @hidden */
@Injectable({ providedIn: 'root' })
export class UpdaterService {
private logger: Logger

View File

@@ -0,0 +1,147 @@
import { Injectable, NgZone } from '@angular/core'
import { AppService } from './services/app.service'
import { BaseTabComponent } from './components/baseTab.component'
import { TabHeaderComponent } from './components/tabHeader.component'
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
/** @hidden */
@Injectable()
export class CloseContextMenu extends TabContextMenuItemProvider {
weight = -5
constructor (
private app: AppService,
private zone: NgZone,
) {
super()
}
async getItems (tab: BaseTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
return [
{
label: 'Close',
click: () => this.zone.run(() => {
this.app.closeTab(tab, true)
})
},
{
label: 'Close other tabs',
click: () => this.zone.run(() => {
for (let t of this.app.tabs.filter(x => x !== tab)) {
this.app.closeTab(t, true)
}
})
},
{
label: 'Close tabs to the right',
click: () => this.zone.run(() => {
for (let t of this.app.tabs.slice(this.app.tabs.indexOf(tab) + 1)) {
this.app.closeTab(t, true)
}
})
},
{
label: 'Close tabs to the left',
click: () => this.zone.run(() => {
for (let t of this.app.tabs.slice(0, this.app.tabs.indexOf(tab))) {
this.app.closeTab(t, true)
}
})
},
]
}
}
const COLORS = [
{ name: 'No color', value: null },
{ name: 'Blue', value: '#0275d8' },
{ name: 'Green', value: '#5cb85c' },
{ name: 'Orange', value: '#f0ad4e' },
{ name: 'Purple', value: '#613d7c' },
{ name: 'Red', value: '#d9534f' },
{ name: 'Yellow', value: '#ffd500' },
]
/** @hidden */
@Injectable()
export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
weight = -1
constructor (
private zone: NgZone,
private app: AppService,
) {
super()
}
async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]> {
return [
{
label: 'Rename',
click: () => this.zone.run(() => tabHeader.showRenameTabModal())
},
{
label: 'Duplicate',
click: () => this.zone.run(() => this.app.duplicateTab(tab))
},
{
label: 'Color',
sublabel: COLORS.find(x => x.value === tab.color).name,
submenu: COLORS.map(color => ({
label: color.name,
type: 'radio',
checked: tab.color === color.value,
click: () => this.zone.run(() => {
tab.color = color.value
}),
})) as Electron.MenuItemConstructorOptions[],
}
]
}
}
/** @hidden */
@Injectable()
export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
constructor (
private app: AppService,
private zone: NgZone,
) {
super()
}
async getItems (tab: BaseTabComponent): Promise<Electron.MenuItemConstructorOptions[]> {
let process = await tab.getCurrentProcess()
if (process) {
return [
{
id: 'process-name',
enabled: false,
label: 'Current process: ' + process.name,
},
{
label: 'Notify when done',
type: 'checkbox',
checked: (tab as any).__completionNotificationEnabled,
click: () => this.zone.run(() => {
(tab as any).__completionNotificationEnabled = !(tab as any).__completionNotificationEnabled
if ((tab as any).__completionNotificationEnabled) {
this.app.observeTabCompletion(tab).subscribe(() => {
new Notification('Process completed', {
body: process.name,
}).addEventListener('click', () => {
this.app.selectTab(tab)
})
;(tab as any).__completionNotificationEnabled = false
})
} else {
this.app.stopObservingTabCompletion(tab)
}
})
},
]
}
return []
}
}

View File

@@ -6,6 +6,10 @@ app-root {
.btn-tab-bar {
line-height: 29px !important;
svg {
height: 14px;
}
}
}

View File

@@ -1,23 +1,26 @@
import { Injectable } from '@angular/core'
import { Theme } from './api'
/** @hidden */
@Injectable()
export class StandardTheme extends Theme {
name = 'Standard'
css = require('./theme.scss')
terminalBackground = '#1D272D'
terminalBackground = '#222a33'
}
/** @hidden */
@Injectable()
export class StandardCompactTheme extends Theme {
name = 'Compact'
css = require('./theme.compact.scss')
terminalBackground = '#1D272D'
terminalBackground = '#222a33'
}
/** @hidden */
@Injectable()
export class PaperTheme extends Theme {
name = 'Paper'
css = require('./theme.paper.scss')
terminalBackground = '#1D272D'
terminalBackground = '#f7f1e0'
}

View File

@@ -14,7 +14,7 @@ module.exports = {
libraryTarget: 'umd',
devtoolModuleFilenameTemplate: 'webpack-terminus-core:///[resource-path]',
},
mode: process.env.DEV ? 'development' : 'production',
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
optimization:{
minimize: false,
},

View File

@@ -1,6 +1,6 @@
{
"name": "terminus-plugin-manager",
"version": "1.0.0-alpha.55",
"version": "1.0.73-c4-ga7d62b0",
"description": "Terminus' plugin manager",
"keywords": [
"terminus-builtin-plugin"
@@ -31,13 +31,14 @@
"@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": {
"axios": "^0.16.2",
"mz": "^2.6.0"
"mz": "^2.6.0",
"npm": "^6.7.0"
},
"false": {}
}

View File

@@ -20,7 +20,7 @@
small {{plugin.description}}
button.btn.btn-primary.ml-2(
*ngIf='npmInstalled && knownUpgrades[plugin.name]',
*ngIf='knownUpgrades[plugin.name]',
(click)='upgradePlugin(plugin)',
[disabled]='busy[plugin.name] != undefined'
)
@@ -42,30 +42,19 @@
button.btn.btn-danger.ml-2(
(click)='uninstallPlugin(plugin)',
*ngIf='!plugin.isBuiltin && npmInstalled',
*ngIf='!plugin.isBuiltin',
[disabled]='busy[plugin.name] != undefined'
)
i.fas.fa-fw.fa-trash(*ngIf='busy[plugin.name] != BusyState.Uninstalling')
i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='busy[plugin.name] == BusyState.Uninstalling')
.text-center.mt-5(*ngIf='npmMissing')
h4 npm not installed
p.mb-2 npm is required to install Terminus plugins.
.btn-group
button.btn.btn-outline-primary((click)='downloadNPM()')
i.fas.fa-download
span Get npm
button.btn.btn-outline-info((click)='checkNPM()')
i.fas.fa-refresh
span Try again
div(*ngIf='npmInstalled')
div
h3.mt-4 Available
.input-group.mb-3
.input-group-prepend
.input-group-text
i.fas.fa-fw.fa-circle-o-notch.fa-spin(*ngIf='!availablePluginsReady')
i.fas.fa-fw.fa-circle-notch.fa-spin(*ngIf='!availablePluginsReady')
i.fas.fa-fw.fa-search(*ngIf='availablePluginsReady')
input.form-control(
type='text',

View File

@@ -3,11 +3,12 @@ import { debounceTime, distinctUntilChanged, first, tap, flatMap } from 'rxjs/op
import * as semver from 'semver'
import { Component, Input } from '@angular/core'
import { ConfigService, HostAppService, ElectronService } from 'terminus-core'
import { ConfigService, ElectronService } from 'terminus-core'
import { IPluginInfo, PluginManagerService } from '../services/pluginManager.service'
enum BusyState { Installing, Uninstalling }
/** @hidden */
@Component({
template: require('./pluginsSettingsTab.component.pug'),
styles: [require('./pluginsSettingsTab.component.scss')],
@@ -21,13 +22,10 @@ export class PluginsSettingsTabComponent {
@Input() busy: {[id: string]: BusyState} = {}
@Input() erroredPlugin: string
@Input() errorMessage: string
@Input() npmInstalled = false
@Input() npmMissing = false
constructor (
private electron: ElectronService,
private config: ConfigService,
private hostApp: HostAppService,
public pluginManager: PluginManagerService
) {
}
@@ -50,20 +48,10 @@ export class PluginsSettingsTabComponent {
this.knownUpgrades[plugin.name] = available.find(x => x.name === plugin.name && semver.gt(x.version, plugin.version))
}
})
this.checkNPM()
}
openPluginsFolder (): void {
this.hostApp.getShell().openItem(this.pluginManager.userPluginsPath)
}
downloadNPM (): void {
this.hostApp.getShell().openExternal('https://nodejs.org/en/download/current/')
}
async checkNPM () {
this.npmInstalled = await this.pluginManager.isNPMInstalled()
this.npmMissing = !this.npmInstalled
this.electron.shell.openItem(this.pluginManager.userPluginsPath)
}
searchAvailable (query: string) {
@@ -117,7 +105,7 @@ export class PluginsSettingsTabComponent {
}
disablePlugin (plugin: IPluginInfo) {
this.config.store.pluginBlacklist.push(plugin.name)
this.config.store.pluginBlacklist = [...this.config.store.pluginBlacklist, plugin.name]
this.config.save()
this.config.requestRestart()
}

View File

@@ -1,7 +0,0 @@
import { ConfigProvider } from 'terminus-core'
export class PluginsConfigProvider extends ConfigProvider {
defaults = {
npm: 'npm',
}
}

View File

@@ -4,12 +4,10 @@ import { FormsModule } from '@angular/forms'
import { NgPipesModule } from 'ngx-pipes'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { ConfigProvider } from 'terminus-core'
import { SettingsTabProvider } from 'terminus-settings'
import { PluginsSettingsTabComponent } from './components/pluginsSettingsTab.component'
import { PluginManagerService } from './services/pluginManager.service'
import { PluginsConfigProvider } from './config'
import { PluginsSettingsTabProvider } from './settings'
@NgModule({
@@ -21,7 +19,6 @@ import { PluginsSettingsTabProvider } from './settings'
],
providers: [
{ provide: SettingsTabProvider, useClass: PluginsSettingsTabProvider, multi: true },
{ provide: ConfigProvider, useClass: PluginsConfigProvider, multi: true },
],
entryComponents: [
PluginsSettingsTabComponent,

View File

@@ -1,11 +1,8 @@
import * as path from 'path'
import * as fs from 'mz/fs'
import { exec } from 'mz/child_process'
import axios from 'axios'
import { Observable, from } from 'rxjs'
import { map } from 'rxjs/operators'
import { Injectable } from '@angular/core'
import { Logger, LogService, ConfigService, HostAppService, Platform } from 'terminus-core'
import { Logger, LogService } from 'terminus-core'
const NAME_PREFIX = 'terminus-'
const KEYWORD = 'terminus-plugin'
@@ -29,45 +26,35 @@ export class PluginManagerService {
builtinPluginsPath: string = (window as any).builtinPluginsPath
userPluginsPath: string = (window as any).userPluginsPath
installedPlugins: IPluginInfo[] = (window as any).installedPlugins
npmPath: string
private envPath: string
private npmReady: Promise<void>
private npm: any
constructor (
log: LogService,
private config: ConfigService,
private hostApp: HostAppService,
) {
this.logger = log.create('pluginManager')
this.detectPath()
}
async detectPath () {
this.npmPath = this.config.store.npm
this.envPath = process.env.PATH
if (await fs.exists(this.npmPath)) {
return
}
if (this.hostApp.platform !== Platform.Windows) {
this.envPath = (await exec('$SHELL -c -i \'echo $PATH\''))[0].toString().trim()
let searchPaths = this.envPath.split(':')
for (let searchPath of searchPaths) {
if (await fs.exists(path.join(searchPath, 'npm'))) {
this.logger.debug('Found npm in', searchPath)
this.npmPath = path.join(searchPath, 'npm')
return
}
async getNPM () {
if (!this.npm) {
if (!this.npmReady) {
this.npmReady = new Promise(resolve => {
const npm = require('npm')
npm.load({
prefix: this.userPluginsPath,
}, err => {
if (err) {
this.logger.error(err)
}
this.npm = npm
resolve()
})
})
}
await this.npmReady
}
}
async isNPMInstalled (): Promise<boolean> {
await this.detectPath()
try {
await exec(`${this.npmPath} -v`, { env: this.getEnv() })
return true
} catch (_) {
return false
}
return this.npm
}
listAvailable (query?: string): Observable<IPluginInfo[]> {
@@ -92,17 +79,21 @@ export class PluginManagerService {
}
async installPlugin (plugin: IPluginInfo) {
await exec(`${this.npmPath} --prefix "${this.userPluginsPath}" install ${plugin.packageName}@${plugin.version}`, { env: this.getEnv() })
this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
this.installedPlugins.push(plugin)
(await this.getNPM()).commands.install([`${plugin.packageName}@${plugin.version}`], err => {
if (err) {
this.logger.error(err)
}
this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
this.installedPlugins.push(plugin)
})
}
async uninstallPlugin (plugin: IPluginInfo) {
await exec(`${this.npmPath} --prefix "${this.userPluginsPath}" remove ${plugin.packageName}`, { env: this.getEnv() })
this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
}
private getEnv (): any {
return Object.assign(process.env, { PATH: this.envPath })
(await this.getNPM()).commands.remove([plugin.packageName], err => {
if (err) {
this.logger.error(err)
}
this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
})
}
}

View File

@@ -3,6 +3,7 @@ import { SettingsTabProvider } from 'terminus-settings'
import { PluginsSettingsTabComponent } from './components/pluginsSettingsTab.component'
/** @hidden */
@Injectable()
export class PluginsSettingsTabProvider extends SettingsTabProvider {
id = 'plugins'

View File

@@ -13,7 +13,7 @@ module.exports = {
libraryTarget: 'umd',
devtoolModuleFilenameTemplate: 'webpack-terminus-plugin-manager:///[resource-path]',
},
mode: process.env.DEV ? 'development' : 'production',
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
optimization:{
minimize: false,
},
@@ -44,6 +44,7 @@ module.exports = {
externals: [
'fs',
'font-manager',
'npm',
'path',
'mz/fs',
'mz/child_process',

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
Terminus Settings Plugin
------------------------
* tabbed settings interface
Using the API:
```ts
import { SettingsTabProvider } from 'terminus-settings'
```
Exporting your subclasses:
```ts
@NgModule({
...
providers: [
...
{ provide: SettingsTabProvider, useClass: MySettingsTab, multi: true },
...
]
})
```

View File

@@ -1,6 +1,6 @@
{
"name": "terminus-settings",
"version": "1.0.0-alpha.55",
"version": "1.0.73-c4-ga7d62b0",
"description": "Terminus terminal settings page",
"keywords": [
"terminus-builtin-plugin"

View File

@@ -1,5 +1,9 @@
/**
* Extend to add your own settings tabs
*/
export abstract class SettingsTabProvider {
id: string
icon: string
title: string
getComponentType (): any {

View File

@@ -4,6 +4,7 @@ import { ToolbarButtonProvider, IToolbarButton, AppService, HostAppService, Hotk
import { SettingsTabComponent } from './components/settingsTab.component'
/** @hidden */
@Injectable()
export class ButtonProvider extends ToolbarButtonProvider {
constructor (
@@ -37,7 +38,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
if (settingsTab) {
this.app.selectTab(settingsTab)
} else {
this.app.openNewTab(SettingsTabComponent)
this.app.openNewTabRaw(SettingsTabComponent)
}
}
}

View File

@@ -6,6 +6,7 @@ import { HotkeysService } from 'terminus-core'
const INPUT_TIMEOUT = 1000
/** @hidden */
@Component({
selector: 'hotkey-input-modal',
template: require('./hotkeyInputModal.component.pug'),

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