Compare commits
34 Commits
v1.0.0-alp
...
snap
Author | SHA1 | Date | |
---|---|---|---|
![]() |
44b4f5ccf8 | ||
![]() |
d861941b15 | ||
![]() |
578a7c1a7b | ||
![]() |
a7bee5dd01 | ||
![]() |
81579fa9cc | ||
![]() |
1826cbe83b | ||
![]() |
56bf5f888c | ||
![]() |
084be557b0 | ||
![]() |
6d81290e1d | ||
![]() |
8243a219de | ||
![]() |
5196069b33 | ||
![]() |
0a4fadd5ba | ||
![]() |
0b56259a36 | ||
![]() |
0e86894d81 | ||
![]() |
6ee5275981 | ||
![]() |
029e4016af | ||
![]() |
6119d211c2 | ||
![]() |
23e93f0969 | ||
![]() |
9e228a4e93 | ||
![]() |
100a8cacdd | ||
![]() |
abb313d118 | ||
![]() |
aaf6209d9f | ||
![]() |
e0e24878e2 | ||
![]() |
deca9a20b4 | ||
![]() |
b57ff8f37a | ||
![]() |
55a54a1399 | ||
![]() |
538b5c4c28 | ||
![]() |
0419900e1d | ||
![]() |
cc9c66c4a9 | ||
![]() |
7e253d72ea | ||
![]() |
9423ce7c10 | ||
![]() |
ac8bb2de49 | ||
![]() |
ace6446790 | ||
![]() |
259a1d26b0 |
2
.pug-lintrc.js
Normal file
@@ -0,0 +1,2 @@
|
||||
module.export = {
|
||||
}
|
38
.travis.yml
@@ -1,27 +1,35 @@
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
env: BUILD_FOR=linux
|
||||
- os: osx
|
||||
env: BUILD_FOR=macos
|
||||
|
||||
language: node_js
|
||||
node_js: 7
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
- app/node_modules
|
||||
|
||||
before_install:
|
||||
- yarn install
|
||||
- scripts/install-deps.js
|
||||
stages:
|
||||
- build
|
||||
- name: deploy
|
||||
if: branch = master
|
||||
|
||||
script:
|
||||
- scripts/build-native.js
|
||||
- yarn run build
|
||||
- scripts/prepackage-plugins.js
|
||||
- scripts/build-$BUILD_FOR.js
|
||||
jobs:
|
||||
include:
|
||||
- stage: build
|
||||
os: linux
|
||||
env: BUILD_FOR=linux
|
||||
script: ./travis.sh
|
||||
|
||||
- stage: build
|
||||
os: osx
|
||||
env: BUILD_FOR=macos
|
||||
script: ./travis.sh
|
||||
|
||||
- stage: deploy
|
||||
os: linux
|
||||
env: BUILD_FOR=linux STAGE=deploy
|
||||
script: ./travis.sh
|
||||
|
||||
dist: trusty
|
||||
sudo: false
|
||||
|
14
README.md
@@ -1,21 +1,11 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/Eugeny/terminus/master/build/icons/128x128.png">
|
||||
<h1>Terminus α</h1>
|
||||
<p>
|
||||
<i>A terminal for a more modern age</i>
|
||||
</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||

|
||||
|
||||
|
||||
[](https://travis-ci.org/Eugeny/terminus) [](https://ci.appveyor.com/project/Eugeny/terminus) [](https://raw.githubusercontent.com/Eugeny/terminus/master/LICENSE) [](https://github.com/Eugeny/terminus/releases/latest)
|
||||
[](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2FEugeny%2Fterminus?ref=badge_shield)
|
||||
|
||||
----
|
||||
|
||||

|
||||
|
||||
**Terminus** is a terminal heavily inspired by Hyper. It is, however, designed for people who need to get things done.
|
||||
|
||||
* Runs on Windows, macOS and Linux
|
||||
|
@@ -9,7 +9,9 @@ html
|
||||
script(src='./preload.js')
|
||||
script(src='./bundle.js', defer)
|
||||
style#custom-css
|
||||
body(style='background: ; min-height: 100vh; overflow: hidden')
|
||||
style.
|
||||
body { transition: 0.5s background; }
|
||||
body
|
||||
app-root
|
||||
.preload-logo
|
||||
div
|
||||
|
41
app/main.js
@@ -1,5 +1,11 @@
|
||||
if (process.platform == 'win32' && require('electron-squirrel-startup')) process.exit(0)
|
||||
|
||||
const electron = require('electron')
|
||||
let electronVibrancy
|
||||
if (process.platform != 'linux') {
|
||||
electronVibrancy = require('electron-vibrancy')
|
||||
}
|
||||
|
||||
if (process.argv.indexOf('--debug') !== -1) {
|
||||
require('electron-debug')({enabled: true, showDevTools: 'undocked'})
|
||||
}
|
||||
@@ -27,6 +33,15 @@ if (!process.env.TERMINUS_PLUGINS) {
|
||||
process.env.TERMINUS_PLUGINS = ''
|
||||
}
|
||||
|
||||
setWindowVibrancy = (enabled) => {
|
||||
if (enabled && !app.window.vibrancyViewID) {
|
||||
app.window.vibrancyViewID = electronVibrancy.SetVibrancy(app.window, 0)
|
||||
} else if (!enabled && app.window.vibrancyViewID) {
|
||||
electronVibrancy.RemoveView(app.window, app.window.vibrancyViewID)
|
||||
app.window.vibrancyViewID = null
|
||||
}
|
||||
}
|
||||
|
||||
setupWindowManagement = () => {
|
||||
app.window.on('show', () => {
|
||||
app.window.webContents.send('host:window-shown')
|
||||
@@ -84,6 +99,10 @@ setupWindowManagement = () => {
|
||||
electron.ipcMain.on('window-set-always-on-top', (event, flag) => {
|
||||
app.window.setAlwaysOnTop(flag)
|
||||
})
|
||||
|
||||
electron.ipcMain.on('window-set-vibrancy', (event, enabled) => {
|
||||
setWindowVibrancy(enabled)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -213,11 +232,9 @@ start = () => {
|
||||
title: 'Terminus',
|
||||
minWidth: 400,
|
||||
minHeight: 300,
|
||||
'web-preferences': {'web-security': false},
|
||||
//- background to avoid the flash of unstyled window
|
||||
backgroundColor: '#131d27',
|
||||
webPreferences: {webSecurity: false},
|
||||
frame: false,
|
||||
//type: 'toolbar',
|
||||
show: false,
|
||||
}
|
||||
Object.assign(options, windowConfig.get('windowBoundaries'))
|
||||
|
||||
@@ -229,18 +246,28 @@ start = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform == 'linux') {
|
||||
options.backgroundColor = '#131d27'
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch('disable-http-cache')
|
||||
|
||||
app.window = new electron.BrowserWindow(options)
|
||||
app.window.once('ready-to-show', () => {
|
||||
if (process.platform == 'darwin') {
|
||||
app.window.setVibrancy('dark')
|
||||
} else if (process.platform == 'windows') {
|
||||
setWindowVibrancy(true)
|
||||
}
|
||||
app.window.show()
|
||||
app.window.focus()
|
||||
})
|
||||
app.window.loadURL(`file://${app.getAppPath()}/dist/index.html`, {extraHeaders: "pragma: no-cache\n"})
|
||||
|
||||
if (process.platform != 'darwin') {
|
||||
app.window.setMenu(null)
|
||||
}
|
||||
|
||||
app.window.show()
|
||||
app.window.focus()
|
||||
|
||||
setupWindowManagement()
|
||||
|
||||
if (process.platform == 'darwin') {
|
||||
|
@@ -25,6 +25,7 @@
|
||||
"electron-debug": "^1.0.1",
|
||||
"electron-is-dev": "0.1.2",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"electron-vibrancy": "^0.1.3",
|
||||
"js-yaml": "3.8.2",
|
||||
"mz": "^2.6.0",
|
||||
"ngx-toastr": "^8.7.3",
|
||||
|
@@ -66,3 +66,9 @@
|
||||
[ngbradiogroup] input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: rgba(0,0,0,.4);
|
||||
}
|
||||
|
@@ -72,6 +72,10 @@ argparse@^1.0.7:
|
||||
dependencies:
|
||||
sprintf-js "~1.0.2"
|
||||
|
||||
bindings@^1.2.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7"
|
||||
|
||||
conf@^0.11.1:
|
||||
version "0.11.2"
|
||||
resolved "https://registry.yarnpkg.com/conf/-/conf-0.11.2.tgz#879f479267600483e502583462ca4063fc9779b2"
|
||||
@@ -135,6 +139,13 @@ electron-squirrel-startup@^1.0.0:
|
||||
dependencies:
|
||||
debug "^2.2.0"
|
||||
|
||||
electron-vibrancy@^0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/electron-vibrancy/-/electron-vibrancy-0.1.3.tgz#04382dd6e030e5ca5e60f8e024033738cb8479e3"
|
||||
dependencies:
|
||||
bindings "^1.2.1"
|
||||
nan "^2.0.5"
|
||||
|
||||
env-paths@^0.3.0:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-0.3.1.tgz#c30ccfcbc30c890943dc08a85582517ef00da463"
|
||||
@@ -195,6 +206,10 @@ mz@^2.6.0:
|
||||
object-assign "^4.0.1"
|
||||
thenify-all "^1.0.0"
|
||||
|
||||
nan@^2.0.5:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
|
||||
|
||||
ngx-toastr@^8.7.3:
|
||||
version "8.7.3"
|
||||
resolved "https://registry.yarnpkg.com/ngx-toastr/-/ngx-toastr-8.7.3.tgz#d3b7a8077ba1c860dd8a44779ccad38c5ea15c92"
|
||||
|
BIN
docs/background.jpeg
Normal file
After Width: | Height: | Size: 2.6 MiB |
BIN
docs/dist/assets/background.jpeg
vendored
Normal file
After Width: | Height: | Size: 2.6 MiB |
BIN
docs/dist/assets/terminal.png
vendored
Normal file
After Width: | Height: | Size: 21 KiB |
1
docs/dist/bundle.js
vendored
Normal file
BIN
docs/dist/fonts/background.jpeg
vendored
Normal file
After Width: | Height: | Size: 2.6 MiB |
@@ -1,50 +1,4 @@
|
||||
<!DOCTYPE html><html><head><link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400" rel="stylesheet"><link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"><link href="https://cdn.jsdelivr.net/g/bootstrap@4.0.0-alpha.6(css/bootstrap.min.css)" rel="stylesheet"><script src="https://cdn.jsdelivr.net/g/jquery@3.2.1,tether@1.4.0,bootstrap@4.0.0-alpha.6,modernizr@3.3.1,detectizr@2.2.0"></script><title>Terminus</title><style>body {
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
background: #111;
|
||||
color: #ccc;
|
||||
min-height: 100vh;
|
||||
background-image: radial-gradient(#111, #000);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
h1, h2, h3, h5 {
|
||||
font-weight: 300;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn i + span,
|
||||
.nav-link i + span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
color: #b6e7ff !important;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
video, img {
|
||||
max-width: 100%;
|
||||
box-shadow: 0 0 50px black;
|
||||
}
|
||||
</style><script defer>setTimeout(function () {
|
||||
/*
|
||||
if (Detectizr.os.name == 'windows') {
|
||||
$('[href="#windows"]').tab('show')
|
||||
}
|
||||
if (Detectizr.os.name == 'mac os') {
|
||||
$('[href="#macos"]').tab('show')
|
||||
}
|
||||
if (Detectizr.os.name == 'linux') {
|
||||
$('[href="#linux"]').tab('show')
|
||||
}
|
||||
*/
|
||||
})</script></head><body><div class="container mt-5 mb-5"><div class="text-center"><h1>Terminus</h1><h5>A terminal for a more modern age</h5><h2 class="text-muted">alpha</h2></div><div class="d-flex flex-row mt-5 mb-5"><ul class="nav nav-pills flex-column mr-5" style="min-width: 200px;"><li class="nav-item"><a class="nav-link active" data-toggle="tab" href="#windows" role="tab"><i class="fa fa-windows"></i><span>Windows</span></a></li><li class="nav-item"><a class="nav-link" data-toggle="tab" href="#macos" role="tab"><i class="fa fa-apple"></i><span>macOS</span></a></li><li class="nav-item"><a class="nav-link" data-toggle="tab" href="#linux" role="tab"><i class="fa fa-linux"></i><span>Linux</span></a></li></ul><div class="tab-content"><div class="tab-pane active" id="windows" role="tabpanel"><div class="row"><div class="col-6"><video src="videos/windows.mp4" autoplay loop></video></div><div class="col-6"><h3>A proper Windows experience</h3><p> <b>Clink </b>provides tab completion, readline-style editing and persistent command history on Windows.</p><p>Also supported:<ul> <li>Classic CMD</li><li>PowerShell </li><li>Bash on Windows </li></ul></p></div></div></div><div class="tab-pane" id="macos" role="tabpanel"><div class="row"><div class="col-6"><!--video(src='videos/windows.mp4', autoplay, loop)--></div><div class="col-6"><h3>Well...</h3><p>Not much to say here, it just works.</p></div></div></div><div class="tab-pane" id="linux" role="tabpanel"><div class="row"><div class="col-6"><img src="linux.png"></div><div class="col-6"><p><ul> <li>Spawn with a global hotkey</li><li>Tabs persist after restart</li><li>Auto-dock to any side of any screen</li><li>Full Unicode and double-width character support</li></ul></p></div></div></div></div></div><div class="text-center"><div class="mt-3 mb-3"><h2></h2><div><div class="btn-group mt-3 mb-1"><a class="btn btn-lg btn-outline-success" href="https://github.com/Eugeny/terminus/releases/latest" target="_blank"><i class="fa fa-download"></i><span>Downloads</span></a><a class="btn btn-lg btn-outline-secondary" href="https://github.com/Eugeny/terminus" target="_blank"><i class="fa fa-github"></i><span>GitHub</span></a></div></div><small class="text-muted">EXE, DMG, DEB, RPM, TGZ</small></div></div><div class="row mt-5"><div class="col-6"><h3>User experience</h3><ul><li>Spawn and hide with a global hotkey</li><li>Fully customizable hotkey schema</li><li>Restores tabs </li><li>Drag in a file to paste the path</li><li>Click paths and URLs to open in browser/file manager</li><li>Keeps the current directory in new tabs</li></ul></div><div class="col-6"><div class="mb-5"><h3>Customizable</h3><p>Multiple app themes and a myriad of community color schemes for the terminal. Color scheme editor included.</p></div><div> <h3>Infinitely extensible</h3><p>Install plugins from the NPM repository, or create your own with Typescript and Angular framework.</p></div></div></div></div><script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
<!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"><a class="btn btn-lg btn-outline-secondary mt-5" 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');
|
||||
|
1
docs/index.js
Normal file
@@ -0,0 +1 @@
|
||||
import './styles.scss'
|
187
docs/index.pug
@@ -1,155 +1,64 @@
|
||||
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")
|
||||
link(href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css", rel="stylesheet")
|
||||
link(href="https://cdn.jsdelivr.net/g/bootstrap@4.0.0-alpha.6(css/bootstrap.min.css)", rel="stylesheet")
|
||||
script(src="https://cdn.jsdelivr.net/g/jquery@3.2.1,tether@1.4.0,bootstrap@4.0.0-alpha.6,modernizr@3.3.1,detectizr@2.2.0")
|
||||
script(src='bundle.js')
|
||||
title Terminus
|
||||
style.
|
||||
body {
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
background: #111;
|
||||
color: #ccc;
|
||||
min-height: 100vh;
|
||||
background-image: radial-gradient(#111, #000);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
h1, h2, h3, h5 {
|
||||
font-weight: 300;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn i + span,
|
||||
.nav-link i + span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
color: #b6e7ff !important;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
video, img {
|
||||
max-width: 100%;
|
||||
box-shadow: 0 0 50px black;
|
||||
}
|
||||
|
||||
script(defer).
|
||||
setTimeout(function () {
|
||||
/*
|
||||
if (Detectizr.os.name == 'windows') {
|
||||
$('[href="#windows"]').tab('show')
|
||||
}
|
||||
if (Detectizr.os.name == 'mac os') {
|
||||
$('[href="#macos"]').tab('show')
|
||||
}
|
||||
if (Detectizr.os.name == 'linux') {
|
||||
$('[href="#linux"]').tab('show')
|
||||
}
|
||||
*/
|
||||
})
|
||||
body
|
||||
.container.mt-5.mb-5
|
||||
.mt-5.mb-5#header
|
||||
.text-center
|
||||
h1 Terminus
|
||||
h5 A terminal for a more modern age
|
||||
h2.text-muted alpha
|
||||
.subtitle.mb-3 A terminal for a more modern age
|
||||
|
||||
.d-flex.flex-row.mt-5.mb-5
|
||||
ul.nav.nav-pills.flex-column.mr-5(style='min-width: 200px')
|
||||
li.nav-item
|
||||
a.nav-link.active(data-toggle='tab', href='#windows', role='tab')
|
||||
i.fa.fa-windows
|
||||
span Windows
|
||||
li.nav-item
|
||||
a.nav-link(data-toggle='tab', href='#macos', role='tab')
|
||||
i.fa.fa-apple
|
||||
span macOS
|
||||
li.nav-item
|
||||
a.nav-link(data-toggle='tab', href='#linux', role='tab')
|
||||
i.fa.fa-linux
|
||||
span Linux
|
||||
|
||||
.tab-content
|
||||
#windows.tab-pane.active(role='tabpanel')
|
||||
.row
|
||||
.col-6
|
||||
video(src='videos/windows.mp4', autoplay, loop)
|
||||
.col-6
|
||||
h3 A proper Windows experience
|
||||
p
|
||||
b Clink
|
||||
| provides tab completion, readline-style editing and persistent command history on Windows.
|
||||
p Also supported:
|
||||
ul
|
||||
li Classic CMD
|
||||
li PowerShell
|
||||
li Bash on Windows
|
||||
|
||||
#macos.tab-pane(role='tabpanel')
|
||||
.row
|
||||
.col-6
|
||||
//video(src='videos/windows.mp4', autoplay, loop)
|
||||
.col-6
|
||||
h3 Well...
|
||||
p Not much to say here, it just works.
|
||||
|
||||
#linux.tab-pane(role='tabpanel')
|
||||
.row
|
||||
.col-6
|
||||
img(src='linux.png')
|
||||
.col-6
|
||||
p
|
||||
ul
|
||||
li Spawn with a global hotkey
|
||||
li Tabs persist after restart
|
||||
li Auto-dock to any side of any screen
|
||||
li Full Unicode and double-width character support
|
||||
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-3.mb-3
|
||||
h2
|
||||
a.btn.btn-lg.btn-outline-secondary.mt-5(href='/terminus/#header')
|
||||
strong BEAM ME UP
|
||||
|
||||
div
|
||||
.btn-group.mt-3.mb-1
|
||||
a.btn.btn-lg.btn-outline-success(href='https://github.com/Eugeny/terminus/releases/latest', target='_blank')
|
||||
i.fa.fa-download
|
||||
span Downloads
|
||||
a.btn.btn-lg.btn-outline-secondary(href='https://github.com/Eugeny/terminus', target='_blank')
|
||||
i.fa.fa-github
|
||||
span GitHub
|
||||
small.text-muted EXE, DMG, DEB, RPM, TGZ
|
||||
|
||||
|
||||
.row.mt-5
|
||||
.col-6
|
||||
h3 User experience
|
||||
ul
|
||||
li Spawn and hide with a global hotkey
|
||||
li Fully customizable hotkey schema
|
||||
li Restores tabs
|
||||
li Drag in a file to paste the path
|
||||
li Click paths and URLs to open in browser/file manager
|
||||
li Keeps the current directory in new tabs
|
||||
|
||||
.col-6
|
||||
.mb-5
|
||||
h3 Customizable
|
||||
p Multiple app themes and a myriad of community color schemes for the terminal. Color scheme editor included.
|
||||
|
||||
div
|
||||
h3 Infinitely extensible
|
||||
p Install plugins from the NPM repository, or create your own with Typescript and Angular framework.
|
||||
|
||||
.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),
|
||||
|
BIN
docs/linux.png
Before Width: | Height: | Size: 138 KiB |
@@ -1,15 +1,24 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "pug index.pug",
|
||||
"watch": "pug -w index.pug"
|
||||
"build": "webpack --progress",
|
||||
"watch": "webpack --progress --watch"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"pug-cli": "^1.0.0-alpha6"
|
||||
"bootstrap": "^4.1.3",
|
||||
"css-loader": "^1.0.0",
|
||||
"file-loader": "^1.1.11",
|
||||
"node-sass": "^4.9.3",
|
||||
"pug": "^2.0.3",
|
||||
"pug-cli": "^1.0.0-alpha6",
|
||||
"pug-html-loader": "^1.1.5",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.22.1",
|
||||
"val-loader": "^1.1.1",
|
||||
"webpack": "^4.16.5",
|
||||
"webpack-cli": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
BIN
docs/readme.png
Normal file
After Width: | Height: | Size: 862 KiB |
141
docs/styles.scss
Normal file
@@ -0,0 +1,141 @@
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
BIN
docs/terminal.png
Normal file
After Width: | Height: | Size: 21 KiB |
27
docs/webpack.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
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]'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
21
package.json
@@ -9,10 +9,11 @@
|
||||
"core-js": "2.4.1",
|
||||
"cross-env": "4.0.0",
|
||||
"css-loader": "0.28.0",
|
||||
"electron": "2.0.0",
|
||||
"electron-builder": "17.1.1",
|
||||
"electron": "2.0.7",
|
||||
"electron-builder": "^20.27.1",
|
||||
"electron-builder-squirrel-windows": "17.0.1",
|
||||
"electron-rebuild": "^1.7.3",
|
||||
"electron-installer-snap": "^3.0.0",
|
||||
"electron-rebuild": "^1.8.2",
|
||||
"file-loader": "^1.1.11",
|
||||
"font-awesome": "4.7.0",
|
||||
"graceful-fs": "^4.1.11",
|
||||
@@ -27,6 +28,7 @@
|
||||
"npx": "^9.7.1",
|
||||
"pug": "^2.0.3",
|
||||
"pug-html-loader": "1.0.9",
|
||||
"pug-lint": "^2.5.0",
|
||||
"pug-loader": "^2.4.0",
|
||||
"pug-static-loader": "0.0.1",
|
||||
"raven-js": "3.16.0",
|
||||
@@ -35,6 +37,7 @@
|
||||
"shelljs": "0.7.7",
|
||||
"source-sans-pro": "2.0.10",
|
||||
"style-loader": "0.13.1",
|
||||
"svg-inline-loader": "^0.8.0",
|
||||
"to-string-loader": "1.1.5",
|
||||
"tslint": "5.1.0",
|
||||
"tslint-config-standard": "5.0.2",
|
||||
@@ -51,6 +54,10 @@
|
||||
"appId": "org.terminus",
|
||||
"productName": "Terminus",
|
||||
"compression": "normal",
|
||||
"files": [
|
||||
"**/*",
|
||||
"dist"
|
||||
],
|
||||
"extraResources": [
|
||||
"builtin-plugins",
|
||||
"clink"
|
||||
@@ -71,7 +78,10 @@
|
||||
"identity": null,
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
],
|
||||
"extendInfo": {
|
||||
"NSRequiresAquaSystemAppearance": false
|
||||
}
|
||||
},
|
||||
"dmg": {
|
||||
"artifactName": "terminus-${version}-${os}-${arch}.dmg"
|
||||
@@ -83,6 +93,9 @@
|
||||
"github"
|
||||
]
|
||||
},
|
||||
"snap": {
|
||||
"confinement": "classic"
|
||||
},
|
||||
"deb": {
|
||||
"depends": [
|
||||
"screen",
|
||||
|
@@ -4,10 +4,11 @@ const vars = require('./vars')
|
||||
|
||||
builder({
|
||||
dir: true,
|
||||
linux: ['deb', 'rpm', 'tar.gz'],
|
||||
extraMetadata: {
|
||||
version: vars.version,
|
||||
linux: ['snap', 'deb', 'rpm', 'tar.gz'],
|
||||
config: {
|
||||
extraMetadata: {
|
||||
version: vars.version,
|
||||
},
|
||||
},
|
||||
publish: 'onTag',
|
||||
draft: false
|
||||
})
|
||||
|
@@ -5,9 +5,10 @@ const vars = require('./vars')
|
||||
builder({
|
||||
dir: true,
|
||||
mac: ['dmg'],
|
||||
extraMetadata: {
|
||||
version: vars.version,
|
||||
config: {
|
||||
extraMetadata: {
|
||||
version: vars.version,
|
||||
},
|
||||
},
|
||||
publish: 'onTag',
|
||||
draft: false
|
||||
})
|
||||
|
@@ -3,13 +3,25 @@ const rebuild = require('electron-rebuild').default
|
||||
const path = require('path')
|
||||
const vars = require('./vars')
|
||||
|
||||
rebuild({
|
||||
lifecycles = []
|
||||
lifecycles.push(rebuild({
|
||||
buildPath: path.resolve(__dirname, '../app'),
|
||||
electronVersion: vars.electronVersion,
|
||||
force: true,
|
||||
}).lifecycle)
|
||||
lifecycles.push(rebuild({
|
||||
buildPath: path.resolve(__dirname, '../terminus-ssh'),
|
||||
electronVersion: vars.electronVersion,
|
||||
force: true,
|
||||
})
|
||||
rebuild({
|
||||
}).lifecycle)
|
||||
lifecycles.push(rebuild({
|
||||
buildPath: path.resolve(__dirname, '../terminus-terminal'),
|
||||
electronVersion: vars.electronVersion,
|
||||
force: true,
|
||||
})
|
||||
}).lifecycle)
|
||||
|
||||
for (let lc of lifecycles) {
|
||||
lc.on('module-found', name => {
|
||||
console.info('Rebuilding', name)
|
||||
})
|
||||
}
|
||||
|
@@ -5,9 +5,10 @@ const vars = require('./vars')
|
||||
builder({
|
||||
dir: true,
|
||||
win: ['squirrel'],
|
||||
extraMetadata: {
|
||||
version: vars.version,
|
||||
config: {
|
||||
extraMetadata: {
|
||||
version: vars.version,
|
||||
},
|
||||
},
|
||||
publish: 'onTag',
|
||||
draft: false
|
||||
})
|
||||
|
@@ -20,5 +20,5 @@ exports.bundledModules = [
|
||||
'@angular',
|
||||
'@ng-bootstrap',
|
||||
]
|
||||
exports.nativeModules = ['node-pty-tmp', 'font-manager', 'xkeychain']
|
||||
exports.nativeModules = ['node-pty-tmp', 'font-manager', 'xkeychain', 'electron-vibrancy']
|
||||
exports.electronVersion = pkgInfo.devDependencies.electron
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-community-color-schemes",
|
||||
"version": "1.0.0-alpha.36",
|
||||
"version": "1.0.0-alpha.48",
|
||||
"description": "Community color schemes for Terminus",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-core",
|
||||
"version": "1.0.0-alpha.36",
|
||||
"version": "1.0.0-alpha.48",
|
||||
"description": "Terminus core",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
@@ -25,6 +25,7 @@
|
||||
"bootstrap": "4.0.0-alpha.6",
|
||||
"core-js": "^2.4.1",
|
||||
"electron-updater": "^2.8.9",
|
||||
"ng2-dnd": "^5.0.2",
|
||||
"ngx-perfect-scrollbar": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import { SafeHtml } from '@angular/platform-browser'
|
||||
|
||||
export interface IToolbarButton {
|
||||
icon: string
|
||||
icon: SafeHtml
|
||||
title: string
|
||||
touchBarTitle?: string
|
||||
weight?: number
|
||||
|
@@ -8,56 +8,64 @@ title-bar(
|
||||
)
|
||||
.tab-bar(
|
||||
*ngIf='!hostApp.isFullScreen',
|
||||
[class.inset]='hostApp.platform == Platform.macOS && config.store.appearance.frame == "thin" && config.store.appearance.tabsLocation == "top"'
|
||||
)
|
||||
.tabs
|
||||
.inset.background(*ngIf='hostApp.platform == Platform.macOS && config.store.appearance.frame == "thin" && config.store.appearance.tabsLocation == "top"')
|
||||
.tabs(
|
||||
dnd-sortable-container,
|
||||
[sortableData]='app.tabs',
|
||||
)
|
||||
tab-header(
|
||||
*ngFor='let tab of app.tabs; let idx = index',
|
||||
dnd-sortable,
|
||||
[sortableIndex]='idx',
|
||||
(onDragStart)='onTabDragStart()',
|
||||
(onDragEnd)='onTabDragEnd()',
|
||||
|
||||
[index]='idx',
|
||||
[tab]='tab',
|
||||
[active]='tab == app.activeTab',
|
||||
[hasActivity]='tab.hasActivity',
|
||||
[class.drag-region]='hostApp.platform == Platform.macOS',
|
||||
@animateTab,
|
||||
(click)='app.selectTab(tab)',
|
||||
[class.fully-draggable]='hostApp.platform != Platform.macOS',
|
||||
[class.drag-region]='hostApp.platform == Platform.macOS && !tabsDragging',
|
||||
)
|
||||
|
||||
.btn-group
|
||||
.btn-group.background
|
||||
button.btn.btn-secondary.btn-tab-bar(
|
||||
*ngFor='let button of leftToolbarButtons',
|
||||
[title]='button.title',
|
||||
(click)='button.click()',
|
||||
[innerHTML]='button.icon',
|
||||
)
|
||||
i.fa([class]='"fa fa-" + button.icon')
|
||||
|
||||
.drag-space([class.persistent]='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS')
|
||||
.drag-space.background([class.persistent]='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS')
|
||||
|
||||
.btn-group
|
||||
.btn-group.background
|
||||
button.btn.btn-secondary.btn-tab-bar(
|
||||
*ngFor='let button of rightToolbarButtons',
|
||||
[title]='button.title',
|
||||
(click)='button.click()',
|
||||
[innerHTML]='button.icon',
|
||||
)
|
||||
i.fa([class]='"fa fa-" + button.icon')
|
||||
button.btn.btn-secondary.btn-tab-bar(
|
||||
|
||||
button.btn.btn-secondary.btn-tab-bar.btn-update(
|
||||
*ngIf='appUpdate',
|
||||
title='Update available',
|
||||
(click)='updateApp()',
|
||||
[innerHTML]='updateIcon'
|
||||
)
|
||||
i.fa.fa-arrow-up.text-info
|
||||
span.text-info Update
|
||||
|
||||
window-controls(
|
||||
window-controls.background(
|
||||
*ngIf='config.store.appearance.frame == "thin" && (hostApp.platform == Platform.Windows || hostApp.platform == Platform.Linux)',
|
||||
)
|
||||
|
||||
start-page(*ngIf='ready && app.tabs.length == 0')
|
||||
|
||||
tab-body(
|
||||
*ngFor='let tab of app.tabs; trackBy: tab?.id',
|
||||
*ngFor='let tab of unsortedTabs',
|
||||
[active]='tab == app.activeTab',
|
||||
[tab]='tab',
|
||||
[scrollable]='tab.scrollable',
|
||||
)
|
||||
|
||||
ng-template(ngbModalContainer)
|
||||
|
@@ -35,6 +35,7 @@ $tab-border-radius: 4px;
|
||||
line-height: $tabs-height + 2px;
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
padding: 0 15px;
|
||||
flex: 0 0 auto;
|
||||
border-bottom: 2px solid transparent;
|
||||
@@ -46,7 +47,6 @@ $tab-border-radius: 4px;
|
||||
color: #aaa;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
|
||||
}
|
||||
|
||||
&>.tabs {
|
||||
@@ -66,12 +66,13 @@ $tab-border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&.inset {
|
||||
padding-left: 85px;
|
||||
& > .inset {
|
||||
width: 85px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
window-controls {
|
||||
margin-left: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,3 +87,13 @@ hotkey-hint {
|
||||
right: 0;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
::ng-deep .btn-tab-bar svg {
|
||||
height: 16px;
|
||||
fill: white;
|
||||
fill-opacity: 0.75;
|
||||
}
|
||||
|
||||
::ng-deep .btn-update svg {
|
||||
fill: cyan;
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Component, Inject, Input, HostListener, HostBinding } from '@angular/core'
|
||||
import { trigger, style, animate, transition, state } from '@angular/animations'
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
import { ElectronService } from '../services/electron.service'
|
||||
@@ -13,6 +14,7 @@ import { ThemesService } from '../services/themes.service'
|
||||
import { UpdaterService, Update } from '../services/updater.service'
|
||||
import { TouchbarService } from '../services/touchbar.service'
|
||||
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { SafeModeModalComponent } from './safeModeModal.component'
|
||||
import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
|
||||
@@ -55,6 +57,9 @@ export class AppRootComponent {
|
||||
@Input() leftToolbarButtons: IToolbarButton[]
|
||||
@Input() rightToolbarButtons: IToolbarButton[]
|
||||
@HostBinding('class') hostClass = `platform-${process.platform}`
|
||||
tabsDragging = false
|
||||
unsortedTabs: BaseTabComponent[] = []
|
||||
updateIcon: SafeHtml
|
||||
private logger: Logger
|
||||
private appUpdate: Update
|
||||
|
||||
@@ -71,6 +76,7 @@ export class AppRootComponent {
|
||||
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
|
||||
log: LogService,
|
||||
ngbModal: NgbModal,
|
||||
domSanitizer: DomSanitizer,
|
||||
_themes: ThemesService,
|
||||
) {
|
||||
this.logger = log.create('main')
|
||||
@@ -79,6 +85,8 @@ export class AppRootComponent {
|
||||
this.leftToolbarButtons = this.getToolbarButtons(false)
|
||||
this.rightToolbarButtons = this.getToolbarButtons(true)
|
||||
|
||||
this.updateIcon = domSanitizer.bypassSecurityTrustHtml(require('../icons/gift.svg')),
|
||||
|
||||
this.hotkeys.matchedHotkey.subscribe((hotkey) => {
|
||||
if (hotkey.startsWith('tab-')) {
|
||||
let index = parseInt(hotkey.split('-')[1])
|
||||
@@ -126,6 +134,14 @@ export class AppRootComponent {
|
||||
})
|
||||
|
||||
this.touchbar.update()
|
||||
|
||||
config.changed$.subscribe(() => this.updateVibrancy())
|
||||
this.updateVibrancy()
|
||||
|
||||
this.app.tabOpened$.subscribe(tab => this.unsortedTabs.push(tab))
|
||||
this.app.tabClosed$.subscribe(tab => {
|
||||
this.unsortedTabs = this.unsortedTabs.filter(x => x !== tab)
|
||||
})
|
||||
}
|
||||
|
||||
onGlobalHotkey () {
|
||||
@@ -180,6 +196,17 @@ export class AppRootComponent {
|
||||
this.electron.shell.openExternal(this.appUpdate.url)
|
||||
}
|
||||
|
||||
onTabDragStart () {
|
||||
this.tabsDragging = true
|
||||
}
|
||||
|
||||
onTabDragEnd () {
|
||||
setTimeout(() => {
|
||||
this.tabsDragging = false
|
||||
this.app.emitTabsChanged()
|
||||
})
|
||||
}
|
||||
|
||||
private getToolbarButtons (aboveZero: boolean): IToolbarButton[] {
|
||||
let buttons: IToolbarButton[] = []
|
||||
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
|
||||
@@ -189,4 +216,9 @@ export class AppRootComponent {
|
||||
.filter((button) => (button.weight > 0) === aboveZero)
|
||||
.sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0))
|
||||
}
|
||||
|
||||
private updateVibrancy () {
|
||||
this.hostApp.setVibrancy(this.config.store.appearance.vibrancy)
|
||||
this.hostApp.getWindow().setOpacity(this.config.store.appearance.opacity)
|
||||
}
|
||||
}
|
||||
|
@@ -1,18 +1,21 @@
|
||||
import { Subject } from 'rxjs'
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { ViewRef } from '@angular/core'
|
||||
|
||||
export abstract class BaseTabComponent {
|
||||
private static lastTabID = 0
|
||||
id: number
|
||||
title: string
|
||||
titleChange$ = new Subject<string>()
|
||||
customTitle: string
|
||||
scrollable: boolean
|
||||
hasActivity = false
|
||||
focused$ = new Subject<void>()
|
||||
blurred$ = new Subject<void>()
|
||||
hasFocus = false
|
||||
hostView: ViewRef
|
||||
protected titleChange = new Subject<string>()
|
||||
protected focused = new Subject<void>()
|
||||
protected blurred = new Subject<void>()
|
||||
|
||||
get focused$ (): Observable<void> { return this.focused }
|
||||
get blurred$ (): Observable<void> { return this.blurred }
|
||||
get titleChange$ (): Observable<string> { return this.titleChange }
|
||||
|
||||
constructor () {
|
||||
this.id = BaseTabComponent.lastTabID++
|
||||
@@ -27,7 +30,7 @@ export abstract class BaseTabComponent {
|
||||
setTitle (title: string) {
|
||||
this.title = title
|
||||
if (!this.customTitle) {
|
||||
this.titleChange$.next(title)
|
||||
this.titleChange.next(title)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +46,17 @@ export abstract class BaseTabComponent {
|
||||
return true
|
||||
}
|
||||
|
||||
emitFocused () {
|
||||
this.focused.next()
|
||||
}
|
||||
|
||||
emitBlurred () {
|
||||
this.blurred.next()
|
||||
}
|
||||
|
||||
destroy (): void {
|
||||
this.focused$.complete()
|
||||
this.blurred$.complete()
|
||||
this.focused.complete()
|
||||
this.blurred.complete()
|
||||
this.titleChange.complete()
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ div
|
||||
*ngFor='let button of getButtons()',
|
||||
(click)='button.click()',
|
||||
)
|
||||
i([class]='"fa fa-fw fa-" + button.icon')
|
||||
.d-flex.align-self-center([innerHTML]='button.icon')
|
||||
span {{button.title}}
|
||||
|
||||
footer
|
||||
|
@@ -27,3 +27,11 @@ footer {
|
||||
a, button {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.list-group-item ::ng-deep svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 10px;
|
||||
fill: white;
|
||||
fill-opacity: 0.75;
|
||||
}
|
||||
|
@@ -4,10 +4,6 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&.scrollable {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&.active {
|
||||
display: flex;
|
||||
|
||||
|
@@ -1,29 +1,36 @@
|
||||
import { Component, Input, ViewChild, HostBinding, ViewContainerRef } from '@angular/core'
|
||||
import { Component, Input, ViewChild, HostBinding, ViewContainerRef, OnChanges } from '@angular/core'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
|
||||
@Component({
|
||||
selector: 'tab-body',
|
||||
template: `
|
||||
<perfect-scrollbar [config]="{ suppressScrollX: true }" *ngIf="scrollable">
|
||||
<!--perfect-scrollbar [config]="{ suppressScrollX: true }" *ngIf="scrollable">
|
||||
<ng-template #scrollablePlaceholder></ng-template>
|
||||
</perfect-scrollbar>
|
||||
<ng-template #nonScrollablePlaceholder *ngIf="!scrollable"></ng-template>
|
||||
</perfect-scrollbar-->
|
||||
<ng-template #placeholder></ng-template>
|
||||
`,
|
||||
styles: [
|
||||
require('./tabBody.component.scss'),
|
||||
require('./tabBody.deep.component.css'),
|
||||
],
|
||||
})
|
||||
export class TabBodyComponent {
|
||||
export class TabBodyComponent implements OnChanges {
|
||||
@Input() @HostBinding('class.active') active: boolean
|
||||
@Input() tab: BaseTabComponent
|
||||
@Input() scrollable: boolean
|
||||
@ViewChild('scrollablePlaceholder', {read: ViewContainerRef}) scrollablePlaceholder: ViewContainerRef
|
||||
@ViewChild('nonScrollablePlaceholder', {read: ViewContainerRef}) nonScrollablePlaceholder: ViewContainerRef
|
||||
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef
|
||||
|
||||
ngAfterViewInit () {
|
||||
setImmediate(() => {
|
||||
(this.scrollable ? this.scrollablePlaceholder : this.nonScrollablePlaceholder).insert(this.tab.hostView)
|
||||
})
|
||||
ngOnChanges (changes) {
|
||||
if (changes.tab) {
|
||||
if (this.placeholder) {
|
||||
this.placeholder.detach()
|
||||
}
|
||||
setImmediate(() => {
|
||||
this.placeholder.insert(this.tab.hostView)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.placeholder.detach()
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,3 @@
|
||||
.index {{index + 1}}
|
||||
.index(#handle) {{index + 1}}
|
||||
.name([title]='tab.customTitle || tab.title') {{tab.customTitle || tab.title}}
|
||||
button((click)='app.closeTab(tab, true)') ×
|
||||
|
@@ -17,6 +17,8 @@ $tabs-height: 36px;
|
||||
.index {
|
||||
flex: none;
|
||||
font-weight: bold;
|
||||
-webkit-app-region: no-drag;
|
||||
cursor: -webkit-grab;
|
||||
|
||||
margin-left: 10px;
|
||||
width: 20px;
|
||||
@@ -67,4 +69,8 @@ $tabs-height: 36px;
|
||||
&.drag-region {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
&.fully-draggable {
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,11 @@
|
||||
import { Component, Input, HostBinding, HostListener, NgZone } from '@angular/core'
|
||||
import { Component, Input, HostBinding, HostListener, NgZone, ViewChild, ElementRef } from '@angular/core'
|
||||
import { SortableComponent } from 'ng2-dnd'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { RenameTabModalComponent } from './renameTabModal.component'
|
||||
import { ElectronService } from '../services/electron.service'
|
||||
import { AppService } from '../services/app.service'
|
||||
import { HostAppService, Platform } from '../services/hostApp.service'
|
||||
|
||||
@Component({
|
||||
selector: 'tab-header',
|
||||
@@ -15,13 +17,16 @@ export class TabHeaderComponent {
|
||||
@Input() @HostBinding('class.active') active: boolean
|
||||
@Input() @HostBinding('class.has-activity') hasActivity: boolean
|
||||
@Input() tab: BaseTabComponent
|
||||
@ViewChild('handle') handle: ElementRef
|
||||
private contextMenu: any
|
||||
|
||||
constructor (
|
||||
zone: NgZone,
|
||||
electron: ElectronService,
|
||||
public app: AppService,
|
||||
private hostApp: HostAppService,
|
||||
private ngbModal: NgbModal,
|
||||
private parentDraggable: SortableComponent,
|
||||
) {
|
||||
this.contextMenu = electron.remote.Menu.buildFromTemplate([
|
||||
{
|
||||
@@ -65,6 +70,12 @@ export class TabHeaderComponent {
|
||||
])
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
if (this.hostApp.platform === Platform.macOS) {
|
||||
this.parentDraggable.setDragHandle(this.handle.nativeElement)
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('dblclick') onDoubleClick (): void {
|
||||
let modal = this.ngbModal.open(RenameTabModalComponent)
|
||||
modal.componentInstance.value = this.tab.customTitle || this.tab.title
|
||||
|
@@ -7,3 +7,5 @@ appearance:
|
||||
theme: Standard
|
||||
frame: thin
|
||||
css: '/* * { color: blue !important; } */'
|
||||
opacity: 1.0
|
||||
vibrancy: false
|
||||
|
1
terminus-core/src/icons/gift.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M32 448c0 17.7 14.3 32 32 32h160V320H32v128zm448-288h-42.1c6.2-12.1 10.1-25.5 10.1-40 0-48.5-39.5-88-88-88-41.6 0-68.5 21.3-103 68.3-34.5-47-61.4-68.3-103-68.3-48.5 0-88 39.5-88 88 0 14.5 3.8 27.9 10.1 40H32c-17.7 0-32 14.3-32 32v80c0 8.8 7.2 16 16 16h480c8.8 0 16-7.2 16-16v-80c0-17.7-14.3-32-32-32zm-326.1 0c-22.1 0-40-17.9-40-40s17.9-40 40-40c19.9 0 34.6 3.3 86.1 80h-86.1zm206.1 0h-86.1c51.4-76.5 65.7-80 86.1-80 22.1 0 40 17.9 40 40s-17.9 40-40 40zm-72 320h160c17.7 0 32-14.3 32-32V320H288v160z"/></svg>
|
After Width: | Height: | Size: 579 B |
@@ -4,6 +4,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
|
||||
import { DndModule } from 'ng2-dnd'
|
||||
|
||||
import { AppService } from './services/app.service'
|
||||
import { ConfigService } from './services/config.service'
|
||||
@@ -35,6 +36,7 @@ import { StandardTheme, StandardCompactTheme } from './theme'
|
||||
import { CoreConfigProvider } from './config'
|
||||
|
||||
import 'perfect-scrollbar/css/perfect-scrollbar.css'
|
||||
import 'ng2-dnd/bundles/style.css'
|
||||
|
||||
const PROVIDERS = [
|
||||
AppService,
|
||||
@@ -62,6 +64,7 @@ const PROVIDERS = [
|
||||
FormsModule,
|
||||
NgbModule.forRoot(),
|
||||
PerfectScrollbarModule,
|
||||
DndModule.forRoot(),
|
||||
],
|
||||
declarations: [
|
||||
AppRootComponent,
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Subject, AsyncSubject } from 'rxjs'
|
||||
import { Observable, Subject, AsyncSubject } from 'rxjs'
|
||||
import { Injectable, ComponentFactoryResolver, Injector, Optional } from '@angular/core'
|
||||
import { DefaultTabProvider } from '../api/defaultTabProvider'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
@@ -11,13 +11,20 @@ export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
|
||||
export class AppService {
|
||||
tabs: BaseTabComponent[] = []
|
||||
activeTab: BaseTabComponent
|
||||
activeTabChange$ = new Subject<BaseTabComponent>()
|
||||
lastTabIndex = 0
|
||||
logger: Logger
|
||||
tabsChanged$ = new Subject<void>()
|
||||
tabOpened$ = new Subject<BaseTabComponent>()
|
||||
tabClosed$ = new Subject<BaseTabComponent>()
|
||||
ready$ = new AsyncSubject<void>()
|
||||
|
||||
private activeTabChange = new Subject<BaseTabComponent>()
|
||||
private tabsChanged = new Subject<void>()
|
||||
private tabOpened = new Subject<BaseTabComponent>()
|
||||
private tabClosed = new Subject<BaseTabComponent>()
|
||||
private ready = new AsyncSubject<void>()
|
||||
|
||||
get activeTabChange$ (): Observable<BaseTabComponent> { return this.activeTabChange }
|
||||
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
|
||||
get tabsChanged$ (): Observable<void> { return this.tabsChanged }
|
||||
get tabClosed$ (): Observable<BaseTabComponent> { return this.tabClosed }
|
||||
get ready$ (): Observable<void> { return this.ready }
|
||||
|
||||
constructor (
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
@@ -37,8 +44,8 @@ export class AppService {
|
||||
|
||||
this.tabs.push(componentRef.instance)
|
||||
this.selectTab(componentRef.instance)
|
||||
this.tabsChanged$.next()
|
||||
this.tabOpened$.next(componentRef.instance)
|
||||
this.tabsChanged.next()
|
||||
this.tabOpened.next(componentRef.instance)
|
||||
|
||||
return componentRef.instance
|
||||
}
|
||||
@@ -60,12 +67,12 @@ export class AppService {
|
||||
}
|
||||
if (this.activeTab) {
|
||||
this.activeTab.hasActivity = false
|
||||
this.activeTab.blurred$.next()
|
||||
this.activeTab.emitBlurred()
|
||||
}
|
||||
this.activeTab = tab
|
||||
this.activeTabChange$.next(tab)
|
||||
this.activeTabChange.next(tab)
|
||||
if (this.activeTab) {
|
||||
this.activeTab.focused$.next()
|
||||
this.activeTab.emitFocused()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +105,10 @@ export class AppService {
|
||||
}
|
||||
}
|
||||
|
||||
emitTabsChanged () {
|
||||
this.tabsChanged.next()
|
||||
}
|
||||
|
||||
async closeTab (tab: BaseTabComponent, checkCanClose?: boolean): Promise<void> {
|
||||
if (!this.tabs.includes(tab)) {
|
||||
return
|
||||
@@ -105,18 +116,18 @@ 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()
|
||||
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
||||
if (tab === this.activeTab) {
|
||||
this.selectTab(this.tabs[newIndex])
|
||||
}
|
||||
this.tabsChanged$.next()
|
||||
this.tabClosed$.next(tab)
|
||||
this.tabsChanged.next()
|
||||
this.tabClosed.next(tab)
|
||||
}
|
||||
|
||||
emitReady () {
|
||||
this.ready$.next(null)
|
||||
this.ready$.complete()
|
||||
this.ready.next(null)
|
||||
this.ready.complete()
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Subject } from 'rxjs'
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import * as yaml from 'js-yaml'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
@@ -52,13 +52,15 @@ export class ConfigProxy {
|
||||
@Injectable()
|
||||
export class ConfigService {
|
||||
store: any
|
||||
changed$ = new Subject<void>()
|
||||
restartRequested: boolean
|
||||
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 }
|
||||
|
||||
constructor (
|
||||
electron: ElectronService,
|
||||
hostApp: HostAppService,
|
||||
@@ -93,7 +95,7 @@ export class ConfigService {
|
||||
}
|
||||
|
||||
emitChange (): void {
|
||||
this.changed$.next()
|
||||
this.changed.next()
|
||||
}
|
||||
|
||||
requestRestart (): void {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Subject } from 'rxjs'
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { Injectable, NgZone, EventEmitter } from '@angular/core'
|
||||
import { ElectronService } from '../services/electron.service'
|
||||
import { Logger, LogService } from '../services/log.service'
|
||||
@@ -14,17 +14,25 @@ export interface Bounds {
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface SecondInstanceArgs {
|
||||
argv: string[],
|
||||
cwd: string
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class HostAppService {
|
||||
platform: Platform
|
||||
nodePlatform: string
|
||||
preferencesMenu$ = new Subject<void>()
|
||||
ready = new EventEmitter<any>()
|
||||
shown = new EventEmitter<any>()
|
||||
secondInstance$ = new Subject<{ argv: string[], cwd: string }>()
|
||||
isFullScreen = false
|
||||
private preferencesMenu = new Subject<void>()
|
||||
private secondInstance = new Subject<SecondInstanceArgs>()
|
||||
private logger: Logger
|
||||
|
||||
get preferencesMenu$ (): Observable<void> { return this.preferencesMenu }
|
||||
get secondInstance$ (): Observable<SecondInstanceArgs> { return this.secondInstance }
|
||||
|
||||
constructor (
|
||||
private zone: NgZone,
|
||||
private electron: ElectronService,
|
||||
@@ -38,7 +46,7 @@ export class HostAppService {
|
||||
linux: Platform.Linux
|
||||
}[this.nodePlatform]
|
||||
|
||||
electron.ipcRenderer.on('host:preferences-menu', () => this.zone.run(() => this.preferencesMenu$.next()))
|
||||
electron.ipcRenderer.on('host:preferences-menu', () => this.zone.run(() => this.preferencesMenu.next()))
|
||||
|
||||
electron.ipcRenderer.on('uncaughtException', ($event, err) => {
|
||||
this.logger.error('Unhandled exception:', err)
|
||||
@@ -57,7 +65,7 @@ export class HostAppService {
|
||||
})
|
||||
|
||||
electron.ipcRenderer.on('host:second-instance', ($event, argv: string[], cwd: string) => {
|
||||
this.zone.run(() => this.secondInstance$.next({ argv, cwd }))
|
||||
this.zone.run(() => this.secondInstance.next({ argv, cwd }))
|
||||
})
|
||||
|
||||
this.ready.subscribe(() => {
|
||||
@@ -118,6 +126,16 @@ export class HostAppService {
|
||||
this.electron.ipcRenderer.send('window-set-always-on-top', flag)
|
||||
}
|
||||
|
||||
setVibrancy (enable: boolean) {
|
||||
document.body.classList.toggle('vibrant', enable)
|
||||
if (this.platform === Platform.macOS) {
|
||||
this.getWindow().setVibrancy(enable ? 'dark' : null)
|
||||
}
|
||||
if (this.platform === Platform.Windows) {
|
||||
this.electron.ipcRenderer.send('window-set-vibrancy', enable)
|
||||
}
|
||||
}
|
||||
|
||||
quit () {
|
||||
this.logger.info('Quitting')
|
||||
this.electron.app.quit()
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Inject, NgZone } from '@angular/core'
|
||||
import { TouchBarSegmentedControl, SegmentedControlSegment } from 'electron'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { AppService } from './app.service'
|
||||
import { ConfigService } from './config.service'
|
||||
import { ElectronService } from './electron.service'
|
||||
@@ -9,7 +9,6 @@ import { IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
|
||||
@Injectable()
|
||||
export class TouchbarService {
|
||||
tabSelected$ = new Subject<number>()
|
||||
private titleSubscriptions = new Map<BaseTabComponent, Subscription>()
|
||||
private tabsSegmentedControl: TouchBarSegmentedControl
|
||||
private tabSegments: SegmentedControlSegment[] = []
|
||||
|
@@ -15,9 +15,10 @@ $pink: #ff5b77 !default;
|
||||
$purple: #613d7c !default;
|
||||
|
||||
|
||||
$body-bg: #1D272D;
|
||||
$body-bg2: #131d27;
|
||||
$body-bg3: #20333e;
|
||||
$content-bg: rgba(39, 49, 60, 0.65); //#1D272D;
|
||||
$content-bg-solid: #1D272D;
|
||||
$body-bg: #131d27;
|
||||
$body-bg2: #20333e;
|
||||
|
||||
$body-color: #aaa;
|
||||
$font-family-sans-serif: "Source Sans Pro";
|
||||
@@ -31,10 +32,10 @@ $btn-secondary-border: #444;
|
||||
//$btn-warning-bg: rgba($orange, .5);
|
||||
|
||||
|
||||
$nav-tabs-border-color: $body-bg2;
|
||||
$nav-tabs-border-color: $body-bg;
|
||||
$nav-tabs-border-width: 1px;
|
||||
$nav-tabs-border-radius: 0;
|
||||
$nav-tabs-link-hover-border-color: $body-bg2;
|
||||
$nav-tabs-link-hover-border-color: $body-bg;
|
||||
$nav-tabs-active-link-hover-color: $white;
|
||||
$nav-tabs-active-link-hover-bg: $blue;
|
||||
$nav-tabs-active-link-hover-border-color: darken($blue, 30%);
|
||||
@@ -52,20 +53,20 @@ $input-bg-focus: $input-bg;
|
||||
//$input-border-focus: lighten($brand-primary, 25%);
|
||||
//$input-box-shadow-focus: $input-box-shadow, rgba($input-border-focus, .6);
|
||||
$input-color-focus: $input-color;
|
||||
$input-group-addon-bg: $body-bg2;
|
||||
$input-group-addon-bg: $body-bg;
|
||||
$input-group-addon-border-color: $input-border-color;
|
||||
|
||||
$modal-content-bg: $body-bg;
|
||||
$modal-content-border-color: $body-bg2;
|
||||
$modal-content-bg: $content-bg-solid;
|
||||
$modal-content-border-color: $body-bg;
|
||||
$modal-header-border-color: transparent;
|
||||
$modal-footer-border-color: transparent;
|
||||
|
||||
$popover-bg: $body-bg2;
|
||||
$popover-bg: $body-bg;
|
||||
|
||||
$dropdown-bg: $body-bg2;
|
||||
$dropdown-bg: $body-bg;
|
||||
$dropdown-link-color: $body-color;
|
||||
$dropdown-link-hover-color: #333;
|
||||
$dropdown-link-hover-bg: $body-bg3;
|
||||
$dropdown-link-hover-bg: $body-bg2;
|
||||
//$dropdown-link-active-color: $component-active-color;
|
||||
//$dropdown-link-active-bg: $component-active-bg;
|
||||
$dropdown-link-disabled-color: #333;
|
||||
@@ -80,7 +81,7 @@ $list-group-link-active-bg: rgba(255,255,255,.2);
|
||||
$pre-bg: $dropdown-bg;
|
||||
$pre-color: $dropdown-link-color;
|
||||
|
||||
$alert-danger-bg: $body-bg2;
|
||||
$alert-danger-bg: $body-bg;
|
||||
$alert-danger-text: $red;
|
||||
$alert-danger-border: $red;
|
||||
|
||||
@@ -89,10 +90,6 @@ $headings-color: #eee;
|
||||
|
||||
@import '~bootstrap/scss/bootstrap.scss';
|
||||
|
||||
title-bar {
|
||||
background: $body-bg2;
|
||||
}
|
||||
|
||||
window-controls {
|
||||
svg {
|
||||
transition: 0.25s fill;
|
||||
@@ -110,35 +107,32 @@ window-controls {
|
||||
|
||||
$border-color: #111;
|
||||
|
||||
body {
|
||||
background: $body-bg;
|
||||
|
||||
&.vibrant {
|
||||
background: rgba(0,0,0,.4);
|
||||
}
|
||||
}
|
||||
|
||||
app-root {
|
||||
&> .content {
|
||||
background: $body-bg2;
|
||||
|
||||
.tab-bar {
|
||||
.btn-tab-bar {
|
||||
&:not(:hover):not(:active) {
|
||||
background: $body-bg2;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.drag-space {
|
||||
background: $body-bg2;
|
||||
}
|
||||
|
||||
&>.tabs {
|
||||
&:empty {
|
||||
background: $body-bg2;
|
||||
}
|
||||
|
||||
tab-header {
|
||||
background: $body-bg2;
|
||||
border-left: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
|
||||
transition: 0.25s all;
|
||||
|
||||
.index {
|
||||
color: #555;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -152,7 +146,7 @@ app-root {
|
||||
|
||||
&.active {
|
||||
color: white;
|
||||
background: $body-bg;
|
||||
background: $content-bg;
|
||||
border-left: 1px solid $border-color;
|
||||
border-right: 1px solid $border-color;
|
||||
}
|
||||
@@ -161,11 +155,12 @@ app-root {
|
||||
}
|
||||
|
||||
&.tabs-on-top .tab-bar {
|
||||
border-bottom: 1px solid $border-color;
|
||||
&>.background {
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
tab-header {
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin-bottom: -1px;
|
||||
|
||||
&.active {
|
||||
border-bottom-color: transparent;
|
||||
@@ -178,11 +173,12 @@ app-root {
|
||||
}
|
||||
|
||||
&:not(.tabs-on-top) .tab-bar {
|
||||
border-top: 1px solid $border-color;
|
||||
&>.background {
|
||||
border-top: 1px solid $border-color;
|
||||
}
|
||||
|
||||
tab-header {
|
||||
border-top: 1px solid $border-color;
|
||||
margin-top: -1px;
|
||||
|
||||
&.active {
|
||||
margin-top: -1px;
|
||||
@@ -204,14 +200,14 @@ app-root {
|
||||
}
|
||||
|
||||
tab-body {
|
||||
background: $body-bg;
|
||||
background: $content-bg;
|
||||
}
|
||||
|
||||
settings-tab > ngb-tabset {
|
||||
border-right: 1px solid $body-bg2;
|
||||
border-right: 1px solid $body-bg;
|
||||
|
||||
& > .nav {
|
||||
background: $body-bg3;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
|
||||
& > .nav-item > .nav-link {
|
||||
border-left: none;
|
||||
@@ -239,7 +235,7 @@ settings-tab > ngb-tabset {
|
||||
|
||||
multi-hotkey-input {
|
||||
.item {
|
||||
background: $body-bg3;
|
||||
background: $body-bg2;
|
||||
border: 1px solid $blue;
|
||||
border-radius: 3px;
|
||||
margin-right: 5px;
|
||||
@@ -249,7 +245,7 @@ multi-hotkey-input {
|
||||
|
||||
.stroke {
|
||||
padding: 0 6px;
|
||||
border-right: 1px solid $body-bg;
|
||||
border-right: 1px solid $content-bg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,8 +260,8 @@ multi-hotkey-input {
|
||||
}
|
||||
|
||||
.add, .item .body, .item .remove {
|
||||
&:hover { background: darken($body-bg3, 5%); }
|
||||
&:active { background: darken($body-bg3, 15%); }
|
||||
&:hover { background: darken($body-bg2, 5%); }
|
||||
&:active { background: darken($body-bg2, 15%); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +274,7 @@ hotkey-input-modal {
|
||||
height: 55px;
|
||||
|
||||
.stroke {
|
||||
background: $body-bg3;
|
||||
background: $body-bg2;
|
||||
border: 1px solid $blue;
|
||||
border-radius: 3px;
|
||||
margin-right: 10px;
|
||||
|
@@ -39,6 +39,7 @@ module.exports = {
|
||||
{ test: /\.css$/, use: ['to-string-loader', 'css-loader'], include: /component\.css/ },
|
||||
{ test: /\.css$/, use: ['style-loader', 'css-loader'], exclude: /component\.css/ },
|
||||
{ test: /\.yaml$/, use: ['json-loader', 'yaml-loader'] },
|
||||
{ test: /\.svg/, use: ['svg-inline-loader'] },
|
||||
]
|
||||
},
|
||||
externals: [
|
||||
|
@@ -185,6 +185,10 @@ ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
||||
ng2-dnd@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ng2-dnd/-/ng2-dnd-5.0.2.tgz#862278ac7dedfa14f5783bbf34014d5d73dfefb4"
|
||||
|
||||
ngx-perfect-scrollbar@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ngx-perfect-scrollbar/-/ngx-perfect-scrollbar-6.0.0.tgz#92b51957c04ed6a6d416beca2707bab005667b68"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-plugin-manager",
|
||||
"version": "1.0.0-alpha.36",
|
||||
"version": "1.0.0-alpha.48",
|
||||
"description": "Terminus' plugin manager",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-settings",
|
||||
"version": "1.0.0-alpha.36",
|
||||
"version": "1.0.0-alpha.48",
|
||||
"description": "Terminus terminal settings page",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { DomSanitizer } from '@angular/platform-browser'
|
||||
import { ToolbarButtonProvider, IToolbarButton, AppService, HostAppService } from 'terminus-core'
|
||||
|
||||
import { SettingsTabComponent } from './components/settingsTab.component'
|
||||
@@ -8,6 +9,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
||||
constructor (
|
||||
hostApp: HostAppService,
|
||||
private app: AppService,
|
||||
private domSanitizer: DomSanitizer,
|
||||
) {
|
||||
super()
|
||||
hostApp.preferencesMenu$.subscribe(() => this.open())
|
||||
@@ -15,7 +17,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
||||
|
||||
provide (): IToolbarButton[] {
|
||||
return [{
|
||||
icon: 'sliders',
|
||||
icon: this.domSanitizer.bypassSecurityTrustHtml(require('./icons/cog.svg')),
|
||||
title: 'Settings',
|
||||
touchBarTitle: '⚙️',
|
||||
weight: 10,
|
||||
|
@@ -28,4 +28,8 @@
|
||||
.add {
|
||||
flex: auto;
|
||||
display: none;
|
||||
|
||||
&:first-child {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
@@ -38,6 +38,42 @@ ngb-tabset.vertical(type='tabs', [activeId]='activeTab')
|
||||
[value]='"bottom"'
|
||||
)
|
||||
| At the bottom
|
||||
|
||||
.form-group(*ngIf='hostApp.platform !== Platform.Linux')
|
||||
label Vibrancy
|
||||
br
|
||||
.btn-group(
|
||||
'[(ngModel)]'='config.store.appearance.vibrancy'
|
||||
'(ngModelChange)'='config.save()'
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='true'
|
||||
)
|
||||
| Enable
|
||||
label.btn.btn-secondary(ngbButtonLabel)
|
||||
input(
|
||||
type='radio',
|
||||
ngbButton,
|
||||
[value]='false'
|
||||
)
|
||||
| Disable
|
||||
|
||||
.form-group(*ngIf='hostApp.platform !== Platform.Linux')
|
||||
label Opacity
|
||||
br
|
||||
input(
|
||||
type='range',
|
||||
'[(ngModel)]'='config.store.appearance.opacity',
|
||||
'(ngModelChange)'='config.save()',
|
||||
min='0.05',
|
||||
max='1',
|
||||
step='0.01'
|
||||
)
|
||||
|
||||
.col.col-lg-6
|
||||
.form-group
|
||||
label Window frame
|
||||
@@ -164,7 +200,7 @@ ngb-tabset.vertical(type='tabs', [activeId]='activeTab')
|
||||
[(ngModel)]='config.store.appearance.css',
|
||||
'(ngModelChange)'='config.save()',
|
||||
)
|
||||
|
||||
|
||||
ngb-tab(id='hotkeys')
|
||||
ng-template(ngbTabTitle)
|
||||
| Hotkeys
|
||||
@@ -176,7 +212,7 @@ ngb-tabset.vertical(type='tabs', [activeId]='activeTab')
|
||||
tr
|
||||
th Name
|
||||
th ID
|
||||
th Hotkey
|
||||
th Hotkey
|
||||
tr(*ngFor='let hotkey of hotkeyDescriptions|filterBy:["name"]:hotkeyFilter')
|
||||
td {{hotkey.name}}
|
||||
td {{hotkey.id}}
|
||||
@@ -185,7 +221,7 @@ ngb-tabset.vertical(type='tabs', [activeId]='activeTab')
|
||||
'[(model)]'='config.store.hotkeys[hotkey.id]'
|
||||
'(modelChange)'='config.save(); docking.dock()'
|
||||
)
|
||||
|
||||
|
||||
ngb-tab(*ngFor='let provider of settingsProviders', [id]='provider.id')
|
||||
ng-template(ngbTabTitle)
|
||||
| {{provider.title}}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, Inject, Input } from '@angular/core'
|
||||
import { ElectronService, DockingService, ConfigService, IHotkeyDescription, HotkeyProvider, BaseTabComponent, Theme, HostAppService } from 'terminus-core'
|
||||
import { ElectronService, DockingService, ConfigService, IHotkeyDescription, HotkeyProvider, BaseTabComponent, Theme, HostAppService, Platform } from 'terminus-core'
|
||||
|
||||
import { SettingsTabProvider } from '../api'
|
||||
|
||||
@@ -16,6 +16,7 @@ export class SettingsTabComponent extends BaseTabComponent {
|
||||
hotkeyFilter = ''
|
||||
hotkeyDescriptions: IHotkeyDescription[]
|
||||
screens: any[]
|
||||
Platform = Platform
|
||||
|
||||
constructor (
|
||||
public config: ConfigService,
|
||||
@@ -29,7 +30,6 @@ export class SettingsTabComponent extends BaseTabComponent {
|
||||
super()
|
||||
this.hotkeyDescriptions = config.enabledServices(hotkeyProviders).map(x => x.hotkeys).reduce((a, b) => a.concat(b))
|
||||
this.setTitle('Settings')
|
||||
this.scrollable = true
|
||||
this.screens = this.docking.getScreens()
|
||||
this.settingsProviders = config.enabledServices(this.settingsProviders)
|
||||
this.themes = config.enabledServices(this.themes)
|
||||
|
1
terminus-settings/src/icons/cog.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M482.696 299.276l-32.61-18.827a195.168 195.168 0 0 0 0-48.899l32.61-18.827c9.576-5.528 14.195-16.902 11.046-27.501-11.214-37.749-31.175-71.728-57.535-99.595-7.634-8.07-19.817-9.836-29.437-4.282l-32.562 18.798a194.125 194.125 0 0 0-42.339-24.48V38.049c0-11.13-7.652-20.804-18.484-23.367-37.644-8.909-77.118-8.91-114.77 0-10.831 2.563-18.484 12.236-18.484 23.367v37.614a194.101 194.101 0 0 0-42.339 24.48L105.23 81.345c-9.621-5.554-21.804-3.788-29.437 4.282-26.36 27.867-46.321 61.847-57.535 99.595-3.149 10.599 1.47 21.972 11.046 27.501l32.61 18.827a195.168 195.168 0 0 0 0 48.899l-32.61 18.827c-9.576 5.528-14.195 16.902-11.046 27.501 11.214 37.748 31.175 71.728 57.535 99.595 7.634 8.07 19.817 9.836 29.437 4.283l32.562-18.798a194.08 194.08 0 0 0 42.339 24.479v37.614c0 11.13 7.652 20.804 18.484 23.367 37.645 8.909 77.118 8.91 114.77 0 10.831-2.563 18.484-12.236 18.484-23.367v-37.614a194.138 194.138 0 0 0 42.339-24.479l32.562 18.798c9.62 5.554 21.803 3.788 29.437-4.283 26.36-27.867 46.321-61.847 57.535-99.595 3.149-10.599-1.47-21.972-11.046-27.501zm-65.479 100.461l-46.309-26.74c-26.988 23.071-36.559 28.876-71.039 41.059v53.479a217.145 217.145 0 0 1-87.738 0v-53.479c-33.621-11.879-43.355-17.395-71.039-41.059l-46.309 26.74c-19.71-22.09-34.689-47.989-43.929-75.958l46.329-26.74c-6.535-35.417-6.538-46.644 0-82.079l-46.329-26.74c9.24-27.969 24.22-53.869 43.929-75.969l46.309 26.76c27.377-23.434 37.063-29.065 71.039-41.069V44.464a216.79 216.79 0 0 1 87.738 0v53.479c33.978 12.005 43.665 17.637 71.039 41.069l46.309-26.76c19.709 22.099 34.689 47.999 43.929 75.969l-46.329 26.74c6.536 35.426 6.538 46.644 0 82.079l46.329 26.74c-9.24 27.968-24.219 53.868-43.929 75.957zM256 160c-52.935 0-96 43.065-96 96s43.065 96 96 96 96-43.065 96-96-43.065-96-96-96zm0 160c-35.29 0-64-28.71-64-64s28.71-64 64-64 64 28.71 64 64-28.71 64-64 64z"/></svg>
|
After Width: | Height: | Size: 1.9 KiB |
@@ -36,6 +36,7 @@ module.exports = {
|
||||
{ test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
|
||||
{ test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] },
|
||||
{ test: /\.css$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] },
|
||||
{ test: /\.svg/, use: ['svg-inline-loader'] },
|
||||
]
|
||||
},
|
||||
externals: [
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-ssh",
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.0-alpha.48",
|
||||
"description": "SSH connection manager for Terminus",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
|
@@ -12,7 +12,9 @@ export interface SSHConnection {
|
||||
export class SSHSession extends BaseSession {
|
||||
constructor (private shell: any) {
|
||||
super()
|
||||
}
|
||||
|
||||
start () {
|
||||
this.open = true
|
||||
|
||||
this.shell.on('data', data => {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { DomSanitizer } from '@angular/platform-browser'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HotkeysService, ToolbarButtonProvider, IToolbarButton } from 'terminus-core'
|
||||
import { SSHModalComponent } from './components/sshModal.component'
|
||||
@@ -7,6 +8,7 @@ import { SSHModalComponent } from './components/sshModal.component'
|
||||
export class ButtonProvider extends ToolbarButtonProvider {
|
||||
constructor (
|
||||
private ngbModal: NgbModal,
|
||||
private domSanitizer: DomSanitizer,
|
||||
hotkeys: HotkeysService,
|
||||
) {
|
||||
super()
|
||||
@@ -23,7 +25,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
||||
|
||||
provide (): IToolbarButton[] {
|
||||
return [{
|
||||
icon: 'globe',
|
||||
icon: this.domSanitizer.bypassSecurityTrustHtml(require('./icons/globe.svg')),
|
||||
weight: 5,
|
||||
title: 'SSH connections',
|
||||
touchBarTitle: 'SSH',
|
||||
|
1
terminus-ssh/src/icons/globe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm179.3 160h-67.2c-6.7-36.5-17.5-68.8-31.2-94.7 42.9 19 77.7 52.7 98.4 94.7zM248 56c18.6 0 48.6 41.2 63.2 112H184.8C199.4 97.2 229.4 56 248 56zM48 256c0-13.7 1.4-27.1 4-40h77.7c-1 13.1-1.7 26.3-1.7 40s.7 26.9 1.7 40H52c-2.6-12.9-4-26.3-4-40zm20.7 88h67.2c6.7 36.5 17.5 68.8 31.2 94.7-42.9-19-77.7-52.7-98.4-94.7zm67.2-176H68.7c20.7-42 55.5-75.7 98.4-94.7-13.7 25.9-24.5 58.2-31.2 94.7zM248 456c-18.6 0-48.6-41.2-63.2-112h126.5c-14.7 70.8-44.7 112-63.3 112zm70.1-160H177.9c-1.1-12.8-1.9-26-1.9-40s.8-27.2 1.9-40h140.3c1.1 12.8 1.9 26 1.9 40s-.9 27.2-2 40zm10.8 142.7c13.7-25.9 24.4-58.2 31.2-94.7h67.2c-20.7 42-55.5 75.7-98.4 94.7zM366.3 296c1-13.1 1.7-26.3 1.7-40s-.7-26.9-1.7-40H444c2.6 12.9 4 26.3 4 40s-1.4 27.1-4 40h-77.7z"/></svg>
|
After Width: | Height: | Size: 874 B |
@@ -34,6 +34,7 @@ module.exports = {
|
||||
},
|
||||
{ test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
|
||||
{ test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] },
|
||||
{ test: /\.svg/, use: ['svg-inline-loader'] },
|
||||
]
|
||||
},
|
||||
externals: [
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "terminus-terminal",
|
||||
"version": "1.0.0-alpha.36",
|
||||
"version": "1.0.0-alpha.48",
|
||||
"description": "Terminus' terminal emulation core",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
@@ -42,7 +42,7 @@
|
||||
"font-manager": "0.3.0",
|
||||
"hterm-umdjs": "1.1.3",
|
||||
"mz": "^2.6.0",
|
||||
"node-pty-tmp": "0.7.1",
|
||||
"node-pty-tmp": "0.7.2",
|
||||
"ps-node": "^0.1.6",
|
||||
"runes": "^0.4.2",
|
||||
"winreg": "^1.2.3"
|
||||
|
@@ -11,8 +11,8 @@ export abstract class TerminalDecorator {
|
||||
}
|
||||
|
||||
export interface ResizeEvent {
|
||||
width: number
|
||||
height: number
|
||||
columns: number
|
||||
rows: number
|
||||
}
|
||||
|
||||
export interface SessionOptions {
|
||||
|
@@ -2,6 +2,7 @@ import * as fs from 'mz/fs'
|
||||
import * as path from 'path'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { DomSanitizer } from '@angular/platform-browser'
|
||||
import { HotkeysService, ToolbarButtonProvider, IToolbarButton, ConfigService, HostAppService, ElectronService } from 'terminus-core'
|
||||
|
||||
import { TerminalService } from './services/terminal.service'
|
||||
@@ -10,6 +11,7 @@ import { TerminalService } from './services/terminal.service'
|
||||
export class ButtonProvider extends ToolbarButtonProvider {
|
||||
constructor (
|
||||
private terminal: TerminalService,
|
||||
private domSanitizer: DomSanitizer,
|
||||
private config: ConfigService,
|
||||
hostApp: HostAppService,
|
||||
electron: ElectronService,
|
||||
@@ -51,7 +53,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
||||
|
||||
provide (): IToolbarButton[] {
|
||||
return [{
|
||||
icon: 'plus',
|
||||
icon: this.domSanitizer.bypassSecurityTrustHtml(require('./icons/plus.svg')),
|
||||
title: 'New terminal',
|
||||
touchBarTitle: 'New',
|
||||
click: async () => {
|
||||
|
@@ -1,15 +1,17 @@
|
||||
import { Observable, BehaviorSubject, Subject, Subscription } from 'rxjs'
|
||||
import { Observable, Subject, Subscription } from 'rxjs'
|
||||
import { first } from 'rxjs/operators'
|
||||
import { ToastrService } from 'ngx-toastr'
|
||||
import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core'
|
||||
import { AppService, ConfigService, BaseTabComponent, ElectronService, ThemesService, HostAppService, HotkeysService, Platform } from 'terminus-core'
|
||||
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, Platform } from 'terminus-core'
|
||||
|
||||
import { IShell } from '../api'
|
||||
import { Session, SessionsService } from '../services/sessions.service'
|
||||
import { TerminalService } from '../services/terminal.service'
|
||||
import { TerminalContainersService } from '../services/terminalContainers.service'
|
||||
|
||||
import { TerminalDecorator, ResizeEvent, SessionOptions } from '../api'
|
||||
import { hterm, preferenceManager } from '../hterm'
|
||||
import { TermContainer } from '../terminalContainers/termContainer'
|
||||
import { hterm } from '../hterm'
|
||||
|
||||
@Component({
|
||||
selector: 'terminalTab',
|
||||
@@ -28,87 +30,63 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
@Input() zoom = 0
|
||||
@ViewChild('content') content
|
||||
@HostBinding('style.background-color') backgroundColor: string
|
||||
hterm: any
|
||||
termContainer: TermContainer
|
||||
sessionCloseSubscription: Subscription
|
||||
hotkeysSubscription: Subscription
|
||||
bell$ = new Subject()
|
||||
size: ResizeEvent
|
||||
resize$: Observable<ResizeEvent>
|
||||
private resize_ = new Subject<ResizeEvent>()
|
||||
input$ = new Subject<string>()
|
||||
output$ = new Subject<string>()
|
||||
contentUpdated$ = new Subject<void>()
|
||||
alternateScreenActive$ = new BehaviorSubject(false)
|
||||
mouseEvent$ = new Subject<Event>()
|
||||
htermVisible = false
|
||||
shell: IShell
|
||||
private output = new Subject<string>()
|
||||
private bellPlayer: HTMLAudioElement
|
||||
private io: any
|
||||
private contextMenu: any
|
||||
private termContainerSubscriptions: Subscription[] = []
|
||||
|
||||
get input$ (): Observable<string> { return this.termContainer.input$ }
|
||||
get output$ (): Observable<string> { return this.output }
|
||||
get resize$ (): Observable<ResizeEvent> { return this.termContainer.resize$ }
|
||||
get alternateScreenActive$ (): Observable<boolean> { return this.termContainer.alternateScreenActive$ }
|
||||
|
||||
constructor (
|
||||
private zone: NgZone,
|
||||
private app: AppService,
|
||||
private themes: ThemesService,
|
||||
private hostApp: HostAppService,
|
||||
private hotkeys: HotkeysService,
|
||||
private sessions: SessionsService,
|
||||
private electron: ElectronService,
|
||||
private terminalService: TerminalService,
|
||||
private terminalContainersService: TerminalContainersService,
|
||||
public config: ConfigService,
|
||||
private toastr: ToastrService,
|
||||
@Optional() @Inject(TerminalDecorator) private decorators: TerminalDecorator[],
|
||||
) {
|
||||
super()
|
||||
this.resize$ = this.resize_.asObservable()
|
||||
this.decorators = this.decorators || []
|
||||
this.setTitle('Terminal')
|
||||
this.resize$.pipe(first()).subscribe(async resizeEvent => {
|
||||
if (!this.session) {
|
||||
this.session = this.sessions.addSession(
|
||||
Object.assign({}, this.sessionOptions, resizeEvent)
|
||||
)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.session.resize(resizeEvent.width, resizeEvent.height)
|
||||
}, 1000)
|
||||
this.session = new Session()
|
||||
|
||||
// this.session.output$.bufferTime(10).subscribe((datas) => {
|
||||
this.session.output$.subscribe(data => {
|
||||
this.zone.run(() => {
|
||||
this.output$.next(data)
|
||||
this.write(data)
|
||||
})
|
||||
})
|
||||
|
||||
this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
|
||||
this.app.closeTab(this)
|
||||
})
|
||||
|
||||
this.session.releaseInitialDataBuffer()
|
||||
})
|
||||
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
||||
if (!this.hasFocus) {
|
||||
return
|
||||
}
|
||||
switch (hotkey) {
|
||||
case 'ctrl-c':
|
||||
if (this.hterm.getSelectionText()) {
|
||||
this.hterm.copySelectionToClipboard()
|
||||
this.hterm.getDocument().getSelection().removeAllRanges()
|
||||
if (this.termContainer.getSelection()) {
|
||||
this.termContainer.copySelection()
|
||||
this.termContainer.clearSelection()
|
||||
this.toastr.info('Copied')
|
||||
} else {
|
||||
this.sendInput('\x03')
|
||||
}
|
||||
break
|
||||
case 'copy':
|
||||
this.hterm.copySelectionToClipboard()
|
||||
this.termContainer.copySelection()
|
||||
this.toastr.info('Copied')
|
||||
break
|
||||
case 'paste':
|
||||
this.paste()
|
||||
break
|
||||
case 'clear':
|
||||
this.clear()
|
||||
this.termContainer.clear()
|
||||
break
|
||||
case 'zoom-in':
|
||||
this.zoomIn()
|
||||
@@ -143,6 +121,29 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
this.bellPlayer.src = require<string>('../bell.ogg')
|
||||
}
|
||||
|
||||
initializeSession (columns: number, rows: number) {
|
||||
this.sessions.addSession(
|
||||
this.session,
|
||||
Object.assign({}, this.sessionOptions, {
|
||||
width: columns,
|
||||
height: rows,
|
||||
})
|
||||
)
|
||||
|
||||
// this.session.output$.bufferTime(10).subscribe((datas) => {
|
||||
this.session.output$.subscribe(data => {
|
||||
this.zone.run(() => {
|
||||
this.output.next(data)
|
||||
this.write(data)
|
||||
})
|
||||
})
|
||||
|
||||
this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
|
||||
this.termContainer.destroy()
|
||||
this.app.closeTab(this)
|
||||
})
|
||||
}
|
||||
|
||||
getRecoveryToken (): any {
|
||||
return {
|
||||
type: 'app:terminal',
|
||||
@@ -153,46 +154,49 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
ngOnInit () {
|
||||
this.focused$.subscribe(() => {
|
||||
this.configure()
|
||||
setTimeout(() => {
|
||||
this.hterm.scrollPort_.resize()
|
||||
this.hterm.scrollPort_.focus()
|
||||
}, 100)
|
||||
this.termContainer.focus()
|
||||
})
|
||||
|
||||
this.hterm = new hterm.hterm.Terminal()
|
||||
this.termContainer = this.terminalContainersService.getContainer(this.session)
|
||||
|
||||
this.termContainer.ready$.subscribe(() => {
|
||||
this.htermVisible = true
|
||||
})
|
||||
|
||||
this.termContainer.resize$.pipe(first()).subscribe(async ({columns, rows}) => {
|
||||
if (!this.session.open) {
|
||||
this.initializeSession(columns, rows)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.session.resize(columns, rows)
|
||||
}, 1000)
|
||||
|
||||
this.session.releaseInitialDataBuffer()
|
||||
})
|
||||
|
||||
this.termContainer.attach(this.content.nativeElement)
|
||||
this.attachTermContainerHandlers()
|
||||
|
||||
this.configure()
|
||||
|
||||
this.config.enabledServices(this.decorators).forEach((decorator) => {
|
||||
decorator.attach(this)
|
||||
})
|
||||
|
||||
this.attachHTermHandlers(this.hterm)
|
||||
|
||||
this.hterm.onTerminalReady = () => {
|
||||
this.htermVisible = true
|
||||
this.hterm.installKeyboard()
|
||||
this.hterm.scrollPort_.setCtrlVPaste(true)
|
||||
this.io = this.hterm.io.push()
|
||||
this.attachIOHandlers(this.io)
|
||||
}
|
||||
this.hterm.decorate(this.content.nativeElement)
|
||||
this.configure()
|
||||
|
||||
setTimeout(() => {
|
||||
this.output$.subscribe(() => {
|
||||
this.output.subscribe(() => {
|
||||
this.displayActivity()
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
this.bell$.subscribe(() => {
|
||||
this.termContainer.bell$.subscribe(() => {
|
||||
if (this.config.store.terminal.bell === 'visual') {
|
||||
preferenceManager.set('background-color', 'rgba(128,128,128,.25)')
|
||||
setTimeout(() => {
|
||||
this.configure()
|
||||
}, 125)
|
||||
this.termContainer.visualBell()
|
||||
}
|
||||
if (this.config.store.terminal.bell === 'audible') {
|
||||
this.bellPlayer.play()
|
||||
}
|
||||
// TODO audible
|
||||
})
|
||||
|
||||
this.contextMenu = this.electron.remote.Menu.buildFromTemplate([
|
||||
@@ -209,7 +213,8 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
click: () => {
|
||||
this.zone.run(() => {
|
||||
setTimeout(() => {
|
||||
this.hterm.copySelectionToClipboard()
|
||||
this.termContainer.copySelection()
|
||||
this.toastr.info('Copied')
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -225,117 +230,64 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
])
|
||||
}
|
||||
|
||||
attachHTermHandlers (hterm: any) {
|
||||
hterm.setWindowTitle = title => this.zone.run(() => this.setTitle(title))
|
||||
|
||||
const _setAlternateMode = hterm.setAlternateMode.bind(hterm)
|
||||
hterm.setAlternateMode = (state) => {
|
||||
_setAlternateMode(state)
|
||||
this.alternateScreenActive$.next(state)
|
||||
}
|
||||
|
||||
const _copySelectionToClipboard = hterm.copySelectionToClipboard.bind(hterm)
|
||||
hterm.copySelectionToClipboard = () => {
|
||||
_copySelectionToClipboard()
|
||||
this.toastr.info('Copied')
|
||||
}
|
||||
|
||||
hterm.primaryScreen_.syncSelectionCaret = () => null
|
||||
hterm.alternateScreen_.syncSelectionCaret = () => null
|
||||
hterm.primaryScreen_.terminal = hterm
|
||||
hterm.alternateScreen_.terminal = hterm
|
||||
|
||||
hterm.scrollPort_.onPaste_ = (event) => {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
const _resize = hterm.scrollPort_.resize.bind(hterm.scrollPort_)
|
||||
hterm.scrollPort_.resize = () => {
|
||||
if (!this.hasFocus) {
|
||||
return
|
||||
}
|
||||
_resize()
|
||||
}
|
||||
|
||||
const _onMouse = hterm.onMouse_.bind(hterm)
|
||||
hterm.onMouse_ = (event) => {
|
||||
this.mouseEvent$.next(event)
|
||||
if (event.type === 'mousedown') {
|
||||
if (event.which === 3) {
|
||||
if (this.config.store.terminal.rightClick === 'menu') {
|
||||
this.contextMenu.popup({
|
||||
x: event.pageX + this.content.nativeElement.getBoundingClientRect().left,
|
||||
y: event.pageY + this.content.nativeElement.getBoundingClientRect().top,
|
||||
async: true,
|
||||
})
|
||||
} else if (this.config.store.terminal.rightClick === 'paste') {
|
||||
this.paste()
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
}
|
||||
if (event.type === 'mousewheel') {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (event.wheelDeltaY > 0) {
|
||||
this.zoomIn()
|
||||
} else {
|
||||
this.zoomOut()
|
||||
}
|
||||
} else if (event.altKey) {
|
||||
event.preventDefault()
|
||||
let delta = Math.round(event.wheelDeltaY / 50)
|
||||
this.sendInput(((delta > 0) ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
|
||||
}
|
||||
}
|
||||
_onMouse(event)
|
||||
}
|
||||
|
||||
hterm.ringBell = () => {
|
||||
this.bell$.next()
|
||||
}
|
||||
|
||||
for (let screen of [hterm.primaryScreen_, hterm.alternateScreen_]) {
|
||||
const _insertString = screen.insertString.bind(screen)
|
||||
screen.insertString = (data) => {
|
||||
_insertString(data)
|
||||
this.contentUpdated$.next()
|
||||
}
|
||||
|
||||
const _deleteChars = screen.deleteChars.bind(screen)
|
||||
screen.deleteChars = (count) => {
|
||||
let ret = _deleteChars(count)
|
||||
this.contentUpdated$.next()
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
const _measureCharacterSize = hterm.scrollPort_.measureCharacterSize.bind(hterm.scrollPort_)
|
||||
hterm.scrollPort_.measureCharacterSize = () => {
|
||||
let size = _measureCharacterSize()
|
||||
size.height += this.config.store.terminal.linePadding
|
||||
return size
|
||||
detachTermContainerHandlers () {
|
||||
for (let subscription of this.termContainerSubscriptions) {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
this.termContainerSubscriptions = []
|
||||
}
|
||||
|
||||
attachIOHandlers (io: any) {
|
||||
io.onVTKeystroke = io.sendString = (data) => {
|
||||
this.sendInput(data)
|
||||
this.zone.run(() => {
|
||||
this.input$.next(data)
|
||||
})
|
||||
}
|
||||
io.onTerminalResize = (columns, rows) => {
|
||||
// console.log(`Resizing to ${columns}x${rows}`)
|
||||
this.zone.run(() => {
|
||||
this.size = { width: columns, height: rows }
|
||||
if (this.session) {
|
||||
this.session.resize(columns, rows)
|
||||
attachTermContainerHandlers () {
|
||||
this.detachTermContainerHandlers()
|
||||
this.termContainerSubscriptions = [
|
||||
this.termContainer.title$.subscribe(title => this.zone.run(() => this.setTitle(title))),
|
||||
|
||||
this.focused$.subscribe(() => this.termContainer.enableResizing = true),
|
||||
this.blurred$.subscribe(() => this.termContainer.enableResizing = false),
|
||||
|
||||
this.termContainer.mouseEvent$.subscribe(event => {
|
||||
if (event.type === 'mousedown') {
|
||||
if (event.which === 3) {
|
||||
if (this.config.store.terminal.rightClick === 'menu') {
|
||||
this.contextMenu.popup({
|
||||
async: true,
|
||||
})
|
||||
} else if (this.config.store.terminal.rightClick === 'paste') {
|
||||
this.paste()
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
}
|
||||
this.resize_.next(this.size)
|
||||
if (event.type === 'mousewheel') {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if ((event as MouseWheelEvent).wheelDeltaY > 0) {
|
||||
this.zoomIn()
|
||||
} else {
|
||||
this.zoomOut()
|
||||
}
|
||||
} else if (event.altKey) {
|
||||
event.preventDefault()
|
||||
let delta = Math.round((event as MouseWheelEvent).wheelDeltaY / 50)
|
||||
this.sendInput(((delta > 0) ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
this.termContainer.input$.subscribe(data => {
|
||||
this.sendInput(data)
|
||||
}),
|
||||
|
||||
this.termContainer.resize$.subscribe(({columns, rows}) => {
|
||||
console.log(`Resizing to ${columns}x${rows}`)
|
||||
this.zone.run(() => {
|
||||
if (this.session.open) {
|
||||
this.session.resize(columns, rows)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
sendInput (data: string) {
|
||||
@@ -343,111 +295,48 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
}
|
||||
|
||||
write (data: string) {
|
||||
this.io.writeUTF8(data)
|
||||
this.termContainer.write(data)
|
||||
}
|
||||
|
||||
paste () {
|
||||
let data = this.electron.clipboard.readText()
|
||||
data = this.hterm.keyboard.encode(data)
|
||||
if (this.hterm.options_.bracketedPaste) {
|
||||
data = hterm.lib.encodeUTF8(data)
|
||||
if (this.config.store.terminal.bracketedPaste) {
|
||||
data = '\x1b[200~' + data + '\x1b[201~'
|
||||
}
|
||||
data = data.replace(/\r\n/g, '\n')
|
||||
data = data.replace(/\n/g, '\r')
|
||||
this.sendInput(data)
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.hterm.wipeContents()
|
||||
this.hterm.onVTKeystroke('\f')
|
||||
}
|
||||
|
||||
configure (): void {
|
||||
let config = this.config.store
|
||||
preferenceManager.set('font-family', `"${config.terminal.font}", "monospace-fallback", monospace`)
|
||||
this.setFontSize()
|
||||
preferenceManager.set('enable-bold', true)
|
||||
// preferenceManager.set('audible-bell-sound', '')
|
||||
preferenceManager.set('desktop-notification-bell', config.terminal.bell === 'notification')
|
||||
preferenceManager.set('enable-clipboard-notice', false)
|
||||
preferenceManager.set('receive-encoding', 'raw')
|
||||
preferenceManager.set('send-encoding', 'raw')
|
||||
preferenceManager.set('ctrl-plus-minus-zero-zoom', false)
|
||||
preferenceManager.set('scrollbar-visible', this.hostApp.platform === Platform.macOS)
|
||||
preferenceManager.set('copy-on-select', config.terminal.copyOnSelect)
|
||||
preferenceManager.set('alt-is-meta', config.terminal.altIsMeta)
|
||||
preferenceManager.set('alt-sends-what', 'browser-key')
|
||||
preferenceManager.set('alt-gr-mode', 'ctrl-alt')
|
||||
preferenceManager.set('pass-alt-number', true)
|
||||
preferenceManager.set('cursor-blink', config.terminal.cursorBlink)
|
||||
preferenceManager.set('clear-selection-after-copy', true)
|
||||
this.termContainer.configure(this.config.store)
|
||||
|
||||
if (config.terminal.colorScheme.foreground) {
|
||||
preferenceManager.set('foreground-color', config.terminal.colorScheme.foreground)
|
||||
}
|
||||
if (config.terminal.background === 'colorScheme') {
|
||||
if (config.terminal.colorScheme.background) {
|
||||
this.backgroundColor = config.terminal.colorScheme.background
|
||||
preferenceManager.set('background-color', config.terminal.colorScheme.background)
|
||||
if (this.config.store.terminal.background === 'colorScheme') {
|
||||
if (this.config.store.terminal.colorScheme.background) {
|
||||
this.backgroundColor = this.config.store.terminal.colorScheme.background
|
||||
}
|
||||
} else {
|
||||
this.backgroundColor = null
|
||||
// hterm can't parse "transparent"
|
||||
preferenceManager.set('background-color', this.themes.findCurrentTheme().terminalBackground)
|
||||
}
|
||||
if (config.terminal.colorScheme.colors) {
|
||||
preferenceManager.set('color-palette-overrides', config.terminal.colorScheme.colors)
|
||||
}
|
||||
if (config.terminal.colorScheme.cursor) {
|
||||
preferenceManager.set('cursor-color', config.terminal.colorScheme.cursor)
|
||||
}
|
||||
|
||||
let css = require('../hterm.userCSS.scss')
|
||||
if (!config.terminal.ligatures) {
|
||||
css += `
|
||||
* {
|
||||
font-feature-settings: "liga" 0;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
`
|
||||
} else {
|
||||
css += `
|
||||
* {
|
||||
font-feature-settings: "liga" 1;
|
||||
font-variant-ligatures: initial;
|
||||
}
|
||||
`
|
||||
}
|
||||
css += config.appearance.css
|
||||
this.hterm.setCSS(css)
|
||||
this.hterm.setBracketedPaste(config.terminal.bracketedPaste)
|
||||
this.hterm.defaultCursorShape = {
|
||||
block: hterm.hterm.Terminal.cursorShape.BLOCK,
|
||||
underline: hterm.hterm.Terminal.cursorShape.UNDERLINE,
|
||||
beam: hterm.hterm.Terminal.cursorShape.BEAM,
|
||||
}[config.terminal.cursor]
|
||||
this.hterm.applyCursorShape()
|
||||
this.hterm.setCursorBlink(config.terminal.cursorBlink)
|
||||
if (config.terminal.cursorBlink) {
|
||||
this.hterm.onCursorBlink_()
|
||||
}
|
||||
}
|
||||
|
||||
zoomIn () {
|
||||
this.zoom++
|
||||
this.setFontSize()
|
||||
this.termContainer.setZoom(this.zoom)
|
||||
}
|
||||
|
||||
zoomOut () {
|
||||
this.zoom--
|
||||
this.setFontSize()
|
||||
this.termContainer.setZoom(this.zoom)
|
||||
}
|
||||
|
||||
resetZoom () {
|
||||
this.zoom = 0
|
||||
this.setFontSize()
|
||||
this.termContainer.setZoom(this.zoom)
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.detachTermContainerHandlers()
|
||||
this.config.enabledServices(this.decorators).forEach(decorator => {
|
||||
decorator.detach(this)
|
||||
})
|
||||
@@ -455,13 +344,7 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
if (this.sessionCloseSubscription) {
|
||||
this.sessionCloseSubscription.unsubscribe()
|
||||
}
|
||||
this.resize_.complete()
|
||||
this.input$.complete()
|
||||
this.output$.complete()
|
||||
this.contentUpdated$.complete()
|
||||
this.alternateScreenActive$.complete()
|
||||
this.mouseEvent$.complete()
|
||||
this.bell$.complete()
|
||||
this.output.complete()
|
||||
}
|
||||
|
||||
async destroy () {
|
||||
@@ -481,8 +364,4 @@ export class TerminalTabComponent extends BaseTabComponent {
|
||||
}
|
||||
return confirm(`"${children[0].command}" is still running. Close?`)
|
||||
}
|
||||
|
||||
private setFontSize () {
|
||||
preferenceManager.set('font-size', this.config.store.terminal.fontSize * Math.pow(1.1, this.zoom))
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ x-screen {
|
||||
transition: 0.125s ease background;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
width: 3px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ x-screen {
|
||||
x-row > span {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
|
||||
&.wc-node {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
1
terminus-terminal/src/icons/plus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M368 224H224V80c0-8.84-7.16-16-16-16h-32c-8.84 0-16 7.16-16 16v144H16c-8.84 0-16 7.16-16 16v32c0 8.84 7.16 16 16 16h144v144c0 8.84 7.16 16 16 16h32c8.84 0 16-7.16 16-16V288h144c8.84 0 16-7.16 16-16v-32c0-8.84-7.16-16-16-16z"/></svg>
|
After Width: | Height: | Size: 303 B |
@@ -14,6 +14,7 @@ import { ColorPickerComponent } from './components/colorPicker.component'
|
||||
|
||||
import { SessionsService, BaseSession } from './services/sessions.service'
|
||||
import { TerminalService } from './services/terminal.service'
|
||||
import { TerminalContainersService } from './services/terminalContainers.service'
|
||||
|
||||
import { ScreenPersistenceProvider } from './persistence/screen'
|
||||
import { TMuxPersistenceProvider } from './persistence/tmux'
|
||||
@@ -50,6 +51,7 @@ import { hterm } from './hterm'
|
||||
providers: [
|
||||
SessionsService,
|
||||
TerminalService,
|
||||
TerminalContainersService,
|
||||
|
||||
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
|
||||
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
|
||||
@@ -123,4 +125,4 @@ export default class TerminalModule {
|
||||
}
|
||||
|
||||
export * from './api'
|
||||
export { TerminalService, BaseSession, TerminalTabComponent }
|
||||
export { TerminalService, BaseSession, TerminalTabComponent, TerminalContainersService }
|
||||
|
@@ -1,20 +1,25 @@
|
||||
import { Subscription } from 'rxjs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TerminalDecorator } from './api'
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
|
||||
@Injectable()
|
||||
export class PathDropDecorator extends TerminalDecorator {
|
||||
private subscriptions: Subscription[] = []
|
||||
|
||||
attach (terminal: TerminalTabComponent): void {
|
||||
setTimeout(() => {
|
||||
terminal.hterm.scrollPort_.document_.addEventListener('dragover', (event) => {
|
||||
event.preventDefault()
|
||||
})
|
||||
terminal.hterm.scrollPort_.document_.addEventListener('drop', (event) => {
|
||||
for (let file of event.dataTransfer.files) {
|
||||
this.injectPath(terminal, file.path)
|
||||
}
|
||||
event.preventDefault()
|
||||
})
|
||||
this.subscriptions = [
|
||||
terminal.termContainer.dragOver$.subscribe(event => {
|
||||
event.preventDefault()
|
||||
}),
|
||||
terminal.termContainer.drop$.subscribe(event => {
|
||||
for (let file of event.dataTransfer.files as any) {
|
||||
this.injectPath(terminal, file.path)
|
||||
}
|
||||
event.preventDefault()
|
||||
}),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,6 +30,9 @@ export class PathDropDecorator extends TerminalDecorator {
|
||||
terminal.sendInput(path + ' ')
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-empty
|
||||
detach (terminal: TerminalTabComponent): void { }
|
||||
detach (terminal: TerminalTabComponent): void {
|
||||
for (let s of this.subscriptions) {
|
||||
s.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,35 +20,31 @@ export abstract class BaseSession {
|
||||
name: string
|
||||
recoveryId: string
|
||||
truePID: number
|
||||
output$: Observable<string>
|
||||
closed$: Observable<void>
|
||||
destroyed$: Observable<void>
|
||||
protected output_ = new Subject<string>()
|
||||
protected closed_ = new Subject<void>()
|
||||
protected destroyed_ = new Subject<void>()
|
||||
protected output = new Subject<string>()
|
||||
protected closed = new Subject<void>()
|
||||
protected destroyed = new Subject<void>()
|
||||
private initialDataBuffer = ''
|
||||
private initialDataBufferReleased = false
|
||||
|
||||
constructor () {
|
||||
this.output$ = this.output_.asObservable()
|
||||
this.closed$ = this.closed_.asObservable()
|
||||
this.destroyed$ = this.destroyed_.asObservable()
|
||||
}
|
||||
get output$ (): Observable<string> { return this.output }
|
||||
get closed$ (): Observable<void> { return this.closed }
|
||||
get destroyed$ (): Observable<void> { return this.destroyed }
|
||||
|
||||
emitOutput (data: string) {
|
||||
if (!this.initialDataBufferReleased) {
|
||||
this.initialDataBuffer += data
|
||||
} else {
|
||||
this.output_.next(data)
|
||||
this.output.next(data)
|
||||
}
|
||||
}
|
||||
|
||||
releaseInitialDataBuffer () {
|
||||
this.initialDataBufferReleased = true
|
||||
this.output_.next(this.initialDataBuffer)
|
||||
this.output.next(this.initialDataBuffer)
|
||||
this.initialDataBuffer = null
|
||||
}
|
||||
|
||||
abstract start (options: SessionOptions)
|
||||
abstract resize (columns, rows)
|
||||
abstract write (data)
|
||||
abstract kill (signal?: string)
|
||||
@@ -59,9 +55,9 @@ export abstract class BaseSession {
|
||||
async destroy (): Promise<void> {
|
||||
if (this.open) {
|
||||
this.open = false
|
||||
this.closed_.next()
|
||||
this.destroyed_.next()
|
||||
this.output_.complete()
|
||||
this.closed.next()
|
||||
this.destroyed.next()
|
||||
this.output.complete()
|
||||
await this.gracefullyKillProcess()
|
||||
}
|
||||
}
|
||||
@@ -70,8 +66,7 @@ export abstract class BaseSession {
|
||||
export class Session extends BaseSession {
|
||||
private pty: any
|
||||
|
||||
constructor (options: SessionOptions) {
|
||||
super()
|
||||
start (options: SessionOptions) {
|
||||
this.name = options.name
|
||||
this.recoveryId = options.recoveryId
|
||||
|
||||
@@ -200,7 +195,7 @@ export class Session extends BaseSession {
|
||||
|
||||
@Injectable()
|
||||
export class SessionsService {
|
||||
sessions: {[id: string]: Session} = {}
|
||||
sessions: {[id: string]: BaseSession} = {}
|
||||
logger: Logger
|
||||
private lastID = 0
|
||||
|
||||
@@ -225,10 +220,10 @@ export class SessionsService {
|
||||
return options
|
||||
}
|
||||
|
||||
addSession (options: SessionOptions): Session {
|
||||
addSession (session: BaseSession, options: SessionOptions) {
|
||||
this.lastID++
|
||||
options.name = `session-${this.lastID}`
|
||||
let session = new Session(options)
|
||||
session.start(options)
|
||||
let persistence = this.getPersistence()
|
||||
session.destroyed$.pipe(first()).subscribe(() => {
|
||||
delete this.sessions[session.name]
|
||||
|
@@ -7,10 +7,11 @@ import { TerminalTabComponent } from '../components/terminalTab.component'
|
||||
|
||||
@Injectable()
|
||||
export class TerminalService {
|
||||
shells$: Observable<IShell[]>
|
||||
private shells_ = new AsyncSubject<IShell[]>()
|
||||
private shells = new AsyncSubject<IShell[]>()
|
||||
private logger: Logger
|
||||
|
||||
get shells$ (): Observable<IShell[]> { return this.shells }
|
||||
|
||||
constructor (
|
||||
private app: AppService,
|
||||
private sessions: SessionsService,
|
||||
@@ -27,11 +28,10 @@ export class TerminalService {
|
||||
}
|
||||
|
||||
async reloadShells () {
|
||||
this.shells_ = new AsyncSubject<IShell[]>()
|
||||
this.shells$ = this.shells_.asObservable()
|
||||
this.shells = new AsyncSubject<IShell[]>()
|
||||
let shellLists = await Promise.all(this.config.enabledServices(this.shellProviders).map(x => x.provide()))
|
||||
this.shells_.next(shellLists.reduce((a, b) => a.concat(b)))
|
||||
this.shells_.complete()
|
||||
this.shells.next(shellLists.reduce((a, b) => a.concat(b)))
|
||||
this.shells.complete()
|
||||
}
|
||||
|
||||
async openTab (shell?: IShell, cwd?: string): Promise<TerminalTabComponent> {
|
||||
|
16
terminus-terminal/src/services/terminalContainers.service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TermContainer } from '../terminalContainers/termContainer'
|
||||
import { HTermContainer } from '../terminalContainers/htermContainer'
|
||||
import { BaseSession } from '../services/sessions.service'
|
||||
|
||||
@Injectable()
|
||||
export class TerminalContainersService {
|
||||
private containers = new WeakMap<BaseSession, TermContainer>()
|
||||
|
||||
getContainer (session: BaseSession): TermContainer {
|
||||
if (!this.containers.has(session)) {
|
||||
this.containers.set(session, new HTermContainer())
|
||||
}
|
||||
return this.containers.get(session)
|
||||
}
|
||||
}
|
226
terminus-terminal/src/terminalContainers/htermContainer.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { TermContainer } from './termContainer'
|
||||
import { hterm, preferenceManager } from '../hterm'
|
||||
|
||||
export class HTermContainer extends TermContainer {
|
||||
term: any
|
||||
io: any
|
||||
private htermIframe: HTMLElement
|
||||
private initialized = false
|
||||
private configuredFontSize = 0
|
||||
private configuredLinePadding = 0
|
||||
private zoom = 0
|
||||
|
||||
attach (host: HTMLElement) {
|
||||
if (!this.initialized) {
|
||||
this.init()
|
||||
this.initialized = true
|
||||
this.term.decorate(host)
|
||||
this.htermIframe = this.term.scrollPort_.iframe_
|
||||
} else {
|
||||
host.appendChild(this.htermIframe)
|
||||
}
|
||||
}
|
||||
|
||||
getSelection (): string {
|
||||
return this.term.getSelectionText()
|
||||
}
|
||||
|
||||
copySelection () {
|
||||
this.term.copySelectionToClipboard()
|
||||
}
|
||||
|
||||
clearSelection () {
|
||||
this.term.getDocument().getSelection().removeAllRanges()
|
||||
}
|
||||
|
||||
focus () {
|
||||
setTimeout(() => {
|
||||
this.term.scrollPort_.resize()
|
||||
this.term.scrollPort_.focus()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
write (data: string): void {
|
||||
this.io.writeUTF8(data)
|
||||
}
|
||||
|
||||
clear (): void {
|
||||
this.term.wipeContents()
|
||||
this.term.onVTKeystroke('\f')
|
||||
}
|
||||
|
||||
configure (config: any): void {
|
||||
this.configuredFontSize = config.terminal.fontSize
|
||||
this.configuredLinePadding = config.terminal.linePadding
|
||||
this.setFontSize()
|
||||
|
||||
preferenceManager.set('font-family', `"${config.terminal.font}", "monospace-fallback", monospace`)
|
||||
preferenceManager.set('enable-bold', true)
|
||||
// preferenceManager.set('audible-bell-sound', '')
|
||||
preferenceManager.set('desktop-notification-bell', config.terminal.bell === 'notification')
|
||||
preferenceManager.set('enable-clipboard-notice', false)
|
||||
preferenceManager.set('receive-encoding', 'raw')
|
||||
preferenceManager.set('send-encoding', 'raw')
|
||||
preferenceManager.set('ctrl-plus-minus-zero-zoom', false)
|
||||
preferenceManager.set('scrollbar-visible', process.platform === 'darwin')
|
||||
preferenceManager.set('copy-on-select', config.terminal.copyOnSelect)
|
||||
preferenceManager.set('alt-is-meta', config.terminal.altIsMeta)
|
||||
preferenceManager.set('alt-sends-what', 'browser-key')
|
||||
preferenceManager.set('alt-gr-mode', 'ctrl-alt')
|
||||
preferenceManager.set('pass-alt-number', true)
|
||||
preferenceManager.set('cursor-blink', config.terminal.cursorBlink)
|
||||
preferenceManager.set('clear-selection-after-copy', true)
|
||||
|
||||
if (config.terminal.colorScheme.foreground) {
|
||||
preferenceManager.set('foreground-color', config.terminal.colorScheme.foreground)
|
||||
}
|
||||
|
||||
if (config.terminal.background === 'colorScheme') {
|
||||
if (config.terminal.colorScheme.background) {
|
||||
preferenceManager.set('background-color', config.terminal.colorScheme.background)
|
||||
}
|
||||
} else {
|
||||
// hterm can't parse "transparent"
|
||||
preferenceManager.set('background-color', 'transparent')
|
||||
}
|
||||
|
||||
if (config.terminal.colorScheme.colors) {
|
||||
preferenceManager.set('color-palette-overrides', config.terminal.colorScheme.colors)
|
||||
}
|
||||
if (config.terminal.colorScheme.cursor) {
|
||||
preferenceManager.set('cursor-color', config.terminal.colorScheme.cursor)
|
||||
}
|
||||
|
||||
let css = require('../hterm.userCSS.scss')
|
||||
if (!config.terminal.ligatures) {
|
||||
css += `
|
||||
* {
|
||||
font-feature-settings: "liga" 0;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
`
|
||||
} else {
|
||||
css += `
|
||||
* {
|
||||
font-feature-settings: "liga" 1;
|
||||
font-variant-ligatures: initial;
|
||||
}
|
||||
`
|
||||
}
|
||||
css += config.appearance.css
|
||||
this.term.setCSS(css)
|
||||
this.term.setBracketedPaste(config.terminal.bracketedPaste)
|
||||
this.term.defaultCursorShape = {
|
||||
block: hterm.hterm.Terminal.cursorShape.BLOCK,
|
||||
underline: hterm.hterm.Terminal.cursorShape.UNDERLINE,
|
||||
beam: hterm.hterm.Terminal.cursorShape.BEAM,
|
||||
}[config.terminal.cursor]
|
||||
this.term.applyCursorShape()
|
||||
this.term.setCursorBlink(config.terminal.cursorBlink)
|
||||
if (config.terminal.cursorBlink) {
|
||||
this.term.onCursorBlink_()
|
||||
}
|
||||
}
|
||||
|
||||
setZoom (zoom: number): void {
|
||||
this.zoom = zoom
|
||||
this.setFontSize()
|
||||
}
|
||||
|
||||
visualBell (): void {
|
||||
const color = preferenceManager.get('background-color')
|
||||
preferenceManager.set('background-color', 'rgba(128,128,128,.25)')
|
||||
setTimeout(() => {
|
||||
preferenceManager.set('background-color', color)
|
||||
}, 125)
|
||||
}
|
||||
|
||||
private setFontSize () {
|
||||
preferenceManager.set('font-size', this.configuredFontSize * Math.pow(1.1, this.zoom))
|
||||
}
|
||||
|
||||
private init () {
|
||||
this.term = new hterm.hterm.Terminal()
|
||||
this.term.onTerminalReady = () => {
|
||||
this.term.installKeyboard()
|
||||
this.term.scrollPort_.setCtrlVPaste(true)
|
||||
this.io = this.term.io.push()
|
||||
this.io.onVTKeystroke = this.io.sendString = data => this.input.next(data)
|
||||
this.io.onTerminalResize = (columns, rows) => {
|
||||
console.log('hterm resize')
|
||||
this.resize.next({ columns, rows })
|
||||
}
|
||||
this.ready.next(null)
|
||||
this.ready.complete()
|
||||
|
||||
this.term.scrollPort_.document_.addEventListener('dragOver', event => {
|
||||
this.dragOver.next(event)
|
||||
})
|
||||
|
||||
this.term.scrollPort_.document_.addEventListener('drop', event => {
|
||||
this.drop.next(event)
|
||||
})
|
||||
}
|
||||
this.term.setWindowTitle = title => this.title.next(title)
|
||||
|
||||
const _setAlternateMode = this.term.setAlternateMode.bind(this.term)
|
||||
this.term.setAlternateMode = (state) => {
|
||||
_setAlternateMode(state)
|
||||
this.alternateScreenActive.next(state)
|
||||
}
|
||||
|
||||
this.term.primaryScreen_.syncSelectionCaret = () => null
|
||||
this.term.alternateScreen_.syncSelectionCaret = () => null
|
||||
this.term.primaryScreen_.terminal = this.term
|
||||
this.term.alternateScreen_.terminal = this.term
|
||||
|
||||
this.term.scrollPort_.onPaste_ = (event) => {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
const _resize = this.term.scrollPort_.resize.bind(this.term.scrollPort_)
|
||||
this.term.scrollPort_.resize = () => {
|
||||
if (this.enableResizing) {
|
||||
_resize()
|
||||
}
|
||||
}
|
||||
|
||||
const _onMouse = this.term.onMouse_.bind(this.term)
|
||||
this.term.onMouse_ = (event) => {
|
||||
this.mouseEvent.next(event)
|
||||
if (event.type === 'mousedown' && event.which === 3) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
if (event.type === 'mousewheel' && event.altKey) {
|
||||
event.preventDefault()
|
||||
}
|
||||
_onMouse(event)
|
||||
}
|
||||
|
||||
this.term.ringBell = () => this.bell.next()
|
||||
|
||||
for (let screen of [this.term.primaryScreen_, this.term.alternateScreen_]) {
|
||||
const _insertString = screen.insertString.bind(screen)
|
||||
screen.insertString = (data) => {
|
||||
_insertString(data)
|
||||
this.contentUpdated.next()
|
||||
}
|
||||
|
||||
const _deleteChars = screen.deleteChars.bind(screen)
|
||||
screen.deleteChars = (count) => {
|
||||
let ret = _deleteChars(count)
|
||||
this.contentUpdated.next()
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
const _measureCharacterSize = this.term.scrollPort_.measureCharacterSize.bind(this.term.scrollPort_)
|
||||
this.term.scrollPort_.measureCharacterSize = () => {
|
||||
let size = _measureCharacterSize()
|
||||
size.height += this.configuredLinePadding
|
||||
return size
|
||||
}
|
||||
}
|
||||
}
|
57
terminus-terminal/src/terminalContainers/termContainer.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Observable, Subject, AsyncSubject, ReplaySubject, BehaviorSubject } from 'rxjs'
|
||||
import { ResizeEvent } from '../api'
|
||||
|
||||
export abstract class TermContainer {
|
||||
enableResizing = true
|
||||
protected ready = new AsyncSubject<void>()
|
||||
protected title = new ReplaySubject<string>(1)
|
||||
protected alternateScreenActive = new BehaviorSubject<boolean>(false)
|
||||
protected mouseEvent = new Subject<MouseEvent>()
|
||||
protected bell = new Subject<void>()
|
||||
protected contentUpdated = new Subject<void>()
|
||||
protected input = new Subject<string>()
|
||||
protected resize = new ReplaySubject<ResizeEvent>(1)
|
||||
protected dragOver = new Subject<DragEvent>()
|
||||
protected drop = new Subject<DragEvent>()
|
||||
|
||||
get ready$ (): Observable<void> { return this.ready }
|
||||
get title$ (): Observable<string> { return this.title }
|
||||
get alternateScreenActive$ (): Observable<boolean> { return this.alternateScreenActive }
|
||||
get mouseEvent$ (): Observable<MouseEvent> { return this.mouseEvent }
|
||||
get bell$ (): Observable<void> { return this.bell }
|
||||
get contentUpdated$ (): Observable<void> { return this.contentUpdated }
|
||||
get input$ (): Observable<string> { return this.input }
|
||||
get resize$ (): Observable<ResizeEvent> { return this.resize }
|
||||
get dragOver$ (): Observable<DragEvent> { return this.dragOver }
|
||||
get drop$ (): Observable<DragEvent> { return this.drop }
|
||||
|
||||
abstract attach (host: HTMLElement): void
|
||||
|
||||
destroy (): void {
|
||||
for (let o of [
|
||||
this.ready,
|
||||
this.title,
|
||||
this.alternateScreenActive,
|
||||
this.mouseEvent,
|
||||
this.bell,
|
||||
this.contentUpdated,
|
||||
this.input,
|
||||
this.resize,
|
||||
this.dragOver,
|
||||
this.drop,
|
||||
]) {
|
||||
o.complete()
|
||||
}
|
||||
}
|
||||
|
||||
abstract getSelection (): string
|
||||
abstract copySelection (): void
|
||||
abstract clearSelection (): void
|
||||
abstract focus (): void
|
||||
abstract write (data: string): void
|
||||
abstract clear (): void
|
||||
abstract visualBell (): void
|
||||
|
||||
abstract configure (configStore: any): void
|
||||
abstract setZoom (zoom: number): void
|
||||
}
|
@@ -36,6 +36,7 @@ module.exports = {
|
||||
{ test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
|
||||
{ test: /\.scss$/, use: ['to-string-loader', 'css-loader', 'sass-loader'] },
|
||||
{ test: /\.css$/, use: ['to-string-loader', 'css-loader'] },
|
||||
{ test: /\.svg/, use: ['svg-inline-loader'] },
|
||||
{
|
||||
test: /\.(ttf|eot|otf|woff|woff2|ogg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
use: {
|
||||
|
@@ -92,19 +92,15 @@ mz@^2.6.0:
|
||||
object-assign "^4.0.1"
|
||||
thenify-all "^1.0.0"
|
||||
|
||||
nan@>=2.10.0:
|
||||
nan@>=2.10.0, nan@^2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
|
||||
|
||||
nan@^2.6.2:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
|
||||
|
||||
node-pty-tmp@0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/node-pty-tmp/-/node-pty-tmp-0.7.1.tgz#0a81179f9087b21f968206c886e543db20650d7a"
|
||||
node-pty-tmp@0.7.2:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/node-pty-tmp/-/node-pty-tmp-0.7.2.tgz#d1528245a46ab193c54e34792ee0b89d0f557417"
|
||||
dependencies:
|
||||
nan "^2.6.2"
|
||||
nan "^2.10.0"
|
||||
|
||||
object-assign@^4.0.1:
|
||||
version "4.1.1"
|
||||
|
14
travis.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -ev
|
||||
|
||||
yarn install
|
||||
scripts/install-deps.js
|
||||
|
||||
scripts/build-native.js
|
||||
yarn run build
|
||||
scripts/prepackage-plugins.js
|
||||
scripts/build-$BUILD_FOR.js
|
||||
|
||||
if [ "${STAGE}" = "deploy" ]; then
|
||||
docker run -v $(pwd):$(pwd) -t snapcore:snapcraft sh -c "apt update -qq && cd $(pwd) && snapcraft && snapcraft push dist/*.snap --release edge"
|
||||
fi
|