173
.all-contributorsrc
Normal file
@ -0,0 +1,173 @@
|
||||
{
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"contributors": [
|
||||
{
|
||||
"login": "mezner",
|
||||
"name": "Russell Myers",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/184085?v=4",
|
||||
"profile": "http://www.russellmyers.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ehwarren",
|
||||
"name": "Austin Warren",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/3991658?v=4",
|
||||
"profile": "http://www.morwire.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Drachenkaetzchen",
|
||||
"name": "Felicia Hummel",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/162974?v=4",
|
||||
"profile": "https://github.com/Drachenkaetzchen",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mikemaccana",
|
||||
"name": "Mike MacCana",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/172594?v=4",
|
||||
"profile": "https://github.com/mikemaccana",
|
||||
"contributions": [
|
||||
"test",
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "yxuko",
|
||||
"name": "Yacine Kanzari",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/1786317?v=4",
|
||||
"profile": "https://github.com/yxuko",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "BBJip",
|
||||
"name": "BBJip",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/32908927?v=4",
|
||||
"profile": "https://github.com/BBJip",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Futagirl",
|
||||
"name": "Futagirl",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/33533958?v=4",
|
||||
"profile": "https://github.com/Futagirl",
|
||||
"contributions": [
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "levrik",
|
||||
"name": "Levin Rickert",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/9491603?v=4",
|
||||
"profile": "https://www.levrik.io",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kwonoj",
|
||||
"name": "OJ Kwon",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/1210596?v=4",
|
||||
"profile": "https://kwonoj.github.io",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Domain",
|
||||
"name": "domain",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/903197?v=4",
|
||||
"profile": "https://github.com/Domain",
|
||||
"contributions": [
|
||||
"plugin",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kbjr",
|
||||
"name": "James Brumond",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/195127?v=4",
|
||||
"profile": "http://www.jbrumond.me",
|
||||
"contributions": [
|
||||
"plugin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Tyriar",
|
||||
"name": "Daniel Imms",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/2193314?v=4",
|
||||
"profile": "http://www.growingwiththeweb.com",
|
||||
"contributions": [
|
||||
"code",
|
||||
"plugin",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "baflo",
|
||||
"name": "Florian Bachmann",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/834350?v=4",
|
||||
"profile": "https://github.com/baflo",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mischah",
|
||||
"name": "Michael Kühnel",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/441011?v=4",
|
||||
"profile": "http://michael-kuehnel.de",
|
||||
"contributions": [
|
||||
"code",
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "NieLeben",
|
||||
"name": "Tilmann Meyer",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/47182955?v=4",
|
||||
"profile": "https://github.com/NieLeben",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "PMExtra",
|
||||
"name": "PM Extra",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/11289158?v=4",
|
||||
"profile": "http://www.jubeat.net",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "IgnusG",
|
||||
"name": "Jonathan",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/6438760?v=4",
|
||||
"profile": "https://jjuhas.keybase.pub//",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"projectName": "terminus",
|
||||
"projectOwner": "Eugeny",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"commitConvention": "none"
|
||||
}
|
82
.eslintrc.yml
Normal file
@ -0,0 +1,82 @@
|
||||
parser: '@typescript-eslint/parser'
|
||||
parserOptions:
|
||||
project: tsconfig.json
|
||||
extends:
|
||||
- 'plugin:@typescript-eslint/all'
|
||||
plugins:
|
||||
- '@typescript-eslint'
|
||||
env:
|
||||
browser: true
|
||||
es6: true
|
||||
node: true
|
||||
commonjs: true
|
||||
rules:
|
||||
'@typescript-eslint/semi':
|
||||
- error
|
||||
- never
|
||||
'@typescript-eslint/indent':
|
||||
- error
|
||||
- 4
|
||||
'@typescript-eslint/explicit-member-accessibility':
|
||||
- error
|
||||
- accessibility: no-public
|
||||
overrides:
|
||||
parameterProperties: explicit
|
||||
'@typescript-eslint/no-require-imports': off
|
||||
'@typescript-eslint/no-parameter-properties': off
|
||||
'@typescript-eslint/explicit-function-return-type': off
|
||||
'@typescript-eslint/no-explicit-any': off
|
||||
'@typescript-eslint/no-magic-numbers': off
|
||||
'@typescript-eslint/member-delimiter-style': off
|
||||
'@typescript-eslint/promise-function-async': off
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': off
|
||||
'@typescript-eslint/require-array-sort-compare': off
|
||||
'@typescript-eslint/no-use-before-define':
|
||||
- error
|
||||
- classes: false
|
||||
no-duplicate-imports: error
|
||||
array-bracket-spacing:
|
||||
- error
|
||||
- never
|
||||
block-scoped-var: error
|
||||
brace-style:
|
||||
- error
|
||||
- 1tbs
|
||||
- allowSingleLine: true
|
||||
computed-property-spacing:
|
||||
- error
|
||||
- never
|
||||
comma-dangle:
|
||||
- error
|
||||
- always-multiline
|
||||
curly: error
|
||||
eol-last: error
|
||||
eqeqeq:
|
||||
- error
|
||||
- smart
|
||||
linebreak-style:
|
||||
- error
|
||||
- unix
|
||||
max-depth:
|
||||
- 1
|
||||
- 5
|
||||
max-statements:
|
||||
- 1
|
||||
- 80
|
||||
no-multiple-empty-lines: error
|
||||
no-mixed-spaces-and-tabs: error
|
||||
no-trailing-spaces: error
|
||||
'@typescript-eslint/no-unused-vars':
|
||||
- error
|
||||
- vars: all
|
||||
args: after-used
|
||||
argsIgnorePattern: ^_
|
||||
no-undef: error
|
||||
object-curly-spacing:
|
||||
- error
|
||||
- always
|
||||
quote-props:
|
||||
- warn
|
||||
- as-needed
|
||||
- keywords: true
|
||||
numbers: true
|
3
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
github: eugeny
|
||||
open_collective: terminus
|
||||
ko_fi: eugeny
|
2
.github/stale.yml
vendored
@ -5,7 +5,7 @@ daysUntilClose: 14
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- "T: Enhancement"
|
||||
- "S: Triaged"
|
||||
- "S: Confirmed"
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: "S: Stale"
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
|
7
.gitignore
vendored
@ -5,6 +5,9 @@ node_modules
|
||||
|
||||
build/files.wxs
|
||||
dist
|
||||
*/dist
|
||||
*/typings
|
||||
*.tsbuildinfo
|
||||
|
||||
*.xcworkspacedata
|
||||
*.xcuserstate
|
||||
@ -17,3 +20,7 @@ npm-debug.log
|
||||
builtin-plugins
|
||||
package-lock.json
|
||||
yarn-error.log
|
||||
|
||||
docs/api
|
||||
.travis.ssh.key
|
||||
*.code-workspace
|
||||
|
BIN
.travis.ssh.key.enc
Normal file
1
.travis.ssh.key.pub
Normal file
@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDDFM4nHSbET5V7EYNgjA8NeVfOxV0wVMdZ2YvsDzD+qPJ4+MYbvsL7ZPaSxQSn7n6ATkLHjKje5RpF/Rl9K3kucGs0P6cqJVeE0qryEteQ3Q+fYAk+bD2J9ZQ/hv/0NtLl8T+7lJUZ3WUxFH73sgph77Sw0z+kMpPaK7U2vqMBQD/7+6iJgya31wP0qW0XKDz1BjKeXgwTg10Pm4vcGsR4c2q7YIzSzBHffcyo0vJyFvOX/ZKHlZRcq/wnQMeOl/hPgf1xCENjQZmFVReQlYSw5cNNDT9HZPKekOAZFFez7/AbPiTIo/bnBYIv0mdUjr3nw8nXF505q8LiD3z/ksaaWDqe9CCLM4W0Bh7/dhP7IGPdfX0fVHLhOnYIOsG21D8rWJjMPkVRSLyEvWNAnVuObJNHoQu8VATnOxfPNnMun72IHyyFWVoADk5JcsMbzcP7gZB+5oJO7U1qpcdndtBOA3ZlF0Uz2jVZnqavoEBWT39tl3vs69hAA3aTPGclg7HMuAJOl4HsKmaUgDxqV2wCX/S4pDqmKMbmumDLX+MM0xl0gXj/zpVJp9BzdnrArkC40ivmC6TSA4wrdN0tNBlqApkH5/jxGWrcu2AXVn9PGF3+QrjW0iu+QMZCaKWDhLIQC835uFwzhnNGlx41B7uxMLuNFxKXdQ3f/cC9QMG8ew== TravisCIDeployKey
|
48
.travis.yml
@ -1,29 +1,25 @@
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
env: BUILD_FOR=linux
|
||||
- os: osx
|
||||
env: BUILD_FOR=macos
|
||||
|
||||
language: node_js
|
||||
node_js: 8
|
||||
node_js: 11
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
- app/node_modules
|
||||
stages:
|
||||
- Docs
|
||||
|
||||
before_install:
|
||||
- yarn install
|
||||
- scripts/install-deps.js
|
||||
jobs:
|
||||
include:
|
||||
- stage: 'Docs'
|
||||
os: linux
|
||||
if: branch = master
|
||||
script:
|
||||
- set -e
|
||||
- openssl aes-256-cbc -K $encrypted_4e2fb4889ef8_key -iv $encrypted_4e2fb4889ef8_iv -in .travis.ssh.key.enc -out .travis.ssh.key -d
|
||||
- eval "$(ssh-agent -s)"
|
||||
- chmod 600 .travis.ssh.key
|
||||
- ssh-add .travis.ssh.key
|
||||
- yarn
|
||||
- yarn run docs
|
||||
- rsync -e "ssh -o StrictHostKeyChecking=no" -arv docs/api/ root@ajenti.org:/srv/terminus-docs/
|
||||
|
||||
script:
|
||||
- scripts/build-native.js
|
||||
- yarn run build
|
||||
- scripts/prepackage-plugins.js
|
||||
- scripts/build-$BUILD_FOR.js
|
||||
|
||||
dist: trusty
|
||||
dist: xenial
|
||||
sudo: false
|
||||
|
||||
addons:
|
||||
@ -31,6 +27,14 @@ addons:
|
||||
packages:
|
||||
- rpm
|
||||
- yarn
|
||||
- libsecret-1-dev
|
||||
sources:
|
||||
- sourceline: 'deb https://dl.yarnpkg.com/debian/ stable main'
|
||||
key_url: 'https://dl.yarnpkg.com/debian/pubkey.gpg'
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- 'terminus-*/node_modules'
|
||||
- $HOME/.cache/yarn
|
||||
- $HOME/.cache/electron
|
||||
- $HOME/.cache/electron-builder
|
||||
|
@ -92,11 +92,11 @@ Plugins provide functionality by exporting singular or multi providers:
|
||||
|
||||
```javascript
|
||||
import { NgModule, Injectable } from '@angular/core'
|
||||
import { ToolbarButtonProvider, IToolbarButton } from 'terminus-core'
|
||||
import { ToolbarButtonProvider, ToolbarButton } from 'terminus-core'
|
||||
|
||||
@Injectable()
|
||||
export class MyButtonProvider extends ToolbarButtonProvider {
|
||||
provide (): IToolbarButton[] {
|
||||
provide (): ToolbarButton[] {
|
||||
return [{
|
||||
icon: 'star',
|
||||
title: 'Foobar',
|
||||
|
BIN
app/assets/activity.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
4
app/dev-app-update.yml
Normal file
@ -0,0 +1,4 @@
|
||||
owner: eugeny
|
||||
repo: terminus
|
||||
provider: github
|
||||
updaterCacheDirName: terminus-updater
|
@ -1,4 +1,5 @@
|
||||
import { app, ipcMain, Menu, Tray, shell } from 'electron'
|
||||
import * as electron from 'electron'
|
||||
import { loadConfig } from './config'
|
||||
import { Window, WindowOptions } from './window'
|
||||
|
||||
@ -18,6 +19,16 @@ export class Application {
|
||||
}
|
||||
|
||||
app.commandLine.appendSwitch('disable-http-cache')
|
||||
app.commandLine.appendSwitch('lang', 'EN')
|
||||
|
||||
for (const flag of configData.flags || [['force_discrete_gpu', '0']]) {
|
||||
console.log('Setting Electron flag:', flag.join('='))
|
||||
app.commandLine.appendSwitch(flag[0], flag[1])
|
||||
}
|
||||
}
|
||||
|
||||
init () {
|
||||
electron.screen.on('display-metrics-changed', () => this.broadcast('host:display-metrics-changed'))
|
||||
}
|
||||
|
||||
async newWindow (options?: WindowOptions): Promise<Window> {
|
||||
@ -103,7 +114,7 @@ export class Application {
|
||||
{
|
||||
label: 'Preferences',
|
||||
accelerator: 'Cmd+,',
|
||||
async click () {
|
||||
click: async () => {
|
||||
if (!this.hasWindows()) {
|
||||
await this.newWindow()
|
||||
}
|
||||
|
@ -13,6 +13,9 @@ export function parseArgs (argv, cwd) {
|
||||
.command('run [command...]', 'run a command in the terminal', {
|
||||
command: { type: 'string' },
|
||||
})
|
||||
.command('profile [profileName]', 'open a tab with specified profile', {
|
||||
profileName: { type: 'string' },
|
||||
})
|
||||
.command('paste [text]', 'paste stdin into the active tab', yargs => {
|
||||
return yargs.option('escape', {
|
||||
alias: 'e',
|
||||
@ -38,6 +41,5 @@ export function parseArgs (argv, cwd) {
|
||||
type: 'boolean'
|
||||
})
|
||||
.help('help')
|
||||
.strict()
|
||||
.parse(argv.slice(1))
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import './lru'
|
||||
import { app, ipcMain, Menu } from 'electron'
|
||||
import electronDebug = require('electron-debug')
|
||||
import { parseArgs } from './cli'
|
||||
import { Application } from './app'
|
||||
if (process.platform === 'win32' && require('electron-squirrel-startup')) process.exit(0)
|
||||
import electronDebug = require('electron-debug')
|
||||
|
||||
if (!process.env.TERMINUS_PLUGINS) {
|
||||
process.env.TERMINUS_PLUGINS = ''
|
||||
@ -12,7 +11,6 @@ if (!process.env.TERMINUS_PLUGINS) {
|
||||
const application = new Application()
|
||||
|
||||
ipcMain.on('app:new-window', () => {
|
||||
console.log('new-window')
|
||||
application.newWindow()
|
||||
})
|
||||
|
||||
@ -45,7 +43,11 @@ if (!app.requestSingleInstanceLock()) {
|
||||
}
|
||||
|
||||
if (argv.d) {
|
||||
electronDebug({ enabled: true, showDevTools: 'undocked' })
|
||||
electronDebug({
|
||||
isEnabled: true,
|
||||
showDevTools: true,
|
||||
devToolsMode: 'undocked'
|
||||
})
|
||||
}
|
||||
|
||||
app.on('ready', () => {
|
||||
@ -59,5 +61,6 @@ app.on('ready', () => {
|
||||
}
|
||||
]))
|
||||
}
|
||||
application.init()
|
||||
application.newWindow({ hidden: argv.hidden })
|
||||
})
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Subject, Observable } from 'rxjs'
|
||||
import { debounceTime } from 'rxjs/operators'
|
||||
import { BrowserWindow, app, ipcMain, Rectangle } from 'electron'
|
||||
import ElectronConfig = require('electron-config')
|
||||
import * as os from 'os'
|
||||
@ -10,7 +11,7 @@ let AccentState: any
|
||||
let DwmEnableBlurBehindWindow: any
|
||||
if (process.platform === 'win32') {
|
||||
SetWindowCompositionAttribute = require('windows-swca').SetWindowCompositionAttribute
|
||||
AccentState = require('windows-swca').AccentState
|
||||
AccentState = require('windows-swca').ACCENT_STATE
|
||||
DwmEnableBlurBehindWindow = require('windows-blurbehind').DwmEnableBlurBehindWindow
|
||||
}
|
||||
|
||||
@ -43,7 +44,9 @@ export class Window {
|
||||
title: 'Terminus',
|
||||
minWidth: 400,
|
||||
minHeight: 300,
|
||||
webPreferences: { webSecurity: false },
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
},
|
||||
frame: false,
|
||||
show: false,
|
||||
backgroundColor: '#00000000'
|
||||
@ -102,16 +105,14 @@ export class Window {
|
||||
if (process.platform === 'win32') {
|
||||
if (parseFloat(os.release()) >= 10) {
|
||||
let attribValue = AccentState.ACCENT_DISABLED
|
||||
let color = 0x00000000
|
||||
if (enabled) {
|
||||
if (parseInt(os.release().split('.')[2]) >= 17063 && type === 'fluent') {
|
||||
attribValue = AccentState.ACCENT_ENABLE_FLUENT
|
||||
color = 0x01000000 // using a small alpha because acrylic bugs out at full transparency.
|
||||
attribValue = AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND
|
||||
} else {
|
||||
attribValue = AccentState.ACCENT_ENABLE_BLURBEHIND
|
||||
}
|
||||
}
|
||||
SetWindowCompositionAttribute(this.window, attribValue, color)
|
||||
SetWindowCompositionAttribute(this.window.getNativeWindowHandle(), attribValue, 0x00000000)
|
||||
} else {
|
||||
DwmEnableBlurBehindWindow(this.window, enabled)
|
||||
}
|
||||
@ -143,6 +144,16 @@ export class Window {
|
||||
this.visible.next(false)
|
||||
})
|
||||
|
||||
let moveSubscription = new Observable<void>(observer => {
|
||||
this.window.on('move', () => observer.next())
|
||||
}).pipe(debounceTime(250)).subscribe(() => {
|
||||
this.window.webContents.send('host:window-moved')
|
||||
})
|
||||
|
||||
this.window.on('closed', () => {
|
||||
moveSubscription.unsubscribe()
|
||||
})
|
||||
|
||||
this.window.on('enter-full-screen', () => this.window.webContents.send('host:window-enter-full-screen'))
|
||||
this.window.on('leave-full-screen', () => this.window.webContents.send('host:window-leave-full-screen'))
|
||||
|
||||
@ -173,28 +184,28 @@ export class Window {
|
||||
})
|
||||
|
||||
ipcMain.on('window-focus', event => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.window.focus()
|
||||
})
|
||||
|
||||
ipcMain.on('window-maximize', event => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.window.maximize()
|
||||
})
|
||||
|
||||
ipcMain.on('window-unmaximize', event => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.window.unmaximize()
|
||||
})
|
||||
|
||||
ipcMain.on('window-toggle-maximize', event => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
if (this.window.isMaximized()) {
|
||||
@ -205,42 +216,42 @@ export class Window {
|
||||
})
|
||||
|
||||
ipcMain.on('window-minimize', event => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.window.minimize()
|
||||
})
|
||||
|
||||
ipcMain.on('window-set-bounds', (event, bounds) => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.window.setBounds(bounds)
|
||||
})
|
||||
|
||||
ipcMain.on('window-set-always-on-top', (event, flag) => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.window.setAlwaysOnTop(flag)
|
||||
})
|
||||
|
||||
ipcMain.on('window-set-vibrancy', (event, enabled, type) => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.setVibrancy(enabled, type)
|
||||
})
|
||||
|
||||
ipcMain.on('window-set-title', (event, title) => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.window.setTitle(title)
|
||||
})
|
||||
|
||||
ipcMain.on('window-bring-to-front', event => {
|
||||
if (event.sender !== this.window.webContents) {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
if (this.window.isMinimized()) {
|
||||
@ -250,7 +261,10 @@ export class Window {
|
||||
this.window.moveTop()
|
||||
})
|
||||
|
||||
ipcMain.on('window-close', () => {
|
||||
ipcMain.on('window-close', event => {
|
||||
if (!this.window || event.sender !== this.window.webContents) {
|
||||
return
|
||||
}
|
||||
this.closing = true
|
||||
this.window.close()
|
||||
})
|
||||
|
@ -13,32 +13,34 @@
|
||||
"watch": "webpack --progress --color --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "7.2.0-beta.1",
|
||||
"@angular/common": "7.2.0-beta.1",
|
||||
"@angular/compiler": "7.2.0-beta.1",
|
||||
"@angular/core": "7.2.0-beta.1",
|
||||
"@angular/forms": "7.2.0-beta.1",
|
||||
"@angular/platform-browser": "7.2.0-beta.1",
|
||||
"@angular/platform-browser-dynamic": "7.2.0-beta.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "^3.3.1",
|
||||
"@angular/animations": "7.2.8",
|
||||
"@angular/common": "7.2.8",
|
||||
"@angular/compiler": "7.2.8",
|
||||
"@angular/core": "7.2.8",
|
||||
"@angular/forms": "7.2.8",
|
||||
"@angular/platform-browser": "7.2.8",
|
||||
"@angular/platform-browser-dynamic": "7.2.8",
|
||||
"@ng-bootstrap/ng-bootstrap": "^4.2.0",
|
||||
"devtron": "1.4.0",
|
||||
"electron-config": "0.2.1",
|
||||
"electron-debug": "^2.0.0",
|
||||
"electron-is-dev": "0.1.2",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"js-yaml": "3.8.2",
|
||||
"mz": "^2.6.0",
|
||||
"ngx-toastr": "^9.1.1",
|
||||
"electron-config": "2.0.0",
|
||||
"electron-debug": "^3.0.0",
|
||||
"electron-is-dev": "1.1.0",
|
||||
"electron-updater": "^4.0.6",
|
||||
"js-yaml": "3.13.1",
|
||||
"mz": "^2.7.0",
|
||||
"ngx-toastr": "^10.0.4",
|
||||
"npm": "~6.9.0",
|
||||
"path": "0.12.7",
|
||||
"rxjs": "^6.3.3",
|
||||
"yargs": "^12.0.1",
|
||||
"zone.js": "^0.8.26"
|
||||
"rxjs": "^6.5.2",
|
||||
"rxjs-compat": "^6.5.2",
|
||||
"yargs": "^13.2.4",
|
||||
"zone.js": "^0.8.29"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"windows-blurbehind": "^1.0.0",
|
||||
"windows-swca": "^1.1.1"
|
||||
"windows-blurbehind": "^1.0.1",
|
||||
"windows-swca": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mz": "0.0.31"
|
||||
"@types/mz": "0.0.32"
|
||||
}
|
||||
}
|
||||
|
@ -4,18 +4,19 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastrModule } from 'ngx-toastr'
|
||||
|
||||
export function getRootModule (plugins: any[]) {
|
||||
let imports = [
|
||||
const imports = [
|
||||
BrowserModule,
|
||||
...plugins,
|
||||
NgbModule.forRoot(),
|
||||
ToastrModule.forRoot({
|
||||
positionClass: 'toast-bottom-center',
|
||||
toastClass: 'toast',
|
||||
preventDuplicates: true,
|
||||
extendedTimeOut: 5000,
|
||||
}),
|
||||
]
|
||||
let bootstrap = [
|
||||
...(plugins.filter(x => x.bootstrap).map(x => x.bootstrap)),
|
||||
const bootstrap = [
|
||||
...plugins.filter(x => x.bootstrap).map(x => x.bootstrap),
|
||||
]
|
||||
|
||||
if (bootstrap.length === 0) {
|
||||
@ -25,7 +26,7 @@ export function getRootModule (plugins: any[]) {
|
||||
@NgModule({
|
||||
imports,
|
||||
bootstrap,
|
||||
}) class RootModule { }
|
||||
}) class RootModule { } // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||
|
||||
return RootModule
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
import '../lib/lru'
|
||||
import 'source-sans-pro'
|
||||
import 'source-sans-pro/source-sans-pro.css'
|
||||
import 'source-code-pro/source-code-pro.css'
|
||||
import 'font-awesome/css/font-awesome.css'
|
||||
import '@fortawesome/fontawesome-free/css/solid.css'
|
||||
import '@fortawesome/fontawesome-free/css/brands.css'
|
||||
import '@fortawesome/fontawesome-free/css/fontawesome.css'
|
||||
import 'ngx-toastr/toastr.css'
|
||||
import './preload.scss'
|
||||
|
||||
@ -14,20 +16,20 @@ Raven.config(
|
||||
{
|
||||
release: require('electron').remote.app.getVersion(),
|
||||
dataCallback: (data: any) => {
|
||||
const normalize = (filename) => {
|
||||
let splitArray = filename.split('/')
|
||||
const normalize = (filename: string) => {
|
||||
const splitArray = filename.split('/')
|
||||
return splitArray[splitArray.length - 1]
|
||||
}
|
||||
|
||||
data.exception.values[0].stacktrace.frames.forEach(frame => {
|
||||
data.exception.values[0].stacktrace.frames.forEach((frame: any) => {
|
||||
frame.filename = normalize(frame.filename)
|
||||
})
|
||||
|
||||
data.culprit = data.exception.values[0].stacktrace.frames[0].filename
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
process.on('uncaughtException' as any, (err) => {
|
||||
|
@ -1,40 +1,43 @@
|
||||
import 'zone.js'
|
||||
import 'core-js/es7/reflect'
|
||||
import 'core-js/core/delay'
|
||||
import 'core-js/proposals/reflect-metadata'
|
||||
import 'rxjs'
|
||||
|
||||
import * as isDev from 'electron-is-dev'
|
||||
|
||||
import './global.scss'
|
||||
import './toastr.scss'
|
||||
|
||||
// Always land on the start view
|
||||
location.hash = ''
|
||||
|
||||
import { enableProdMode, NgModuleRef } from '@angular/core'
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
||||
|
||||
import { getRootModule } from './app.module'
|
||||
import { findPlugins, loadPlugins, IPluginInfo } from './plugins'
|
||||
import { findPlugins, loadPlugins, PluginInfo } from './plugins'
|
||||
|
||||
// Always land on the start view
|
||||
location.hash = ''
|
||||
|
||||
;(process as any).enablePromiseAPI = true
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
process.env.HOME = process.env.HOMEDRIVE + process.env.HOMEPATH
|
||||
}
|
||||
|
||||
if (require('electron-is-dev')) {
|
||||
if (isDev) {
|
||||
console.warn('Running in debug mode')
|
||||
} else {
|
||||
enableProdMode()
|
||||
}
|
||||
|
||||
async function bootstrap (plugins: IPluginInfo[], safeMode = false): Promise<NgModuleRef<any>> {
|
||||
async function bootstrap (plugins: PluginInfo[], safeMode = false): Promise<NgModuleRef<any>> {
|
||||
if (safeMode) {
|
||||
plugins = plugins.filter(x => x.isBuiltin)
|
||||
}
|
||||
let pluginsModules = await loadPlugins(plugins, (current, total) => {
|
||||
(document.querySelector('.progress .bar') as HTMLElement).style.width = 100 * current / total + '%'
|
||||
const pluginsModules = await loadPlugins(plugins, (current, total) => {
|
||||
(document.querySelector('.progress .bar') as HTMLElement).style.width = `${100 * current / total}%` // eslint-disable-line
|
||||
})
|
||||
let module = getRootModule(pluginsModules)
|
||||
const module = getRootModule(pluginsModules)
|
||||
window['rootModule'] = module
|
||||
return await platformBrowserDynamic().bootstrapModule(module)
|
||||
return platformBrowserDynamic().bootstrapModule(module)
|
||||
}
|
||||
|
||||
findPlugins().then(async plugins => {
|
||||
|
@ -1,10 +1,8 @@
|
||||
import * as fs from 'mz/fs'
|
||||
import * as path from 'path'
|
||||
const nodeModule = require('module')
|
||||
const nodeModule = require('module') // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
const nodeRequire = (global as any).require
|
||||
|
||||
declare function delay (ms: number): Promise<void>
|
||||
|
||||
function normalizePath (path: string): string {
|
||||
const cygwinPrefix = '/cygdrive/'
|
||||
if (path.startsWith(cygwinPrefix)) {
|
||||
@ -14,13 +12,13 @@ function normalizePath (path: string): string {
|
||||
return path
|
||||
}
|
||||
|
||||
nodeRequire.main.paths.map(x => nodeModule.globalPaths.push(normalizePath(x)))
|
||||
nodeRequire.main.paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x)))
|
||||
|
||||
if (process.env.DEV) {
|
||||
if (process.env.TERMINUS_DEV) {
|
||||
nodeModule.globalPaths.unshift(path.dirname(require('electron').remote.app.getAppPath()))
|
||||
}
|
||||
|
||||
const builtinPluginsPath = process.env.DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
|
||||
const builtinPluginsPath = process.env.TERMINUS_DEV ? path.dirname(require('electron').remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
|
||||
|
||||
const userPluginsPath = path.join(
|
||||
require('electron').remote.app.getPath('appData'),
|
||||
@ -28,6 +26,10 @@ const userPluginsPath = path.join(
|
||||
'plugins',
|
||||
)
|
||||
|
||||
if (!fs.existsSync(userPluginsPath)) {
|
||||
fs.mkdir(userPluginsPath)
|
||||
}
|
||||
|
||||
Object.assign(window, { builtinPluginsPath, userPluginsPath })
|
||||
nodeModule.globalPaths.unshift(builtinPluginsPath)
|
||||
nodeModule.globalPaths.unshift(path.join(userPluginsPath, 'node_modules'))
|
||||
@ -36,9 +38,9 @@ if (process.env.TERMINUS_PLUGINS) {
|
||||
process.env.TERMINUS_PLUGINS.split(':').map(x => nodeModule.globalPaths.push(normalizePath(x)))
|
||||
}
|
||||
|
||||
export declare type ProgressCallback = (current, total) => void
|
||||
export type ProgressCallback = (current: number, total: number) => void // eslint-disable-line @typescript-eslint/no-type-alias
|
||||
|
||||
export interface IPluginInfo {
|
||||
export interface PluginInfo {
|
||||
name: string
|
||||
description: string
|
||||
packageName: string
|
||||
@ -62,6 +64,7 @@ const builtinModules = [
|
||||
'ngx-toastr',
|
||||
'rxjs',
|
||||
'rxjs/operators',
|
||||
'rxjs-compat/Subject',
|
||||
'terminus-core',
|
||||
'terminus-settings',
|
||||
'terminus-terminal',
|
||||
@ -70,47 +73,53 @@ const builtinModules = [
|
||||
|
||||
const cachedBuiltinModules = {}
|
||||
builtinModules.forEach(m => {
|
||||
const label = 'Caching ' + m
|
||||
console.time(label)
|
||||
cachedBuiltinModules[m] = nodeRequire(m)
|
||||
console.timeEnd(label)
|
||||
})
|
||||
|
||||
const originalRequire = nodeRequire('module').prototype.require
|
||||
nodeRequire('module').prototype.require = function (query) {
|
||||
const originalRequire = (global as any).require
|
||||
;(global as any).require = function (query: string) {
|
||||
if (cachedBuiltinModules[query]) {
|
||||
return cachedBuiltinModules[query]
|
||||
}
|
||||
return originalRequire.apply(this, arguments)
|
||||
}
|
||||
|
||||
export async function findPlugins (): Promise<IPluginInfo[]> {
|
||||
let paths = nodeModule.globalPaths
|
||||
let foundPlugins: IPluginInfo[] = []
|
||||
let candidateLocations: { pluginDir: string, packageName: string }[] = []
|
||||
export async function findPlugins (): Promise<PluginInfo[]> {
|
||||
const paths = nodeModule.globalPaths
|
||||
let foundPlugins: PluginInfo[] = []
|
||||
const candidateLocations: { pluginDir: string, packageName: string }[] = []
|
||||
const PREFIX = 'terminus-'
|
||||
|
||||
for (let pluginDir of paths) {
|
||||
pluginDir = normalizePath(pluginDir)
|
||||
if (!await fs.exists(pluginDir)) {
|
||||
continue
|
||||
}
|
||||
let pluginNames = await fs.readdir(pluginDir)
|
||||
const pluginNames = await fs.readdir(pluginDir)
|
||||
if (await fs.exists(path.join(pluginDir, 'package.json'))) {
|
||||
candidateLocations.push({
|
||||
pluginDir: path.dirname(pluginDir),
|
||||
packageName: path.basename(pluginDir)
|
||||
packageName: path.basename(pluginDir),
|
||||
})
|
||||
}
|
||||
for (let packageName of pluginNames) {
|
||||
candidateLocations.push({ pluginDir, packageName })
|
||||
for (const packageName of pluginNames) {
|
||||
if (packageName.startsWith(PREFIX)) {
|
||||
candidateLocations.push({ pluginDir, packageName })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let { pluginDir, packageName } of candidateLocations) {
|
||||
let pluginPath = path.join(pluginDir, packageName)
|
||||
let infoPath = path.join(pluginPath, 'package.json')
|
||||
for (const { pluginDir, packageName } of candidateLocations) {
|
||||
const pluginPath = path.join(pluginDir, packageName)
|
||||
const infoPath = path.join(pluginPath, 'package.json')
|
||||
if (!await fs.exists(infoPath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
let name = packageName.substring('terminus-'.length)
|
||||
const name = packageName.substring(PREFIX.length)
|
||||
|
||||
if (foundPlugins.some(x => x.name === name)) {
|
||||
console.info(`Plugin ${packageName} already exists, overriding`)
|
||||
@ -118,7 +127,7 @@ export async function findPlugins (): Promise<IPluginInfo[]> {
|
||||
}
|
||||
|
||||
try {
|
||||
let info = JSON.parse(await fs.readFile(infoPath, {encoding: 'utf-8'}))
|
||||
const info = JSON.parse(await fs.readFile(infoPath, { encoding: 'utf-8' }))
|
||||
if (!info.keywords || !(info.keywords.includes('terminus-plugin') || info.keywords.includes('terminus-builtin-plugin'))) {
|
||||
continue
|
||||
}
|
||||
@ -143,23 +152,25 @@ export async function findPlugins (): Promise<IPluginInfo[]> {
|
||||
return foundPlugins
|
||||
}
|
||||
|
||||
export async function loadPlugins (foundPlugins: IPluginInfo[], progress: ProgressCallback): Promise<any[]> {
|
||||
let plugins: any[] = []
|
||||
export async function loadPlugins (foundPlugins: PluginInfo[], progress: ProgressCallback): Promise<any[]> {
|
||||
const plugins: any[] = []
|
||||
progress(0, 1)
|
||||
let index = 0
|
||||
for (let foundPlugin of foundPlugins) {
|
||||
for (const foundPlugin of foundPlugins) {
|
||||
console.info(`Loading ${foundPlugin.name}: ${nodeRequire.resolve(foundPlugin.path)}`)
|
||||
progress(index, foundPlugins.length)
|
||||
try {
|
||||
let packageModule = nodeRequire(foundPlugin.path)
|
||||
let pluginModule = packageModule.default.forRoot ? packageModule.default.forRoot() : packageModule.default
|
||||
const label = 'Loading ' + foundPlugin.name
|
||||
console.time(label)
|
||||
const packageModule = nodeRequire(foundPlugin.path)
|
||||
const pluginModule = packageModule.default.forRoot ? packageModule.default.forRoot() : packageModule.default
|
||||
pluginModule['pluginName'] = foundPlugin.name
|
||||
pluginModule['bootstrap'] = packageModule.bootstrap
|
||||
plugins.push(pluginModule)
|
||||
console.timeEnd(label)
|
||||
} catch (error) {
|
||||
console.error(`Could not load ${foundPlugin.name}:`, error)
|
||||
}
|
||||
await delay(1)
|
||||
index++
|
||||
}
|
||||
progress(1, 1)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
template: '<app-root></app-root>'
|
||||
template: '<app-root></app-root>',
|
||||
})
|
||||
export class RootComponent { }
|
||||
export class RootComponent { } // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||
|
@ -2,6 +2,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
|
||||
.toast {
|
||||
box-shadow: 0 1px 0 rgba(0,0,0,.25);
|
||||
|
@ -9,7 +9,6 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedParameters": true,
|
||||
|
@ -5,11 +5,11 @@ module.exports = {
|
||||
name: 'terminus',
|
||||
target: 'node',
|
||||
entry: {
|
||||
'index.ignore': 'file-loader?name=index.html!val-loader!pug-html-loader!' + path.resolve(__dirname, './index.pug'),
|
||||
'index.ignore': 'file-loader?name=index.html!pug-html-loader!' + path.resolve(__dirname, './index.pug'),
|
||||
preload: path.resolve(__dirname, 'src/entry.preload.ts'),
|
||||
bundle: path.resolve(__dirname, 'src/entry.ts'),
|
||||
},
|
||||
mode: process.env.DEV ? 'development' : 'production',
|
||||
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
|
||||
optimization:{
|
||||
minimize: false,
|
||||
},
|
||||
|
@ -7,7 +7,7 @@ module.exports = {
|
||||
entry: {
|
||||
main: path.resolve(__dirname, 'lib/index.ts'),
|
||||
},
|
||||
mode: process.env.DEV ? 'development' : 'production',
|
||||
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
|
||||
context: __dirname,
|
||||
devtool: 'source-map',
|
||||
output: {
|
||||
@ -36,7 +36,6 @@ module.exports = {
|
||||
electron: 'commonjs electron',
|
||||
'electron-config': 'commonjs electron-config',
|
||||
'electron-vibrancy': 'commonjs electron-vibrancy',
|
||||
'electron-squirrel-startup': 'commonjs electron-squirrel-startup',
|
||||
fs: 'commonjs fs',
|
||||
mz: 'commonjs mz',
|
||||
path: 'commonjs path',
|
||||
|
3091
app/yarn.lock
17
appveyor.yml
@ -6,23 +6,24 @@ platform:
|
||||
environment:
|
||||
nodejs_version: "10"
|
||||
|
||||
cache:
|
||||
- '%USERPROFILE%\.electron'
|
||||
|
||||
version: "{build}"
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version $env:platform
|
||||
- npm install
|
||||
- node scripts/install-deps.js
|
||||
- yarn
|
||||
- node scripts/build-native.js
|
||||
|
||||
build_script:
|
||||
- npm run build
|
||||
- yarn run build:typings
|
||||
- yarn run build
|
||||
- node scripts/prepackage-plugins.js
|
||||
- node scripts/build-windows.js
|
||||
|
||||
artifacts:
|
||||
- path: 'dist\win\*.exe'
|
||||
- path: 'dist\squirrel-windows\*.exe'
|
||||
- path: 'dist\*.exe'
|
||||
|
||||
cache:
|
||||
- node_modules
|
||||
- "*\\node_modules"
|
||||
- "%USERPROFILE%\\.electron"
|
||||
- "%LOCALAPPDATA%\\Yarn"
|
||||
|
209
azure-pipelines.yml
Normal file
@ -0,0 +1,209 @@
|
||||
trigger:
|
||||
- master
|
||||
|
||||
variables:
|
||||
- group: Vars
|
||||
|
||||
jobs:
|
||||
- job: Windows
|
||||
pool:
|
||||
vmImage: 'vs2017-win2016'
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
displayName: 'Install Node.js'
|
||||
|
||||
- script: yarn
|
||||
displayName: 'Install dependencies'
|
||||
|
||||
- script: node scripts/build-native.js
|
||||
displayName: 'Rebuild native dependencies'
|
||||
|
||||
- script: yarn run build:typings
|
||||
displayName: 'Build typings'
|
||||
|
||||
- script: yarn run build
|
||||
displayName: 'Build'
|
||||
|
||||
- script: node scripts/prepackage-plugins.js
|
||||
displayName: 'Prepackage plugins'
|
||||
|
||||
- script: node scripts/build-windows.js
|
||||
displayName: 'Package'
|
||||
env:
|
||||
WIN_CSC_LINK: $(WIN_CSC_LINK)
|
||||
WIN_CSC_KEY_PASSWORD: $(WIN_CSC_KEY_PASSWORD)
|
||||
BT_TOKEN: $(BT_TOKEN)
|
||||
GH_TOKEN: $(GH_TOKEN)
|
||||
|
||||
- task: CopyFiles@2
|
||||
inputs:
|
||||
contents: 'dist\\*-setup.exe'
|
||||
targetFolder: $(Build.ArtifactStagingDirectory)
|
||||
flattenFolders: true
|
||||
cleanTargetFolder: true
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
pathtoPublish: $(Build.ArtifactStagingDirectory)
|
||||
artifactName: Windows - Installer
|
||||
condition: eq(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
||||
- task: CopyFiles@2
|
||||
inputs:
|
||||
contents: 'dist\\*-portable.exe'
|
||||
targetFolder: $(Build.ArtifactStagingDirectory)
|
||||
flattenFolders: true
|
||||
cleanTargetFolder: true
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
pathtoPublish: $(Build.ArtifactStagingDirectory)
|
||||
artifactName: Windows - Portable build
|
||||
condition: eq(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
||||
- job: Linux
|
||||
pool:
|
||||
vmImage: 'ubuntu-16.04'
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
displayName: 'Install Node.js'
|
||||
|
||||
- script: yarn
|
||||
displayName: 'Install dependencies'
|
||||
|
||||
- script: node scripts/build-native.js
|
||||
displayName: 'Rebuild native dependencies'
|
||||
|
||||
- script: yarn run build:typings
|
||||
displayName: 'Build typings'
|
||||
|
||||
- script: yarn run build
|
||||
displayName: 'Build'
|
||||
|
||||
- script: node scripts/prepackage-plugins.js
|
||||
displayName: 'Prepackage plugins'
|
||||
|
||||
- script: node scripts/build-linux.js
|
||||
displayName: 'Package'
|
||||
env:
|
||||
BT_TOKEN: $(BT_TOKEN)
|
||||
GH_TOKEN: $(GH_TOKEN)
|
||||
|
||||
- task: CopyFiles@2
|
||||
inputs:
|
||||
contents: 'dist/*.deb'
|
||||
targetFolder: $(Build.ArtifactStagingDirectory)
|
||||
flattenFolders: true
|
||||
cleanTargetFolder: true
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
pathtoPublish: $(Build.ArtifactStagingDirectory)
|
||||
artifactName: Linux - DEB
|
||||
condition: eq(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
||||
- task: CopyFiles@2
|
||||
inputs:
|
||||
contents: 'dist/*.rpm'
|
||||
targetFolder: $(Build.ArtifactStagingDirectory)
|
||||
flattenFolders: true
|
||||
cleanTargetFolder: true
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
pathtoPublish: $(Build.ArtifactStagingDirectory)
|
||||
artifactName: Linux - RPM
|
||||
condition: eq(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
||||
- task: CopyFiles@2
|
||||
inputs:
|
||||
contents: 'dist/*.snap'
|
||||
targetFolder: $(Build.ArtifactStagingDirectory)
|
||||
flattenFolders: true
|
||||
cleanTargetFolder: true
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
pathtoPublish: $(Build.ArtifactStagingDirectory)
|
||||
artifactName: Linux - Snap
|
||||
condition: eq(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
||||
- task: CopyFiles@2
|
||||
inputs:
|
||||
contents: 'dist/*.tar.gz'
|
||||
targetFolder: $(Build.ArtifactStagingDirectory)
|
||||
flattenFolders: true
|
||||
cleanTargetFolder: true
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
pathtoPublish: $(Build.ArtifactStagingDirectory)
|
||||
artifactName: Linux - tar.gz
|
||||
condition: eq(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
||||
- job: macOS
|
||||
pool:
|
||||
vmImage: 'macOS-10.14'
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
displayName: 'Install Node.js'
|
||||
|
||||
- script: yarn
|
||||
displayName: 'Install dependencies'
|
||||
|
||||
- script: node scripts/build-native.js
|
||||
displayName: 'Rebuild native dependencies'
|
||||
|
||||
- script: yarn run build:typings
|
||||
displayName: 'Build typings'
|
||||
|
||||
- script: yarn run build
|
||||
displayName: 'Build'
|
||||
|
||||
- script: node scripts/prepackage-plugins.js
|
||||
displayName: 'Prepackage plugins'
|
||||
|
||||
- script: node scripts/build-macos.js
|
||||
displayName: 'Package'
|
||||
env:
|
||||
CSC_LINK: $(CSC_LINK)
|
||||
CSC_KEY_PASSWORD: $(CSC_KEY_PASSWORD)
|
||||
BT_TOKEN: $(BT_TOKEN)
|
||||
GH_TOKEN: $(GH_TOKEN)
|
||||
APPSTORE_USERNAME: $(APPSTORE_USERNAME)
|
||||
APPSTORE_PASSWORD: $(APPSTORE_PASSWORD)
|
||||
|
||||
- task: CopyFiles@2
|
||||
inputs:
|
||||
contents: 'dist/*.dmg'
|
||||
targetFolder: $(Build.ArtifactStagingDirectory)
|
||||
flattenFolders: true
|
||||
cleanTargetFolder: true
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
pathtoPublish: $(Build.ArtifactStagingDirectory)
|
||||
artifactName: macOS - DMG
|
||||
condition: eq(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
||||
- task: CopyFiles@2
|
||||
inputs:
|
||||
contents: 'dist/*.zip'
|
||||
targetFolder: $(Build.ArtifactStagingDirectory)
|
||||
flattenFolders: true
|
||||
cleanTargetFolder: true
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
inputs:
|
||||
pathtoPublish: $(Build.ArtifactStagingDirectory)
|
||||
artifactName: macOS - app.zip
|
||||
condition: eq(variables['Build.SourceBranch'], 'refs/heads/master')
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 950 B |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 3.9 KiB |
3
build/installer.nsh
Normal file
@ -0,0 +1,3 @@
|
||||
!macro customInit
|
||||
nsExec::Exec '"$LOCALAPPDATA\terminus\Update.exe" --uninstall -s'
|
||||
!macroend
|
36
build/mac/afterSignHook.js
Normal file
@ -0,0 +1,36 @@
|
||||
// See: https://medium.com/@TwitterArchiveEraser/notarize-electron-apps-7a5f988406db
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const notarizer = require('electron-notarize')
|
||||
|
||||
module.exports = async function (params) {
|
||||
console.log('env: ', process.env)
|
||||
// notarize the app on Mac OS only.
|
||||
if (process.platform !== 'darwin' || process.env.BUILD_SOURCEBRANCH !== 'refs/heads/master') {
|
||||
return
|
||||
}
|
||||
console.log('afterSign hook triggered', params)
|
||||
|
||||
let appId = 'org.terminus'
|
||||
|
||||
let appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`)
|
||||
if (!fs.existsSync(appPath)) {
|
||||
throw new Error(`Cannot find application at: ${appPath}`)
|
||||
}
|
||||
|
||||
console.log(`Notarizing ${appId} found at ${appPath}`)
|
||||
|
||||
try {
|
||||
await notarizer.notarize({
|
||||
appBundleId: appId,
|
||||
appPath: appPath,
|
||||
appleId: process.env.APPSTORE_USERNAME,
|
||||
appleIdPassword: process.env.APPSTORE_PASSWORD,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
console.log(`Done notarizing ${appId}`)
|
||||
}
|
12
build/mac/entitlements.plist
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
BIN
build/windows/squirrel.gif
Normal file
After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 2.6 MiB |
BIN
docs/dist/assets/background.jpeg
vendored
Before Width: | Height: | Size: 2.6 MiB |
BIN
docs/dist/assets/terminal.png
vendored
Before Width: | Height: | Size: 21 KiB |
1
docs/dist/bundle.js
vendored
BIN
docs/dist/fonts/background.jpeg
vendored
Before Width: | Height: | Size: 2.6 MiB |
@ -1,9 +0,0 @@
|
||||
<!DOCTYPE html><html><head><base href="dist/"><meta name="viewport" content="initial-scale=1, minimal-ui, shrink-to-fit=no"><link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400" rel="stylesheet"><script src="bundle.js"></script><title>Terminus</title></head><body><div class="mt-5 mb-5" id="header"><div class="text-center"><h1>Terminus</h1><div class="subtitle mb-3">A terminal for a more modern age</div><a class="btn btn-lg btn-outline-dark mt-4" href="https://github.com/Eugeny/terminus/releases/latest" target="_blank"><strong>DOWNLOAD</strong></a><a class="btn btn-lg btn-outline-secondary mt-4 ml-3" href="https://github.com/Eugeny/terminus" target="_blank"><strong>GITHUB</strong></a></div></div><div class="background-stripe"><div class="overlay overlay1"></div><div class="overlay overlay2"></div><div class="terminal"></div></div><div class="container mt-5 mb-5"><div class="d-flex flex-wrap flex-md-nowrap"><div class="w-100"><div class="feature">windows</div><div class="feature">linux</div><div class="feature">macos</div><br><div class="feature">powershell</div><div class="feature">wsl</div><div class="feature">cygwin</div><div class="feature">git-bash</div><div class="feature">cmder</div><div class="feature">clink</div></div><div class="w-100"><div class="feature">full unicode</div><div class="feature">global hotkey</div><div class="feature">plugins</div><div class="feature">tab recovery</div><div class="feature">custom css</div><div class="feature">themes</div><div class="feature">font ligatures</div><div class="feature">clickable paths</div><div class="feature">tabs on top/bottom</div><div class="feature">vibrancy</div><div class="feature">bracketed paste</div></div></div></div><div class="container mt-5 mb-5"><div class="text-center mt-5"><div class="mb-4 mt-5"><script type="text/javascript" src="https://ko-fi.com/widgets/widget_2.js"></script><script type="text/javascript">kofiwidget2.init('Buy me a coffee', '#46b798', 'J3J8KWTF')
|
||||
kofiwidget2.draw()
|
||||
</script></div><a class="btn btn-lg btn-outline-secondary mt-3" href="/terminus/#header"><strong>BEAM ME UP</strong></a></div></div><div class="background-stripe2"><div class="overlay overlay1"></div></div><script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-3278102-18', 'auto');
|
||||
ga('send', 'pageview');</script></body></html>
|
@ -1 +0,0 @@
|
||||
import './styles.scss'
|
@ -1,75 +0,0 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
base(href='dist/')
|
||||
meta(name='viewport', content='initial-scale=1, minimal-ui, shrink-to-fit=no')
|
||||
link(href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400", rel="stylesheet")
|
||||
script(src='bundle.js')
|
||||
title Terminus
|
||||
body
|
||||
.mt-5.mb-5#header
|
||||
.text-center
|
||||
h1 Terminus
|
||||
.subtitle.mb-3 A terminal for a more modern age
|
||||
|
||||
a.btn.btn-lg.btn-outline-dark.mt-4(href='https://github.com/Eugeny/terminus/releases/latest', target='_blank')
|
||||
strong DOWNLOAD
|
||||
|
||||
a.btn.btn-lg.btn-outline-secondary.mt-4.ml-3(href='https://github.com/Eugeny/terminus', target='_blank')
|
||||
strong GITHUB
|
||||
|
||||
|
||||
.background-stripe
|
||||
.overlay.overlay1
|
||||
.overlay.overlay2
|
||||
.terminal
|
||||
|
||||
.container.mt-5.mb-5
|
||||
.d-flex.flex-wrap.flex-md-nowrap
|
||||
.w-100
|
||||
.feature windows
|
||||
.feature linux
|
||||
.feature macos
|
||||
br
|
||||
.feature powershell
|
||||
.feature wsl
|
||||
.feature cygwin
|
||||
.feature git-bash
|
||||
.feature cmder
|
||||
.feature clink
|
||||
|
||||
.w-100
|
||||
.feature full unicode
|
||||
.feature global hotkey
|
||||
.feature plugins
|
||||
.feature tab recovery
|
||||
.feature custom css
|
||||
.feature themes
|
||||
.feature font ligatures
|
||||
.feature clickable paths
|
||||
.feature tabs on top/bottom
|
||||
.feature vibrancy
|
||||
.feature bracketed paste
|
||||
|
||||
.container.mt-5.mb-5
|
||||
.text-center.mt-5
|
||||
.mb-4.mt-5
|
||||
script(type='text/javascript', src='https://ko-fi.com/widgets/widget_2.js')
|
||||
script(type='text/javascript').
|
||||
kofiwidget2.init('Buy me a coffee', '#46b798', 'J3J8KWTF')
|
||||
kofiwidget2.draw()
|
||||
|
||||
a.btn.btn-lg.btn-outline-secondary.mt-3(href='/terminus/#header')
|
||||
strong BEAM ME UP
|
||||
|
||||
.background-stripe2
|
||||
.overlay.overlay1
|
||||
|
||||
script.
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-3278102-18', 'auto');
|
||||
ga('send', 'pageview');
|
@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "webpack --progress",
|
||||
"watch": "webpack --progress --watch"
|
||||
},
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"bootstrap": "^4.1.3",
|
||||
"css-loader": "^1.0.0",
|
||||
"file-loader": "^1.1.11",
|
||||
"node-sass": "^4.9.3",
|
||||
"pug": "^2.0.3",
|
||||
"pug-cli": "^1.0.0-alpha6",
|
||||
"pug-html-loader": "^1.1.5",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.22.1",
|
||||
"val-loader": "^1.1.1",
|
||||
"webpack": "^4.16.5",
|
||||
"webpack-cli": "^3.1.0"
|
||||
}
|
||||
}
|
BIN
docs/readme.png
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.6 MiB |
141
docs/styles.scss
@ -1,141 +0,0 @@
|
||||
$font-family-sans-serif: "Source Sans Pro";
|
||||
$border-radius-lg: 0;
|
||||
$btn-border-width: 3px;
|
||||
|
||||
@import "node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
|
||||
h1 {
|
||||
font-size: 10vw;
|
||||
font-weight: 200;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-style: italic;
|
||||
color: #999;
|
||||
font-size: 5vw;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.background-stripe {
|
||||
width: 100vw;
|
||||
background-image: url('./background.jpeg');
|
||||
background-size: cover;
|
||||
height: 30vw;
|
||||
margin: 200px 0 150px;
|
||||
min-height: 1000px;
|
||||
position: relative;
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
|
||||
&.overlay1 {
|
||||
top: -1px;
|
||||
left: 0;
|
||||
border-top: 10vw solid white;
|
||||
border-right: 100vw solid transparent;
|
||||
}
|
||||
|
||||
&.overlay2 {
|
||||
bottom: -1px;
|
||||
right: 0;
|
||||
border-bottom: 10vw solid white;
|
||||
border-left: 100vw solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 5vw;
|
||||
|
||||
width: 1304px;
|
||||
margin-left: -652px;
|
||||
height: 972px;
|
||||
border-radius: 9px;
|
||||
|
||||
box-shadow: 0 0 100px black;
|
||||
background: url('./terminal.png');
|
||||
background-size: cover;
|
||||
|
||||
animation: slideIn ease-out 1s;
|
||||
opacity: .95;
|
||||
}
|
||||
|
||||
@media(max-width: 1500px) {
|
||||
min-height: 500px;
|
||||
margin: 200px 0 100px;
|
||||
|
||||
.terminal {
|
||||
width: 652px;
|
||||
top: -100px;
|
||||
margin-left: -326px;
|
||||
height: 486px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 750px) {
|
||||
min-height: 250px;
|
||||
margin: 100px 0 50px;
|
||||
|
||||
.terminal {
|
||||
width: 326px;
|
||||
top: -50px;
|
||||
margin-left: -163px;
|
||||
height: 243px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.feature {
|
||||
font-size: 45px;
|
||||
line-height: 40px;
|
||||
opacity: .5;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
margin-top: 200px;
|
||||
}
|
||||
to {
|
||||
opacity: .95;
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.background-stripe2 {
|
||||
width: 100vw;
|
||||
background-image: url('./background.jpeg');
|
||||
background-size: cover;
|
||||
height: 30vw;
|
||||
margin: 100px 0 0;
|
||||
position: relative;
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
|
||||
&.overlay1 {
|
||||
top: -1px;
|
||||
right: 0;
|
||||
border-top: 10vw solid white;
|
||||
border-left: 100vw solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 21 KiB |
@ -1,27 +0,0 @@
|
||||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
'index.ignore': 'file-loader?name=../index.html!pug-html-loader!' + path.resolve(__dirname, './index.pug'),
|
||||
'bundle': path.resolve(__dirname, 'index.js'),
|
||||
},
|
||||
context: __dirname,
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
filename: '[name].js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
|
||||
{
|
||||
test: /\.(jpeg|png)?$/,
|
||||
use: {
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: 'assets/[name].[ext]'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
BIN
extras/UAC.exe
Normal file
153
package.json
@ -1,64 +1,65 @@
|
||||
{
|
||||
"name": "term",
|
||||
"devDependencies": {
|
||||
"@types/electron-config": "^0.2.1",
|
||||
"@types/electron-debug": "^1.1.0",
|
||||
"@types/fs-promise": "1.0.1",
|
||||
"@types/js-yaml": "^3.11.2",
|
||||
"@types/node": "^10.11.5",
|
||||
"@types/webpack-env": "1.13.0",
|
||||
"app-builder-lib": "^20.28.4",
|
||||
"apply-loader": "0.1.0",
|
||||
"@fortawesome/fontawesome-free": "^5.9.0",
|
||||
"@types/electron-config": "^3.2.2",
|
||||
"@types/electron-debug": "^2.1.0",
|
||||
"@types/fs-promise": "1.0.3",
|
||||
"@types/js-yaml": "^3.12.1",
|
||||
"@types/node": "^12.0.10",
|
||||
"@types/webpack-env": "1.13.9",
|
||||
"@typescript-eslint/eslint-plugin": "^1.11.0",
|
||||
"@typescript-eslint/parser": "^1.11.0",
|
||||
"app-builder-lib": "^21.0.3",
|
||||
"apply-loader": "2.0.0",
|
||||
"awesome-typescript-loader": "^5.0.0",
|
||||
"core-js": "2.4.1",
|
||||
"cross-env": "4.0.0",
|
||||
"css-loader": "0.28.0",
|
||||
"electron": "4.0.0-beta.8",
|
||||
"electron-builder": "^20.38.2",
|
||||
"electron-builder-squirrel-windows": "^20.28.3",
|
||||
"electron-installer-snap": "^3.0.0",
|
||||
"electron-rebuild": "^1.8.2",
|
||||
"file-loader": "^1.1.11",
|
||||
"font-awesome": "4.7.0",
|
||||
"graceful-fs": "^4.1.11",
|
||||
"html-loader": "0.4.4",
|
||||
"json-loader": "0.5.4",
|
||||
"less": "2.7.1",
|
||||
"less-loader": "2.2.3",
|
||||
"node-abi": "^2.4.4",
|
||||
"node-gyp": "^3.6.2",
|
||||
"node-sass": "^4.5.3",
|
||||
"npmlog": "4.1.0",
|
||||
"core-js": "^3.1.4",
|
||||
"cross-env": "5.2.0",
|
||||
"css-loader": "3.0.0",
|
||||
"electron": "^5.0.5",
|
||||
"electron-builder": "^20.44.4",
|
||||
"electron-installer-snap": "^4.0.0",
|
||||
"electron-notarize": "^0.1.1",
|
||||
"electron-rebuild": "^1.8.5",
|
||||
"eslint": "^5.16.0",
|
||||
"file-loader": "^4.0.0",
|
||||
"graceful-fs": "^4.1.15",
|
||||
"html-loader": "0.5.5",
|
||||
"json-loader": "0.5.7",
|
||||
"node-abi": "^2.9.0",
|
||||
"node-gyp": "^5.0.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"npmlog": "4.1.2",
|
||||
"npx": "^10.2.0",
|
||||
"pug": "^2.0.3",
|
||||
"pug-html-loader": "1.0.9",
|
||||
"pug-lint": "^2.5.0",
|
||||
"pug": "^2.0.4",
|
||||
"pug-html-loader": "1.1.5",
|
||||
"pug-lint": "^2.6.0",
|
||||
"pug-loader": "^2.4.0",
|
||||
"pug-static-loader": "0.0.1",
|
||||
"raven-js": "3.16.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"pug-static-loader": "2.0.0",
|
||||
"raven-js": "3.27.2",
|
||||
"raw-loader": "3.0.0",
|
||||
"sass-loader": "^7.0.1",
|
||||
"shelljs": "0.7.7",
|
||||
"shelljs": "0.8.3",
|
||||
"source-code-pro": "^2.30.1",
|
||||
"source-sans-pro": "2.0.10",
|
||||
"style-loader": "0.13.1",
|
||||
"source-sans-pro": "2.45.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"svg-inline-loader": "^0.8.0",
|
||||
"to-string-loader": "1.1.5",
|
||||
"tslint": "5.1.0",
|
||||
"tslint-config-standard": "5.0.2",
|
||||
"tslint-eslint-rules": "4.0.0",
|
||||
"typescript": "^3.1.3",
|
||||
"url-loader": "^1.1.1",
|
||||
"val-loader": "0.5.0",
|
||||
"webpack": "^4.22.0",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"yaml-loader": "0.4.0",
|
||||
"yarn": "^1.10.1"
|
||||
"typedoc": "^0.14.2",
|
||||
"typescript": "^3.5.2",
|
||||
"url-loader": "^2.0.0",
|
||||
"val-loader": "1.1.1",
|
||||
"webpack": "^4.35.0",
|
||||
"webpack-cli": "^3.3.5",
|
||||
"yaml-loader": "0.5.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"*/node-abi": "^2.8.0"
|
||||
},
|
||||
"build": {
|
||||
"appId": "org.terminus",
|
||||
"productName": "Terminus",
|
||||
"compression": "normal",
|
||||
"afterSign": "./build/mac/afterSignHook.js",
|
||||
"files": [
|
||||
"**/*",
|
||||
"dist"
|
||||
@ -69,24 +70,33 @@
|
||||
],
|
||||
"win": {
|
||||
"icon": "./build/windows/icon.ico",
|
||||
"publish": [
|
||||
"github"
|
||||
],
|
||||
"artifactName": "terminus-${version}-setup.exe"
|
||||
"artifactName": "terminus-${version}-setup.exe",
|
||||
"rfc3161TimeStampServer": "http://sha256timestamp.ws.symantec.com/sha256/timestamp"
|
||||
},
|
||||
"squirrelWindows": {
|
||||
"iconUrl": "https://github.com/Eugeny/terminus/raw/master/build/windows/icon.ico",
|
||||
"artifactName": "terminus-${version}-setup.exe"
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"artifactName": "terminus-${version}-setup.${ext}",
|
||||
"installerIcon": "./build/windows/icon.ico"
|
||||
},
|
||||
"publish": [
|
||||
{
|
||||
"provider": "bintray",
|
||||
"token": "d993c4faa708a4cba84fa3a8e822457e7298d75c",
|
||||
"component": "main"
|
||||
},
|
||||
{
|
||||
"provider": "github"
|
||||
}
|
||||
],
|
||||
"portable": {
|
||||
"artifactName": "terminus-${version}-portable.exe"
|
||||
},
|
||||
"mac": {
|
||||
"category": "public.app-category.video",
|
||||
"icon": "./build/mac/icon.icns",
|
||||
"publish": [
|
||||
"github"
|
||||
],
|
||||
"artifactName": "terminus-${version}-macos.${ext}",
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "./build/mac/entitlements.plist",
|
||||
"extendInfo": {
|
||||
"NSRequiresAquaSystemAppearance": false
|
||||
}
|
||||
@ -97,21 +107,17 @@
|
||||
"linux": {
|
||||
"category": "Utilities",
|
||||
"icon": "./build/icons",
|
||||
"artifactName": "terminus-${version}-linux.${ext}",
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
"artifactName": "terminus-${version}-linux.${ext}"
|
||||
},
|
||||
"deb": {
|
||||
"depends": [
|
||||
"screen",
|
||||
"gconf2",
|
||||
"gconf-service",
|
||||
"libnotify4",
|
||||
"libsecret-1-0",
|
||||
"libappindicator1",
|
||||
"libxtst6",
|
||||
"libnss3",
|
||||
"tmux"
|
||||
"libnss3"
|
||||
],
|
||||
"afterInstall": "build/linux/after-install.tpl"
|
||||
},
|
||||
@ -123,12 +129,17 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js",
|
||||
"watch": "cross-env DEV=1 webpack --progress --color --watch",
|
||||
"start": "cross-env DEV=1 electron app --debug",
|
||||
"prod": "cross-env DEV=1 electron app",
|
||||
"lint": "tslint -c tslint.json -t stylish terminus-*/src/**/*.ts terminus-*/src/*.ts app/src/*.ts",
|
||||
"postinstall": "install-app-deps"
|
||||
"build": "webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js",
|
||||
"build:typings": "tsc --project terminus-core/tsconfig.typings.json && tsc --project terminus-settings/tsconfig.typings.json && tsc --project terminus-terminal/tsconfig.typings.json && tsc --project terminus-plugin-manager/tsconfig.typings.json && tsc --project terminus-ssh/tsconfig.typings.json",
|
||||
"watch": "cross-env TERMINUS_DEV=1 webpack --progress --color --watch",
|
||||
"start": "cross-env TERMINUS_DEV=1 electron app --debug",
|
||||
"prod": "cross-env TERMINUS_DEV=1 electron app",
|
||||
"docs": "typedoc --out docs/api terminus-core/src && typedoc --out docs/api/terminal --tsconfig terminus-terminal/tsconfig.typings.json terminus-terminal/src && typedoc --out docs/api/settings --tsconfig terminus-settings/tsconfig.typings.json terminus-settings/src",
|
||||
"lint": "eslint --ext ts */src",
|
||||
"postinstall": "node ./scripts/install-deps.js"
|
||||
},
|
||||
"repository": "eugeny/terminus"
|
||||
"repository": "eugeny/terminus",
|
||||
"dependencies": {
|
||||
"eslint-plugin-import": "^2.18.0"
|
||||
}
|
||||
}
|
||||
|
@ -11,4 +11,4 @@ builder({
|
||||
},
|
||||
},
|
||||
publish: 'onTag',
|
||||
})
|
||||
}).catch(() => process.exit(1))
|
||||
|
@ -11,4 +11,4 @@ builder({
|
||||
},
|
||||
},
|
||||
publish: 'onTag',
|
||||
})
|
||||
}).catch(() => process.exit(1))
|
||||
|
@ -4,24 +4,20 @@ const path = require('path')
|
||||
const vars = require('./vars')
|
||||
|
||||
lifecycles = []
|
||||
lifecycles.push(rebuild({
|
||||
buildPath: path.resolve(__dirname, '../app'),
|
||||
electronVersion: vars.electronVersion,
|
||||
force: true,
|
||||
}).lifecycle)
|
||||
lifecycles.push(rebuild({
|
||||
buildPath: path.resolve(__dirname, '../terminus-ssh'),
|
||||
electronVersion: vars.electronVersion,
|
||||
force: true,
|
||||
}).lifecycle)
|
||||
lifecycles.push(rebuild({
|
||||
buildPath: path.resolve(__dirname, '../terminus-terminal'),
|
||||
electronVersion: vars.electronVersion,
|
||||
force: true,
|
||||
}).lifecycle)
|
||||
for (let dir of ['app', 'terminus-core', 'terminus-ssh', 'terminus-terminal']) {
|
||||
build = rebuild({
|
||||
buildPath: path.resolve(__dirname, '../' + dir),
|
||||
electronVersion: vars.electronVersion,
|
||||
force: true,
|
||||
})
|
||||
build.catch(() => process.exit(1))
|
||||
lifecycles.push([build.lifecycle, dir])
|
||||
}
|
||||
|
||||
for (let lc of lifecycles) {
|
||||
console.info('Building against Electron', vars.electronVersion)
|
||||
|
||||
for (let [lc, dir] of lifecycles) {
|
||||
lc.on('module-found', name => {
|
||||
console.info('Rebuilding', name)
|
||||
console.info('Rebuilding', dir + '/' + name)
|
||||
})
|
||||
}
|
||||
|
@ -4,11 +4,11 @@ const vars = require('./vars')
|
||||
|
||||
builder({
|
||||
dir: true,
|
||||
win: ['squirrel', 'portable'],
|
||||
win: ['nsis', 'portable'],
|
||||
config: {
|
||||
extraMetadata: {
|
||||
version: vars.version,
|
||||
},
|
||||
},
|
||||
publish: 'onTag',
|
||||
})
|
||||
}).catch(() => process.exit(1))
|
||||
|
@ -8,7 +8,6 @@ const localBinPath = path.resolve(__dirname, '../node_modules/.bin');
|
||||
const npx = `${localBinPath}/npx`;
|
||||
|
||||
log.info('deps', 'app')
|
||||
sh.exec(`${npx} yarn install`)
|
||||
|
||||
sh.cd('app')
|
||||
sh.exec(`${npx} yarn install`)
|
||||
|
@ -1,12 +1,18 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const semver = require('semver')
|
||||
const childProcess = require('child_process')
|
||||
|
||||
const appInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../app/package.json')))
|
||||
const pkgInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json')))
|
||||
const electronInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../node_modules/electron/package.json')))
|
||||
|
||||
exports.version = childProcess.execSync('git describe --tags', {encoding:'utf-8'})
|
||||
exports.version = exports.version.substring(1, exports.version.length - 1)
|
||||
exports.version = exports.version.substring(1).trim()
|
||||
exports.version = exports.version.replace('-', '-c')
|
||||
|
||||
if (exports.version.includes('-c')) {
|
||||
exports.version = semver.inc(exports.version, 'prepatch').replace('-0', '-nightly.0')
|
||||
}
|
||||
|
||||
exports.builtinPlugins = [
|
||||
'terminus-core',
|
||||
@ -20,4 +26,4 @@ exports.bundledModules = [
|
||||
'@angular',
|
||||
'@ng-bootstrap',
|
||||
]
|
||||
exports.electronVersion = pkgInfo.devDependencies.electron
|
||||
exports.electronVersion = electronInfo.version
|
||||
|
26
snap/snapcraft.yaml
Normal file
@ -0,0 +1,26 @@
|
||||
name: terminus
|
||||
version: '1.0.0'
|
||||
summary: A terminal for a modern age
|
||||
description: |
|
||||
Terminus is a terminal heavily inspired by Hyper. It is, however, designed for people who need to get things done.
|
||||
|
||||
grade: devel
|
||||
confinement: devmode
|
||||
|
||||
apps:
|
||||
terminus:
|
||||
command: opt/terminus/terminus
|
||||
|
||||
parts:
|
||||
app:
|
||||
plugin: nodejs
|
||||
source: .
|
||||
build-packages:
|
||||
- libfontconfig-dev
|
||||
override-build: |
|
||||
yarn
|
||||
./scripts/build-native.js
|
||||
yarn run build
|
||||
./scripts/build-linux.js
|
||||
mkdir -p $SNAPCRAFT_PART_INSTALL/opt/terminus || true
|
||||
cp -ar dist/linux-unpacked/* $SNAPCRAFT_PART_INSTALL/opt/terminus/
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "terminus-community-color-schemes",
|
||||
"version": "1.0.0-alpha.55",
|
||||
"version": "1.0.73-c4-ga7d62b0",
|
||||
"description": "Community color schemes for Terminus",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"typings": "typings/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "webpack --progress --color",
|
||||
"watch": "webpack --progress --color --watch"
|
||||
@ -22,8 +22,7 @@
|
||||
"terminus-terminal": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "7.0.12",
|
||||
"@types/node": "12.0.10",
|
||||
"@types/webpack-env": "^1.13.0"
|
||||
},
|
||||
"false": {}
|
||||
}
|
||||
}
|
||||
|
36
terminus-community-color-schemes/schemes/Relaxed
Normal file
@ -0,0 +1,36 @@
|
||||
! special
|
||||
*.foreground: #d8d8d8
|
||||
*.background: #343a43
|
||||
*.cursorColor: #d8d8d8
|
||||
|
||||
! black
|
||||
*.color0: #2c3037
|
||||
*.color8: #626262
|
||||
|
||||
! red
|
||||
*.color1: #bb5653
|
||||
*.color9: #c35956
|
||||
|
||||
! green
|
||||
*.color2: #909d62
|
||||
*.color10: #9fab76
|
||||
|
||||
! yellow
|
||||
*.color3: #eac179
|
||||
*.color11: #ecc179
|
||||
|
||||
! blue
|
||||
*.color4: #698698
|
||||
*.color12: #7da9c7
|
||||
|
||||
! magenta
|
||||
*.color5: #b06597
|
||||
*.color13: #ba6ca0
|
||||
|
||||
! cyan
|
||||
*.color6: #c9dfff
|
||||
*.color14: #abbacf
|
||||
|
||||
! white
|
||||
*.color7: #d8d8d8
|
||||
*.color15: #f7f7f7
|
@ -1,26 +1,26 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TerminalColorSchemeProvider, ITerminalColorScheme } from 'terminus-terminal'
|
||||
import { TerminalColorSchemeProvider, TerminalColorScheme } from 'terminus-terminal'
|
||||
|
||||
const schemeContents = require.context('../schemes/', true, /.*/)
|
||||
|
||||
@Injectable()
|
||||
export class ColorSchemes extends TerminalColorSchemeProvider {
|
||||
async getSchemes (): Promise<ITerminalColorScheme[]> {
|
||||
let schemes: ITerminalColorScheme[] = []
|
||||
async getSchemes (): Promise<TerminalColorScheme[]> {
|
||||
const schemes: TerminalColorScheme[] = []
|
||||
|
||||
schemeContents.keys().forEach(schemeFile => {
|
||||
let lines = (schemeContents(schemeFile) as string).split('\n')
|
||||
const lines = (schemeContents(schemeFile).default as string).split('\n')
|
||||
|
||||
// process #define variables
|
||||
let variables: any = {}
|
||||
const variables: any = {}
|
||||
lines
|
||||
.filter(x => x.startsWith('#define'))
|
||||
.map(x => x.split(' ').map(v => v.trim()))
|
||||
.forEach(([ignore, variableName, variableValue]) => {
|
||||
.forEach(([_, variableName, variableValue]) => {
|
||||
variables[variableName] = variableValue
|
||||
})
|
||||
|
||||
let values: any = {}
|
||||
const values: any = {}
|
||||
lines
|
||||
.filter(x => x.startsWith('*.'))
|
||||
.map(x => x.substring(2))
|
||||
@ -29,7 +29,7 @@ export class ColorSchemes extends TerminalColorSchemeProvider {
|
||||
values[key] = variables[value] ? variables[value] : value
|
||||
})
|
||||
|
||||
let colors: string[] = []
|
||||
const colors: string[] = []
|
||||
let colorIndex = 0
|
||||
while (values[`color${colorIndex}`]) {
|
||||
colors.push(values[`color${colorIndex}`])
|
||||
|
@ -8,4 +8,4 @@ import { ColorSchemes } from './colorSchemes'
|
||||
{ provide: TerminalColorSchemeProvider, useClass: ColorSchemes, multi: true },
|
||||
],
|
||||
})
|
||||
export default class PopularThemesModule { }
|
||||
export default class PopularThemesModule { } // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||
|
@ -2,7 +2,6 @@
|
||||
"extends": "../tsconfig.json",
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"declarationDir": "dist"
|
||||
"baseUrl": "src"
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ module.exports = {
|
||||
libraryTarget: 'umd',
|
||||
devtoolModuleFilenameTemplate: 'webpack-terminus-community-color-schemes:///[resource-path]',
|
||||
},
|
||||
mode: process.env.DEV ? 'development' : 'production',
|
||||
mode: process.env.TERMINUS_DEV ? 'development' : 'production',
|
||||
optimization:{
|
||||
minimize: false,
|
||||
},
|
||||
|
@ -2,12 +2,12 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/node@7.0.12":
|
||||
version "7.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.12.tgz#ae5f67a19c15f752148004db07cbbb372e69efc9"
|
||||
integrity sha1-rl9noZwV91IUgATbB8u7Ny5p78k=
|
||||
"@types/node@12.0.10":
|
||||
version "12.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.10.tgz#51babf9c7deadd5343620055fc8aff7995c8b031"
|
||||
integrity sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ==
|
||||
|
||||
"@types/webpack-env@^1.13.0":
|
||||
version "1.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.1.tgz#b45c222e24301bd006e3edfc762cc6b51bda236a"
|
||||
integrity sha512-oHyg0NssP2RCpCvE35hhbSqMJRsc5lSW+GFe+Vc65JL+kHII1VMYM+0KeV/z4utFuUqPoQRmq8KMMp7ba0dj6Q==
|
||||
version "1.13.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.9.tgz#a67287861c928ebf4159a908d1fb1a2a34d4097a"
|
||||
integrity sha512-p8zp5xqkly3g4cCmo2mKOHI9+Z/kObmDj0BmjbDDJQlgDTiEGTbm17MEwTAusV6XceCy+bNw9q/ZHXHyKo3zkg==
|
||||
|
31
terminus-core/README.md
Normal file
@ -0,0 +1,31 @@
|
||||
Terminus Core Plugin
|
||||
--------------------
|
||||
|
||||
See also: [Settings plugin API](./settings/), [Terminal plugin API](./settings/)
|
||||
|
||||
* tabbed interface services
|
||||
* toolbar UI
|
||||
* config file management
|
||||
* hotkeys
|
||||
* tab recovery
|
||||
* logging
|
||||
* theming
|
||||
|
||||
Using the API:
|
||||
|
||||
```ts
|
||||
import { AppService, TabContextMenuItemProvider } from 'terminus-core'
|
||||
```
|
||||
|
||||
Exporting your subclasses:
|
||||
|
||||
```ts
|
||||
@NgModule({
|
||||
...
|
||||
providers: [
|
||||
...
|
||||
{ provide: TabContextMenuItemProvider, useClass: MyContextMenu, multi: true },
|
||||
...
|
||||
]
|
||||
})
|
||||
```
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "terminus-core",
|
||||
"version": "1.0.0-alpha.55",
|
||||
"version": "1.0.73-c4-ga7d62b0",
|
||||
"description": "Terminus core",
|
||||
"keywords": [
|
||||
"terminus-builtin-plugin"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"typings": "typings/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "webpack --progress --color --display-modules",
|
||||
"watch": "webpack --progress --color --watch"
|
||||
@ -18,18 +18,25 @@
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^3.9.0",
|
||||
"@types/node": "^7.0.37",
|
||||
"@types/node": "^12.0.2",
|
||||
"@types/shell-escape": "^0.2.0",
|
||||
"@types/webpack-env": "^1.13.0",
|
||||
"@types/winston": "^2.3.6",
|
||||
"axios": "^0.18.0",
|
||||
"axios": "^0.19.0",
|
||||
"bootstrap": "^4.1.3",
|
||||
"core-js": "^2.4.1",
|
||||
"electron-updater": "^2.8.9",
|
||||
"core-js": "^3.1.2",
|
||||
"deepmerge": "^3.2.0",
|
||||
"electron-updater": "^4.0.6",
|
||||
"js-yaml": "^3.9.0",
|
||||
"mixpanel": "^0.10.2",
|
||||
"ng2-dnd": "^5.0.2",
|
||||
"ngx-perfect-scrollbar": "^6.0.0",
|
||||
"rage-edit-tmp": "^1.1.0",
|
||||
"shell-escape": "^0.2.0",
|
||||
"universal-analytics": "^0.4.17"
|
||||
"uuid": "^3.3.2",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"windows-native-registry": "^1.0.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "4.0.1",
|
||||
@ -40,10 +47,5 @@
|
||||
"@angular/platform-browser-dynamic": "4.0.1",
|
||||
"rxjs": "5.3.0",
|
||||
"zone.js": "0.8.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": "^1.5.0",
|
||||
"js-yaml": "^3.9.0",
|
||||
"winston": "^2.4.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,37 @@
|
||||
/**
|
||||
* Extend to add your own config options
|
||||
*/
|
||||
export abstract class ConfigProvider {
|
||||
/**
|
||||
* Default values, e.g.
|
||||
*
|
||||
* ```ts
|
||||
* defaults = {
|
||||
* myPlugin: {
|
||||
* foo: 1
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
defaults: any = {}
|
||||
platformDefaults: any = {}
|
||||
|
||||
/**
|
||||
* [[Platform]] specific defaults, e.g.
|
||||
*
|
||||
* ```ts
|
||||
* platformDefaults = {
|
||||
* [Platform.Windows]: {
|
||||
* myPlugin: {
|
||||
* bar: true
|
||||
* }
|
||||
* },
|
||||
* [Platform.macOS]: {
|
||||
* myPlugin: {
|
||||
* bar: false
|
||||
* }
|
||||
* },
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
platformDefaults: {[platform: string]: any} = {}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
export interface IHotkeyDescription {
|
||||
id: string,
|
||||
name: string,
|
||||
export interface HotkeyDescription {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend to provide your own hotkeys. A corresponding [[ConfigProvider]]
|
||||
* must also provide the `hotkeys.foo` config options with the default values
|
||||
*/
|
||||
export abstract class HotkeyProvider {
|
||||
hotkeys: IHotkeyDescription[] = []
|
||||
hotkeys: HotkeyDescription[] = []
|
||||
|
||||
abstract provide (): Promise<IHotkeyDescription[]>
|
||||
abstract provide (): Promise<HotkeyDescription[]>
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
|
||||
export { SplitTabComponent, SplitContainer } from '../components/splitTab.component'
|
||||
export { TabRecoveryProvider, RecoveredTab } from './tabRecovery'
|
||||
export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
|
||||
export { ToolbarButtonProvider, ToolbarButton } from './toolbarButtonProvider'
|
||||
export { ConfigProvider } from './configProvider'
|
||||
export { HotkeyProvider, IHotkeyDescription } from './hotkeyProvider'
|
||||
export { HotkeyProvider, HotkeyDescription } from './hotkeyProvider'
|
||||
export { Theme } from './theme'
|
||||
export { TabContextMenuItemProvider } from './tabContextMenuProvider'
|
||||
|
||||
export { AppService } from '../services/app.service'
|
||||
export { ConfigService } from '../services/config.service'
|
||||
@ -15,3 +17,4 @@ export { HotkeysService } from '../services/hotkeys.service'
|
||||
export { HostAppService, Platform } from '../services/hostApp.service'
|
||||
export { ShellIntegrationService } from '../services/shellIntegration.service'
|
||||
export { ThemesService } from '../services/themes.service'
|
||||
export { TabsService } from '../services/tabs.service'
|
||||
|
11
terminus-core/src/api/tabContextMenuProvider.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
import { TabHeaderComponent } from '../components/tabHeader.component'
|
||||
|
||||
/**
|
||||
* Extend to add items to the tab header's context menu
|
||||
*/
|
||||
export abstract class TabContextMenuItemProvider {
|
||||
weight = 0
|
||||
|
||||
abstract async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<Electron.MenuItemConstructorOptions[]>
|
||||
}
|
@ -1,10 +1,38 @@
|
||||
import { TabComponentType } from '../services/app.service'
|
||||
import { TabComponentType } from '../services/tabs.service'
|
||||
|
||||
export interface RecoveredTab {
|
||||
type: TabComponentType,
|
||||
options?: any,
|
||||
/**
|
||||
* Component type to be instantiated
|
||||
*/
|
||||
type: TabComponentType
|
||||
|
||||
/**
|
||||
* Component instance inputs
|
||||
*/
|
||||
options?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend to enable recovery for your custom tab.
|
||||
* This works in conjunction with [[getRecoveryToken()]]
|
||||
*
|
||||
* Terminus will try to find any [[TabRecoveryProvider]] that is able to process
|
||||
* the recovery token previously returned by [[getRecoveryToken]].
|
||||
*
|
||||
* Recommended token format:
|
||||
*
|
||||
* ```json
|
||||
* {
|
||||
* type: 'my-tab-type',
|
||||
* foo: 'bar',
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export abstract class TabRecoveryProvider {
|
||||
abstract async recover (recoveryToken: any): Promise<RecoveredTab|null>
|
||||
/**
|
||||
* @param recoveryToken a recovery token found in the saved tabs list
|
||||
* @returns [[RecoveredTab]] descriptor containing tab type and component inputs
|
||||
* or `null` if this token is from a different tab type or is not supported
|
||||
*/
|
||||
abstract async recover (recoveryToken: any): Promise<RecoveredTab | null>
|
||||
}
|
||||
|
@ -1,5 +1,13 @@
|
||||
/**
|
||||
* Extend to add a custom CSS theme
|
||||
*/
|
||||
export abstract class Theme {
|
||||
name: string
|
||||
|
||||
/**
|
||||
* Complete CSS stylesheet
|
||||
*/
|
||||
css: string
|
||||
|
||||
terminalBackground: string
|
||||
}
|
||||
|
@ -1,14 +1,39 @@
|
||||
import { SafeHtml } from '@angular/platform-browser'
|
||||
|
||||
export interface IToolbarButton {
|
||||
/**
|
||||
* See [[ToolbarButtonProvider]]
|
||||
*/
|
||||
export interface ToolbarButton {
|
||||
/**
|
||||
* Raw SVG icon code
|
||||
*/
|
||||
icon: SafeHtml
|
||||
touchBarNSImage?: string
|
||||
|
||||
title: string
|
||||
|
||||
/**
|
||||
* Optional Touch Bar icon ID
|
||||
*/
|
||||
touchBarNSImage?: string
|
||||
|
||||
/**
|
||||
* Optional Touch Bar button label
|
||||
*/
|
||||
touchBarTitle?: string
|
||||
|
||||
weight?: number
|
||||
click: () => void
|
||||
|
||||
click?: () => void
|
||||
|
||||
submenu?: () => Promise<ToolbarButton[]>
|
||||
|
||||
/** @hidden */
|
||||
submenuItems?: ToolbarButton[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend to add buttons to the toolbar
|
||||
*/
|
||||
export abstract class ToolbarButtonProvider {
|
||||
abstract provide (): IToolbarButton[]
|
||||
abstract provide (): ToolbarButton[]
|
||||
}
|
||||
|
@ -32,22 +32,48 @@ title-bar(
|
||||
)
|
||||
|
||||
.btn-group.background
|
||||
button.btn.btn-secondary.btn-tab-bar(
|
||||
*ngFor='let button of leftToolbarButtons',
|
||||
[title]='button.title',
|
||||
(click)='button.click()',
|
||||
[innerHTML]='button.icon',
|
||||
.d-flex(
|
||||
*ngFor='let button of leftToolbarButtons',
|
||||
ngbDropdown,
|
||||
(openChange)='generateButtonSubmenu(button)',
|
||||
)
|
||||
button.btn.btn-secondary.btn-tab-bar(
|
||||
[title]='button.title',
|
||||
(click)='button.click && button.click()',
|
||||
[innerHTML]='button.icon',
|
||||
ngbDropdownToggle,
|
||||
)
|
||||
div(*ngIf='button.submenu', ngbDropdownMenu)
|
||||
button.dropdown-item.d-flex.align-items-center(
|
||||
*ngFor='let item of button.submenuItems',
|
||||
(click)='item.click()',
|
||||
ngbDropdownItem,
|
||||
)
|
||||
.icon-wrapper([innerHTML]='item.icon')
|
||||
.ml-3 {{item.title}}
|
||||
|
||||
.drag-space.background([class.persistent]='config.store.appearance.frame == "thin" && hostApp.platform != Platform.macOS')
|
||||
|
||||
.btn-group.background
|
||||
button.btn.btn-secondary.btn-tab-bar(
|
||||
*ngFor='let button of rightToolbarButtons',
|
||||
[title]='button.title',
|
||||
(click)='button.click()',
|
||||
[innerHTML]='button.icon',
|
||||
.d-flex(
|
||||
*ngFor='let button of rightToolbarButtons',
|
||||
ngbDropdown,
|
||||
(openChange)='generateButtonSubmenu(button)',
|
||||
)
|
||||
button.btn.btn-secondary.btn-tab-bar(
|
||||
[title]='button.title',
|
||||
(click)='button.click && button.click()',
|
||||
[innerHTML]='button.icon',
|
||||
ngbDropdownToggle,
|
||||
)
|
||||
div(*ngIf='button.submenu', ngbDropdownMenu)
|
||||
button.dropdown-item.d-flex.align-items-center(
|
||||
*ngFor='let item of button.submenuItems',
|
||||
(click)='item.click()',
|
||||
ngbDropdownItem,
|
||||
)
|
||||
.icon-wrapper([innerHTML]='item.icon')
|
||||
.ml-3 {{item.title}}
|
||||
|
||||
button.btn.btn-secondary.btn-tab-bar.btn-update(
|
||||
*ngIf='updatesAvailable',
|
||||
|
@ -48,6 +48,10 @@ $tab-border-radius: 4px;
|
||||
color: #aaa;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
|
||||
&.dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&>.tabs {
|
||||
@ -88,12 +92,20 @@ hotkey-hint {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
::ng-deep .btn-tab-bar svg {
|
||||
::ng-deep .btn-tab-bar svg,
|
||||
::ng-deep .btn-tab-bar + .dropdown-menu svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: white;
|
||||
fill-opacity: 0.75;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
width: 16px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
::ng-deep .btn-update svg {
|
||||
fill: cyan;
|
||||
}
|
||||
|
@ -9,15 +9,15 @@ import { HotkeysService } from '../services/hotkeys.service'
|
||||
import { Logger, LogService } from '../services/log.service'
|
||||
import { ConfigService } from '../services/config.service'
|
||||
import { DockingService } from '../services/docking.service'
|
||||
import { TabRecoveryService } from '../services/tabRecovery.service'
|
||||
import { ThemesService } from '../services/themes.service'
|
||||
import { UpdaterService } from '../services/updater.service'
|
||||
import { TouchbarService } from '../services/touchbar.service'
|
||||
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { SafeModeModalComponent } from './safeModeModal.component'
|
||||
import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
import { AppService, ToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: require('./appRoot.component.pug'),
|
||||
@ -26,36 +26,36 @@ import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
trigger('animateTab', [
|
||||
state('in', style({
|
||||
'flex-basis': '200px',
|
||||
'width': '200px',
|
||||
width: '200px',
|
||||
})),
|
||||
transition(':enter', [
|
||||
style({
|
||||
'flex-basis': '1px',
|
||||
'width': '1px',
|
||||
width: '1px',
|
||||
}),
|
||||
animate('250ms ease-in-out', style({
|
||||
'flex-basis': '200px',
|
||||
'width': '200px',
|
||||
}))
|
||||
width: '200px',
|
||||
})),
|
||||
]),
|
||||
transition(':leave', [
|
||||
style({
|
||||
'flex-basis': '200px',
|
||||
'width': '200px',
|
||||
width: '200px',
|
||||
}),
|
||||
animate('250ms ease-in-out', style({
|
||||
'flex-basis': '1px',
|
||||
'width': '1px',
|
||||
}))
|
||||
])
|
||||
])
|
||||
]
|
||||
width: '1px',
|
||||
})),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class AppRootComponent {
|
||||
Platform = Platform
|
||||
@Input() ready = false
|
||||
@Input() leftToolbarButtons: IToolbarButton[]
|
||||
@Input() rightToolbarButtons: IToolbarButton[]
|
||||
@Input() leftToolbarButtons: ToolbarButton[]
|
||||
@Input() rightToolbarButtons: ToolbarButton[]
|
||||
@HostBinding('class.platform-win32') platformClassWindows = process.platform === 'win32'
|
||||
@HostBinding('class.platform-darwin') platformClassMacOS = process.platform === 'darwin'
|
||||
@HostBinding('class.platform-linux') platformClassLinux = process.platform === 'linux'
|
||||
@ -69,7 +69,6 @@ export class AppRootComponent {
|
||||
constructor (
|
||||
private docking: DockingService,
|
||||
private electron: ElectronService,
|
||||
private tabRecovery: TabRecoveryService,
|
||||
private hotkeys: HotkeysService,
|
||||
private updater: UpdaterService,
|
||||
private touchbar: TouchbarService,
|
||||
@ -90,9 +89,9 @@ export class AppRootComponent {
|
||||
|
||||
this.updateIcon = domSanitizer.bypassSecurityTrustHtml(require('../icons/gift.svg')),
|
||||
|
||||
this.hotkeys.matchedHotkey.subscribe((hotkey) => {
|
||||
this.hotkeys.matchedHotkey.subscribe((hotkey: string) => {
|
||||
if (hotkey.startsWith('tab-')) {
|
||||
let index = parseInt(hotkey.split('-')[1])
|
||||
const index = parseInt(hotkey.split('-')[1])
|
||||
if (index <= this.app.tabs.length) {
|
||||
this.app.selectTab(this.app.tabs[index - 1])
|
||||
}
|
||||
@ -128,6 +127,11 @@ export class AppRootComponent {
|
||||
this.onGlobalHotkey()
|
||||
})
|
||||
|
||||
this.hostApp.windowCloseRequest$.subscribe(async () => {
|
||||
await this.app.closeAllTabs()
|
||||
this.hostApp.closeWindow()
|
||||
})
|
||||
|
||||
if (window['safeModeReason']) {
|
||||
ngbModal.open(SafeModeModalComponent)
|
||||
}
|
||||
@ -199,9 +203,7 @@ export class AppRootComponent {
|
||||
}
|
||||
|
||||
async ngOnInit () {
|
||||
await this.tabRecovery.recoverTabs()
|
||||
this.ready = true
|
||||
this.tabRecovery.saveTabs(this.app.tabs)
|
||||
|
||||
this.app.emitReady()
|
||||
}
|
||||
@ -231,14 +233,20 @@ export class AppRootComponent {
|
||||
})
|
||||
}
|
||||
|
||||
private getToolbarButtons (aboveZero: boolean): IToolbarButton[] {
|
||||
let buttons: IToolbarButton[] = []
|
||||
async generateButtonSubmenu (button: ToolbarButton) {
|
||||
if (button.submenu) {
|
||||
button.submenuItems = await button.submenu()
|
||||
}
|
||||
}
|
||||
|
||||
private getToolbarButtons (aboveZero: boolean): ToolbarButton[] {
|
||||
let buttons: ToolbarButton[] = []
|
||||
this.config.enabledServices(this.toolbarButtonProviders).forEach(provider => {
|
||||
buttons = buttons.concat(provider.provide())
|
||||
})
|
||||
return buttons
|
||||
.filter((button) => (button.weight > 0) === aboveZero)
|
||||
.sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0))
|
||||
.filter(button => button.weight > 0 === aboveZero)
|
||||
.sort((a: ToolbarButton, b: ToolbarButton) => (a.weight || 0) - (b.weight || 0))
|
||||
}
|
||||
|
||||
private updateVibrancy () {
|
||||
|
@ -1,27 +1,58 @@
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { ViewRef } from '@angular/core'
|
||||
|
||||
/**
|
||||
* Represents an active "process" inside a tab,
|
||||
* for example, a user process running inside a terminal tab
|
||||
*/
|
||||
export interface BaseTabProcess {
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for custom tab components
|
||||
*/
|
||||
export abstract class BaseTabComponent {
|
||||
private static lastTabID = 0
|
||||
id: number
|
||||
/**
|
||||
* Current tab title
|
||||
*/
|
||||
title: string
|
||||
|
||||
/**
|
||||
* User-defined title override
|
||||
*/
|
||||
customTitle: string
|
||||
hasFocus = false
|
||||
|
||||
/**
|
||||
* Last tab activity state
|
||||
*/
|
||||
hasActivity = false
|
||||
|
||||
/**
|
||||
* ViewRef to the tab DOM element
|
||||
*/
|
||||
hostView: ViewRef
|
||||
|
||||
/**
|
||||
* CSS color override for the tab's header
|
||||
*/
|
||||
color: string = null
|
||||
protected titleChange = new Subject<string>()
|
||||
protected focused = new Subject<void>()
|
||||
protected blurred = new Subject<void>()
|
||||
protected progress = new Subject<number>()
|
||||
protected activity = new Subject<boolean>()
|
||||
protected destroyed = new Subject<void>()
|
||||
|
||||
protected hasFocus = false
|
||||
|
||||
/**
|
||||
* Ping this if your recovery state has been changed and you want
|
||||
* your tab state to be saved sooner
|
||||
*/
|
||||
protected recoveryStateChangedHint = new Subject<void>()
|
||||
|
||||
private progressClearTimeout: number
|
||||
private titleChange = new Subject<string>()
|
||||
private focused = new Subject<void>()
|
||||
private blurred = new Subject<void>()
|
||||
private progress = new Subject<number>()
|
||||
private activity = new Subject<boolean>()
|
||||
private destroyed = new Subject<void>()
|
||||
|
||||
get focused$ (): Observable<void> { return this.focused }
|
||||
get blurred$ (): Observable<void> { return this.blurred }
|
||||
@ -29,9 +60,9 @@ export abstract class BaseTabComponent {
|
||||
get progress$ (): Observable<number> { return this.progress }
|
||||
get activity$ (): Observable<boolean> { return this.activity }
|
||||
get destroyed$ (): Observable<void> { return this.destroyed }
|
||||
get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint }
|
||||
|
||||
constructor () {
|
||||
this.id = BaseTabComponent.lastTabID++
|
||||
this.focused$.subscribe(() => {
|
||||
this.hasFocus = true
|
||||
})
|
||||
@ -47,6 +78,11 @@ export abstract class BaseTabComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets visual progressbar on the tab
|
||||
*
|
||||
* @param {type} progress: value between 0 and 1, or `null` to remove
|
||||
*/
|
||||
setProgress (progress: number) {
|
||||
this.progress.next(progress)
|
||||
if (progress) {
|
||||
@ -55,28 +91,47 @@ export abstract class BaseTabComponent {
|
||||
}
|
||||
this.progressClearTimeout = setTimeout(() => {
|
||||
this.setProgress(null)
|
||||
}, 5000)
|
||||
}, 5000) as any
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the acticity marker on the tab header
|
||||
*/
|
||||
displayActivity (): void {
|
||||
this.hasActivity = true
|
||||
this.activity.next(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the acticity marker from the tab header
|
||||
*/
|
||||
clearActivity (): void {
|
||||
this.hasActivity = false
|
||||
this.activity.next(false)
|
||||
}
|
||||
|
||||
getRecoveryToken (): any {
|
||||
/**
|
||||
* Override this and implement a [[TabRecoveryProvider]] to enable recovery
|
||||
* for your custom tab
|
||||
*
|
||||
* @return JSON serializable tab state representation
|
||||
* for your [[TabRecoveryProvider]] to parse
|
||||
*/
|
||||
async getRecoveryToken (): Promise<any> {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this to enable task completion notifications for the tab
|
||||
*/
|
||||
async getCurrentProcess (): Promise<BaseTabProcess> {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Return false to prevent the tab from being closed
|
||||
*/
|
||||
async canClose (): Promise<boolean> {
|
||||
return true
|
||||
}
|
||||
@ -89,11 +144,15 @@ export abstract class BaseTabComponent {
|
||||
this.blurred.next()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before the tab is closed
|
||||
*/
|
||||
destroy (): void {
|
||||
this.focused.complete()
|
||||
this.blurred.complete()
|
||||
this.titleChange.complete()
|
||||
this.progress.complete()
|
||||
this.recoveryStateChangedHint.complete()
|
||||
this.destroyed.next()
|
||||
this.destroyed.complete()
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
.icon(tabindex='0', [class.active]='model', (keyup.space)='click()')
|
||||
i.fa.fa-square-o.off
|
||||
i.fa.fa-check-square.on
|
||||
i.fas.fa-square.off
|
||||
i.fas.fa-check-square.on
|
||||
.text {{text}}
|
||||
|
@ -20,6 +20,10 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.off {
|
||||
color: rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
flex: none;
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { NgZone, Component, Input, HostBinding, HostListener } from '@angular/core'
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'checkbox',
|
||||
template: require('./checkbox.component.pug'),
|
||||
styles: [require('./checkbox.component.scss')],
|
||||
providers: [
|
||||
{ provide: NG_VALUE_ACCESSOR, useExisting: CheckboxComponent, multi: true },
|
||||
]
|
||||
],
|
||||
})
|
||||
export class CheckboxComponent implements ControlValueAccessor {
|
||||
@HostBinding('class.active') @Input() model: boolean
|
||||
@ -22,7 +23,7 @@ export class CheckboxComponent implements ControlValueAccessor {
|
||||
}
|
||||
|
||||
this.model = !this.model
|
||||
for (let fx of this.changed) {
|
||||
for (const fx of this.changed) {
|
||||
fx(this.model)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Component, Input, ElementRef, ViewChild } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'rename-tab-modal',
|
||||
template: require('./renameTabModal.component.pug'),
|
||||
@ -16,6 +17,7 @@ export class RenameTabModalComponent {
|
||||
ngOnInit () {
|
||||
setTimeout(() => {
|
||||
this.input.nativeElement.focus()
|
||||
this.input.nativeElement.select()
|
||||
}, 250)
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
template: require('./safeModeModal.component.pug'),
|
||||
})
|
||||
|
5
terminus-core/src/components/splitTab.component.scss
Normal file
@ -0,0 +1,5 @@
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
flex: auto;
|
||||
}
|
524
terminus-core/src/components/splitTab.component.ts
Normal file
@ -0,0 +1,524 @@
|
||||
import { Observable, Subject, Subscription } from 'rxjs'
|
||||
import { Component, Injectable, ViewChild, ViewContainerRef, EmbeddedViewRef, OnInit, OnDestroy } from '@angular/core'
|
||||
import { BaseTabComponent, BaseTabProcess } from './baseTab.component'
|
||||
import { TabRecoveryProvider, RecoveredTab } from '../api/tabRecovery'
|
||||
import { TabsService } from '../services/tabs.service'
|
||||
import { HotkeysService } from '../services/hotkeys.service'
|
||||
import { TabRecoveryService } from '../services/tabRecovery.service'
|
||||
|
||||
export type SplitOrientation = 'v' | 'h' // eslint-disable-line @typescript-eslint/no-type-alias
|
||||
export type SplitDirection = 'r' | 't' | 'b' | 'l' // eslint-disable-line @typescript-eslint/no-type-alias
|
||||
|
||||
/**
|
||||
* Describes a horizontal or vertical split row or column
|
||||
*/
|
||||
export class SplitContainer {
|
||||
orientation: SplitOrientation = 'h'
|
||||
|
||||
/**
|
||||
* Children could be tabs or other containers
|
||||
*/
|
||||
children: (BaseTabComponent | SplitContainer)[] = []
|
||||
|
||||
/**
|
||||
* Relative sizes of children, between 0 and 1. Total sum is 1
|
||||
*/
|
||||
ratios: number[] = []
|
||||
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
|
||||
/**
|
||||
* @return Flat list of all tabs inside this container
|
||||
*/
|
||||
getAllTabs () {
|
||||
let r = []
|
||||
for (const child of this.children) {
|
||||
if (child instanceof SplitContainer) {
|
||||
r = r.concat(child.getAllTabs())
|
||||
} else {
|
||||
r.push(child)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove unnecessarily nested child containers and renormalizes [[ratios]]
|
||||
*/
|
||||
normalize () {
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
const child = this.children[i]
|
||||
|
||||
if (child instanceof SplitContainer) {
|
||||
child.normalize()
|
||||
|
||||
if (child.children.length === 0) {
|
||||
this.children.splice(i, 1)
|
||||
this.ratios.splice(i, 1)
|
||||
i--
|
||||
continue
|
||||
} else if (child.children.length === 1) {
|
||||
this.children[i] = child.children[0]
|
||||
} else if (child.orientation === this.orientation) {
|
||||
const ratio = this.ratios[i]
|
||||
this.children.splice(i, 1)
|
||||
this.ratios.splice(i, 1)
|
||||
for (let j = 0; j < child.children.length; j++) {
|
||||
this.children.splice(i, 0, child.children[j])
|
||||
this.ratios.splice(i, 0, child.ratios[j] * ratio)
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let s = 0
|
||||
for (const x of this.ratios) {
|
||||
s += x
|
||||
}
|
||||
this.ratios = this.ratios.map(x => x / s)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the left/top side offset for the given element index (between 0 and 1)
|
||||
*/
|
||||
getOffsetRatio (index: number): number {
|
||||
let s = 0
|
||||
for (let i = 0; i < index; i++) {
|
||||
s += this.ratios[i]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
async serialize () {
|
||||
const children = []
|
||||
for (const child of this.children) {
|
||||
if (child instanceof SplitContainer) {
|
||||
children.push(await child.serialize())
|
||||
} else {
|
||||
children.push(await child.getRecoveryToken())
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'app:split-tab',
|
||||
ratios: this.ratios,
|
||||
orientation: this.orientation,
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a spanner (draggable border between two split areas)
|
||||
*/
|
||||
export interface SplitSpannerInfo {
|
||||
container: SplitContainer
|
||||
|
||||
/**
|
||||
* Number of the right/bottom split in the container
|
||||
*/
|
||||
index: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Split tab is a tab that contains other tabs and allows further splitting them
|
||||
* You'll mainly encounter it inside [[AppService]].tabs
|
||||
*/
|
||||
@Component({
|
||||
selector: 'split-tab',
|
||||
template: `
|
||||
<ng-container #vc></ng-container>
|
||||
<split-tab-spanner
|
||||
*ngFor='let spanner of _spanners'
|
||||
[container]='spanner.container'
|
||||
[index]='spanner.index'
|
||||
(change)='onSpannerAdjusted(spanner)'
|
||||
></split-tab-spanner>
|
||||
`,
|
||||
styles: [require('./splitTab.component.scss')],
|
||||
})
|
||||
export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
|
||||
/** @hidden */
|
||||
@ViewChild('vc', { read: ViewContainerRef }) viewContainer: ViewContainerRef
|
||||
|
||||
/**
|
||||
* Top-level split container
|
||||
*/
|
||||
root: SplitContainer
|
||||
|
||||
/** @hidden */
|
||||
_recoveredState: any
|
||||
|
||||
/** @hidden */
|
||||
_spanners: SplitSpannerInfo[] = []
|
||||
|
||||
private focusedTab: BaseTabComponent
|
||||
private hotkeysSubscription: Subscription
|
||||
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
|
||||
|
||||
private tabAdded = new Subject<BaseTabComponent>()
|
||||
private tabRemoved = new Subject<BaseTabComponent>()
|
||||
private splitAdjusted = new Subject<SplitSpannerInfo>()
|
||||
private focusChanged = new Subject<BaseTabComponent>()
|
||||
|
||||
get tabAdded$ (): Observable<BaseTabComponent> { return this.tabAdded }
|
||||
get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
|
||||
|
||||
/**
|
||||
* Fired when split ratio is changed for a given spanner
|
||||
*/
|
||||
get splitAdjusted$ (): Observable<SplitSpannerInfo> { return this.splitAdjusted }
|
||||
|
||||
/**
|
||||
* Fired when a different sub-tab gains focus
|
||||
*/
|
||||
get focusChanged$ (): Observable<BaseTabComponent> { return this.focusChanged }
|
||||
|
||||
/** @hidden */
|
||||
constructor (
|
||||
private hotkeys: HotkeysService,
|
||||
private tabsService: TabsService,
|
||||
private tabRecovery: TabRecoveryService,
|
||||
) {
|
||||
super()
|
||||
this.root = new SplitContainer()
|
||||
this.setTitle('')
|
||||
|
||||
this.focused$.subscribe(() => {
|
||||
this.getAllTabs().forEach(x => x.emitFocused())
|
||||
this.focus(this.focusedTab)
|
||||
})
|
||||
this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred()))
|
||||
|
||||
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
|
||||
if (!this.hasFocus) {
|
||||
return
|
||||
}
|
||||
switch (hotkey) {
|
||||
case 'split-right':
|
||||
this.splitTab(this.focusedTab, 'r')
|
||||
break
|
||||
case 'split-bottom':
|
||||
this.splitTab(this.focusedTab, 'b')
|
||||
break
|
||||
case 'split-top':
|
||||
this.splitTab(this.focusedTab, 't')
|
||||
break
|
||||
case 'split-left':
|
||||
this.splitTab(this.focusedTab, 'l')
|
||||
break
|
||||
case 'pane-nav-left':
|
||||
this.navigate('l')
|
||||
break
|
||||
case 'pane-nav-right':
|
||||
this.navigate('r')
|
||||
break
|
||||
case 'pane-nav-up':
|
||||
this.navigate('t')
|
||||
break
|
||||
case 'pane-nav-down':
|
||||
this.navigate('b')
|
||||
break
|
||||
case 'close-pane':
|
||||
this.removeTab(this.focusedTab)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
async ngOnInit () {
|
||||
if (this._recoveredState) {
|
||||
await this.recoverContainer(this.root, this._recoveredState)
|
||||
this.layout()
|
||||
setImmediate(() => {
|
||||
this.getAllTabs().forEach(x => x.emitFocused())
|
||||
this.focusAnyIn(this.root)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
ngOnDestroy () {
|
||||
this.hotkeysSubscription.unsubscribe()
|
||||
}
|
||||
|
||||
/** @returns Flat list of all sub-tabs */
|
||||
getAllTabs () {
|
||||
return this.root.getAllTabs()
|
||||
}
|
||||
|
||||
getFocusedTab (): BaseTabComponent {
|
||||
return this.focusedTab
|
||||
}
|
||||
|
||||
focus (tab: BaseTabComponent) {
|
||||
this.focusedTab = tab
|
||||
for (const x of this.getAllTabs()) {
|
||||
if (x !== tab) {
|
||||
x.emitBlurred()
|
||||
}
|
||||
}
|
||||
if (tab) {
|
||||
tab.emitFocused()
|
||||
this.focusChanged.next(tab)
|
||||
}
|
||||
this.layout()
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses the first available tab inside the given [[SplitContainer]]
|
||||
*/
|
||||
focusAnyIn (parent: BaseTabComponent | SplitContainer) {
|
||||
if (!parent) {
|
||||
return
|
||||
}
|
||||
if (parent instanceof SplitContainer) {
|
||||
this.focusAnyIn(parent.children[0])
|
||||
} else {
|
||||
this.focus(parent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new `tab` to the `side` of the `relative` tab
|
||||
*/
|
||||
addTab (tab: BaseTabComponent, relative: BaseTabComponent, side: SplitDirection) {
|
||||
let target = this.getParentOf(relative) || this.root
|
||||
let insertIndex = target.children.indexOf(relative)
|
||||
|
||||
if (
|
||||
target.orientation === 'v' && ['l', 'r'].includes(side) ||
|
||||
target.orientation === 'h' && ['t', 'b'].includes(side)
|
||||
) {
|
||||
const newContainer = new SplitContainer()
|
||||
newContainer.orientation = target.orientation === 'v' ? 'h' : 'v'
|
||||
newContainer.children = [relative]
|
||||
newContainer.ratios = [1]
|
||||
target.children[insertIndex] = newContainer
|
||||
target = newContainer
|
||||
insertIndex = 0
|
||||
}
|
||||
|
||||
if (insertIndex === -1) {
|
||||
insertIndex = 0
|
||||
} else {
|
||||
insertIndex += side === 'l' || side === 't' ? 0 : 1
|
||||
}
|
||||
|
||||
for (let i = 0; i < target.children.length; i++) {
|
||||
target.ratios[i] *= target.children.length / (target.children.length + 1)
|
||||
}
|
||||
target.ratios.splice(insertIndex, 0, 1 / (target.children.length + 1))
|
||||
target.children.splice(insertIndex, 0, tab)
|
||||
|
||||
this.recoveryStateChangedHint.next()
|
||||
this.attachTabView(tab)
|
||||
|
||||
setImmediate(() => {
|
||||
this.layout()
|
||||
this.tabAdded.next(tab)
|
||||
this.focus(tab)
|
||||
})
|
||||
}
|
||||
|
||||
removeTab (tab: BaseTabComponent) {
|
||||
const parent = this.getParentOf(tab)
|
||||
const index = parent.children.indexOf(tab)
|
||||
parent.ratios.splice(index, 1)
|
||||
parent.children.splice(index, 1)
|
||||
|
||||
this.detachTabView(tab)
|
||||
|
||||
this.layout()
|
||||
|
||||
this.tabRemoved.next(tab)
|
||||
|
||||
if (this.root.children.length === 0) {
|
||||
this.destroy()
|
||||
} else {
|
||||
this.focusAnyIn(parent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves focus in the given direction
|
||||
*/
|
||||
navigate (dir: SplitDirection) {
|
||||
let rel: BaseTabComponent | SplitContainer = this.focusedTab
|
||||
let parent = this.getParentOf(rel)
|
||||
const orientation = ['l', 'r'].includes(dir) ? 'h' : 'v'
|
||||
|
||||
while (parent !== this.root && parent.orientation !== orientation) {
|
||||
rel = parent
|
||||
parent = this.getParentOf(rel)
|
||||
}
|
||||
|
||||
if (parent.orientation !== orientation) {
|
||||
return
|
||||
}
|
||||
|
||||
const index = parent.children.indexOf(rel)
|
||||
if (['l', 't'].includes(dir)) {
|
||||
if (index > 0) {
|
||||
this.focusAnyIn(parent.children[index - 1])
|
||||
}
|
||||
} else {
|
||||
if (index < parent.children.length - 1) {
|
||||
this.focusAnyIn(parent.children[index + 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async splitTab (tab: BaseTabComponent, dir: SplitDirection) {
|
||||
const newTab = await this.tabsService.duplicate(tab)
|
||||
this.addTab(newTab, tab, dir)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the immediate parent of `tab`
|
||||
*/
|
||||
getParentOf (tab: BaseTabComponent | SplitContainer, root?: SplitContainer): SplitContainer {
|
||||
root = root || this.root
|
||||
for (const child of root.children) {
|
||||
if (child instanceof SplitContainer) {
|
||||
const r = this.getParentOf(tab, child)
|
||||
if (r) {
|
||||
return r
|
||||
}
|
||||
}
|
||||
if (child === tab) {
|
||||
return root
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
async canClose (): Promise<boolean> {
|
||||
return !(await Promise.all(this.getAllTabs().map(x => x.canClose()))).some(x => !x)
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
async getRecoveryToken (): Promise<any> {
|
||||
return this.root.serialize()
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
async getCurrentProcess (): Promise<BaseTabProcess> {
|
||||
return (await Promise.all(this.getAllTabs().map(x => x.getCurrentProcess()))).find(x => !!x)
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
onSpannerAdjusted (spanner: SplitSpannerInfo) {
|
||||
this.layout()
|
||||
this.splitAdjusted.next(spanner)
|
||||
}
|
||||
|
||||
private attachTabView (tab: BaseTabComponent) {
|
||||
const ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any> // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
this.viewRefs.set(tab, ref)
|
||||
|
||||
ref.rootNodes[0].addEventListener('click', () => this.focus(tab))
|
||||
|
||||
tab.titleChange$.subscribe(t => this.setTitle(t))
|
||||
tab.activity$.subscribe(a => a ? this.displayActivity() : this.clearActivity())
|
||||
tab.progress$.subscribe(p => this.setProgress(p))
|
||||
if (tab.title) {
|
||||
this.setTitle(tab.title)
|
||||
}
|
||||
tab.destroyed$.subscribe(() => {
|
||||
this.removeTab(tab)
|
||||
})
|
||||
}
|
||||
|
||||
private detachTabView (tab: BaseTabComponent) {
|
||||
const ref = this.viewRefs.get(tab)
|
||||
this.viewRefs.delete(tab)
|
||||
this.viewContainer.remove(this.viewContainer.indexOf(ref))
|
||||
}
|
||||
|
||||
private layout () {
|
||||
this.root.normalize()
|
||||
this._spanners = []
|
||||
this.layoutInternal(this.root, 0, 0, 100, 100)
|
||||
}
|
||||
|
||||
private layoutInternal (root: SplitContainer, x: number, y: number, w: number, h: number) {
|
||||
const size = root.orientation === 'v' ? h : w
|
||||
const sizes = root.ratios.map(x => x * size)
|
||||
|
||||
root.x = x
|
||||
root.y = y
|
||||
root.w = w
|
||||
root.h = h
|
||||
|
||||
let offset = 0
|
||||
root.children.forEach((child, i) => {
|
||||
const childX = root.orientation === 'v' ? x : x + offset
|
||||
const childY = root.orientation === 'v' ? y + offset : y
|
||||
const childW = root.orientation === 'v' ? w : sizes[i]
|
||||
const childH = root.orientation === 'v' ? sizes[i] : h
|
||||
if (child instanceof SplitContainer) {
|
||||
this.layoutInternal(child, childX, childY, childW, childH)
|
||||
} else {
|
||||
const element = this.viewRefs.get(child).rootNodes[0]
|
||||
element.style.position = 'absolute'
|
||||
element.style.left = `${childX}%`
|
||||
element.style.top = `${childY}%`
|
||||
element.style.width = `${childW}%`
|
||||
element.style.height = `${childH}%`
|
||||
|
||||
element.style.opacity = child === this.focusedTab ? 1 : 0.75
|
||||
}
|
||||
offset += sizes[i]
|
||||
|
||||
if (i !== 0) {
|
||||
this._spanners.push({
|
||||
container: root,
|
||||
index: i,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async recoverContainer (root: SplitContainer, state: any) {
|
||||
const children: (SplitContainer | BaseTabComponent)[] = []
|
||||
root.orientation = state.orientation
|
||||
root.ratios = state.ratios
|
||||
root.children = children
|
||||
for (const childState of state.children) {
|
||||
if (childState.type === 'app:split-tab') {
|
||||
const child = new SplitContainer()
|
||||
await this.recoverContainer(child, childState)
|
||||
children.push(child)
|
||||
} else {
|
||||
const recovered = await this.tabRecovery.recoverTab(childState)
|
||||
if (recovered) {
|
||||
const tab = this.tabsService.create(recovered.type, recovered.options)
|
||||
children.push(tab)
|
||||
this.attachTabView(tab)
|
||||
} else {
|
||||
state.ratios.splice(state.children.indexOf(childState), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
@Injectable()
|
||||
export class SplitTabRecoveryProvider extends TabRecoveryProvider {
|
||||
async recover (recoveryToken: any): Promise<RecoveredTab> {
|
||||
if (recoveryToken && recoveryToken.type === 'app:split-tab') {
|
||||
return {
|
||||
type: SplitTabComponent,
|
||||
options: { _recoveredState: recoveryToken },
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
22
terminus-core/src/components/splitTabSpanner.component.scss
Normal file
@ -0,0 +1,22 @@
|
||||
:host {
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
transition: 0.125s background;
|
||||
|
||||
&.v {
|
||||
cursor: ns-resize;
|
||||
height: 10px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
&.h {
|
||||
cursor: ew-resize;
|
||||
width: 10px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
&:hover, &.active {
|
||||
background: rgba(255, 255, 255, .125);
|
||||
}
|
||||
}
|
88
terminus-core/src/components/splitTabSpanner.component.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
|
||||
import { SplitContainer } from './splitTab.component'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'split-tab-spanner',
|
||||
template: '',
|
||||
styles: [require('./splitTabSpanner.component.scss')],
|
||||
})
|
||||
export class SplitTabSpannerComponent {
|
||||
@Input() container: SplitContainer
|
||||
@Input() index: number
|
||||
@Output() change = new EventEmitter<void>()
|
||||
@HostBinding('class.active') isActive = false
|
||||
@HostBinding('class.h') isHorizontal = false
|
||||
@HostBinding('class.v') isVertical = true
|
||||
@HostBinding('style.left') cssLeft: string
|
||||
@HostBinding('style.top') cssTop: string
|
||||
@HostBinding('style.width') cssWidth: string
|
||||
@HostBinding('style.height') cssHeight: string
|
||||
private marginOffset = -5
|
||||
|
||||
constructor (private element: ElementRef) { }
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.element.nativeElement.addEventListener('mousedown', (e: MouseEvent) => {
|
||||
this.isActive = true
|
||||
const start = this.isVertical ? e.pageY : e.pageX
|
||||
let current = start
|
||||
const oldPosition: number = this.isVertical ? this.element.nativeElement.offsetTop : this.element.nativeElement.offsetLeft
|
||||
|
||||
const dragHandler = (e: MouseEvent) => {
|
||||
current = this.isVertical ? e.pageY : e.pageX
|
||||
const newPosition = oldPosition + (current - start)
|
||||
if (this.isVertical) {
|
||||
this.element.nativeElement.style.top = `${newPosition - this.marginOffset}px`
|
||||
} else {
|
||||
this.element.nativeElement.style.left = `${newPosition - this.marginOffset}px`
|
||||
}
|
||||
}
|
||||
|
||||
const offHandler = () => {
|
||||
this.isActive = false
|
||||
document.removeEventListener('mouseup', offHandler)
|
||||
this.element.nativeElement.parentElement.removeEventListener('mousemove', dragHandler)
|
||||
|
||||
let diff = (current - start) / (this.isVertical ? this.element.nativeElement.parentElement.clientHeight : this.element.nativeElement.parentElement.clientWidth)
|
||||
|
||||
diff = Math.max(diff, -this.container.ratios[this.index - 1] + 0.1)
|
||||
diff = Math.min(diff, this.container.ratios[this.index] - 0.1)
|
||||
|
||||
this.container.ratios[this.index - 1] += diff
|
||||
this.container.ratios[this.index] -= diff
|
||||
this.change.emit()
|
||||
}
|
||||
|
||||
document.addEventListener('mouseup', offHandler)
|
||||
this.element.nativeElement.parentElement.addEventListener('mousemove', dragHandler)
|
||||
})
|
||||
}
|
||||
|
||||
ngOnChanges () {
|
||||
this.isHorizontal = this.container.orientation === 'h'
|
||||
this.isVertical = this.container.orientation === 'v'
|
||||
if (this.isVertical) {
|
||||
this.setDimensions(
|
||||
this.container.x,
|
||||
this.container.y + this.container.h * this.container.getOffsetRatio(this.index),
|
||||
this.container.w,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
this.setDimensions(
|
||||
this.container.x + this.container.w * this.container.getOffsetRatio(this.index),
|
||||
this.container.y,
|
||||
null,
|
||||
this.container.h
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private setDimensions (x: number, y: number, w: number, h: number) {
|
||||
this.cssLeft = `${x}%`
|
||||
this.cssTop = `${y}%`
|
||||
this.cssWidth = w ? `${w}%` : null
|
||||
this.cssHeight = h ? `${h}%` : null
|
||||
}
|
||||
}
|
@ -14,10 +14,10 @@ div
|
||||
footer.d-flex.align-items-center
|
||||
.btn-group.mr-auto
|
||||
button.btn.btn-secondary((click)='homeBase.openGitHub()')
|
||||
i.fa.fa-github
|
||||
i.fab.fa-github
|
||||
span GitHub
|
||||
button.btn.btn-secondary((click)='homeBase.reportBug()')
|
||||
i.fa.fa-bug
|
||||
i.fas.fa-bug
|
||||
span Report a problem
|
||||
|
||||
.form-control-static.selectable.no-drag Version: {{homeBase.appVersion}}
|
||||
|
@ -2,7 +2,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
-webkit-app-region: drag;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@ -25,10 +24,6 @@ footer {
|
||||
background: rgba(0,0,0,.5);
|
||||
}
|
||||
|
||||
a, button {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.list-group-item ::ng-deep svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { ConfigService } from '../services/config.service'
|
||||
import { HomeBaseService } from '../services/homeBase.service'
|
||||
import { IToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
import { ToolbarButton, ToolbarButtonProvider } from '../api'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'start-page',
|
||||
template: require('./startPage.component.pug'),
|
||||
@ -18,10 +19,11 @@ export class StartPageComponent {
|
||||
) {
|
||||
}
|
||||
|
||||
getButtons (): IToolbarButton[] {
|
||||
getButtons (): ToolbarButton[] {
|
||||
return this.config.enabledServices(this.toolbarButtonProviders)
|
||||
.map(provider => provider.provide())
|
||||
.reduce((a, b) => a.concat(b))
|
||||
.sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0))
|
||||
.filter(x => !!x.click)
|
||||
.sort((a: ToolbarButton, b: ToolbarButton) => (a.weight || 0) - (b.weight || 0))
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Component, Input, ViewChild, HostBinding, ViewContainerRef, OnChanges } from '@angular/core'
|
||||
import { BaseTabComponent } from '../components/baseTab.component'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'tab-body',
|
||||
template: `
|
||||
@ -17,7 +18,7 @@ import { BaseTabComponent } from '../components/baseTab.component'
|
||||
export class TabBodyComponent implements OnChanges {
|
||||
@Input() @HostBinding('class.active') active: boolean
|
||||
@Input() tab: BaseTabComponent
|
||||
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef
|
||||
@ViewChild('placeholder', { read: ViewContainerRef }) placeholder: ViewContainerRef
|
||||
|
||||
ngOnChanges (changes) {
|
||||
if (changes.tab) {
|
||||
|
@ -1,22 +1,15 @@
|
||||
import { Component, Input, HostBinding, HostListener, NgZone, ViewChild, ElementRef } from '@angular/core'
|
||||
import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef } from '@angular/core'
|
||||
import { SortableComponent } from 'ng2-dnd'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider'
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { RenameTabModalComponent } from './renameTabModal.component'
|
||||
import { HotkeysService } from '../services/hotkeys.service'
|
||||
import { ElectronService } from '../services/electron.service'
|
||||
import { AppService } from '../services/app.service'
|
||||
import { HostAppService, Platform } from '../services/hostApp.service'
|
||||
|
||||
const COLORS = [
|
||||
{ name: 'No color', value: null },
|
||||
{ name: 'Blue', value: '#0275d8' },
|
||||
{ name: 'Green', value: '#5cb85c' },
|
||||
{ name: 'Orange', value: '#f0ad4e' },
|
||||
{ name: 'Purple', value: '#613d7c' },
|
||||
{ name: 'Red', value: '#d9534f' },
|
||||
{ name: 'Yellow', value: '#ffd500' },
|
||||
]
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'tab-header',
|
||||
template: require('./tabHeader.component.pug'),
|
||||
@ -30,16 +23,24 @@ export class TabHeaderComponent {
|
||||
@Input() progress: number
|
||||
@ViewChild('handle') handle: ElementRef
|
||||
|
||||
private completionNotificationEnabled = false
|
||||
|
||||
constructor (
|
||||
public app: AppService,
|
||||
private electron: ElectronService,
|
||||
private zone: NgZone,
|
||||
private hostApp: HostAppService,
|
||||
private ngbModal: NgbModal,
|
||||
private hotkeys: HotkeysService,
|
||||
private parentDraggable: SortableComponent,
|
||||
) { }
|
||||
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
|
||||
) {
|
||||
this.hotkeys.matchedHotkey.subscribe((hotkey) => {
|
||||
if (this.app.activeTab === this.tab) {
|
||||
if (hotkey === 'rename-tab') {
|
||||
this.showRenameTabModal()
|
||||
}
|
||||
}
|
||||
})
|
||||
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
if (this.hostApp.platform === Platform.macOS) {
|
||||
@ -50,8 +51,8 @@ export class TabHeaderComponent {
|
||||
})
|
||||
}
|
||||
|
||||
@HostListener('dblclick') onDoubleClick (): void {
|
||||
let modal = this.ngbModal.open(RenameTabModalComponent)
|
||||
showRenameTabModal (): void {
|
||||
const modal = this.ngbModal.open(RenameTabModalComponent)
|
||||
modal.componentInstance.value = this.tab.customTitle || this.tab.title
|
||||
modal.result.then(result => {
|
||||
this.tab.setTitle(result)
|
||||
@ -59,6 +60,19 @@ export class TabHeaderComponent {
|
||||
}).catch(() => null)
|
||||
}
|
||||
|
||||
async buildContextMenu (): Promise<Electron.MenuItemConstructorOptions[]> {
|
||||
let items: Electron.MenuItemConstructorOptions[] = []
|
||||
for (const section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this.tab, this)))) {
|
||||
items.push({ type: 'separator' })
|
||||
items = items.concat(section)
|
||||
}
|
||||
return items.slice(1)
|
||||
}
|
||||
|
||||
@HostListener('dblclick') onDoubleClick (): void {
|
||||
this.showRenameTabModal()
|
||||
}
|
||||
|
||||
@HostListener('auxclick', ['$event']) async onAuxClick ($event: MouseEvent) {
|
||||
if ($event.which === 2) {
|
||||
this.app.closeTab(this.tab, true)
|
||||
@ -66,90 +80,11 @@ export class TabHeaderComponent {
|
||||
if ($event.which === 3) {
|
||||
event.preventDefault()
|
||||
|
||||
let contextMenu = this.electron.remote.Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Close',
|
||||
click: () => this.zone.run(() => {
|
||||
this.app.closeTab(this.tab, true)
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Close other tabs',
|
||||
click: () => this.zone.run(() => {
|
||||
for (let tab of this.app.tabs.filter(x => x !== this.tab)) {
|
||||
this.app.closeTab(tab, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Close tabs to the right',
|
||||
click: () => this.zone.run(() => {
|
||||
for (let tab of this.app.tabs.slice(this.app.tabs.indexOf(this.tab) + 1)) {
|
||||
this.app.closeTab(tab, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Close tabs to the left',
|
||||
click: () => this.zone.run(() => {
|
||||
for (let tab of this.app.tabs.slice(0, this.app.tabs.indexOf(this.tab))) {
|
||||
this.app.closeTab(tab, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Color',
|
||||
sublabel: COLORS.find(x => x.value === this.tab.color).name,
|
||||
submenu: COLORS.map(color => ({
|
||||
label: color.name,
|
||||
type: 'radio',
|
||||
checked: this.tab.color === color.value,
|
||||
click: () => this.zone.run(() => {
|
||||
this.tab.color = color.value
|
||||
}),
|
||||
})),
|
||||
}
|
||||
])
|
||||
|
||||
let process = await this.tab.getCurrentProcess()
|
||||
if (process) {
|
||||
contextMenu.append(new this.electron.MenuItem({
|
||||
id: 'sep',
|
||||
type: 'separator',
|
||||
}))
|
||||
contextMenu.append(new this.electron.MenuItem({
|
||||
id: 'process-name',
|
||||
enabled: false,
|
||||
label: 'Current process: ' + process.name,
|
||||
}))
|
||||
contextMenu.append(new this.electron.MenuItem({
|
||||
id: 'completion',
|
||||
label: 'Notify when done',
|
||||
type: 'checkbox',
|
||||
checked: this.completionNotificationEnabled,
|
||||
click: () => this.zone.run(() => {
|
||||
this.completionNotificationEnabled = !this.completionNotificationEnabled
|
||||
|
||||
if (this.completionNotificationEnabled) {
|
||||
this.app.observeTabCompletion(this.tab).subscribe(() => {
|
||||
new Notification('Process completed', {
|
||||
body: process.name,
|
||||
}).addEventListener('click', () => {
|
||||
this.app.selectTab(this.tab)
|
||||
})
|
||||
this.completionNotificationEnabled = false
|
||||
})
|
||||
} else {
|
||||
this.app.stopObservingTabCompletion(this.tab)
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
const contextMenu = this.electron.remote.Menu.buildFromTemplate(await this.buildContextMenu())
|
||||
|
||||
contextMenu.popup({
|
||||
x: $event.pageX,
|
||||
y: $event.pageY,
|
||||
async: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'title-bar',
|
||||
template: require('./titleBar.component.pug'),
|
||||
styles: [require('./titleBar.component.scss')],
|
||||
})
|
||||
export class TitleBarComponent { }
|
||||
export class TitleBarComponent { } // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||
|
@ -2,6 +2,7 @@ import { Component } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { CheckboxComponent } from './checkbox.component'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'toggle',
|
||||
template: `
|
||||
@ -16,7 +17,7 @@ import { CheckboxComponent } from './checkbox.component'
|
||||
styles: [require('./toggle.component.scss')],
|
||||
providers: [
|
||||
{ provide: NG_VALUE_ACCESSOR, useExisting: ToggleComponent, multi: true },
|
||||
]
|
||||
],
|
||||
})
|
||||
export class ToggleComponent extends CheckboxComponent {
|
||||
}
|
||||
|
19
terminus-core/src/components/welcomeTab.component.pug
Normal file
@ -0,0 +1,19 @@
|
||||
.mb-4
|
||||
.terminus-logo
|
||||
h1.terminus-title Terminus
|
||||
sup α
|
||||
|
||||
.container
|
||||
.text-center.mb-5 Thank you for downloading Terminus!
|
||||
|
||||
.form-line
|
||||
.header
|
||||
.title Enable analytics
|
||||
.description Help us track the number of Terminus installs across the world!
|
||||
toggle(
|
||||
[(ngModel)]='config.store.enableAnalytics',
|
||||
(ngModelChange)='config.save(); config.requestRestart()',
|
||||
)
|
||||
|
||||
.text-center.mt-5
|
||||
button.btn.btn-primary((click)='closeAndDisable()') Close and never show again
|
6
terminus-core/src/components/welcomeTab.component.scss
Normal file
@ -0,0 +1,6 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
flex: 0 1 500px;
|
||||
}
|
26
terminus-core/src/components/welcomeTab.component.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { BaseTabComponent } from './baseTab.component'
|
||||
import { ConfigService } from '../services/config.service'
|
||||
import { AppService } from '../services/app.service'
|
||||
|
||||
/** @hidden */
|
||||
@Component({
|
||||
selector: 'welcome-page',
|
||||
template: require('./welcomeTab.component.pug'),
|
||||
styles: [require('./welcomeTab.component.scss')],
|
||||
})
|
||||
export class WelcomeTabComponent extends BaseTabComponent {
|
||||
constructor (
|
||||
private app: AppService,
|
||||
public config: ConfigService,
|
||||
) {
|
||||
super()
|
||||
this.setTitle('Welcome')
|
||||
}
|
||||
|
||||
closeAndDisable () {
|
||||
this.config.store.enableWelcomeTab = false
|
||||
this.config.save()
|
||||
this.app.closeTab(this)
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ button.btn.btn-secondary.btn-maximize(
|
||||
svg(version='1.1', width='10', height='10')
|
||||
path(d='M 0,0 0,10 10,10 10,0 Z M 1,1 9,1 9,9 1,9 Z')
|
||||
button.btn.btn-secondary.btn-close(
|
||||
(click)='app.closeWindow()'
|
||||
(click)='closeWindow()'
|
||||
)
|
||||
svg(version='1.1', width='10', height='10')
|
||||
path(d='M 0,0 0,0.7 4.3,5 0,9.3 0,10 0.7,10 5,5.7 9.3,10 10,10 10,9.3 5.7,5 10,0.7 10,0 9.3,0 5,4.3 0.7,0 Z')
|
||||
|