prepared terminus-core for typedocs

This commit is contained in:
Eugene Pankov
2019-03-07 01:51:15 +01:00
parent cf5e31be79
commit 2ed35cb400
47 changed files with 766 additions and 219 deletions

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

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

View File

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

View File

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

View File

@@ -1,6 +1,9 @@
import { BaseTabComponent } from '../components/baseTab.component'
import { TabHeaderComponent } from '../components/tabHeader.component'
/**
* Extend to add items to the tab header's context menu
*/
export abstract class TabContextMenuItemProvider {
weight = 0

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ import { BaseTabComponent } from './baseTab.component'
import { SafeModeModalComponent } from './safeModeModal.component'
import { AppService, IToolbarButton, ToolbarButtonProvider } from '../api'
/** @hidden */
@Component({
selector: 'app-root',
template: require('./appRoot.component.pug'),
@@ -126,6 +127,11 @@ export class AppRootComponent {
this.onGlobalHotkey()
})
this.hostApp.windowCloseRequest$.subscribe(async () => {
await this.app.closeAllTabs()
this.hostApp.closeWindow()
})
if (window['safeModeReason']) {
ngbModal.open(SafeModeModalComponent)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,15 +9,30 @@ import { TabRecoveryService } from '../services/tabRecovery.service'
export declare type SplitOrientation = 'v' | 'h'
export declare type SplitDirection = 'r' | 't' | 'b' | 'l'
/**
* Describes a horizontal or vertical split row or column
*/
export class SplitContainer {
orientation: SplitOrientation = 'h'
/**
* Children could be tabs or other containers
*/
children: (BaseTabComponent | SplitContainer)[] = []
/**
* Relative sizes of children, between 0 and 1. Total sum is 1
*/
ratios: number[] = []
x: number
y: number
w: number
h: number
/**
* @return Flat list of all tabs inside this container
*/
getAllTabs () {
let r = []
for (let child of this.children) {
@@ -30,6 +45,9 @@ export class SplitContainer {
return r
}
/**
* Remove unnecessarily nested child containers and renormalizes [[ratios]]
*/
normalize () {
for (let i = 0; i < this.children.length; i++) {
let child = this.children[i]
@@ -64,6 +82,9 @@ export class SplitContainer {
this.ratios = this.ratios.map(x => x / s)
}
/**
* Gets the left/top side offset for the given element index (between 0 and 1)
*/
getOffsetRatio (index: number): number {
let s = 0
for (let i = 0; i < index; i++) {
@@ -90,11 +111,22 @@ export class SplitContainer {
}
}
/**
* Represents a spanner (draggable border between two split areas)
*/
export interface SplitSpannerInfo {
container: SplitContainer
/**
* Number of the right/bottom split in the container
*/
index: number
}
/**
* Split tab is a tab that contains other tabs and allows further splitting them
* You'll mainly encounter it inside [[AppService]].tabs
*/
@Component({
selector: 'split-tab',
template: `
@@ -109,23 +141,43 @@ export interface SplitSpannerInfo {
styles: [require('./splitTab.component.scss')],
})
export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDestroy {
/** @hidden */
@ViewChild('vc', { read: ViewContainerRef }) viewContainer: ViewContainerRef
/**
* Top-level split container
*/
root: SplitContainer
/** @hidden */
_recoveredState: any
/** @hidden */
_spanners: SplitSpannerInfo[] = []
private focusedTab: BaseTabComponent
private hotkeysSubscription: Subscription
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
protected tabAdded = new Subject<BaseTabComponent>()
protected tabRemoved = new Subject<BaseTabComponent>()
protected splitAdjusted = new Subject<SplitSpannerInfo>()
protected focusChanged = new Subject<BaseTabComponent>()
private tabAdded = new Subject<BaseTabComponent>()
private tabRemoved = new Subject<BaseTabComponent>()
private splitAdjusted = new Subject<SplitSpannerInfo>()
private focusChanged = new Subject<BaseTabComponent>()
get tabAdded$ (): Observable<BaseTabComponent> { return this.tabAdded }
get tabRemoved$ (): Observable<BaseTabComponent> { return this.tabRemoved }
/**
* Fired when split ratio is changed for a given spanner
*/
get splitAdjusted$ (): Observable<SplitSpannerInfo> { return this.splitAdjusted }
/**
* Fired when a different sub-tab gains focus
*/
get focusChanged$ (): Observable<BaseTabComponent> { return this.focusChanged }
/** @hidden */
constructor (
private hotkeys: HotkeysService,
private tabsService: TabsService,
@@ -174,6 +226,7 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
})
}
/** @hidden */
async ngOnInit () {
if (this._recoveredState) {
await this.recoverContainer(this.root, this._recoveredState)
@@ -185,10 +238,12 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
}
}
/** @hidden */
ngOnDestroy () {
this.hotkeysSubscription.unsubscribe()
}
/** @returns Flat list of all sub-tabs */
getAllTabs () {
return this.root.getAllTabs()
}
@@ -211,6 +266,9 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
this.layout()
}
/**
* Focuses the first available tab inside the given [[SplitContainer]]
*/
focusAnyIn (parent: BaseTabComponent | SplitContainer) {
if (!parent) {
return
@@ -222,13 +280,16 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
}
}
addTab (tab: BaseTabComponent, relative: BaseTabComponent, dir: SplitDirection) {
/**
* Inserts a new `tab` to the `side` of the `relative` tab
*/
addTab (tab: BaseTabComponent, relative: BaseTabComponent, side: SplitDirection) {
let target = this.getParentOf(relative) || this.root
let insertIndex = target.children.indexOf(relative)
if (
(target.orientation === 'v' && ['l', 'r'].includes(dir)) ||
(target.orientation === 'h' && ['t', 'b'].includes(dir))
(target.orientation === 'v' && ['l', 'r'].includes(side)) ||
(target.orientation === 'h' && ['t', 'b'].includes(side))
) {
let newContainer = new SplitContainer()
newContainer.orientation = (target.orientation === 'v') ? 'h' : 'v'
@@ -242,7 +303,7 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
if (insertIndex === -1) {
insertIndex = 0
} else {
insertIndex += (dir === 'l' || dir === 't') ? 0 : 1
insertIndex += (side === 'l' || side === 't') ? 0 : 1
}
for (let i = 0; i < target.children.length; i++) {
@@ -278,6 +339,9 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
}
}
/**
* Moves focus in the given direction
*/
navigate (dir: SplitDirection) {
let rel: BaseTabComponent | SplitContainer = this.focusedTab
let parent = this.getParentOf(rel)
@@ -309,6 +373,9 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
this.addTab(newTab, tab, dir)
}
/**
* @returns the immediate parent of `tab`
*/
getParentOf (tab: BaseTabComponent | SplitContainer, root?: SplitContainer): SplitContainer {
root = root || this.root
for (let child of root.children) {
@@ -325,18 +392,22 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
return null
}
/** @hidden */
async canClose (): Promise<boolean> {
return !(await Promise.all(this.getAllTabs().map(x => x.canClose()))).some(x => !x)
}
/** @hidden */
async getRecoveryToken (): Promise<any> {
return this.root.serialize()
}
/** @hidden */
async getCurrentProcess (): Promise<BaseTabProcess> {
return (await Promise.all(this.getAllTabs().map(x => x.getCurrentProcess()))).find(x => !!x)
}
/** @hidden */
onSpannerAdjusted (spanner: SplitSpannerInfo) {
this.layout()
this.splitAdjusted.next(spanner)
@@ -433,6 +504,7 @@ export class SplitTabComponent extends BaseTabComponent implements OnInit, OnDes
}
}
/** @hidden */
@Injectable()
export class SplitTabRecoveryProvider extends TabRecoveryProvider {
async recover (recoveryToken: any): Promise<RecoveredTab> {

View File

@@ -1,6 +1,7 @@
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
import { SplitContainer } from './splitTab.component'
/** @hidden */
@Component({
selector: 'split-tab-spanner',
template: '',

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import { ElectronService } from '../services/electron.service'
import { AppService } from '../services/app.service'
import { HostAppService, Platform } from '../services/hostApp.service'
/** @hidden */
@Component({
selector: 'tab-header',
template: require('./tabHeader.component.pug'),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,8 +6,6 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { PerfectScrollbarModule, PERFECT_SCROLLBAR_CONFIG } from 'ngx-perfect-scrollbar'
import { DndModule } from 'ng2-dnd'
import { AppHotkeyProvider } from './services/hotkeys.service'
import { AppRootComponent } from './components/appRoot.component'
import { CheckboxComponent } from './components/checkbox.component'
import { TabBodyComponent } from './components/tabBody.component'
@@ -31,6 +29,7 @@ import { TabRecoveryProvider } from './api/tabRecovery'
import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
import { CoreConfigProvider } from './config'
import { AppHotkeyProvider } from './hotkeys'
import { TaskCompletionContextMenu, CommonOptionsContextMenu, CloseContextMenu } from './tabContextMenu'
import 'perfect-scrollbar/css/perfect-scrollbar.css'
@@ -49,6 +48,7 @@ const PROVIDERS = [
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } }
]
/** @hidden */
@NgModule({
imports: [
BrowserModule,

View File

@@ -3,7 +3,6 @@ import { takeUntil } from 'rxjs/operators'
import { Injectable } from '@angular/core'
import { BaseTabComponent } from '../components/baseTab.component'
import { SplitTabComponent } from '../components/splitTab.component'
import { Logger, LogService } from './log.service'
import { ConfigService } from './config.service'
import { HostAppService } from './hostApp.service'
import { TabRecoveryService } from './tabRecovery.service'
@@ -39,9 +38,11 @@ class CompletionObserver {
@Injectable({ providedIn: 'root' })
export class AppService {
tabs: BaseTabComponent[] = []
activeTab: BaseTabComponent
lastTabIndex = 0
logger: Logger
get activeTab (): BaseTabComponent { return this._activeTab }
private lastTabIndex = 0
private _activeTab: BaseTabComponent
private activeTabChange = new Subject<BaseTabComponent>()
private tabsChanged = new Subject<void>()
@@ -55,19 +56,17 @@ export class AppService {
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
get tabsChanged$ (): Observable<void> { return this.tabsChanged }
get tabClosed$ (): Observable<BaseTabComponent> { return this.tabClosed }
/** Fires once when the app is ready */
get ready$ (): Observable<void> { return this.ready }
/** @hidden */
constructor (
private config: ConfigService,
private hostApp: HostAppService,
private tabRecovery: TabRecoveryService,
private tabsService: TabsService,
log: LogService,
) {
this.logger = log.create('app')
this.hostApp.windowCloseRequest$.subscribe(() => this.closeWindow())
this.tabRecovery.recoverTabs().then(tabs => {
for (let tab of tabs) {
this.openNewTabRaw(tab.type, tab.options)
@@ -82,7 +81,7 @@ export class AppService {
})
}
addTabRaw (tab: BaseTabComponent) {
private addTabRaw (tab: BaseTabComponent) {
this.tabs.push(tab)
this.selectTab(tab)
this.tabsChanged.next()
@@ -93,7 +92,7 @@ export class AppService {
})
tab.titleChange$.subscribe(title => {
if (tab === this.activeTab) {
if (tab === this._activeTab) {
this.hostApp.setTitle(title)
}
})
@@ -101,7 +100,7 @@ export class AppService {
tab.destroyed$.subscribe(() => {
let newIndex = Math.max(0, this.tabs.indexOf(tab) - 1)
this.tabs = this.tabs.filter((x) => x !== tab)
if (tab === this.activeTab) {
if (tab === this._activeTab) {
this.selectTab(this.tabs[newIndex])
}
this.tabsChanged.next()
@@ -109,12 +108,20 @@ export class AppService {
})
}
/**
* Adds a new tab **without** wrapping it in a SplitTabComponent
* @param inputs Properties to be assigned on the new tab component instance
*/
openNewTabRaw (type: TabComponentType, inputs?: any): BaseTabComponent {
let tab = this.tabsService.create(type, inputs)
this.addTabRaw(tab)
return tab
}
/**
* Adds a new tab while wrapping it in a SplitTabComponent
* @param inputs Properties to be assigned on the new tab component instance
*/
openNewTab (type: TabComponentType, inputs?: any): BaseTabComponent {
let splitTab = this.tabsService.create(SplitTabComponent) as SplitTabComponent
let tab = this.tabsService.create(type, inputs)
@@ -124,29 +131,30 @@ export class AppService {
}
selectTab (tab: BaseTabComponent) {
if (this.activeTab === tab) {
this.activeTab.emitFocused()
if (this._activeTab === tab) {
this._activeTab.emitFocused()
return
}
if (this.tabs.includes(this.activeTab)) {
this.lastTabIndex = this.tabs.indexOf(this.activeTab)
if (this.tabs.includes(this._activeTab)) {
this.lastTabIndex = this.tabs.indexOf(this._activeTab)
} else {
this.lastTabIndex = null
}
if (this.activeTab) {
this.activeTab.clearActivity()
this.activeTab.emitBlurred()
if (this._activeTab) {
this._activeTab.clearActivity()
this._activeTab.emitBlurred()
}
this.activeTab = tab
this._activeTab = tab
this.activeTabChange.next(tab)
if (this.activeTab) {
if (this._activeTab) {
setImmediate(() => {
this.activeTab.emitFocused()
this._activeTab.emitFocused()
})
this.hostApp.setTitle(this.activeTab.title)
this.hostApp.setTitle(this._activeTab.title)
}
}
/** Switches between the current tab and the previously active one */
toggleLastTab () {
if (!this.lastTabIndex || this.lastTabIndex >= this.tabs.length) {
this.lastTabIndex = 0
@@ -156,7 +164,7 @@ export class AppService {
nextTab () {
if (this.tabs.length > 1) {
let tabIndex = this.tabs.indexOf(this.activeTab)
let tabIndex = this.tabs.indexOf(this._activeTab)
if (tabIndex < this.tabs.length - 1) {
this.selectTab(this.tabs[tabIndex + 1])
} else if (this.config.store.appearance.cycleTabs) {
@@ -167,7 +175,7 @@ export class AppService {
previousTab () {
if (this.tabs.length > 1) {
let tabIndex = this.tabs.indexOf(this.activeTab)
let tabIndex = this.tabs.indexOf(this._activeTab)
if (tabIndex > 0) {
this.selectTab(this.tabs[tabIndex - 1])
} else if (this.config.store.appearance.cycleTabs) {
@@ -176,6 +184,7 @@ export class AppService {
}
}
/** @hidden */
emitTabsChanged () {
this.tabsChanged.next()
}
@@ -197,7 +206,7 @@ export class AppService {
}
}
async closeWindow () {
async closeAllTabs () {
for (let tab of this.tabs) {
if (!await tab.canClose()) {
return
@@ -206,15 +215,19 @@ export class AppService {
for (let tab of this.tabs) {
tab.destroy()
}
this.hostApp.closeWindow()
}
/** @hidden */
emitReady () {
this.ready.next(null)
this.ready.complete()
this.hostApp.emitReady()
}
/**
* Returns an observable that fires once
* the tab's internal "process" (see [[BaseTabProcess]]) completes
*/
observeTabCompletion (tab: BaseTabComponent): Observable<void> {
if (!this.completionObservers.has(tab)) {
let observer = new CompletionObserver(tab)

View File

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

View File

@@ -10,6 +10,7 @@ export interface IScreen {
@Injectable({ providedIn: 'root' })
export class DockingService {
/** @hidden */
constructor (
private electron: ElectronService,
private config: ConfigService,
@@ -78,7 +79,7 @@ export class DockingService {
})
}
repositionWindow () {
private repositionWindow () {
let [x, y] = this.hostApp.getWindow().getPosition()
for (let screen of this.electron.screen.getAllDisplays()) {
let bounds = screen.bounds

View File

@@ -24,6 +24,7 @@ export class ElectronService {
MenuItem: typeof MenuItem
private electron: any
/** @hidden */
constructor () {
this.electron = require('electron')
this.remote = this.electron.remote
@@ -42,18 +43,9 @@ export class ElectronService {
this.MenuItem = this.remote.MenuItem
}
remoteRequire (name: string): any {
return this.remote.require(name)
}
remoteRequirePluginModule (plugin: string, module: string, globals: any): any {
return this.remoteRequire(this.remoteResolvePluginModule(plugin, module, globals))
}
remoteResolvePluginModule (plugin: string, module: string, globals: any): any {
return globals.require.resolve(`${plugin}/node_modules/${module}`)
}
/**
* Removes OS focus from Terminus' window
*/
loseFocus () {
if (process.platform === 'darwin') {
this.remote.Menu.sendActionToFirstResponder('hide:')

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ export class Logger {
private name: string,
) {}
doLog (level: string, ...args: any[]) {
private doLog (level: string, ...args: any[]) {
console[level](`%c[${this.name}]`, 'color: #aaa', ...args)
if (this.winstonLogger) {
this.winstonLogger[level](...args)
@@ -57,6 +57,7 @@ export class Logger {
export class LogService {
private log: any
/** @hidden */
constructor (electron: ElectronService) {
this.log = initializeWinston(electron)
}

View File

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

View File

@@ -4,6 +4,7 @@ import { BaseTabComponent } from '../components/baseTab.component'
import { Logger, LogService } from '../services/log.service'
import { ConfigService } from '../services/config.service'
/** @hidden */
@Injectable({ providedIn: 'root' })
export class TabRecoveryService {
logger: Logger

View File

@@ -6,12 +6,16 @@ export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
@Injectable({ providedIn: 'root' })
export class TabsService {
/** @hidden */
constructor (
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector,
private tabRecovery: TabRecoveryService,
) { }
/**
* Instantiates a tab component and assigns given inputs
*/
create (type: TabComponentType, inputs?: any): BaseTabComponent {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(type)
let componentRef = componentFactory.create(this.injector)
@@ -21,6 +25,9 @@ export class TabsService {
return tab
}
/**
* Duplicates an existing tab instance (using the tab recovery system)
*/
async duplicate (tab: BaseTabComponent): Promise<BaseTabComponent> {
let token = await tab.getRecoveryToken()
if (!token) {
@@ -32,5 +39,4 @@ export class TabsService {
}
return null
}
}

View File

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

View File

@@ -6,6 +6,7 @@ import { ElectronService } from './electron.service'
import { HostAppService, Platform } from './hostApp.service'
import { IToolbarButton, ToolbarButtonProvider } from '../api'
/** @hidden */
@Injectable({ providedIn: 'root' })
export class TouchbarService {
private tabsSegmentedControl: TouchBarSegmentedControl

View File

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

View File

@@ -4,6 +4,7 @@ import { BaseTabComponent } from './components/baseTab.component'
import { TabHeaderComponent } from './components/tabHeader.component'
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
/** @hidden */
@Injectable()
export class CloseContextMenu extends TabContextMenuItemProvider {
weight = -5
@@ -61,6 +62,7 @@ const COLORS = [
{ name: 'Yellow', value: '#ffd500' },
]
/** @hidden */
@Injectable()
export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
weight = -1
@@ -98,6 +100,7 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
}
}
/** @hidden */
@Injectable()
export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
constructor (
@@ -121,7 +124,7 @@ export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
type: 'checkbox',
checked: (tab as any).__completionNotificationEnabled,
click: () => this.zone.run(() => {
;(tab as any).__completionNotificationEnabled = !(tab as any).__completionNotificationEnabled
(tab as any).__completionNotificationEnabled = !(tab as any).__completionNotificationEnabled
if ((tab as any).__completionNotificationEnabled) {
this.app.observeTabCompletion(tab).subscribe(() => {

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'
import { Theme } from './api'
/** @hidden */
@Injectable()
export class StandardTheme extends Theme {
name = 'Standard'
@@ -8,6 +9,7 @@ export class StandardTheme extends Theme {
terminalBackground = '#222a33'
}
/** @hidden */
@Injectable()
export class StandardCompactTheme extends Theme {
name = 'Compact'
@@ -15,6 +17,7 @@ export class StandardCompactTheme extends Theme {
terminalBackground = '#222a33'
}
/** @hidden */
@Injectable()
export class PaperTheme extends Theme {
name = 'Paper'