mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-20 02:18:01 +00:00
wip
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
appearance: { }
|
||||
hotkeys: { }
|
||||
terminal: { }
|
@@ -1,48 +0,0 @@
|
||||
appearance:
|
||||
dock: 'off'
|
||||
dockScreen: 'current'
|
||||
dockFill: 50
|
||||
tabsOnTop: true
|
||||
hotkeys:
|
||||
close-tab:
|
||||
- 'Ctrl-Shift-W'
|
||||
- ['Ctrl-A', 'K']
|
||||
toggle-last-tab:
|
||||
- ['Ctrl-A', 'A']
|
||||
- ['Ctrl-A', 'Ctrl-A']
|
||||
next-tab:
|
||||
- 'Ctrl-Shift-ArrowRight'
|
||||
- ['Ctrl-A', 'N']
|
||||
previous-tab:
|
||||
- 'Ctrl-Shift-ArrowLeft'
|
||||
- ['Ctrl-A', 'P']
|
||||
tab-1:
|
||||
- 'Alt-1'
|
||||
- ['Ctrl-A', '1']
|
||||
tab-2:
|
||||
- 'Alt-2'
|
||||
- ['Ctrl-A', '2']
|
||||
tab-3:
|
||||
- 'Alt-3'
|
||||
- ['Ctrl-A', '3']
|
||||
tab-4:
|
||||
- 'Alt-4'
|
||||
- ['Ctrl-A', '4']
|
||||
tab-5:
|
||||
- 'Alt-5'
|
||||
- ['Ctrl-A', '5']
|
||||
tab-6:
|
||||
- 'Alt-6'
|
||||
- ['Ctrl-A', '6']
|
||||
tab-7:
|
||||
- 'Alt-7'
|
||||
- ['Ctrl-A', '7']
|
||||
tab-8:
|
||||
- 'Alt-8'
|
||||
- ['Ctrl-A', '8']
|
||||
tab-9:
|
||||
- 'Alt-9'
|
||||
- ['Ctrl-A', '9']
|
||||
tab-10:
|
||||
- 'Alt-0'
|
||||
- ['Ctrl-A', '0']
|
@@ -1,4 +0,0 @@
|
||||
export abstract class ConfigProvider {
|
||||
configStructure: any = {}
|
||||
defaultConfigValues: any = {}
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
export abstract class DefaultTabProvider {
|
||||
abstract async openNewTab (): Promise<void>
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
export interface IHotkeyDescription {
|
||||
id: string,
|
||||
name: string,
|
||||
}
|
||||
|
||||
export abstract class HotkeyProvider {
|
||||
hotkeys: IHotkeyDescription[] = []
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
export { BaseTabComponent } from '../components/baseTab'
|
||||
export { TabRecoveryProvider } from './tabRecovery'
|
||||
export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
|
||||
export { ConfigProvider } from './configProvider'
|
||||
export { HotkeyProvider, IHotkeyDescription } from './hotkeyProvider'
|
||||
export { DefaultTabProvider } from './defaultTabProvider'
|
||||
|
||||
export { AppService } from 'services/app'
|
||||
export { ConfigService } from 'services/config'
|
||||
export { PluginsService } from 'services/plugins'
|
||||
export { ElectronService } from 'services/electron'
|
||||
export { HotkeysService } from 'services/hotkeys'
|
@@ -1,3 +0,0 @@
|
||||
export abstract class TabRecoveryProvider {
|
||||
abstract async recover (recoveryToken: any): Promise<void>
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
export interface IToolbarButton {
|
||||
icon: string
|
||||
title: string
|
||||
weight?: number
|
||||
click: () => void
|
||||
}
|
||||
|
||||
export abstract class ToolbarButtonProvider {
|
||||
abstract provide (): IToolbarButton[]
|
||||
}
|
15
app/src/app.d.ts
vendored
15
app/src/app.d.ts
vendored
@@ -1,15 +0,0 @@
|
||||
declare var nodeRequire: any
|
||||
interface IPromise {}
|
||||
|
||||
declare interface Window {
|
||||
require: any
|
||||
process: any
|
||||
__dirname: any
|
||||
__platform: any
|
||||
}
|
||||
|
||||
declare var window: Window
|
||||
|
||||
declare interface Console {
|
||||
timeStamp(...args: any[])
|
||||
}
|
@@ -1,75 +1,21 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { HttpModule } from '@angular/http'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ToasterModule } from 'angular2-toaster'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
import { AppService } from 'services/app'
|
||||
import { ConfigService } from 'services/config'
|
||||
import { ElectronService } from 'services/electron'
|
||||
import { HostAppService } from 'services/hostApp'
|
||||
import { LogService } from 'services/log'
|
||||
import { HotkeysService, AppHotkeyProvider } from 'services/hotkeys'
|
||||
import { ModalService } from 'services/modal'
|
||||
import { NotifyService } from 'services/notify'
|
||||
import { PluginsService } from 'services/plugins'
|
||||
import { QuitterService } from 'services/quitter'
|
||||
import { DockingService } from 'services/docking'
|
||||
import { TabRecoveryService } from 'services/tabRecovery'
|
||||
|
||||
import { AppRootComponent } from 'components/appRoot'
|
||||
import { CheckboxComponent } from 'components/checkbox'
|
||||
import { TabBodyComponent } from 'components/tabBody'
|
||||
import { TabHeaderComponent } from 'components/tabHeader'
|
||||
import { TitleBarComponent } from 'components/titleBar'
|
||||
|
||||
import { HotkeyProvider } from 'api/hotkeyProvider'
|
||||
|
||||
|
||||
let plugins = [
|
||||
require('./settings').default,
|
||||
require('./terminal').default,
|
||||
require('./link-highlighter').default,
|
||||
(<any>global).require('../terminus-settings').default,
|
||||
(<any>global).require('../terminus-terminal').default,
|
||||
(<any>global).require('../terminus-clickable-links').default,
|
||||
(<any>global).require('../terminus-community-color-schemes').default,
|
||||
]
|
||||
|
||||
const core = (<any>global).require('../terminus-core').default,
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
HttpModule,
|
||||
FormsModule,
|
||||
ToasterModule,
|
||||
core.forRoot(),
|
||||
...plugins,
|
||||
NgbModule.forRoot(),
|
||||
].concat(plugins),
|
||||
providers: [
|
||||
AppService,
|
||||
ConfigService,
|
||||
DockingService,
|
||||
ElectronService,
|
||||
HostAppService,
|
||||
HotkeysService,
|
||||
LogService,
|
||||
ModalService,
|
||||
NotifyService,
|
||||
PluginsService,
|
||||
TabRecoveryService,
|
||||
QuitterService,
|
||||
{ provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true },
|
||||
],
|
||||
entryComponents: [
|
||||
],
|
||||
declarations: [
|
||||
AppRootComponent,
|
||||
CheckboxComponent,
|
||||
TabBodyComponent,
|
||||
TabHeaderComponent,
|
||||
TitleBarComponent,
|
||||
],
|
||||
bootstrap: [
|
||||
AppRootComponent,
|
||||
]
|
||||
//bootstrap: [AppRootComponent]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
export class RootModule { }
|
||||
|
@@ -1,90 +0,0 @@
|
||||
@import "~variables.less";
|
||||
|
||||
.button-states() {
|
||||
transition: 0.125s all;
|
||||
border: none;
|
||||
|
||||
&:hover:not(.active) {
|
||||
background: rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
&:active:not(.active),
|
||||
&.active {
|
||||
background: rgba(0, 0, 0, .3);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@title-bg: #131d27;
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
width: ~"calc(100vw - 2px)";
|
||||
height: ~"calc(100vh - 2px)";
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
-webkit-user-select: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
cursor: default;
|
||||
background: @body-bg;
|
||||
}
|
||||
|
||||
@tabs-height: 40px;
|
||||
@tab-border-radius: 4px;
|
||||
|
||||
|
||||
.content {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
background: @title-bg;
|
||||
|
||||
&.tabs-on-top {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex: none;
|
||||
height: @tabs-height;
|
||||
background: @body-bg;
|
||||
display: flex;
|
||||
|
||||
&>button {
|
||||
line-height: @tabs-height - 2px;
|
||||
cursor: pointer;
|
||||
|
||||
padding: 0 15px;
|
||||
flex: 0 0 auto;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: 0.25s all;
|
||||
font-size: 12px;
|
||||
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
color: #aaa;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
|
||||
&:not(:hover):not(:active) {
|
||||
background: @title-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&>.tabs-container {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
hotkey-hint {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
max-width: 300px;
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
title-bar(*ngIf='!config.full().appearance.useNativeFrame && config.store.appearance.dock == "off"')
|
||||
|
||||
.content(
|
||||
[class.tabs-on-top]='config.full().appearance.tabsOnTop'
|
||||
)
|
||||
.tabs
|
||||
button.btn.btn-secondary(
|
||||
*ngFor='let button of getLeftToolbarButtons()',
|
||||
[title]='button.title',
|
||||
(click)='button.click()',
|
||||
)
|
||||
i.fa([class]='"fa fa-" + button.icon')
|
||||
|
||||
.tabs-container
|
||||
tab-header(
|
||||
*ngFor='let tab of app.tabs; let idx = index',
|
||||
[class.pre-selected]='idx == app.tabs.indexOf(app.activeTab) - 1',
|
||||
[class.post-selected]='idx == app.tabs.indexOf(app.activeTab) + 1',
|
||||
[index]='idx',
|
||||
[tab]='tab',
|
||||
[active]='tab == app.activeTab',
|
||||
[hasActivity]='tab.hasActivity',
|
||||
@animateTab,
|
||||
(click)='app.selectTab(tab)',
|
||||
(closeClicked)='app.closeTab(tab)',
|
||||
)
|
||||
|
||||
button.btn.btn-secondary(
|
||||
*ngFor='let button of getRightToolbarButtons()',
|
||||
[title]='button.title',
|
||||
(click)='button.click()',
|
||||
)
|
||||
i.fa([class]='"fa fa-" + button.icon')
|
||||
|
||||
.tabs-content
|
||||
tab-body(
|
||||
*ngFor='let tab of app.tabs; trackBy: tab?.id',
|
||||
[active]='tab == app.activeTab',
|
||||
[tab]='tab',
|
||||
[class.scrollable]='tab.scrollable',
|
||||
)
|
||||
|
||||
toaster-container([toasterconfig]="toasterconfig")
|
||||
ng-template(ngbModalContainer)
|
||||
|
||||
div.window-resizer.window-resizer-tl
|
@@ -1,155 +0,0 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { trigger, style, animate, transition, state } from '@angular/animations'
|
||||
import { ToasterConfig } from 'angular2-toaster'
|
||||
|
||||
import { ElectronService } from 'services/electron'
|
||||
import { HostAppService } from 'services/hostApp'
|
||||
import { HotkeysService } from 'services/hotkeys'
|
||||
import { Logger, LogService } from 'services/log'
|
||||
import { QuitterService } from 'services/quitter'
|
||||
import { ConfigService } from 'services/config'
|
||||
import { DockingService } from 'services/docking'
|
||||
import { TabRecoveryService } from 'services/tabRecovery'
|
||||
|
||||
import { AppService, IToolbarButton, ToolbarButtonProvider } from 'api'
|
||||
|
||||
import 'angular2-toaster/toaster.css'
|
||||
import 'overrides.scss'
|
||||
import 'global.less'
|
||||
import 'theme.scss'
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: require('./appRoot.pug'),
|
||||
styles: [require('./appRoot.less')],
|
||||
animations: [
|
||||
trigger('animateTab', [
|
||||
state('in', style({
|
||||
'flex-grow': '1000',
|
||||
})),
|
||||
transition(':enter', [
|
||||
style({
|
||||
'flex-grow': '1',
|
||||
}),
|
||||
animate('250ms ease-in-out')
|
||||
]),
|
||||
transition(':leave', [
|
||||
animate('250ms ease-in-out', style({
|
||||
'flex-grow': '1',
|
||||
}))
|
||||
])
|
||||
])
|
||||
]
|
||||
})
|
||||
export class AppRootComponent {
|
||||
toasterConfig: ToasterConfig
|
||||
logger: Logger
|
||||
|
||||
constructor(
|
||||
private docking: DockingService,
|
||||
private electron: ElectronService,
|
||||
private tabRecovery: TabRecoveryService,
|
||||
public hostApp: HostAppService,
|
||||
public hotkeys: HotkeysService,
|
||||
public config: ConfigService,
|
||||
public app: AppService,
|
||||
@Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
|
||||
log: LogService,
|
||||
_quitter: QuitterService,
|
||||
) {
|
||||
console.timeStamp('AppComponent ctor')
|
||||
|
||||
this.logger = log.create('main')
|
||||
this.logger.info('v', electron.app.getVersion())
|
||||
|
||||
this.toasterConfig = new ToasterConfig({
|
||||
mouseoverTimerStop: true,
|
||||
preventDuplicates: true,
|
||||
timeout: 4000,
|
||||
})
|
||||
|
||||
this.hotkeys.matchedHotkey.subscribe((hotkey) => {
|
||||
if (hotkey.startsWith('tab-')) {
|
||||
let index = parseInt(hotkey.split('-')[1])
|
||||
if (index <= this.app.tabs.length) {
|
||||
this.app.selectTab(this.app.tabs[index - 1])
|
||||
}
|
||||
}
|
||||
if (this.app.activeTab) {
|
||||
if (hotkey == 'close-tab') {
|
||||
this.app.closeTab(this.app.activeTab)
|
||||
}
|
||||
if (hotkey == 'toggle-last-tab') {
|
||||
this.app.toggleLastTab()
|
||||
}
|
||||
if (hotkey == 'next-tab') {
|
||||
this.app.nextTab()
|
||||
}
|
||||
if (hotkey == 'previous-tab') {
|
||||
this.app.previousTab()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.docking.dock()
|
||||
this.hostApp.shown.subscribe(() => {
|
||||
this.docking.dock()
|
||||
})
|
||||
|
||||
this.hotkeys.registerHotkeys()
|
||||
this.hostApp.secondInstance.subscribe(() => {
|
||||
this.onGlobalHotkey()
|
||||
})
|
||||
this.hotkeys.globalHotkey.subscribe(() => {
|
||||
this.onGlobalHotkey()
|
||||
})
|
||||
}
|
||||
|
||||
onGlobalHotkey () {
|
||||
if (this.electron.app.window.isFocused()) {
|
||||
// focused
|
||||
this.electron.app.window.hide()
|
||||
} else {
|
||||
if (!this.electron.app.window.isVisible()) {
|
||||
// unfocused, invisible
|
||||
this.electron.app.window.show()
|
||||
} else {
|
||||
if (this.config.full().appearance.dock == 'off') {
|
||||
// not docked, visible
|
||||
setTimeout(() => {
|
||||
this.electron.app.window.focus()
|
||||
})
|
||||
} else {
|
||||
// docked, visible
|
||||
this.electron.app.window.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
this.docking.dock()
|
||||
}
|
||||
|
||||
getLeftToolbarButtons (): IToolbarButton[] { return this.getToolbarButtons(false) }
|
||||
|
||||
getRightToolbarButtons (): IToolbarButton[] { return this.getToolbarButtons(true) }
|
||||
|
||||
async ngOnInit () {
|
||||
await this.tabRecovery.recoverTabs()
|
||||
this.tabRecovery.saveTabs(this.app.tabs)
|
||||
|
||||
if (this.app.tabs.length == 0) {
|
||||
this.app.openDefaultTab()
|
||||
}
|
||||
}
|
||||
|
||||
private getToolbarButtons (aboveZero: boolean): IToolbarButton[] {
|
||||
let buttons: IToolbarButton[] = []
|
||||
this.toolbarButtonProviders.forEach((provider) => {
|
||||
buttons = buttons.concat(provider.provide())
|
||||
})
|
||||
return buttons
|
||||
.filter((button) => (button.weight > 0) === aboveZero)
|
||||
.sort((a: IToolbarButton, b: IToolbarButton) => (a.weight || 0) - (b.weight || 0))
|
||||
}
|
||||
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { EventEmitter, ViewRef } from '@angular/core'
|
||||
|
||||
|
||||
export abstract class BaseTabComponent {
|
||||
id: number
|
||||
title$ = new BehaviorSubject<string>(null)
|
||||
scrollable: boolean
|
||||
hasActivity = false
|
||||
focused = new EventEmitter<any>()
|
||||
blurred = new EventEmitter<any>()
|
||||
hostView: ViewRef
|
||||
private static lastTabID = 0
|
||||
|
||||
constructor () {
|
||||
this.id = BaseTabComponent.lastTabID++
|
||||
}
|
||||
|
||||
displayActivity (): void {
|
||||
this.hasActivity = true
|
||||
}
|
||||
|
||||
getRecoveryToken (): any {
|
||||
return null
|
||||
}
|
||||
|
||||
destroy (): void {
|
||||
}
|
||||
}
|
@@ -1,53 +0,0 @@
|
||||
:host {
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
background: rgba(255,255,255,.05);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(255,255,255,.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
flex: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 1px;
|
||||
transition: 0.25s opacity;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
i.on {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
i.on, &.active i.off {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
i.off, &.active i.on {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: auto;
|
||||
}
|
||||
}
|
@@ -1,4 +0,0 @@
|
||||
.icon((click)='click()', tabindex='0', [class.active]='model', (keyup.space)='click()')
|
||||
i.fa.fa-square-o.off
|
||||
i.fa.fa-check-square.on
|
||||
.text((click)='click()') {{text}}
|
@@ -1,25 +0,0 @@
|
||||
import { NgZone, Component, Input, Output, EventEmitter } from '@angular/core'
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'checkbox',
|
||||
template: require('./checkbox.pug'),
|
||||
styles: [require('./checkbox.less')]
|
||||
})
|
||||
export class CheckboxComponent {
|
||||
public click() {
|
||||
NgZone.assertInAngularZone()
|
||||
if (this.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
this.model = !this.model
|
||||
this.modelChange.emit(this.model)
|
||||
}
|
||||
|
||||
@Input() model: boolean
|
||||
@Output() modelChange = new EventEmitter()
|
||||
@Input() disabled: boolean
|
||||
|
||||
@Input() text: string
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
:host {
|
||||
display: none;
|
||||
flex: auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&.scrollable {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&.active {
|
||||
display: flex;
|
||||
|
||||
>* {
|
||||
flex: auto;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
import { Component, Input, ViewChild, HostBinding, ViewContainerRef } from '@angular/core'
|
||||
import { BaseTabComponent } from 'components/baseTab'
|
||||
|
||||
@Component({
|
||||
selector: 'tab-body',
|
||||
template: '<ng-template #placeholder></ng-template>',
|
||||
styles: [require('./tabBody.scss')],
|
||||
})
|
||||
export class TabBodyComponent {
|
||||
@Input() @HostBinding('class.active') active: boolean
|
||||
@Input() tab: BaseTabComponent
|
||||
@ViewChild('placeholder', {read: ViewContainerRef}) placeholder: ViewContainerRef
|
||||
|
||||
ngAfterViewInit () {
|
||||
setImmediate(() => {
|
||||
this.placeholder.insert(this.tab.hostView)
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,4 +0,0 @@
|
||||
.content-wrapper
|
||||
.index {{index + 1}}
|
||||
.name {{(tab.title$ || "Terminal") | async}}
|
||||
button((click)='closeClicked.emit()') ×
|
@@ -1,80 +0,0 @@
|
||||
@import '~variables.scss';
|
||||
|
||||
:host {
|
||||
line-height: $tabs-height - 2px;
|
||||
cursor: pointer;
|
||||
|
||||
flex: auto;
|
||||
flex-basis: 0;
|
||||
flex-grow: 1000;
|
||||
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
min-width: 0;
|
||||
transition: 0.25s all;
|
||||
//.button-states();
|
||||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: auto;
|
||||
min-width: 0;
|
||||
transition: 0.25s all;
|
||||
border-top: 1px solid transparent;
|
||||
|
||||
.index {
|
||||
flex: none;
|
||||
font-weight: bold;
|
||||
align-self: center;
|
||||
|
||||
margin-left: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
transition: 0.25s all;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: auto;
|
||||
margin: 0 1px 0 10px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: none;
|
||||
|
||||
background: transparent;
|
||||
|
||||
display: block;
|
||||
opacity: 0;
|
||||
|
||||
$button-size: $tabs-height * 0.6;
|
||||
width: $button-size;
|
||||
height: $button-size;
|
||||
border-radius: $button-size / 2;
|
||||
line-height: $button-size * 0.8;
|
||||
margin-top: ($tabs-height - $button-size) * 0.4;
|
||||
margin-right: 10px;
|
||||
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
|
||||
//.button-states();
|
||||
}
|
||||
|
||||
&:hover button {
|
||||
transition: 0.25s opacity;
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
//border-bottom: 2px solid transparent;
|
||||
transition: 0.25s all;
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
import { Component, Input, Output, EventEmitter, HostBinding } from '@angular/core'
|
||||
import { BaseTabComponent } from 'components/baseTab'
|
||||
|
||||
import './tabHeader.scss'
|
||||
|
||||
@Component({
|
||||
selector: 'tab-header',
|
||||
template: require('./tabHeader.pug'),
|
||||
styles: [require('./tabHeader.scss')],
|
||||
})
|
||||
export class TabHeaderComponent {
|
||||
@Input() index: number
|
||||
@Input() @HostBinding('class.active') active: boolean
|
||||
@Input() @HostBinding('class.has-activity') hasActivity: boolean
|
||||
@Input() tab: BaseTabComponent
|
||||
@Output() closeClicked = new EventEmitter()
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
.title((dblclick)='hostApp.toggleMaximize()') Term
|
||||
button.btn.btn-secondary.btn-minimize((click)='hostApp.minimize()')
|
||||
i.fa.fa-window-minimize
|
||||
button.btn.btn-secondary.btn-maximize((click)='hostApp.toggleMaximize()')
|
||||
i.fa.fa-window-maximize
|
||||
button.btn.btn-secondary.btn-close((click)='hostApp.quit()')
|
||||
i.fa.fa-close
|
@@ -1,35 +0,0 @@
|
||||
@import '~variables.scss';
|
||||
|
||||
$titlebar-height: 30px;
|
||||
|
||||
:host {
|
||||
flex: 0 0 $titlebar-height;
|
||||
display: flex;
|
||||
|
||||
.title {
|
||||
flex: auto;
|
||||
padding-left: 15px;
|
||||
line-height: $titlebar-height;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
font-size: 8px;
|
||||
width: 40px;
|
||||
padding: 0;
|
||||
line-height: $titlebar-height;
|
||||
text-align: center;
|
||||
|
||||
&:not(:hover):not(:active) {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { HostAppService } from 'services/hostApp'
|
||||
|
||||
@Component({
|
||||
selector: 'title-bar',
|
||||
template: require('./titleBar.pug'),
|
||||
styles: [require('./titleBar.scss')],
|
||||
})
|
||||
export class TitleBarComponent {
|
||||
constructor (public hostApp: HostAppService) {
|
||||
}
|
||||
}
|
@@ -8,18 +8,18 @@ import 'jquery'
|
||||
// Always land on the start view
|
||||
location.hash = ''
|
||||
|
||||
import { AppModule } from 'app.module'
|
||||
import { RootModule } from 'app.module'
|
||||
import { enableProdMode } from '@angular/core'
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
|
||||
|
||||
if (nodeRequire('electron-is-dev')) {
|
||||
if ((<any>global).require('electron-is-dev')) {
|
||||
console.warn('Running in debug mode')
|
||||
} else {
|
||||
enableProdMode()
|
||||
}
|
||||
|
||||
console.timeStamp('angular bootstrap started')
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
platformBrowserDynamic().bootstrapModule(RootModule);
|
||||
|
||||
|
||||
(<any>process).emitWarning = function () { console.log(arguments) }
|
||||
|
@@ -1,101 +0,0 @@
|
||||
@import "~variables.less";
|
||||
@import "~mixins.less";
|
||||
|
||||
|
||||
html.platform-win32 {
|
||||
body.focused {
|
||||
//border: 1px solid #9c9c00 !important;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
border: 1px solid #131313;
|
||||
transition: 0.5s border;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.no-drag, a, button, checkbox, .form-control, #toast-container {
|
||||
-webkit-app-region: no-drag;
|
||||
outline: 0 !important;
|
||||
|
||||
* {
|
||||
outline: 0 !important;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
}
|
||||
|
||||
.window-resizer {
|
||||
-webkit-app-region: no-drag;
|
||||
position: fixed;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.window-resizer-tl {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.no-wrap {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.word-wrap {
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
|
||||
#toast-container.toast-top-full-width {
|
||||
width: 100%;
|
||||
top: 50px;
|
||||
|
||||
> div {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
box-shadow: 0 0 2px rgba(0,0,0,.75);
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.btn {
|
||||
i + * {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
margin: none;
|
||||
|
||||
> .btn {
|
||||
float: right;
|
||||
margin: -7px -11px 0 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0,0,0,.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ngb-typeahead-window {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
>button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
-webkit-appearance: none;
|
||||
//border-bottom: 1px solid @dark-border;
|
||||
}
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
export abstract class LinkHandler {
|
||||
regex: string
|
||||
convert (uri: string): string { return uri }
|
||||
verify (_uri: string): boolean { return true }
|
||||
abstract handle (uri: string): void
|
||||
}
|
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
This plugin is based on Hyperterm Hyperlinks:
|
||||
https://github.com/zeit/hyperlinks/blob/master/index.js
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { LinkHandler } from './api'
|
||||
import { TerminalDecorator, TerminalTabComponent } from '../terminal/api'
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class LinkHighlighterDecorator extends TerminalDecorator {
|
||||
constructor (@Inject(LinkHandler) private handlers: LinkHandler[]) {
|
||||
super()
|
||||
}
|
||||
|
||||
attach (terminal: TerminalTabComponent): void {
|
||||
terminal.contentUpdated$
|
||||
.debounceTime(1000)
|
||||
.subscribe(() => {
|
||||
this.insertLinks(terminal.hterm.screen_)
|
||||
})
|
||||
}
|
||||
|
||||
insertLinks (screen) {
|
||||
if ('#text' === screen.cursorNode_.nodeName) {
|
||||
// replace text node to element
|
||||
const cursorNode = document.createElement('span');
|
||||
cursorNode.textContent = screen.cursorNode_.textContent;
|
||||
screen.cursorRowNode_.replaceChild(cursorNode, screen.cursorNode_);
|
||||
screen.cursorNode_ = cursorNode;
|
||||
}
|
||||
|
||||
const traverse = (parentNode: Node) => {
|
||||
Array.from(parentNode.childNodes).forEach((node) => {
|
||||
if (node.nodeName == '#text') {
|
||||
parentNode.replaceChild(this.urlizeNode(node), node)
|
||||
} else if (node.nodeName != 'A') {
|
||||
traverse(node)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
screen.rowsArray.forEach((x) => traverse(x))
|
||||
}
|
||||
|
||||
urlizeNode (node) {
|
||||
let matches = []
|
||||
this.handlers.forEach((handler) => {
|
||||
let regex = new RegExp(handler.regex, 'gi')
|
||||
let match
|
||||
while (match = regex.exec(node.textContent)) {
|
||||
let uri = handler.convert(match[0])
|
||||
if (!handler.verify(uri)) {
|
||||
continue;
|
||||
}
|
||||
matches.push({
|
||||
start: regex.lastIndex - match[0].length,
|
||||
end: regex.lastIndex,
|
||||
text: match[0],
|
||||
uri,
|
||||
handler
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (matches.length == 0) {
|
||||
return node
|
||||
}
|
||||
|
||||
matches.sort((a, b) => a.start < b.start ? -1 : 1)
|
||||
|
||||
let span = document.createElement('span')
|
||||
let position = 0
|
||||
matches.forEach((match) => {
|
||||
if (match.start < position) {
|
||||
return
|
||||
}
|
||||
if (match.start > position) {
|
||||
span.appendChild(document.createTextNode(node.textContent.slice(position, match.start)))
|
||||
}
|
||||
|
||||
let a = document.createElement('a')
|
||||
a.textContent = match.text
|
||||
a.addEventListener('click', () => {
|
||||
match.handler.handle(match.uri)
|
||||
})
|
||||
span.appendChild(a)
|
||||
|
||||
position = match.end
|
||||
})
|
||||
span.appendChild(document.createTextNode(node.textContent.slice(position)))
|
||||
return span
|
||||
}
|
||||
}
|
@@ -1,41 +0,0 @@
|
||||
import * as fs from 'fs'
|
||||
const untildify = require('untildify')
|
||||
|
||||
import { Injectable } from '@angular/core'
|
||||
import { LinkHandler } from './api'
|
||||
import { ElectronService } from 'api'
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class URLHandler extends LinkHandler {
|
||||
regex = 'http(s)?://[^\\s;\'"]+[^,;\\s]'
|
||||
|
||||
constructor (private electron: ElectronService) {
|
||||
super()
|
||||
}
|
||||
|
||||
handle (uri: string) {
|
||||
this.electron.shell.openExternal(uri)
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class FileHandler extends LinkHandler {
|
||||
regex = '[~/][^\\s.,;\'"]+'
|
||||
|
||||
constructor (private electron: ElectronService) {
|
||||
super()
|
||||
}
|
||||
|
||||
convert (uri: string): string {
|
||||
return untildify(uri)
|
||||
}
|
||||
|
||||
verify (uri: string) {
|
||||
return fs.existsSync(uri)
|
||||
}
|
||||
|
||||
handle (uri: string) {
|
||||
this.electron.shell.openExternal('file://' + uri)
|
||||
}
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
|
||||
import { LinkHandler } from './api'
|
||||
import { FileHandler, URLHandler } from './handlers'
|
||||
import { TerminalDecorator } from '../terminal/api'
|
||||
import { LinkHighlighterDecorator } from './decorator'
|
||||
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
{ provide: LinkHandler, useClass: FileHandler, multi: true },
|
||||
{ provide: LinkHandler, useClass: URLHandler, multi: true },
|
||||
{ provide: TerminalDecorator, useClass: LinkHighlighterDecorator, multi: true },
|
||||
],
|
||||
})
|
||||
class LinkHighlighterModule {
|
||||
}
|
||||
|
||||
|
||||
export default LinkHighlighterModule
|
@@ -1,24 +0,0 @@
|
||||
@import "~variables.less";
|
||||
|
||||
.button-states() {
|
||||
transition: 0.125s all;
|
||||
border: none;
|
||||
|
||||
&:hover:not(.active) {
|
||||
background: rgba(255, 255, 255, .033);
|
||||
}
|
||||
|
||||
&:active:not(.active),
|
||||
&.active {
|
||||
background: rgba(0, 0, 0, .1);
|
||||
}
|
||||
}
|
||||
|
||||
.list-group-item-style() {
|
||||
display: block;
|
||||
padding: 10px 15px;
|
||||
background: @component-bg;
|
||||
color: @text-color;
|
||||
text-align: left;
|
||||
.button-states();
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
.form-group label {
|
||||
margin-bottom: 2px;
|
||||
}
|
@@ -1,95 +0,0 @@
|
||||
import { Subject } from 'rxjs'
|
||||
import { Injectable, ComponentFactoryResolver, Injector, Optional } from '@angular/core'
|
||||
import { Logger, LogService } from 'services/log'
|
||||
import { DefaultTabProvider } from 'api/defaultTabProvider'
|
||||
import { BaseTabComponent } from 'components/baseTab'
|
||||
|
||||
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
tabs: BaseTabComponent[] = []
|
||||
activeTab: BaseTabComponent
|
||||
lastTabIndex = 0
|
||||
logger: Logger
|
||||
tabsChanged$ = new Subject()
|
||||
|
||||
constructor (
|
||||
private componentFactoryResolver: ComponentFactoryResolver,
|
||||
@Optional() private defaultTabProvider: DefaultTabProvider,
|
||||
private injector: Injector,
|
||||
log: LogService,
|
||||
) {
|
||||
this.logger = log.create('app')
|
||||
}
|
||||
|
||||
openNewTab (type: TabComponentType, inputs?: any): BaseTabComponent {
|
||||
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(type)
|
||||
let componentRef = componentFactory.create(this.injector)
|
||||
componentRef.instance.hostView = componentRef.hostView
|
||||
Object.assign(componentRef.instance, inputs || {})
|
||||
|
||||
this.tabs.push(componentRef.instance)
|
||||
this.selectTab(componentRef.instance)
|
||||
this.tabsChanged$.next()
|
||||
|
||||
return componentRef.instance
|
||||
}
|
||||
|
||||
openDefaultTab (): void {
|
||||
if (this.defaultTabProvider) {
|
||||
this.defaultTabProvider.openNewTab()
|
||||
}
|
||||
}
|
||||
|
||||
selectTab (tab: BaseTabComponent) {
|
||||
if (this.tabs.includes(this.activeTab)) {
|
||||
this.lastTabIndex = this.tabs.indexOf(this.activeTab)
|
||||
} else {
|
||||
this.lastTabIndex = null
|
||||
}
|
||||
if (this.activeTab) {
|
||||
this.activeTab.hasActivity = false
|
||||
this.activeTab.blurred.emit()
|
||||
}
|
||||
this.activeTab = tab
|
||||
if (this.activeTab) {
|
||||
this.activeTab.focused.emit()
|
||||
}
|
||||
}
|
||||
|
||||
toggleLastTab () {
|
||||
if (!this.lastTabIndex || this.lastTabIndex >= this.tabs.length) {
|
||||
this.lastTabIndex = 0
|
||||
}
|
||||
this.selectTab(this.tabs[this.lastTabIndex])
|
||||
}
|
||||
|
||||
nextTab () {
|
||||
let tabIndex = this.tabs.indexOf(this.activeTab)
|
||||
if (tabIndex < this.tabs.length - 1) {
|
||||
this.selectTab(this.tabs[tabIndex + 1])
|
||||
}
|
||||
}
|
||||
|
||||
previousTab () {
|
||||
let tabIndex = this.tabs.indexOf(this.activeTab)
|
||||
if (tabIndex > 0) {
|
||||
this.selectTab(this.tabs[tabIndex - 1])
|
||||
}
|
||||
}
|
||||
|
||||
closeTab (tab: BaseTabComponent) {
|
||||
tab.destroy()
|
||||
/* if (tab.session) {
|
||||
this.sessions.destroySession(tab.session)
|
||||
} */
|
||||
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
|
||||
this.tabs = this.tabs.filter((x) => x != tab)
|
||||
if (tab == this.activeTab) {
|
||||
this.selectTab(this.tabs[newIndex])
|
||||
}
|
||||
this.tabsChanged$.next()
|
||||
}
|
||||
}
|
@@ -1,54 +0,0 @@
|
||||
import * as yaml from 'js-yaml'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { EventEmitter, Injectable, Inject } from '@angular/core'
|
||||
import { ElectronService } from 'services/electron'
|
||||
import { ConfigProvider } from 'api/configProvider'
|
||||
|
||||
const configMerge = (a, b) => require('deepmerge')(a, b, { arrayMerge: (_d, s) => s })
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class ConfigService {
|
||||
store: any
|
||||
change = new EventEmitter()
|
||||
restartRequested: boolean
|
||||
private path: string
|
||||
private configStructure: any = require('../../defaultConfigStructure.yaml')
|
||||
private defaultConfigValues: any = require('../../defaultConfigValues.yaml')
|
||||
|
||||
constructor (
|
||||
electron: ElectronService,
|
||||
@Inject(ConfigProvider) configProviders: ConfigProvider[],
|
||||
) {
|
||||
this.path = path.join(electron.app.getPath('userData'), 'config.yaml')
|
||||
this.configStructure = configProviders.map(x => x.configStructure).reduce(configMerge, this.configStructure)
|
||||
this.defaultConfigValues = configProviders.map(x => x.defaultConfigValues).reduce(configMerge, this.defaultConfigValues)
|
||||
this.load()
|
||||
}
|
||||
|
||||
load (): void {
|
||||
if (fs.existsSync(this.path)) {
|
||||
this.store = configMerge(this.configStructure, yaml.safeLoad(fs.readFileSync(this.path, 'utf8')))
|
||||
} else {
|
||||
this.store = Object.assign({}, this.configStructure)
|
||||
}
|
||||
}
|
||||
|
||||
save (): void {
|
||||
fs.writeFileSync(this.path, yaml.safeDump(this.store), 'utf8')
|
||||
this.emitChange()
|
||||
}
|
||||
|
||||
full (): any {
|
||||
return configMerge(this.defaultConfigValues, this.store)
|
||||
}
|
||||
|
||||
emitChange (): void {
|
||||
this.change.emit()
|
||||
}
|
||||
|
||||
requestRestart (): void {
|
||||
this.restartRequested = true
|
||||
}
|
||||
}
|
@@ -1,74 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HostAppService } from 'services/hostApp'
|
||||
import { ConfigService } from 'services/config'
|
||||
import { ElectronService } from 'services/electron'
|
||||
|
||||
|
||||
export interface IScreen {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DockingService {
|
||||
constructor(
|
||||
private electron: ElectronService,
|
||||
private config: ConfigService,
|
||||
private hostApp: HostAppService,
|
||||
) {}
|
||||
|
||||
dock () {
|
||||
let display = this.electron.screen.getAllDisplays()
|
||||
.filter((x) => x.id == this.config.full().appearance.dockScreen)[0]
|
||||
if (!display) {
|
||||
display = this.getCurrentScreen()
|
||||
}
|
||||
|
||||
let dockSide = this.config.full().appearance.dock
|
||||
let newBounds: Electron.Rectangle = { x: 0, y: 0, width: 0, height: 0 }
|
||||
let fill = this.config.full().appearance.dockFill
|
||||
|
||||
if (dockSide == 'off') {
|
||||
this.hostApp.setAlwaysOnTop(false)
|
||||
return
|
||||
}
|
||||
if (dockSide == 'left' || dockSide == 'right') {
|
||||
newBounds.width = Math.round(fill * display.bounds.width)
|
||||
newBounds.height = display.bounds.height
|
||||
}
|
||||
if (dockSide == 'top' || dockSide == 'bottom') {
|
||||
newBounds.width = display.bounds.width
|
||||
newBounds.height = Math.round(fill * display.bounds.height)
|
||||
}
|
||||
if (dockSide == 'right') {
|
||||
newBounds.x = display.bounds.x + display.bounds.width * (1.0 - fill)
|
||||
} else {
|
||||
newBounds.x = display.bounds.x
|
||||
}
|
||||
if (dockSide == 'bottom') {
|
||||
newBounds.y = display.bounds.y + display.bounds.height * (1.0 - fill)
|
||||
} else {
|
||||
newBounds.y = display.bounds.y
|
||||
}
|
||||
|
||||
this.hostApp.setAlwaysOnTop(true)
|
||||
this.hostApp.unmaximize()
|
||||
this.hostApp.setBounds(newBounds)
|
||||
}
|
||||
|
||||
getCurrentScreen () {
|
||||
return this.electron.screen.getDisplayNearestPoint(this.electron.screen.getCursorScreenPoint())
|
||||
}
|
||||
|
||||
getScreens () {
|
||||
return this.electron.screen.getAllDisplays().map((display, index) => {
|
||||
return {
|
||||
id: display.id,
|
||||
name: {
|
||||
0: 'Primary display',
|
||||
1: 'Secondary display',
|
||||
}[index] || `Display ${index + 1}`
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,43 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class ElectronService {
|
||||
constructor() {
|
||||
if (process.env.TEST_ENV) {
|
||||
this.initTest()
|
||||
} else {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.electron = require('electron')
|
||||
this.remoteElectron = this.remoteRequire('electron')
|
||||
this.app = this.remoteElectron.app
|
||||
this.screen = this.remoteElectron.screen
|
||||
this.dialog = this.remoteElectron.dialog
|
||||
this.shell = this.electron.shell
|
||||
this.clipboard = this.electron.clipboard
|
||||
this.ipcRenderer = this.electron.ipcRenderer
|
||||
this.globalShortcut = this.remoteElectron.globalShortcut
|
||||
}
|
||||
|
||||
initTest() {
|
||||
;
|
||||
}
|
||||
|
||||
remoteRequire(name: string): any {
|
||||
return this.electron.remote.require(name)
|
||||
}
|
||||
|
||||
app: any
|
||||
ipcRenderer: any
|
||||
shell: any
|
||||
dialog: any
|
||||
clipboard: any
|
||||
globalShortcut: any
|
||||
screen: any
|
||||
private electron: any
|
||||
private remoteElectron: any
|
||||
}
|
@@ -1,107 +0,0 @@
|
||||
import { Injectable, NgZone, EventEmitter } from '@angular/core'
|
||||
import { ElectronService } from 'services/electron'
|
||||
import { Logger, LogService } from 'services/log'
|
||||
|
||||
export const PLATFORM_WINDOWS = 'win32'
|
||||
export const PLATFORM_MAC = 'darwin'
|
||||
export const PLATFORM_LINUX = 'linux'
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class HostAppService {
|
||||
constructor(
|
||||
private zone: NgZone,
|
||||
private electron: ElectronService,
|
||||
log: LogService,
|
||||
) {
|
||||
this.platform = require('os').platform()
|
||||
this.logger = log.create('hostApp')
|
||||
|
||||
electron.ipcRenderer.on('host:quit-request', () => this.zone.run(() => this.quitRequested.emit()))
|
||||
|
||||
electron.ipcRenderer.on('uncaughtException', function(err) {
|
||||
console.error('Unhandled exception:', err)
|
||||
})
|
||||
|
||||
electron.ipcRenderer.on('host:window-shown', () => {
|
||||
this.shown.emit()
|
||||
})
|
||||
|
||||
electron.ipcRenderer.on('host:second-instance', () => {
|
||||
this.secondInstance.emit()
|
||||
})
|
||||
|
||||
this.ready.subscribe(() => {
|
||||
electron.ipcRenderer.send('app:ready')
|
||||
})
|
||||
}
|
||||
|
||||
platform: string;
|
||||
quitRequested = new EventEmitter<any>()
|
||||
ready = new EventEmitter<any>()
|
||||
shown = new EventEmitter<any>()
|
||||
secondInstance = new EventEmitter<any>()
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
getWindow() {
|
||||
return this.electron.app.window
|
||||
}
|
||||
|
||||
getShell() {
|
||||
return this.electron.shell
|
||||
}
|
||||
|
||||
getAppPath() {
|
||||
return this.electron.app.getAppPath()
|
||||
}
|
||||
|
||||
getPath(type: string) {
|
||||
return this.electron.app.getPath(type)
|
||||
}
|
||||
|
||||
openDevTools() {
|
||||
this.electron.app.webContents.openDevTools()
|
||||
}
|
||||
|
||||
setCloseable(flag: boolean) {
|
||||
this.electron.ipcRenderer.send('window-set-closeable', flag)
|
||||
}
|
||||
|
||||
focusWindow() {
|
||||
this.electron.ipcRenderer.send('window-focus')
|
||||
}
|
||||
|
||||
toggleWindow() {
|
||||
this.electron.ipcRenderer.send('window-toggle-focus')
|
||||
}
|
||||
|
||||
minimize () {
|
||||
this.electron.ipcRenderer.send('window-minimize')
|
||||
}
|
||||
|
||||
maximize () {
|
||||
this.electron.ipcRenderer.send('window-maximize')
|
||||
}
|
||||
|
||||
unmaximize () {
|
||||
this.electron.ipcRenderer.send('window-unmaximize')
|
||||
}
|
||||
|
||||
toggleMaximize () {
|
||||
this.electron.ipcRenderer.send('window-toggle-maximize')
|
||||
}
|
||||
|
||||
setBounds (bounds: Electron.Rectangle) {
|
||||
this.electron.ipcRenderer.send('window-set-bounds', bounds)
|
||||
}
|
||||
|
||||
setAlwaysOnTop (flag: boolean) {
|
||||
this.electron.ipcRenderer.send('window-set-always-on-top', flag)
|
||||
}
|
||||
|
||||
quit () {
|
||||
this.logger.info('Quitting')
|
||||
this.electron.app.quit()
|
||||
}
|
||||
}
|
@@ -1,227 +0,0 @@
|
||||
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
|
||||
import { ElectronService } from 'services/electron'
|
||||
import { ConfigService } from 'services/config'
|
||||
import { NativeKeyEvent, stringifyKeySequence } from './hotkeys.util'
|
||||
import { IHotkeyDescription, HotkeyProvider } from 'api/hotkeyProvider'
|
||||
|
||||
|
||||
export interface PartialHotkeyMatch {
|
||||
id: string,
|
||||
strokes: string[],
|
||||
matchedLength: number,
|
||||
}
|
||||
|
||||
const KEY_TIMEOUT = 2000
|
||||
|
||||
interface EventBufferEntry {
|
||||
event: NativeKeyEvent,
|
||||
time: number,
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class HotkeysService {
|
||||
key = new EventEmitter<NativeKeyEvent>()
|
||||
matchedHotkey = new EventEmitter<string>()
|
||||
globalHotkey = new EventEmitter()
|
||||
private currentKeystrokes: EventBufferEntry[] = []
|
||||
private disabledLevel = 0
|
||||
private hotkeyDescriptions: IHotkeyDescription[]
|
||||
|
||||
constructor(
|
||||
private zone: NgZone,
|
||||
private electron: ElectronService,
|
||||
private config: ConfigService,
|
||||
@Inject(HotkeyProvider) hotkeyProviders: HotkeyProvider[],
|
||||
) {
|
||||
let events = ['keydown', 'keyup']
|
||||
events.forEach((event) => {
|
||||
document.addEventListener(event, (nativeEvent) => {
|
||||
if (document.querySelectorAll('input:focus').length == 0) {
|
||||
this.pushKeystroke(event, nativeEvent)
|
||||
this.processKeystrokes()
|
||||
this.emitKeyEvent(nativeEvent)
|
||||
}
|
||||
})
|
||||
})
|
||||
this.hotkeyDescriptions = hotkeyProviders.map(x => x.hotkeys).reduce((a, b) => a.concat(b))
|
||||
}
|
||||
|
||||
pushKeystroke (name, nativeEvent) {
|
||||
nativeEvent.event = name
|
||||
this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() })
|
||||
}
|
||||
|
||||
processKeystrokes () {
|
||||
if (this.isEnabled()) {
|
||||
this.zone.run(() => {
|
||||
let matched = this.getCurrentFullyMatchedHotkey()
|
||||
if (matched) {
|
||||
console.log('Matched hotkey', matched)
|
||||
this.matchedHotkey.emit(matched)
|
||||
this.clearCurrentKeystrokes()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
emitKeyEvent (nativeEvent) {
|
||||
this.zone.run(() => {
|
||||
this.key.emit(nativeEvent)
|
||||
})
|
||||
}
|
||||
|
||||
clearCurrentKeystrokes () {
|
||||
this.currentKeystrokes = []
|
||||
}
|
||||
|
||||
getCurrentKeystrokes () : string[] {
|
||||
this.currentKeystrokes = this.currentKeystrokes.filter((x) => performance.now() - x.time < KEY_TIMEOUT )
|
||||
return stringifyKeySequence(this.currentKeystrokes.map((x) => x.event))
|
||||
}
|
||||
|
||||
registerHotkeys () {
|
||||
this.electron.globalShortcut.unregisterAll()
|
||||
// TODO
|
||||
this.electron.globalShortcut.register('Ctrl+Space', () => {
|
||||
this.globalHotkey.emit()
|
||||
})
|
||||
}
|
||||
|
||||
getHotkeysConfig () {
|
||||
let keys = {}
|
||||
for (let key in this.config.full().hotkeys) {
|
||||
let value = this.config.full().hotkeys[key]
|
||||
if (typeof value == 'string') {
|
||||
value = [value]
|
||||
}
|
||||
value = value.map((item) => (typeof item == 'string') ? [item] : item)
|
||||
keys[key] = value
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
getCurrentFullyMatchedHotkey () : string {
|
||||
for (let id in this.getHotkeysConfig()) {
|
||||
for (let sequence of this.getHotkeysConfig()[id]) {
|
||||
let currentStrokes = this.getCurrentKeystrokes()
|
||||
if (currentStrokes.length < sequence.length) {
|
||||
break
|
||||
}
|
||||
if (sequence.every((x, index) => {
|
||||
return x.toLowerCase() == currentStrokes[currentStrokes.length - sequence.length + index].toLowerCase()
|
||||
})) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
getCurrentPartiallyMatchedHotkeys () : PartialHotkeyMatch[] {
|
||||
let result = []
|
||||
for (let id in this.getHotkeysConfig()) {
|
||||
for (let sequence of this.getHotkeysConfig()[id]) {
|
||||
let currentStrokes = this.getCurrentKeystrokes()
|
||||
|
||||
for (let matchLength = Math.min(currentStrokes.length, sequence.length); matchLength > 0; matchLength--) {
|
||||
//console.log(sequence, currentStrokes.slice(currentStrokes.length - sequence.length))
|
||||
if (sequence.slice(0, matchLength).every((x, index) => {
|
||||
return x.toLowerCase() == currentStrokes[currentStrokes.length - matchLength + index].toLowerCase()
|
||||
})) {
|
||||
result.push({
|
||||
matchedLength: matchLength,
|
||||
id,
|
||||
strokes: sequence
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
getHotkeyDescription (id: string) : IHotkeyDescription {
|
||||
return this.hotkeyDescriptions.filter((x) => x.id == id)[0]
|
||||
}
|
||||
|
||||
enable () {
|
||||
this.disabledLevel--
|
||||
}
|
||||
|
||||
disable () {
|
||||
this.disabledLevel++
|
||||
}
|
||||
|
||||
isEnabled () {
|
||||
return this.disabledLevel == 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class AppHotkeyProvider extends HotkeyProvider {
|
||||
hotkeys: IHotkeyDescription[] = [
|
||||
{
|
||||
id: 'new-tab',
|
||||
name: 'New tab',
|
||||
},
|
||||
{
|
||||
id: 'close-tab',
|
||||
name: 'Close tab',
|
||||
},
|
||||
{
|
||||
id: 'toggle-last-tab',
|
||||
name: 'Toggle last tab',
|
||||
},
|
||||
{
|
||||
id: 'next-tab',
|
||||
name: 'Next tab',
|
||||
},
|
||||
{
|
||||
id: 'previous-tab',
|
||||
name: 'Previous tab',
|
||||
},
|
||||
{
|
||||
id: 'tab-1',
|
||||
name: 'Tab 1',
|
||||
},
|
||||
{
|
||||
id: 'tab-2',
|
||||
name: 'Tab 2',
|
||||
},
|
||||
{
|
||||
id: 'tab-3',
|
||||
name: 'Tab 3',
|
||||
},
|
||||
{
|
||||
id: 'tab-4',
|
||||
name: 'Tab 4',
|
||||
},
|
||||
{
|
||||
id: 'tab-5',
|
||||
name: 'Tab 5',
|
||||
},
|
||||
{
|
||||
id: 'tab-6',
|
||||
name: 'Tab 6',
|
||||
},
|
||||
{
|
||||
id: 'tab-7',
|
||||
name: 'Tab 7',
|
||||
},
|
||||
{
|
||||
id: 'tab-8',
|
||||
name: 'Tab 8',
|
||||
},
|
||||
{
|
||||
id: 'tab-9',
|
||||
name: 'Tab 9',
|
||||
},
|
||||
{
|
||||
id: 'tab-10',
|
||||
name: 'Tab 10',
|
||||
},
|
||||
]
|
||||
}
|
@@ -1,62 +0,0 @@
|
||||
import * as os from 'os'
|
||||
|
||||
|
||||
export const metaKeyName = {
|
||||
darwin: '⌘',
|
||||
win32: 'Win',
|
||||
linux: 'Super',
|
||||
}[os.platform()]
|
||||
|
||||
export const altKeyName = {
|
||||
darwin: 'Option',
|
||||
win32: 'Alt',
|
||||
linux: 'Alt',
|
||||
}[os.platform()]
|
||||
|
||||
|
||||
export interface NativeKeyEvent {
|
||||
event?: string,
|
||||
altKey: boolean,
|
||||
ctrlKey: boolean,
|
||||
metaKey: boolean,
|
||||
shiftKey: boolean,
|
||||
key: string,
|
||||
keyCode: string,
|
||||
}
|
||||
|
||||
|
||||
export function stringifyKeySequence(events: NativeKeyEvent[]): string[] {
|
||||
let items: string[] = []
|
||||
events = events.slice()
|
||||
|
||||
while (events.length > 0) {
|
||||
let event = events.shift()
|
||||
if (event.event == 'keydown') {
|
||||
let itemKeys: string[] = []
|
||||
if (event.ctrlKey) {
|
||||
itemKeys.push('Ctrl')
|
||||
}
|
||||
if (event.metaKey) {
|
||||
itemKeys.push(metaKeyName)
|
||||
}
|
||||
if (event.altKey) {
|
||||
itemKeys.push(altKeyName)
|
||||
}
|
||||
if (event.shiftKey) {
|
||||
itemKeys.push('Shift')
|
||||
}
|
||||
|
||||
if (['Control', 'Shift', 'Alt', 'Command'].includes(event.key)) {
|
||||
// TODO make this optional?
|
||||
continue
|
||||
}
|
||||
if (event.key.length == 1) {
|
||||
itemKeys.push(event.key.toUpperCase())
|
||||
} else {
|
||||
itemKeys.push(event.key)
|
||||
}
|
||||
items.push(itemKeys.join('-'))
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
@@ -1,24 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
|
||||
|
||||
export class Logger {
|
||||
constructor(
|
||||
private name: string,
|
||||
) {}
|
||||
|
||||
log (level: string, ...args: any[]) {
|
||||
console[level](`%c[${this.name}]`, 'color: #aaa', ...args)
|
||||
}
|
||||
|
||||
debug(...args: any[]) { this.log('debug', ...args) }
|
||||
info(...args: any[]) { this.log('info', ...args) }
|
||||
warn(...args: any[]) { this.log('warn', ...args) }
|
||||
error(...args: any[]) { this.log('error', ...args) }
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class LogService {
|
||||
create (name: string): Logger {
|
||||
return new Logger(name)
|
||||
}
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class ModalService {
|
||||
constructor(
|
||||
private ngbModal: NgbModal,
|
||||
) {}
|
||||
|
||||
open(content: any, config?: any) {
|
||||
config = config || {}
|
||||
config.windowClass = 'out'
|
||||
let modal = this.ngbModal.open(content, config)
|
||||
|
||||
let fx = (<any>modal)._removeModalElements.bind(modal);
|
||||
|
||||
(<any>modal)._removeModalElements = () => {
|
||||
(<any>modal)._windowCmptRef.instance.windowClass = 'out'
|
||||
setTimeout(() => fx(), 500)
|
||||
}
|
||||
setTimeout(() => {
|
||||
(<any>modal)._windowCmptRef.instance.windowClass = ''
|
||||
}, 1)
|
||||
|
||||
return modal
|
||||
}
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ToasterService } from 'angular2-toaster'
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class NotifyService {
|
||||
constructor(
|
||||
private toaster: ToasterService,
|
||||
) {}
|
||||
|
||||
pop(options) {
|
||||
this.toaster.pop(options)
|
||||
}
|
||||
|
||||
info(title: string, body: string = null) {
|
||||
return this.pop({
|
||||
type: 'info',
|
||||
title, body,
|
||||
timeout: 4000,
|
||||
})
|
||||
}
|
||||
|
||||
success(title: string, body: string = null) {
|
||||
return this.pop({
|
||||
type: 'success',
|
||||
title, body,
|
||||
timeout: 4000,
|
||||
})
|
||||
}
|
||||
|
||||
warning(title: string, body: string = null) {
|
||||
return this.pop({
|
||||
type: 'warning',
|
||||
title, body,
|
||||
timeout: 4000,
|
||||
})
|
||||
}
|
||||
|
||||
error(title: string, body: string = null) {
|
||||
return this.pop({
|
||||
type: 'error',
|
||||
title, body,
|
||||
timeout: 4000,
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
|
||||
|
||||
class Plugin {
|
||||
ngModule: any
|
||||
name: string
|
||||
}
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class PluginsService {
|
||||
plugins: Plugin[] = []
|
||||
|
||||
register (plugin: Plugin): void {
|
||||
this.plugins.push(plugin)
|
||||
}
|
||||
|
||||
getModules (): any[] {
|
||||
return this.plugins.map((plugin) => plugin.ngModule)
|
||||
}
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HostAppService } from 'services/hostApp'
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class QuitterService {
|
||||
constructor(
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
hostApp.quitRequested.subscribe(() => {
|
||||
this.quit()
|
||||
})
|
||||
}
|
||||
|
||||
quit() {
|
||||
this.hostApp.setCloseable(true)
|
||||
this.hostApp.quit()
|
||||
}
|
||||
}
|
@@ -1,45 +0,0 @@
|
||||
import { Injectable, Inject } from '@angular/core'
|
||||
import { Logger, LogService } from 'services/log'
|
||||
import { BaseTabComponent } from 'components/baseTab'
|
||||
import { TabRecoveryProvider } from 'api/tabRecovery'
|
||||
import { AppService } from 'services/app'
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class TabRecoveryService {
|
||||
logger: Logger
|
||||
|
||||
constructor(
|
||||
@Inject(TabRecoveryProvider) private tabRecoveryProviders: TabRecoveryProvider[],
|
||||
app: AppService,
|
||||
log: LogService
|
||||
) {
|
||||
this.logger = log.create('tabRecovery')
|
||||
app.tabsChanged$.subscribe(() => {
|
||||
this.saveTabs(app.tabs)
|
||||
})
|
||||
}
|
||||
|
||||
saveTabs (tabs: BaseTabComponent[]) {
|
||||
window.localStorage.tabsRecovery = JSON.stringify(
|
||||
tabs
|
||||
.map((tab) => tab.getRecoveryToken())
|
||||
.filter((token) => !!token)
|
||||
)
|
||||
}
|
||||
|
||||
async recoverTabs (): Promise<void> {
|
||||
if (window.localStorage.tabsRecovery) {
|
||||
for (let token of JSON.parse(window.localStorage.tabsRecovery)) {
|
||||
for (let provider of this.tabRecoveryProviders) {
|
||||
try {
|
||||
await provider.recover(token)
|
||||
} catch (error) {
|
||||
this.logger.warn('Tab recovery crashed:', token, provider, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
import { TerminalTabComponent } from './components/terminalTab'
|
||||
export { TerminalTabComponent } from './components/terminalTab'
|
||||
|
||||
export abstract class TerminalDecorator {
|
||||
attach (_terminal: TerminalTabComponent): void { }
|
||||
detach (_terminal: TerminalTabComponent): void { }
|
||||
}
|
||||
|
||||
export interface ResizeEvent {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface SessionOptions {
|
||||
name?: string
|
||||
command?: string
|
||||
args?: string[]
|
||||
cwd?: string
|
||||
env?: any
|
||||
recoveryId?: string
|
||||
recoveredTruePID?: number
|
||||
}
|
||||
|
||||
export abstract class SessionPersistenceProvider {
|
||||
abstract async attachSession (recoveryId: any): Promise<SessionOptions>
|
||||
abstract async startSession (options: SessionOptions): Promise<any>
|
||||
abstract async terminateSession (recoveryId: string): Promise<void>
|
||||
}
|
||||
|
||||
export interface ITerminalColorScheme {
|
||||
name: string
|
||||
foreground: string
|
||||
background: string
|
||||
colors: string[]
|
||||
}
|
||||
|
||||
export abstract class TerminalColorSchemeProvider {
|
||||
abstract async getSchemes (): Promise<ITerminalColorScheme[]>
|
||||
}
|
@@ -1,42 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HotkeysService, ToolbarButtonProvider, IToolbarButton, AppService } from 'api'
|
||||
import { SessionsService } from './services/sessions'
|
||||
import { TerminalTabComponent } from './components/terminalTab'
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class ButtonProvider extends ToolbarButtonProvider {
|
||||
constructor (
|
||||
private app: AppService,
|
||||
private sessions: SessionsService,
|
||||
hotkeys: HotkeysService,
|
||||
) {
|
||||
super()
|
||||
hotkeys.matchedHotkey.subscribe(async (hotkey) => {
|
||||
if (hotkey == 'new-tab') {
|
||||
this.openNewTab()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async openNewTab (): Promise<void> {
|
||||
let cwd = null
|
||||
if (this.app.activeTab instanceof TerminalTabComponent) {
|
||||
cwd = await this.app.activeTab.session.getWorkingDirectory()
|
||||
}
|
||||
this.app.openNewTab(
|
||||
TerminalTabComponent,
|
||||
{ session: await this.sessions.createNewSession({ command: 'zsh', cwd }) }
|
||||
)
|
||||
}
|
||||
|
||||
provide (): IToolbarButton[] {
|
||||
return [{
|
||||
icon: 'plus',
|
||||
title: 'New terminal',
|
||||
click: async () => {
|
||||
this.openNewTab()
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
import * as fs from 'fs-promise'
|
||||
import * as path from 'path'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TerminalColorSchemeProvider, ITerminalColorScheme } from './api'
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class HyperColorSchemes extends TerminalColorSchemeProvider {
|
||||
async getSchemes (): Promise<ITerminalColorScheme[]> {
|
||||
let pluginsPath = path.join(process.env.HOME, '.hyper_plugins', 'node_modules')
|
||||
if (!(await fs.exists(pluginsPath))) return []
|
||||
let plugins = await fs.readdir(pluginsPath)
|
||||
|
||||
let themes: ITerminalColorScheme[] = []
|
||||
|
||||
plugins.forEach(plugin => {
|
||||
let module = (<any>global).require(path.join(pluginsPath, plugin))
|
||||
if (module.decorateConfig) {
|
||||
let config = module.decorateConfig({})
|
||||
if (config.colors) {
|
||||
themes.push({
|
||||
name: plugin,
|
||||
foreground: config.foregroundColor,
|
||||
background: config.backgroundColor,
|
||||
colors: config.colors.black ? [
|
||||
config.colors.black,
|
||||
config.colors.red,
|
||||
config.colors.green,
|
||||
config.colors.yellow,
|
||||
config.colors.blue,
|
||||
config.colors.magenta,
|
||||
config.colors.cyan,
|
||||
config.colors.white,
|
||||
config.colors.lightBlack,
|
||||
config.colors.lightRed,
|
||||
config.colors.lightGreen,
|
||||
config.colors.lightYellow,
|
||||
config.colors.lightBlue,
|
||||
config.colors.lightMagenta,
|
||||
config.colors.lightCyan,
|
||||
config.colors.lightWhite,
|
||||
] : config.colors,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return themes
|
||||
}
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
template(#content)
|
||||
.preview(
|
||||
[style.width]='"100%"',
|
||||
[style.background]='model',
|
||||
)
|
||||
input.form-control(type='text', '[(ngModel)]'='model', (ngModelChange)='onChange()', #input)
|
||||
|
||||
div(
|
||||
[ngbPopover]='content',
|
||||
[style.background]='model',
|
||||
(click)='open()',
|
||||
container='body',
|
||||
#popover='ngbPopover',
|
||||
) {{ title }}
|
@@ -1,15 +0,0 @@
|
||||
div {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
box-shadow: 0 1px 1px rgba(0,0,0,.5);
|
||||
border: none;
|
||||
margin: 5px 10px 5px 0;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
display: inline-block;
|
||||
color: #fff;
|
||||
line-height: 31px;
|
||||
text-shadow: 0 1px 1px rgba(0,0,0,.5);
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
import { Component, Input, Output, EventEmitter, HostListener, ViewChild } from '@angular/core'
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'color-picker',
|
||||
template: require('./colorPicker.pug'),
|
||||
styles: [require('./colorPicker.scss')],
|
||||
})
|
||||
export class ColorPickerComponent {
|
||||
@Input() model: string
|
||||
@Input() title: string
|
||||
@Output() modelChange = new EventEmitter<string>()
|
||||
@ViewChild('popover') popover: NgbPopover
|
||||
@ViewChild('input') input
|
||||
|
||||
open () {
|
||||
setImmediate(() => {
|
||||
this.popover.open()
|
||||
setImmediate(() => {
|
||||
this.input.nativeElement.focus()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@HostListener('document:click', ['$event']) onOutsideClick ($event) {
|
||||
let windowRef = (<any>this.popover)._windowRef
|
||||
if (!windowRef) {
|
||||
return
|
||||
}
|
||||
if ($event.target !== windowRef.location.nativeElement &&
|
||||
!windowRef.location.nativeElement.contains($event.target)) {
|
||||
this.popover.close()
|
||||
}
|
||||
}
|
||||
|
||||
onChange () {
|
||||
this.modelChange.emit(this.model)
|
||||
}
|
||||
}
|
@@ -1,140 +0,0 @@
|
||||
.row
|
||||
.col-lg-6
|
||||
.form-group
|
||||
label Preview
|
||||
.appearance-preview(
|
||||
[style.font-family]='config.full().terminal.font',
|
||||
[style.font-size]='config.full().terminal.fontSize + "px"',
|
||||
[style.background-color]='(config.full().terminal.background == "theme") ? null : config.full().terminal.colorScheme.background',
|
||||
[style.color]='config.full().terminal.colorScheme.foreground',
|
||||
)
|
||||
div
|
||||
span john@doe-pc
|
||||
span([style.color]='config.full().terminal.colorScheme.colors[1]') $
|
||||
span webpack
|
||||
div
|
||||
span Asset Size
|
||||
div
|
||||
span([style.color]='config.full().terminal.colorScheme.colors[2]') main.js
|
||||
span 234 kB
|
||||
span([style.color]='config.full().terminal.colorScheme.colors[2]') [emitted]
|
||||
div
|
||||
span([style.color]='config.full().terminal.colorScheme.colors[3]') big.js
|
||||
span([style.color]='config.full().terminal.colorScheme.colors[3]') 1.2 MB
|
||||
span([style.color]='config.full().terminal.colorScheme.colors[2]') [emitted]
|
||||
span([style.color]='config.full().terminal.colorScheme.colors[3]') [big]
|
||||
div
|
||||
span
|
||||
div
|
||||
span john@doe-pc
|
||||
span([style.color]='config.full().terminal.colorScheme.colors[1]') $
|
||||
span ls -l
|
||||
div
|
||||
span drwxr-xr-x 1 root root
|
||||
span([style.color]='config.full().terminal.colorScheme.colors[4]') directory
|
||||
div
|
||||
span -rw-r--r-- 1 root root file
|
||||
div
|
||||
span -rwxr-xr-x 1 root root
|
||||
span([style.color]='config.full().terminal.colorScheme.colors[2]') executable
|
||||
div
|
||||
span -rwxr-xr-x 1 root root
|
||||
span([style.color]='config.full().terminal.colorScheme.colors[6]') sym
|
||||
span ->
|
||||
span([style.color]='config.full().terminal.colorScheme.colors[1]') link
|
||||
div
|
||||
|
||||
|
||||
.col-lg-6
|
||||
.form-group
|
||||
label Font
|
||||
.row
|
||||
.col-8
|
||||
input.form-control(
|
||||
type='text',
|
||||
[ngbTypeahead]='fontAutocomplete',
|
||||
'[(ngModel)]'='config.store.terminal.font',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
.col-4
|
||||
input.form-control(
|
||||
type='number',
|
||||
'[(ngModel)]'='config.store.terminal.fontSize',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
small.form-text.text-muted Font to be used in the terminal
|
||||
|
||||
.form-group
|
||||
label Color scheme
|
||||
select.form-control(
|
||||
[compareWith]='equalComparator',
|
||||
'[(ngModel)]'='config.store.terminal.colorScheme',
|
||||
(ngModelChange)='config.save()',
|
||||
)
|
||||
option(*ngFor='let scheme of colorSchemes', [ngValue]='scheme') {{scheme.name}}
|
||||
|
||||
div(*ngIf='config.store.terminal.colorScheme.colors')
|
||||
color-picker(
|
||||
'[(model)]'='config.store.terminal.colorScheme.foreground',
|
||||
(modelChange)='config.save()',
|
||||
title='FG',
|
||||
)
|
||||
color-picker(
|
||||
'[(model)]'='config.store.terminal.colorScheme.background',
|
||||
(modelChange)='config.save()',
|
||||
title='BG',
|
||||
)
|
||||
color-picker(
|
||||
*ngFor='let _ of config.store.terminal.colorScheme.colors; let idx = index',
|
||||
'[(model)]'='config.store.terminal.colorScheme.colors[idx]',
|
||||
(modelChange)='config.save()',
|
||||
[title]='idx',
|
||||
)
|
||||
|
||||
.form-group
|
||||
label Terminal background
|
||||
br
|
||||
div(
|
||||
'[(ngModel)]'='config.store.terminal.background',
|
||||
(ngModelChange)='config.save()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary
|
||||
input(
|
||||
type='radio',
|
||||
[value]='"theme"'
|
||||
)
|
||||
| From the app theme
|
||||
label.btn.btn-secondary
|
||||
input(
|
||||
type='radio',
|
||||
[value]='"colorScheme"'
|
||||
)
|
||||
| From the terminal colors
|
||||
|
||||
.form-group
|
||||
label Terminal bell
|
||||
br
|
||||
div(
|
||||
'[(ngModel)]'='config.store.terminal.bell',
|
||||
(ngModelChange)='config.save()',
|
||||
ngbRadioGroup
|
||||
)
|
||||
label.btn.btn-secondary
|
||||
input(
|
||||
type='radio',
|
||||
[value]='"off"'
|
||||
)
|
||||
| Off
|
||||
label.btn.btn-secondary
|
||||
input(
|
||||
type='radio',
|
||||
[value]='"visual"'
|
||||
)
|
||||
| Visual
|
||||
label.btn.btn-secondary
|
||||
input(
|
||||
type='radio',
|
||||
[value]='"audible"'
|
||||
)
|
||||
| Audible
|
@@ -1,7 +0,0 @@
|
||||
.appearance-preview {
|
||||
padding: 10px 20px;
|
||||
margin: 0 0 10px;
|
||||
span {
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
@@ -1,49 +0,0 @@
|
||||
import { Observable } from 'rxjs/Observable'
|
||||
import 'rxjs/add/operator/map'
|
||||
import 'rxjs/add/operator/debounceTime'
|
||||
import 'rxjs/add/operator/distinctUntilChanged'
|
||||
const childProcessPromise = require('child-process-promise')
|
||||
const equal = require('deep-equal')
|
||||
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { TerminalColorSchemeProvider, ITerminalColorScheme } from '../api'
|
||||
import { ConfigService } from 'services/config'
|
||||
|
||||
|
||||
@Component({
|
||||
template: require('./settings.pug'),
|
||||
styles: [require('./settings.scss')],
|
||||
})
|
||||
export class SettingsComponent {
|
||||
fonts: string[] = []
|
||||
colorSchemes: ITerminalColorScheme[] = []
|
||||
equalComparator = equal
|
||||
|
||||
constructor(
|
||||
public config: ConfigService,
|
||||
@Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[],
|
||||
) { }
|
||||
|
||||
async ngOnInit () {
|
||||
childProcessPromise.exec('fc-list :spacing=mono').then((result) => {
|
||||
this.fonts = result.stdout
|
||||
.split('\n')
|
||||
.filter((x) => !!x)
|
||||
.map((x) => x.split(':')[1].trim())
|
||||
.map((x) => x.split(',')[0].trim())
|
||||
this.fonts.sort()
|
||||
})
|
||||
|
||||
this.colorSchemes = (await Promise.all(this.colorSchemeProviders.map(x => x.getSchemes()))).reduce((a, b) => a.concat(b))
|
||||
}
|
||||
|
||||
fontAutocomplete = (text$: Observable<string>) => {
|
||||
return text$
|
||||
.debounceTime(200)
|
||||
.distinctUntilChanged()
|
||||
.map(query => this.fonts.filter(v => new RegExp(query, 'gi').test(v)))
|
||||
.map(list => Array.from(new Set(list)))
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
* {
|
||||
font-feature-settings: "liga" 0; // disable ligatures (they break monospacing)
|
||||
}
|
||||
|
||||
x-screen {
|
||||
transition: 0.125s ease background;
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
:host {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
&> .content {
|
||||
flex: auto;
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
margin: 15px;
|
||||
|
||||
div[style]:last-child {
|
||||
background: black !important;
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,226 +0,0 @@
|
||||
import { BehaviorSubject, ReplaySubject, Subject, Subscription } from 'rxjs'
|
||||
import { Component, NgZone, Inject, ViewChild, HostBinding, Input } from '@angular/core'
|
||||
|
||||
import { TerminalDecorator, ResizeEvent } from '../api'
|
||||
import { Session } from '../services/sessions'
|
||||
|
||||
import { AppService, ConfigService, BaseTabComponent } from 'api'
|
||||
import { hterm, preferenceManager } from '../hterm'
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'terminalTab',
|
||||
template: '<div #content class="content"></div>',
|
||||
styles: [require('./terminalTab.scss')],
|
||||
})
|
||||
export class TerminalTabComponent extends BaseTabComponent {
|
||||
hterm: any
|
||||
configSubscription: Subscription
|
||||
focusedSubscription: Subscription
|
||||
bell$ = new Subject()
|
||||
size$ = new ReplaySubject<ResizeEvent>(1)
|
||||
input$ = new Subject<string>()
|
||||
output$ = new Subject<string>()
|
||||
contentUpdated$ = new Subject<void>()
|
||||
alternateScreenActive$ = new BehaviorSubject(false)
|
||||
mouseEvent$ = new Subject<Event>()
|
||||
@Input() session: Session
|
||||
@ViewChild('content') content
|
||||
@HostBinding('style.background-color') backgroundColor: string
|
||||
private io: any
|
||||
|
||||
constructor(
|
||||
private zone: NgZone,
|
||||
private app: AppService,
|
||||
public config: ConfigService,
|
||||
@Inject(TerminalDecorator) private decorators: TerminalDecorator[],
|
||||
) {
|
||||
super()
|
||||
this.configSubscription = config.change.subscribe(() => {
|
||||
this.configure()
|
||||
})
|
||||
}
|
||||
|
||||
getRecoveryToken (): any {
|
||||
return {
|
||||
type: 'app:terminal',
|
||||
recoveryId: this.session.recoveryId,
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.focusedSubscription = this.focused.subscribe(() => {
|
||||
this.hterm.scrollPort_.focus()
|
||||
})
|
||||
|
||||
this.hterm = new hterm.hterm.Terminal()
|
||||
this.decorators.forEach((decorator) => {
|
||||
decorator.attach(this)
|
||||
})
|
||||
|
||||
this.attachHTermHandlers(this.hterm)
|
||||
|
||||
this.hterm.onTerminalReady = () => {
|
||||
this.hterm.installKeyboard()
|
||||
this.io = this.hterm.io.push()
|
||||
this.attachIOHandlers(this.io)
|
||||
this.session.output$.subscribe((data) => {
|
||||
this.zone.run(() => {
|
||||
this.output$.next(data)
|
||||
})
|
||||
this.write(data)
|
||||
})
|
||||
this.session.closed$.first().subscribe(() => {
|
||||
this.app.closeTab(this)
|
||||
})
|
||||
|
||||
this.session.releaseInitialDataBuffer()
|
||||
}
|
||||
this.hterm.decorate(this.content.nativeElement)
|
||||
this.configure()
|
||||
|
||||
setTimeout(() => {
|
||||
this.output$.subscribe(() => {
|
||||
this.displayActivity()
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
this.bell$.subscribe(() => {
|
||||
if (this.config.full().terminal.bell != 'off') {
|
||||
let bg = preferenceManager.get('background-color')
|
||||
preferenceManager.set('background-color', 'rgba(128,128,128,.25)')
|
||||
setTimeout(() => {
|
||||
preferenceManager.set('background-color', bg)
|
||||
}, 125)
|
||||
}
|
||||
// TODO audible
|
||||
})
|
||||
}
|
||||
|
||||
attachHTermHandlers (hterm: any) {
|
||||
hterm.setWindowTitle = (title) => {
|
||||
this.zone.run(() => {
|
||||
this.title$.next(title)
|
||||
})
|
||||
}
|
||||
|
||||
const _decorate = hterm.scrollPort_.decorate.bind(hterm.scrollPort_)
|
||||
hterm.scrollPort_.decorate = (...args) => {
|
||||
_decorate(...args)
|
||||
hterm.scrollPort_.screen_.style.cssText += `; padding-right: ${hterm.scrollPort_.screen_.offsetWidth - hterm.scrollPort_.screen_.clientWidth}px;`
|
||||
}
|
||||
|
||||
const _setAlternateMode = hterm.setAlternateMode.bind(hterm)
|
||||
hterm.setAlternateMode = (state) => {
|
||||
_setAlternateMode(state)
|
||||
this.alternateScreenActive$.next(state)
|
||||
}
|
||||
|
||||
const _onPaste_ = hterm.onPaste_.bind(hterm)
|
||||
hterm.onPaste_ = (event) => {
|
||||
event.text = event.text.trim()
|
||||
_onPaste_(event)
|
||||
}
|
||||
|
||||
const _onMouse_ = hterm.onMouse_.bind(hterm)
|
||||
hterm.onMouse_ = (event) => {
|
||||
this.mouseEvent$.next(event)
|
||||
if ((event.ctrlKey || event.metaKey) && event.type === 'mousewheel') {
|
||||
event.preventDefault()
|
||||
let delta = Math.round(event.wheelDeltaY / 50)
|
||||
this.sendInput(((delta > 0) ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
|
||||
}
|
||||
_onMouse_(event)
|
||||
}
|
||||
|
||||
hterm.ringBell = () => {
|
||||
this.bell$.next()
|
||||
}
|
||||
|
||||
for (let screen of [hterm.primaryScreen_, hterm.alternateScreen_]) {
|
||||
const _insertString = screen.insertString.bind(screen)
|
||||
screen.insertString = (data) => {
|
||||
_insertString(data)
|
||||
this.contentUpdated$.next()
|
||||
}
|
||||
|
||||
const _deleteChars = screen.deleteChars.bind(screen)
|
||||
screen.deleteChars = (count) => {
|
||||
let ret = _deleteChars(count)
|
||||
this.contentUpdated$.next()
|
||||
return ret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attachIOHandlers (io: any) {
|
||||
io.onVTKeystroke = io.sendString = (data) => {
|
||||
this.sendInput(data)
|
||||
this.zone.run(() => {
|
||||
this.input$.next(data)
|
||||
})
|
||||
}
|
||||
io.onTerminalResize = (columns, rows) => {
|
||||
// console.log(`Resizing to ${columns}x${rows}`)
|
||||
this.zone.run(() => {
|
||||
this.session.resize(columns, rows)
|
||||
this.size$.next({ width: columns, height: rows })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sendInput (data: string) {
|
||||
this.session.write(data)
|
||||
}
|
||||
|
||||
write (data: string) {
|
||||
this.io.writeUTF8(data)
|
||||
}
|
||||
|
||||
async configure (): Promise<void> {
|
||||
let config = this.config.full()
|
||||
preferenceManager.set('font-family', config.terminal.font)
|
||||
preferenceManager.set('font-size', config.terminal.fontSize)
|
||||
preferenceManager.set('audible-bell-sound', '')
|
||||
preferenceManager.set('desktop-notification-bell', config.terminal.bell == 'notification')
|
||||
preferenceManager.set('enable-clipboard-notice', false)
|
||||
preferenceManager.set('receive-encoding', 'raw')
|
||||
preferenceManager.set('send-encoding', 'raw')
|
||||
|
||||
if (config.terminal.colorScheme.foreground) {
|
||||
preferenceManager.set('foreground-color', config.terminal.colorScheme.foreground)
|
||||
}
|
||||
if (config.terminal.background == 'colorScheme') {
|
||||
if (config.terminal.colorScheme.background) {
|
||||
this.backgroundColor = config.terminal.colorScheme.background
|
||||
preferenceManager.set('background-color', config.terminal.colorScheme.background)
|
||||
}
|
||||
} else {
|
||||
this.backgroundColor = null
|
||||
preferenceManager.set('background-color', 'transparent')
|
||||
}
|
||||
if (config.terminal.colorScheme.colors) {
|
||||
preferenceManager.set('color-palette-overrides', config.terminal.colorScheme.colors)
|
||||
}
|
||||
|
||||
this.hterm.setBracketedPaste(config.terminal.bracketedPaste)
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.decorators.forEach((decorator) => {
|
||||
decorator.detach(this)
|
||||
})
|
||||
this.focusedSubscription.unsubscribe()
|
||||
this.configSubscription.unsubscribe()
|
||||
this.title$.complete()
|
||||
this.size$.complete()
|
||||
this.input$.complete()
|
||||
this.output$.complete()
|
||||
this.contentUpdated$.complete()
|
||||
this.alternateScreenActive$.complete()
|
||||
this.mouseEvent$.complete()
|
||||
this.bell$.complete()
|
||||
|
||||
this.session.gracefullyDestroy()
|
||||
}
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
import { ConfigProvider } from 'api'
|
||||
|
||||
|
||||
export class TerminalConfigProvider extends ConfigProvider {
|
||||
defaultConfigValues: any = {
|
||||
terminal: {
|
||||
font: 'monospace',
|
||||
fontSize: 14,
|
||||
bell: 'off',
|
||||
bracketedPaste: true,
|
||||
background: 'theme',
|
||||
colorScheme: {
|
||||
foreground: null,
|
||||
background: null,
|
||||
colors: null,
|
||||
},
|
||||
},
|
||||
hotkeys: {
|
||||
'new-tab': [
|
||||
['Ctrl-A', 'C'],
|
||||
['Ctrl-A', 'Ctrl-C'],
|
||||
'Ctrl-Shift-T',
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
configStructure: any = {
|
||||
terminal: {
|
||||
colorScheme: {},
|
||||
},
|
||||
hotkeys: {},
|
||||
}
|
||||
}
|
@@ -1,30 +0,0 @@
|
||||
const dataurl = require('dataurl')
|
||||
export const hterm = require('hterm-commonjs')
|
||||
hterm.hterm.defaultStorage = new hterm.lib.Storage.Memory()
|
||||
export const preferenceManager = new hterm.hterm.PreferenceManager('default')
|
||||
|
||||
|
||||
hterm.hterm.VT.ESC['k'] = function(parseState) {
|
||||
parseState.resetArguments();
|
||||
|
||||
function parseOSC(ps) {
|
||||
if (!this.parseUntilStringTerminator_(ps) || ps.func == parseOSC) {
|
||||
return
|
||||
}
|
||||
|
||||
this.terminal.setWindowTitle(ps.args[0])
|
||||
}
|
||||
parseState.func = parseOSC
|
||||
}
|
||||
|
||||
preferenceManager.set('user-css', dataurl.convert({
|
||||
data: require('./components/terminal.userCSS.scss'),
|
||||
mimetype: 'text/css',
|
||||
charset: 'utf8',
|
||||
}))
|
||||
preferenceManager.set('background-color', '#1D272D')
|
||||
preferenceManager.set('color-palette-overrides', {
|
||||
0: '#1D272D',
|
||||
})
|
||||
|
||||
hterm.hterm.Terminal.prototype.showOverlay = () => null
|
@@ -1,80 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
import { ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService } from 'api'
|
||||
|
||||
import { SettingsTabProvider } from '../settings/api'
|
||||
|
||||
import { TerminalTabComponent } from './components/terminalTab'
|
||||
import { SettingsComponent } from './components/settings'
|
||||
import { ColorPickerComponent } from './components/colorPicker'
|
||||
import { SessionsService } from './services/sessions'
|
||||
import { ScreenPersistenceProvider } from './persistenceProviders'
|
||||
import { ButtonProvider } from './buttonProvider'
|
||||
import { RecoveryProvider } from './recoveryProvider'
|
||||
import { SessionPersistenceProvider, TerminalColorSchemeProvider } from './api'
|
||||
import { TerminalSettingsProvider } from './settings'
|
||||
import { TerminalConfigProvider } from './config'
|
||||
import { HyperColorSchemes } from './colorSchemes'
|
||||
import { hterm } from './hterm'
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
NgbModule,
|
||||
],
|
||||
providers: [
|
||||
SessionsService,
|
||||
{ provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true },
|
||||
{ provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true },
|
||||
{ provide: SessionPersistenceProvider, useClass: ScreenPersistenceProvider },
|
||||
// { provide: SessionPersistenceProvider, useValue: null },
|
||||
{ provide: SettingsTabProvider, useClass: TerminalSettingsProvider, multi: true },
|
||||
{ provide: ConfigProvider, useClass: TerminalConfigProvider, multi: true },
|
||||
{ provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
|
||||
],
|
||||
entryComponents: [
|
||||
TerminalTabComponent,
|
||||
SettingsComponent,
|
||||
],
|
||||
declarations: [
|
||||
ColorPickerComponent,
|
||||
TerminalTabComponent,
|
||||
SettingsComponent,
|
||||
],
|
||||
})
|
||||
class TerminalModule {
|
||||
constructor (hotkeys: HotkeysService) {
|
||||
let events = [
|
||||
{
|
||||
name: 'keydown',
|
||||
htermHandler: 'onKeyDown_',
|
||||
},
|
||||
{
|
||||
name: 'keyup',
|
||||
htermHandler: 'onKeyUp_',
|
||||
},
|
||||
]
|
||||
events.forEach((event) => {
|
||||
let oldHandler = hterm.hterm.Keyboard.prototype[event.htermHandler]
|
||||
hterm.hterm.Keyboard.prototype[event.htermHandler] = function (nativeEvent) {
|
||||
hotkeys.pushKeystroke(event.name, nativeEvent)
|
||||
if (hotkeys.getCurrentPartiallyMatchedHotkeys().length == 0) {
|
||||
oldHandler.bind(this)(nativeEvent)
|
||||
} else {
|
||||
nativeEvent.stopPropagation()
|
||||
nativeEvent.preventDefault()
|
||||
}
|
||||
hotkeys.processKeystrokes()
|
||||
hotkeys.emitKeyEvent(nativeEvent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default TerminalModule
|
@@ -1,69 +0,0 @@
|
||||
import * as fs from 'fs-promise'
|
||||
const exec = require('child-process-promise').exec
|
||||
const spawn = require('child-process-promise').spawn
|
||||
|
||||
import { SessionOptions, SessionPersistenceProvider } from './api'
|
||||
|
||||
|
||||
export class ScreenPersistenceProvider extends SessionPersistenceProvider {
|
||||
/*
|
||||
list(): Promise<any[]> {
|
||||
return exec('screen -list').then((result) => {
|
||||
return result.stdout.split('\n')
|
||||
.filter((line) => /\bterm-tab-/.exec(line))
|
||||
.map((line) => line.trim().split('.')[0])
|
||||
}).catch(() => {
|
||||
return []
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
async attachSession (recoveryId: any): Promise<SessionOptions> {
|
||||
let lines = (await exec('screen -list')).stdout.split('\n')
|
||||
let screenPID = lines
|
||||
.filter(line => line.indexOf('.' + recoveryId) !== -1)
|
||||
.map(line => parseInt(line.trim().split('.')[0]))[0]
|
||||
|
||||
if (!screenPID) {
|
||||
return null
|
||||
}
|
||||
|
||||
lines = (await exec(`ps -o pid --ppid ${screenPID}`)).stdout.split('\n')
|
||||
let recoveredTruePID = parseInt(lines[1].split(/\s/).filter(x => !!x)[0])
|
||||
|
||||
return {
|
||||
recoveryId,
|
||||
recoveredTruePID,
|
||||
command: 'screen',
|
||||
args: ['-r', recoveryId],
|
||||
}
|
||||
}
|
||||
|
||||
async startSession (options: SessionOptions): Promise<any> {
|
||||
let configPath = '/tmp/.termScreenConfig'
|
||||
await fs.writeFile(configPath, `
|
||||
escape ^^^
|
||||
vbell off
|
||||
term xterm-color
|
||||
bindkey "^[OH" beginning-of-line
|
||||
bindkey "^[OF" end-of-line
|
||||
bindkey "\\027[?1049h" stuff ----alternate enter-----
|
||||
bindkey "\\027[?1049l" stuff ----alternate leave-----
|
||||
termcapinfo xterm* 'hs:ts=\\E]0;:fs=\\007:ds=\\E]0;\\007'
|
||||
defhstatus "^Et"
|
||||
hardstatus off
|
||||
altscreen on
|
||||
`, 'utf-8')
|
||||
let recoveryId = `term-tab-${Date.now()}`
|
||||
let args = ['-d', '-m', '-c', configPath, '-U', '-S', recoveryId, '--', options.command].concat(options.args || [])
|
||||
await spawn('screen', args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env || process.env,
|
||||
})
|
||||
return recoveryId
|
||||
}
|
||||
|
||||
async terminateSession (recoveryId: string): Promise<void> {
|
||||
await exec(`screen -S ${recoveryId} -X quit`)
|
||||
}
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { TabRecoveryProvider, AppService } from 'api'
|
||||
import { SessionsService } from './services/sessions'
|
||||
import { TerminalTabComponent } from './components/terminalTab'
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class RecoveryProvider extends TabRecoveryProvider {
|
||||
constructor (
|
||||
private sessions: SessionsService,
|
||||
private app: AppService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async recover (recoveryToken: any): Promise<void> {
|
||||
if (recoveryToken.type == 'app:terminal') {
|
||||
let session = await this.sessions.recover(recoveryToken.recoveryId)
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
this.app.openNewTab(TerminalTabComponent, { session })
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,166 +0,0 @@
|
||||
import * as nodePTY from 'node-pty'
|
||||
import * as fs from 'fs-promise'
|
||||
|
||||
import { Subject } from 'rxjs'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Logger, LogService } from 'services/log'
|
||||
import { SessionOptions, SessionPersistenceProvider } from '../api'
|
||||
|
||||
|
||||
export class Session {
|
||||
open: boolean
|
||||
name: string
|
||||
output$ = new Subject<string>()
|
||||
closed$ = new Subject<void>()
|
||||
destroyed$ = new Subject<void>()
|
||||
recoveryId: string
|
||||
truePID: number
|
||||
private pty: any
|
||||
private initialDataBuffer = ''
|
||||
private initialDataBufferReleased = false
|
||||
|
||||
constructor (options: SessionOptions) {
|
||||
this.name = options.name
|
||||
this.recoveryId = options.recoveryId
|
||||
console.log('Spawning', options.command)
|
||||
|
||||
let env = {
|
||||
...process.env,
|
||||
...options.env,
|
||||
TERM: 'xterm-256color',
|
||||
}
|
||||
if (options.command.includes(' ')) {
|
||||
options.args = ['-c', options.command]
|
||||
options.command = 'sh'
|
||||
}
|
||||
this.pty = nodePTY.spawn(options.command, options.args || [], {
|
||||
//name: 'screen-256color',
|
||||
name: 'xterm-256color',
|
||||
//name: 'xterm-color',
|
||||
cols: 80,
|
||||
rows: 30,
|
||||
cwd: options.cwd || process.env.HOME,
|
||||
env: env,
|
||||
})
|
||||
|
||||
this.truePID = options.recoveredTruePID || (<any>this.pty).pid
|
||||
|
||||
this.open = true
|
||||
|
||||
this.pty.on('data', (data) => {
|
||||
if (!this.initialDataBufferReleased) {
|
||||
this.initialDataBuffer += data
|
||||
} else {
|
||||
this.output$.next(data)
|
||||
}
|
||||
})
|
||||
|
||||
this.pty.on('close', () => {
|
||||
this.close()
|
||||
})
|
||||
}
|
||||
|
||||
releaseInitialDataBuffer () {
|
||||
this.initialDataBufferReleased = true
|
||||
this.output$.next(this.initialDataBuffer)
|
||||
this.initialDataBuffer = null
|
||||
}
|
||||
|
||||
resize (columns, rows) {
|
||||
this.pty.resize(columns, rows)
|
||||
}
|
||||
|
||||
write (data) {
|
||||
this.pty.write(data)
|
||||
}
|
||||
|
||||
sendSignal (signal) {
|
||||
this.pty.kill(signal)
|
||||
}
|
||||
|
||||
close () {
|
||||
this.open = false
|
||||
this.closed$.next()
|
||||
this.pty.end()
|
||||
}
|
||||
|
||||
gracefullyDestroy () {
|
||||
return new Promise((resolve) => {
|
||||
this.sendSignal('SIGTERM')
|
||||
if (!this.open) {
|
||||
resolve()
|
||||
this.destroy()
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (this.open) {
|
||||
this.sendSignal('SIGKILL')
|
||||
this.destroy()
|
||||
}
|
||||
resolve()
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
destroy () {
|
||||
if (open) {
|
||||
this.close()
|
||||
}
|
||||
this.destroyed$.next()
|
||||
this.pty.destroy()
|
||||
this.output$.complete()
|
||||
}
|
||||
|
||||
async getWorkingDirectory (): Promise<string> {
|
||||
return await fs.readlink(`/proc/${this.truePID}/cwd`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class SessionsService {
|
||||
sessions: {[id: string]: Session} = {}
|
||||
logger: Logger
|
||||
private lastID = 0
|
||||
|
||||
constructor(
|
||||
private persistence: SessionPersistenceProvider,
|
||||
log: LogService,
|
||||
) {
|
||||
this.logger = log.create('sessions')
|
||||
}
|
||||
|
||||
async createNewSession (options: SessionOptions) : Promise<Session> {
|
||||
if (this.persistence) {
|
||||
let recoveryId = await this.persistence.startSession(options)
|
||||
options = await this.persistence.attachSession(recoveryId)
|
||||
}
|
||||
let session = this.addSession(options)
|
||||
return session
|
||||
}
|
||||
|
||||
addSession (options: SessionOptions) : Session {
|
||||
this.lastID++
|
||||
options.name = `session-${this.lastID}`
|
||||
let session = new Session(options)
|
||||
session.destroyed$.first().subscribe(() => {
|
||||
delete this.sessions[session.name]
|
||||
if (this.persistence) {
|
||||
this.persistence.terminateSession(session.recoveryId)
|
||||
}
|
||||
})
|
||||
this.sessions[session.name] = session
|
||||
return session
|
||||
}
|
||||
|
||||
async recover (recoveryId: string) : Promise<Session> {
|
||||
if (!this.persistence) {
|
||||
return null
|
||||
}
|
||||
const options = await this.persistence.attachSession(recoveryId)
|
||||
if (!options) {
|
||||
return null
|
||||
}
|
||||
return this.addSession(options)
|
||||
}
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { SettingsTabProvider, ComponentType } from '../settings/api'
|
||||
import { SettingsComponent } from './components/settings'
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class TerminalSettingsProvider extends SettingsTabProvider {
|
||||
title = 'Terminal'
|
||||
|
||||
getComponentType (): ComponentType {
|
||||
return SettingsComponent
|
||||
}
|
||||
}
|
@@ -1,283 +0,0 @@
|
||||
$white: #fff !default;
|
||||
$black: #000 !default;
|
||||
$red: #d9534f !default;
|
||||
$orange: #f0ad4e !default;
|
||||
$yellow: #ffd500 !default;
|
||||
$green: #5cb85c !default;
|
||||
$blue: #0275d8 !default;
|
||||
$teal: #5bc0de !default;
|
||||
$pink: #ff5b77 !default;
|
||||
$purple: #613d7c !default;
|
||||
|
||||
|
||||
$body-bg: #1D272D;
|
||||
$body-bg2: #131d27;
|
||||
$body-bg3: #20333e;
|
||||
|
||||
$body-color: #aaa;
|
||||
$font-family-sans-serif: "Source Sans Pro";
|
||||
$font-size-base: 14rem / 16;
|
||||
|
||||
$btn-secondary-color: #ccc;
|
||||
$btn-secondary-bg: #222;
|
||||
$btn-secondary-border: #444;
|
||||
|
||||
//$btn-warning-bg: rgba($orange, .5);
|
||||
|
||||
|
||||
$nav-tabs-border-color: $body-bg2;
|
||||
$nav-tabs-border-width: 1px;
|
||||
$nav-tabs-border-radius: 0;
|
||||
$nav-tabs-link-hover-border-color: $body-bg2;
|
||||
$nav-tabs-active-link-hover-color: $white;
|
||||
$nav-tabs-active-link-hover-bg: $blue;
|
||||
$nav-tabs-active-link-hover-border-color: darken($blue, 30%);
|
||||
|
||||
$input-bg: #111;
|
||||
$input-bg-disabled: #333;
|
||||
|
||||
$input-color: $body-color;
|
||||
//$input-border-color: rgba($black,.15);
|
||||
//$input-box-shadow: inset 0 1px 1px rgba($black,.075);
|
||||
|
||||
$input-border-radius: 0;
|
||||
|
||||
$input-bg-focus: $input-bg;
|
||||
//$input-border-focus: lighten($brand-primary, 25%);
|
||||
//$input-box-shadow-focus: $input-box-shadow, rgba($input-border-focus, .6);
|
||||
$input-color-focus: $input-color;
|
||||
|
||||
$modal-content-bg: $body-bg;
|
||||
$modal-content-border-color: $body-bg2;
|
||||
$modal-header-border-color: $body-bg2;
|
||||
$modal-footer-border-color: $body-bg2;
|
||||
|
||||
$popover-bg: $body-bg2;
|
||||
|
||||
|
||||
@import '~bootstrap/scss/bootstrap.scss';
|
||||
|
||||
.nav-tabs {
|
||||
background: $btn-secondary-bg;
|
||||
.nav-link {
|
||||
transition: 0.25s all;
|
||||
border-bottom-color: $nav-tabs-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
ngb-tabset .tab-content {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
[ngbradiogroup] > label.active {
|
||||
background: $blue;
|
||||
}
|
||||
|
||||
$tab-border-radius: 5px;
|
||||
$button-hover-bg: rgba(0, 0, 0, .25);
|
||||
$button-active-bg: rgba(0, 0, 0, .5);
|
||||
|
||||
title-bar {
|
||||
background: $body-bg2;
|
||||
|
||||
button {
|
||||
&:hover { background: $button-hover-bg !important; }
|
||||
&:active { background: $button-active-bg !important; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
app-root > .content {
|
||||
background: $body-bg2;
|
||||
|
||||
.tabs {
|
||||
background: $body-bg;
|
||||
|
||||
tab-header {
|
||||
background: $body-bg;
|
||||
|
||||
.content-wrapper {
|
||||
background: $body-bg2;
|
||||
|
||||
.index {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
button {
|
||||
color: $body-color;
|
||||
border: none;
|
||||
transition: 0.25s all;
|
||||
|
||||
&:hover { background: $button-hover-bg !important; }
|
||||
&:active { background: $button-active-bg !important; }
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $body-bg2;
|
||||
|
||||
.content-wrapper {
|
||||
background: $body-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-activity:not(.active) {
|
||||
/*
|
||||
.content-wrapper .index {
|
||||
background: $blue;
|
||||
color: white;
|
||||
text-shadow: 0 1px 1px rgba(0,0,0,.95);
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tabs-on-top .tabs {
|
||||
margin-top: 3px;
|
||||
|
||||
tab-header {
|
||||
&.pre-selected {
|
||||
.content-wrapper {
|
||||
border-bottom-right-radius: $tab-border-radius;
|
||||
}
|
||||
}
|
||||
|
||||
&.post-selected {
|
||||
.content-wrapper {
|
||||
border-bottom-left-radius: $tab-border-radius;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
border-top: 1px solid transparent;
|
||||
}
|
||||
|
||||
&.active .content-wrapper {
|
||||
border-top: 1px solid $teal;
|
||||
border-top-left-radius: $tab-border-radius;
|
||||
border-top-right-radius: $tab-border-radius;
|
||||
}
|
||||
|
||||
&.has-activity:not(.active) .content-wrapper {
|
||||
border-top: 1px solid $green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.tabs-on-top) .tabs {
|
||||
margin-bottom: 3px;
|
||||
|
||||
tab-header {
|
||||
&.pre-selected {
|
||||
.content-wrapper {
|
||||
border-top-right-radius: $tab-border-radius;
|
||||
}
|
||||
}
|
||||
|
||||
&.post-selected {
|
||||
.content-wrapper {
|
||||
border-top-left-radius: $tab-border-radius;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
&.active .content-wrapper {
|
||||
border-bottom: 1px solid $teal;
|
||||
border-bottom-left-radius: $tab-border-radius;
|
||||
border-bottom-right-radius: $tab-border-radius;
|
||||
}
|
||||
|
||||
&.has-activity:not(.active) .content-wrapper {
|
||||
border-bottom: 1px solid $green;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tab-body {
|
||||
background: $body-bg;
|
||||
}
|
||||
|
||||
settings-tab > ngb-tabset {
|
||||
border-right: 1px solid $body-bg2;
|
||||
|
||||
& > .nav {
|
||||
background: $body-bg3;
|
||||
|
||||
& > .nav-item > .nav-link {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-top: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
padding: 10px 50px 10px 20px;
|
||||
font-size: 14px;
|
||||
|
||||
&.active {
|
||||
border-top-color: $nav-tabs-active-link-hover-border-color;
|
||||
border-bottom-color: $nav-tabs-active-link-hover-border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
multi-hotkey-input {
|
||||
.item {
|
||||
background: $body-bg3;
|
||||
border: 1px solid $blue;
|
||||
border-radius: 3px;
|
||||
margin-right: 5px;
|
||||
|
||||
.body {
|
||||
padding: 3px 0 2px;
|
||||
|
||||
.stroke {
|
||||
padding: 0 6px;
|
||||
border-right: 1px solid $body-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
padding: 3px 8px 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.add {
|
||||
color: #777;
|
||||
padding: 4px 10px 0;
|
||||
}
|
||||
|
||||
.add, .item .body, .item .remove {
|
||||
&:hover { background: darken($body-bg3, 5%); }
|
||||
&:active { background: darken($body-bg3, 15%); }
|
||||
}
|
||||
}
|
||||
|
||||
hotkey-input-modal {
|
||||
.input {
|
||||
background: $input-bg;
|
||||
padding: 10px;
|
||||
font-size: 24px;
|
||||
line-height: 27px;
|
||||
height: 55px;
|
||||
|
||||
.stroke {
|
||||
background: $body-bg3;
|
||||
border: 1px solid $blue;
|
||||
border-radius: 3px;
|
||||
margin-right: 10px;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.timeout {
|
||||
background: $input-bg;
|
||||
|
||||
div {
|
||||
background: $blue;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
@brand-primary: #f7e61d;
|
||||
@brand-success: #42B500;
|
||||
@brand-info: #01BAEF;
|
||||
@brand-warning: #DB8A00;
|
||||
@brand-danger: #EF2F00;
|
||||
|
||||
@body-bg: #1D272D;
|
||||
@text-color: #aaa;
|
||||
|
||||
@font-family: "Source Sans Pro";
|
||||
@font-size: 14px;
|
||||
|
||||
@dark-border: rgba(0,0,0,.25);
|
||||
@light-border: rgba(255,255,255,.25);
|
||||
|
||||
@component-bg: #161d21;
|
@@ -1 +0,0 @@
|
||||
$tabs-height: 40px;
|
Reference in New Issue
Block a user