From b3f15e27c612a73161c539394196a087e6891510 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Fri, 26 Oct 2018 14:03:46 +0200 Subject: [PATCH] process completion notifications --- .../src/components/baseTab.component.ts | 12 ++ .../src/components/tabHeader.component.ts | 124 +++++++++++------- terminus-core/src/services/app.service.ts | 45 +++++++ .../src/services/electron.service.ts | 4 +- .../src/components/terminalTab.component.ts | 12 +- 5 files changed, 147 insertions(+), 50 deletions(-) diff --git a/terminus-core/src/components/baseTab.component.ts b/terminus-core/src/components/baseTab.component.ts index c9993895..72214f3a 100644 --- a/terminus-core/src/components/baseTab.component.ts +++ b/terminus-core/src/components/baseTab.component.ts @@ -1,6 +1,10 @@ import { Observable, Subject } from 'rxjs' import { ViewRef } from '@angular/core' +export interface BaseTabProcess { + name: string +} + export abstract class BaseTabComponent { private static lastTabID = 0 id: number @@ -14,6 +18,7 @@ export abstract class BaseTabComponent { protected blurred = new Subject() protected progress = new Subject() protected activity = new Subject() + protected destroyed = new Subject() private progressClearTimeout: number @@ -22,6 +27,7 @@ export abstract class BaseTabComponent { get titleChange$ (): Observable { return this.titleChange } get progress$ (): Observable { return this.progress } get activity$ (): Observable { return this.activity } + get destroyed$ (): Observable { return this.destroyed } constructor () { this.id = BaseTabComponent.lastTabID++ @@ -66,6 +72,10 @@ export abstract class BaseTabComponent { return null } + async getCurrentProcess (): Promise { + return null + } + async canClose (): Promise { return true } @@ -83,5 +93,7 @@ export abstract class BaseTabComponent { this.blurred.complete() this.titleChange.complete() this.progress.complete() + this.destroyed.next() + this.destroyed.complete() } } diff --git a/terminus-core/src/components/tabHeader.component.ts b/terminus-core/src/components/tabHeader.component.ts index 6a55a740..65efb1b6 100644 --- a/terminus-core/src/components/tabHeader.component.ts +++ b/terminus-core/src/components/tabHeader.component.ts @@ -20,57 +20,16 @@ export class TabHeaderComponent { @Input() progress: number @ViewChild('handle') handle: ElementRef - private contextMenu: any + private completionNotificationEnabled = false constructor ( - zone: NgZone, - electron: ElectronService, public app: AppService, + private electron: ElectronService, + private zone: NgZone, private hostApp: HostAppService, private ngbModal: NgbModal, 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 () { if (this.hostApp.platform === Platform.macOS) { @@ -90,17 +49,86 @@ export class TabHeaderComponent { }).catch(() => null) } - @HostListener('auxclick', ['$event']) onAuxClick ($event: MouseEvent): void { + @HostListener('auxclick', ['$event']) async onAuxClick ($event: MouseEvent) { if ($event.which === 2) { this.app.closeTab(this.tab, true) } 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, y: $event.pageY, async: true, }) - event.preventDefault() } } } diff --git a/terminus-core/src/services/app.service.ts b/terminus-core/src/services/app.service.ts index 6784a785..c7cd8e21 100644 --- a/terminus-core/src/services/app.service.ts +++ b/terminus-core/src/services/app.service.ts @@ -1,4 +1,5 @@ import { Observable, Subject, AsyncSubject } from 'rxjs' +import { takeUntil } from 'rxjs/operators' import { Injectable, ComponentFactoryResolver, Injector } from '@angular/core' import { BaseTabComponent } from '../components/baseTab.component' import { Logger, LogService } from './log.service' @@ -7,6 +8,33 @@ import { HostAppService } from './hostApp.service' export declare type TabComponentType = new (...args: any[]) => BaseTabComponent +class CompletionObserver { + get done$ (): Observable { return this.done } + get destroyed$ (): Observable { return this.destroyed } + private done = new AsyncSubject() + private destroyed = new AsyncSubject() + 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() export class AppService { tabs: BaseTabComponent[] = [] @@ -20,6 +48,8 @@ export class AppService { private tabClosed = new Subject() private ready = new AsyncSubject() + private completionObservers = new Map() + get activeTabChange$ (): Observable { return this.activeTabChange } get tabOpened$ (): Observable { return this.tabOpened } get tabsChanged$ (): Observable { return this.tabsChanged } @@ -133,4 +163,19 @@ export class AppService { this.ready.complete() this.hostApp.emitReady() } + + observeTabCompletion (tab: BaseTabComponent): Observable { + 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) + } } diff --git a/terminus-core/src/services/electron.service.ts b/terminus-core/src/services/electron.service.ts index 51237a12..cf703324 100644 --- a/terminus-core/src/services/electron.service.ts +++ b/terminus-core/src/services/electron.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { TouchBar, BrowserWindow, Menu } from 'electron' +import { TouchBar, BrowserWindow, Menu, MenuItem } from 'electron' @Injectable() export class ElectronService { @@ -15,6 +15,7 @@ export class ElectronService { TouchBar: typeof TouchBar BrowserWindow: typeof BrowserWindow Menu: typeof Menu + MenuItem: typeof MenuItem private electron: any constructor () { @@ -31,6 +32,7 @@ export class ElectronService { this.TouchBar = this.remote.TouchBar this.BrowserWindow = this.remote.BrowserWindow this.Menu = this.remote.Menu + this.MenuItem = this.remote.MenuItem } remoteRequire (name: string): any { diff --git a/terminus-terminal/src/components/terminalTab.component.ts b/terminus-terminal/src/components/terminalTab.component.ts index f30762c8..c7b752e2 100644 --- a/terminus-terminal/src/components/terminalTab.component.ts +++ b/terminus-terminal/src/components/terminalTab.component.ts @@ -2,7 +2,7 @@ import { Observable, Subject, Subscription } from 'rxjs' import { first } from 'rxjs/operators' import { ToastrService } from 'ngx-toastr' import { Component, NgZone, Inject, Optional, ViewChild, HostBinding, Input } from '@angular/core' -import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, Platform } from 'terminus-core' +import { AppService, ConfigService, BaseTabComponent, BaseTabProcess, ElectronService, HostAppService, HotkeysService, Platform } from 'terminus-core' import { IShell } from '../api' import { Session, SessionsService } from '../services/sessions.service' @@ -347,6 +347,16 @@ export class TerminalTabComponent extends BaseTabComponent { this.frontend.setZoom(this.zoom) } + async getCurrentProcess (): Promise { + let children = await this.session.getChildProcesses() + if (!children.length) { + return null + } + return { + name: children[0].command + } + } + ngOnDestroy () { this.frontend.detach(this.content.nativeElement) this.detachTermContainerHandlers() -- GitLab