mirror of
https://github.com/Eugeny/tabby.git
synced 2025-09-23 00:26:04 +00:00
process completion notifications
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
import { Observable, Subject } from 'rxjs'
|
import { Observable, Subject } from 'rxjs'
|
||||||
import { ViewRef } from '@angular/core'
|
import { ViewRef } from '@angular/core'
|
||||||
|
|
||||||
|
export interface BaseTabProcess {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class BaseTabComponent {
|
export abstract class BaseTabComponent {
|
||||||
private static lastTabID = 0
|
private static lastTabID = 0
|
||||||
id: number
|
id: number
|
||||||
@@ -14,6 +18,7 @@ export abstract class BaseTabComponent {
|
|||||||
protected blurred = new Subject<void>()
|
protected blurred = new Subject<void>()
|
||||||
protected progress = new Subject<number>()
|
protected progress = new Subject<number>()
|
||||||
protected activity = new Subject<boolean>()
|
protected activity = new Subject<boolean>()
|
||||||
|
protected destroyed = new Subject<void>()
|
||||||
|
|
||||||
private progressClearTimeout: number
|
private progressClearTimeout: number
|
||||||
|
|
||||||
@@ -22,6 +27,7 @@ export abstract class BaseTabComponent {
|
|||||||
get titleChange$ (): Observable<string> { return this.titleChange }
|
get titleChange$ (): Observable<string> { return this.titleChange }
|
||||||
get progress$ (): Observable<number> { return this.progress }
|
get progress$ (): Observable<number> { return this.progress }
|
||||||
get activity$ (): Observable<boolean> { return this.activity }
|
get activity$ (): Observable<boolean> { return this.activity }
|
||||||
|
get destroyed$ (): Observable<void> { return this.destroyed }
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
this.id = BaseTabComponent.lastTabID++
|
this.id = BaseTabComponent.lastTabID++
|
||||||
@@ -66,6 +72,10 @@ export abstract class BaseTabComponent {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCurrentProcess (): Promise<BaseTabProcess> {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
async canClose (): Promise<boolean> {
|
async canClose (): Promise<boolean> {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -83,5 +93,7 @@ export abstract class BaseTabComponent {
|
|||||||
this.blurred.complete()
|
this.blurred.complete()
|
||||||
this.titleChange.complete()
|
this.titleChange.complete()
|
||||||
this.progress.complete()
|
this.progress.complete()
|
||||||
|
this.destroyed.next()
|
||||||
|
this.destroyed.complete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,57 +20,16 @@ export class TabHeaderComponent {
|
|||||||
@Input() progress: number
|
@Input() progress: number
|
||||||
@ViewChild('handle') handle: ElementRef
|
@ViewChild('handle') handle: ElementRef
|
||||||
|
|
||||||
private contextMenu: any
|
private completionNotificationEnabled = false
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
zone: NgZone,
|
|
||||||
electron: ElectronService,
|
|
||||||
public app: AppService,
|
public app: AppService,
|
||||||
|
private electron: ElectronService,
|
||||||
|
private zone: NgZone,
|
||||||
private hostApp: HostAppService,
|
private hostApp: HostAppService,
|
||||||
private ngbModal: NgbModal,
|
private ngbModal: NgbModal,
|
||||||
private parentDraggable: SortableComponent,
|
private parentDraggable: SortableComponent,
|
||||||
) {
|
) { }
|
||||||
this.contextMenu = electron.remote.Menu.buildFromTemplate([
|
|
||||||
{
|
|
||||||
label: 'Close',
|
|
||||||
click: () => {
|
|
||||||
zone.run(() => {
|
|
||||||
app.closeTab(this.tab, true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Close other tabs',
|
|
||||||
click: () => {
|
|
||||||
zone.run(() => {
|
|
||||||
for (let tab of app.tabs.filter(x => x !== this.tab)) {
|
|
||||||
app.closeTab(tab, true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Close tabs to the right',
|
|
||||||
click: () => {
|
|
||||||
zone.run(() => {
|
|
||||||
for (let tab of app.tabs.slice(app.tabs.indexOf(this.tab) + 1)) {
|
|
||||||
app.closeTab(tab, true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Close tabs to the left',
|
|
||||||
click: () => {
|
|
||||||
zone.run(() => {
|
|
||||||
for (let tab of app.tabs.slice(0, app.tabs.indexOf(this.tab))) {
|
|
||||||
app.closeTab(tab, true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
if (this.hostApp.platform === Platform.macOS) {
|
if (this.hostApp.platform === Platform.macOS) {
|
||||||
@@ -90,17 +49,86 @@ export class TabHeaderComponent {
|
|||||||
}).catch(() => null)
|
}).catch(() => null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('auxclick', ['$event']) onAuxClick ($event: MouseEvent): void {
|
@HostListener('auxclick', ['$event']) async onAuxClick ($event: MouseEvent) {
|
||||||
if ($event.which === 2) {
|
if ($event.which === 2) {
|
||||||
this.app.closeTab(this.tab, true)
|
this.app.closeTab(this.tab, true)
|
||||||
}
|
}
|
||||||
if ($event.which === 3) {
|
if ($event.which === 3) {
|
||||||
this.contextMenu.popup({
|
event.preventDefault()
|
||||||
|
|
||||||
|
let contextMenu = this.electron.remote.Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: 'Close',
|
||||||
|
click: () => this.zone.run(() => {
|
||||||
|
this.app.closeTab(this.tab, true)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Close other tabs',
|
||||||
|
click: () => this.zone.run(() => {
|
||||||
|
for (let tab of this.app.tabs.filter(x => x !== this.tab)) {
|
||||||
|
this.app.closeTab(tab, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Close tabs to the right',
|
||||||
|
click: () => this.zone.run(() => {
|
||||||
|
for (let tab of this.app.tabs.slice(this.app.tabs.indexOf(this.tab) + 1)) {
|
||||||
|
this.app.closeTab(tab, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Close tabs to the left',
|
||||||
|
click: () => this.zone.run(() => {
|
||||||
|
for (let tab of this.app.tabs.slice(0, this.app.tabs.indexOf(this.tab))) {
|
||||||
|
this.app.closeTab(tab, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
let process = await this.tab.getCurrentProcess()
|
||||||
|
if (process) {
|
||||||
|
contextMenu.append(new this.electron.MenuItem({
|
||||||
|
id: 'sep',
|
||||||
|
type: 'separator',
|
||||||
|
}))
|
||||||
|
contextMenu.append(new this.electron.MenuItem({
|
||||||
|
id: 'process-name',
|
||||||
|
enabled: false,
|
||||||
|
label: 'Current process: ' + process.name,
|
||||||
|
}))
|
||||||
|
contextMenu.append(new this.electron.MenuItem({
|
||||||
|
id: 'completion',
|
||||||
|
label: 'Notify when done',
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: this.completionNotificationEnabled,
|
||||||
|
click: () => this.zone.run(() => {
|
||||||
|
this.completionNotificationEnabled = !this.completionNotificationEnabled
|
||||||
|
|
||||||
|
if (this.completionNotificationEnabled) {
|
||||||
|
this.app.observeTabCompletion(this.tab).subscribe(() => {
|
||||||
|
new Notification('Process completed', {
|
||||||
|
body: process.name,
|
||||||
|
}).addEventListener('click', () => {
|
||||||
|
this.app.selectTab(this.tab)
|
||||||
|
})
|
||||||
|
this.completionNotificationEnabled = false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.app.stopObservingTabCompletion(this.tab)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
contextMenu.popup({
|
||||||
x: $event.pageX,
|
x: $event.pageX,
|
||||||
y: $event.pageY,
|
y: $event.pageY,
|
||||||
async: true,
|
async: true,
|
||||||
})
|
})
|
||||||
event.preventDefault()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Observable, Subject, AsyncSubject } from 'rxjs'
|
import { Observable, Subject, AsyncSubject } from 'rxjs'
|
||||||
|
import { takeUntil } from 'rxjs/operators'
|
||||||
import { Injectable, ComponentFactoryResolver, Injector } from '@angular/core'
|
import { Injectable, ComponentFactoryResolver, Injector } from '@angular/core'
|
||||||
import { BaseTabComponent } from '../components/baseTab.component'
|
import { BaseTabComponent } from '../components/baseTab.component'
|
||||||
import { Logger, LogService } from './log.service'
|
import { Logger, LogService } from './log.service'
|
||||||
@@ -7,6 +8,33 @@ import { HostAppService } from './hostApp.service'
|
|||||||
|
|
||||||
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
|
export declare type TabComponentType = new (...args: any[]) => BaseTabComponent
|
||||||
|
|
||||||
|
class CompletionObserver {
|
||||||
|
get done$ (): Observable<void> { return this.done }
|
||||||
|
get destroyed$ (): Observable<void> { return this.destroyed }
|
||||||
|
private done = new AsyncSubject<void>()
|
||||||
|
private destroyed = new AsyncSubject<void>()
|
||||||
|
private interval: number
|
||||||
|
|
||||||
|
constructor (private tab: BaseTabComponent) {
|
||||||
|
this.interval = setInterval(() => this.tick(), 1000)
|
||||||
|
this.tab.destroyed$.pipe(takeUntil(this.destroyed$)).subscribe(() => this.stop())
|
||||||
|
}
|
||||||
|
|
||||||
|
async tick () {
|
||||||
|
if (!(await this.tab.getCurrentProcess())) {
|
||||||
|
this.done.next(null)
|
||||||
|
this.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop () {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
this.destroyed.next(null)
|
||||||
|
this.destroyed.complete()
|
||||||
|
this.done.complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppService {
|
export class AppService {
|
||||||
tabs: BaseTabComponent[] = []
|
tabs: BaseTabComponent[] = []
|
||||||
@@ -20,6 +48,8 @@ export class AppService {
|
|||||||
private tabClosed = new Subject<BaseTabComponent>()
|
private tabClosed = new Subject<BaseTabComponent>()
|
||||||
private ready = new AsyncSubject<void>()
|
private ready = new AsyncSubject<void>()
|
||||||
|
|
||||||
|
private completionObservers = new Map<BaseTabComponent, CompletionObserver>()
|
||||||
|
|
||||||
get activeTabChange$ (): Observable<BaseTabComponent> { return this.activeTabChange }
|
get activeTabChange$ (): Observable<BaseTabComponent> { return this.activeTabChange }
|
||||||
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
|
get tabOpened$ (): Observable<BaseTabComponent> { return this.tabOpened }
|
||||||
get tabsChanged$ (): Observable<void> { return this.tabsChanged }
|
get tabsChanged$ (): Observable<void> { return this.tabsChanged }
|
||||||
@@ -133,4 +163,19 @@ export class AppService {
|
|||||||
this.ready.complete()
|
this.ready.complete()
|
||||||
this.hostApp.emitReady()
|
this.hostApp.emitReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observeTabCompletion (tab: BaseTabComponent): Observable<void> {
|
||||||
|
if (!this.completionObservers.has(tab)) {
|
||||||
|
let observer = new CompletionObserver(tab)
|
||||||
|
observer.destroyed$.subscribe(() => {
|
||||||
|
this.stopObservingTabCompletion(tab)
|
||||||
|
})
|
||||||
|
this.completionObservers.set(tab, observer)
|
||||||
|
}
|
||||||
|
return this.completionObservers.get(tab).done$
|
||||||
|
}
|
||||||
|
|
||||||
|
stopObservingTabCompletion (tab: BaseTabComponent) {
|
||||||
|
this.completionObservers.delete(tab)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { TouchBar, BrowserWindow, Menu } from 'electron'
|
import { TouchBar, BrowserWindow, Menu, MenuItem } from 'electron'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ElectronService {
|
export class ElectronService {
|
||||||
@@ -15,6 +15,7 @@ export class ElectronService {
|
|||||||
TouchBar: typeof TouchBar
|
TouchBar: typeof TouchBar
|
||||||
BrowserWindow: typeof BrowserWindow
|
BrowserWindow: typeof BrowserWindow
|
||||||
Menu: typeof Menu
|
Menu: typeof Menu
|
||||||
|
MenuItem: typeof MenuItem
|
||||||
private electron: any
|
private electron: any
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
@@ -31,6 +32,7 @@ export class ElectronService {
|
|||||||
this.TouchBar = this.remote.TouchBar
|
this.TouchBar = this.remote.TouchBar
|
||||||
this.BrowserWindow = this.remote.BrowserWindow
|
this.BrowserWindow = this.remote.BrowserWindow
|
||||||
this.Menu = this.remote.Menu
|
this.Menu = this.remote.Menu
|
||||||
|
this.MenuItem = this.remote.MenuItem
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteRequire (name: string): any {
|
remoteRequire (name: string): any {
|
||||||
|
@@ -2,7 +2,7 @@ import { Observable, Subject, Subscription } from 'rxjs'
|
|||||||
import { first } from 'rxjs/operators'
|
import { first } from 'rxjs/operators'
|
||||||
import { ToastrService } from 'ngx-toastr'
|
import { ToastrService } from 'ngx-toastr'
|
||||||
import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core'
|
import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core'
|
||||||
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, Platform } from 'terminus-core'
|
import { AppService, ConfigService, BaseTabComponent, BaseTabProcess, ElectronService, HostAppService, HotkeysService, Platform } from 'terminus-core'
|
||||||
|
|
||||||
import { IShell } from '../api'
|
import { IShell } from '../api'
|
||||||
import { Session, SessionsService } from '../services/sessions.service'
|
import { Session, SessionsService } from '../services/sessions.service'
|
||||||
@@ -347,6 +347,16 @@ export class TerminalTabComponent extends BaseTabComponent {
|
|||||||
this.frontend.setZoom(this.zoom)
|
this.frontend.setZoom(this.zoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCurrentProcess (): Promise<BaseTabProcess> {
|
||||||
|
let children = await this.session.getChildProcesses()
|
||||||
|
if (!children.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: children[0].command
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy () {
|
ngOnDestroy () {
|
||||||
this.frontend.detach(this.content.nativeElement)
|
this.frontend.detach(this.content.nativeElement)
|
||||||
this.detachTermContainerHandlers()
|
this.detachTermContainerHandlers()
|
||||||
|
Reference in New Issue
Block a user