diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index b8cedd5e..75e01db4 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -28,6 +28,9 @@ jobs: cd app ../node_modules/.bin/patch-package cd .. + cd terminus-terminal + ../node_modules/.bin/patch-package + cd .. - name: Build native deps run: scripts/build-native.js diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 2ac62f6b..fe6ce80b 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -32,6 +32,9 @@ jobs: cd app ../node_modules/.bin/patch-package cd .. + cd terminus-terminal + ../node_modules/.bin/patch-package + cd .. - name: Build native deps run: scripts/build-native.js diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 0bb5bdbb..f44141d9 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -21,6 +21,11 @@ jobs: npm i -g yarn@1.19.1 yarn node scripts/build-native.js + + cd terminus-terminal + ../node_modules/.bin/patch-package + cd .. + yarn run build node scripts/prepackage-plugins.js diff --git a/app/lib/window.ts b/app/lib/window.ts index 1955abc9..1d413265 100644 --- a/app/lib/window.ts +++ b/app/lib/window.ts @@ -309,20 +309,6 @@ export class Window { this.window.focus() }) - ipcMain.on('window-maximize', event => { - if (!this.window || event.sender !== this.window.webContents) { - return - } - this.window.maximize() - }) - - ipcMain.on('window-unmaximize', event => { - if (!this.window || event.sender !== this.window.webContents) { - return - } - this.window.unmaximize() - }) - ipcMain.on('window-toggle-maximize', event => { if (!this.window || event.sender !== this.window.webContents) { return diff --git a/terminus-core/src/api/hostWindow.ts b/terminus-core/src/api/hostWindow.ts new file mode 100644 index 00000000..e9579080 --- /dev/null +++ b/terminus-core/src/api/hostWindow.ts @@ -0,0 +1,12 @@ +import { Observable } from 'rxjs' + +export abstract class HostWindowService { + abstract readonly closeRequest$: Observable + abstract readonly isFullscreen: boolean + abstract reload (): void + abstract setTitle (title?: string): void + abstract toggleFullscreen (): void + abstract minimize (): void + abstract toggleMaximize (): void + abstract close (): void +} diff --git a/terminus-core/src/api/index.ts b/terminus-core/src/api/index.ts index ea2ae531..0618d379 100644 --- a/terminus-core/src/api/index.ts +++ b/terminus-core/src/api/index.ts @@ -10,9 +10,10 @@ export { Theme } from './theme' export { TabContextMenuItemProvider } from './tabContextMenuProvider' export { SelectorOption } from './selector' export { CLIHandler, CLIEvent } from './cli' -export { PlatformService, ClipboardContent } from './platform' +export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions } from './platform' export { MenuItemOptions } from './menu' export { BootstrapData, BOOTSTRAP_DATA } from './mainProcess' +export { HostWindowService } from './hostWindow' export { AppService } from '../services/app.service' export { ConfigService } from '../services/config.service' diff --git a/terminus-core/src/api/platform.ts b/terminus-core/src/api/platform.ts index b8ee0cb7..79d6fe40 100644 --- a/terminus-core/src/api/platform.ts +++ b/terminus-core/src/api/platform.ts @@ -6,9 +6,22 @@ export interface ClipboardContent { html?: string } +export interface MessageBoxOptions { + type: 'warning'|'error' + message: string + detail?: string + buttons: string[] + defaultId?: number +} + +export interface MessageBoxResult { + response: number +} + export abstract class PlatformService { supportsWindowControls = false + abstract readClipboard (): string abstract setClipboard (content: ClipboardContent): void abstract loadConfig (): Promise abstract saveConfig (content: string): Promise @@ -66,4 +79,5 @@ export abstract class PlatformService { abstract openExternal (url: string): void abstract listFonts (): Promise abstract popupContextMenu (menu: MenuItemOptions[], event?: MouseEvent): void + abstract showMessageBox (options: MessageBoxOptions): Promise } diff --git a/terminus-core/src/components/appRoot.component.pug b/terminus-core/src/components/appRoot.component.pug index 40cf2d16..11bdf211 100644 --- a/terminus-core/src/components/appRoot.component.pug +++ b/terminus-core/src/components/appRoot.component.pug @@ -1,15 +1,16 @@ title-bar( - *ngIf='!hostApp.isFullScreen && config.store.appearance.frame == "full" && config.store.appearance.dock == "off"', - [class.inset]='hostApp.platform == Platform.macOS && !hostApp.isFullScreen' + *ngIf='ready && !hostWindow.isFullScreen && config.store.appearance.frame == "full" && config.store.appearance.dock == "off"', + [class.inset]='hostApp.platform == Platform.macOS && !hostWindow.isFullScreen' ) .content( + *ngIf='ready', [class.tabs-on-top]='config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "left"', [class.tabs-on-side]='hasVerticalTabs()', ) .tab-bar .inset.background(*ngIf='hostApp.platform == Platform.macOS \ - && !hostApp.isFullScreen \ + && !hostWindow.isFullScreen \ && config.store.appearance.frame == "thin" \ && (config.store.appearance.tabsLocation == "top" || config.store.appearance.tabsLocation == "left")') .tabs( diff --git a/terminus-core/src/components/appRoot.component.ts b/terminus-core/src/components/appRoot.component.ts index 902aa907..16bde733 100644 --- a/terminus-core/src/components/appRoot.component.ts +++ b/terminus-core/src/components/appRoot.component.ts @@ -12,7 +12,7 @@ import { UpdaterService } from '../services/updater.service' import { BaseTabComponent } from './baseTab.component' import { SafeModeModalComponent } from './safeModeModal.component' -import { AppService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api' +import { AppService, HostWindowService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api' /** @hidden */ @Component({ @@ -66,6 +66,7 @@ export class AppRootComponent { private constructor ( private hotkeys: HotkeysService, private updater: UpdaterService, + public hostWindow: HostWindowService, public hostApp: HostAppService, public config: ConfigService, public app: AppService, @@ -78,9 +79,6 @@ export class AppRootComponent { this.logger = log.create('main') this.logger.info('v', platform.getAppVersion()) - this.leftToolbarButtons = this.getToolbarButtons(false) - this.rightToolbarButtons = this.getToolbarButtons(true) - this.updateIcon = require('../icons/gift.svg') this.hotkeys.matchedHotkey.subscribe((hotkey: string) => { @@ -114,7 +112,7 @@ export class AppRootComponent { } } if (hotkey === 'toggle-fullscreen') { - this.hostApp.toggleFullscreen() + hostWindow.toggleFullscreen() } }) @@ -126,14 +124,6 @@ export class AppRootComponent { ngbModal.open(SafeModeModalComponent) } - setInterval(() => { - if (this.config.store.enableAutomaticUpdates) { - this.updater.check().then(available => { - this.updatesAvailable = available - }) - } - }, 3600 * 12 * 1000) - this.app.tabOpened$.subscribe(tab => { this.unsortedTabs.push(tab) this.noTabs = false @@ -143,12 +133,26 @@ export class AppRootComponent { this.unsortedTabs = this.unsortedTabs.filter(x => x !== tab) this.noTabs = app.tabs.length === 0 }) + + config.ready$.toPromise().then(() => { + this.leftToolbarButtons = this.getToolbarButtons(false) + this.rightToolbarButtons = this.getToolbarButtons(true) + + setInterval(() => { + if (this.config.store.enableAutomaticUpdates) { + this.updater.check().then(available => { + this.updatesAvailable = available + }) + } + }, 3600 * 12 * 1000) + }) } async ngOnInit () { - this.ready = true - - this.app.emitReady() + this.config.ready$.toPromise().then(() => { + this.ready = true + this.app.emitReady() + }) } @HostListener('dragover') diff --git a/terminus-core/src/components/welcomeTab.component.ts b/terminus-core/src/components/welcomeTab.component.ts index c125ec6e..025c7574 100644 --- a/terminus-core/src/components/welcomeTab.component.ts +++ b/terminus-core/src/components/welcomeTab.component.ts @@ -2,7 +2,7 @@ import { Component } from '@angular/core' import { BaseTabComponent } from './baseTab.component' import { ConfigService } from '../services/config.service' -import { HostAppService } from '../services/hostApp.service' +import { HostWindowService } from '../api/hostWindow' /** @hidden */ @Component({ @@ -16,7 +16,7 @@ export class WelcomeTabComponent extends BaseTabComponent { enableGlobalHotkey = true constructor ( - private hostApp: HostAppService, + private hostWindow: HostWindowService, public config: ConfigService, ) { super() @@ -38,6 +38,6 @@ export class WelcomeTabComponent extends BaseTabComponent { this.config.store.hotkeys['toggle-window'] = [] } this.config.save() - this.hostApp.getWindow().reload() + this.hostWindow.reload() } } diff --git a/terminus-core/src/index.ts b/terminus-core/src/index.ts index 92e53807..49db2b9a 100644 --- a/terminus-core/src/index.ts +++ b/terminus-core/src/index.ts @@ -1,4 +1,4 @@ -import { NgModule, ModuleWithProviders, APP_INITIALIZER } from '@angular/core' +import { NgModule, ModuleWithProviders } from '@angular/core' import { BrowserModule } from '@angular/platform-browser' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { FormsModule } from '@angular/forms' @@ -38,10 +38,6 @@ import { LastCLIHandler } from './cli' import 'perfect-scrollbar/css/perfect-scrollbar.css' import 'ng2-dnd/bundles/style.css' -function initialize (config: ConfigService) { - return () => config.ready$.toPromise() -} - const PROVIDERS = [ { provide: HotkeyProvider, useClass: AppHotkeyProvider, multi: true }, { provide: Theme, useClass: StandardTheme, multi: true }, @@ -54,7 +50,6 @@ const PROVIDERS = [ { provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true }, { provide: CLIHandler, useClass: LastCLIHandler, multi: true }, { provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } }, - { provide: APP_INITIALIZER, useFactory: initialize, deps: [ConfigService], multi: true }, ] /** @hidden */ diff --git a/terminus-core/src/services/app.service.ts b/terminus-core/src/services/app.service.ts index eade55ee..fc07e483 100644 --- a/terminus-core/src/services/app.service.ts +++ b/terminus-core/src/services/app.service.ts @@ -10,6 +10,7 @@ import { SelectorModalComponent } from '../components/selectorModal.component' import { SelectorOption } from '../api/selector' import { RecoveryToken } from '../api/tabRecovery' import { BootstrapData, BOOTSTRAP_DATA } from '../api/mainProcess' +import { HostWindowService } from '../api/hostWindow' import { ConfigService } from './config.service' import { HostAppService } from './hostApp.service' @@ -73,6 +74,7 @@ export class AppService { private constructor ( private config: ConfigService, private hostApp: HostAppService, + private hostWindow: HostWindowService, private tabRecovery: TabRecoveryService, private tabsService: TabsService, private ngbModal: NgbModal, @@ -127,7 +129,7 @@ export class AppService { tab.titleChange$.subscribe(title => { if (tab === this._activeTab) { - this.hostApp.setTitle(title) + this.hostWindow.setTitle(title) } }) @@ -205,7 +207,7 @@ export class AppService { setImmediate(() => { this._activeTab?.emitFocused() }) - this.hostApp.setTitle(this._activeTab?.title) + this.hostWindow.setTitle(this._activeTab?.title) } getParentTab (tab: BaseTabComponent): SplitTabComponent|null { @@ -332,7 +334,7 @@ export class AppService { this.tabRecovery.enabled = false await this.tabRecovery.saveTabs(this.tabs) if (await this.closeAllTabs()) { - this.hostApp.closeWindow() + this.hostWindow.close() } else { this.tabRecovery.enabled = true } diff --git a/terminus-core/src/services/config.service.ts b/terminus-core/src/services/config.service.ts index d1e42e67..8dc53452 100644 --- a/terminus-core/src/services/config.service.ts +++ b/terminus-core/src/services/config.service.ts @@ -89,9 +89,9 @@ export class ConfigService { restartRequested: boolean /** Fires once when the config is loaded */ - get ready$ (): Observable { return this.ready } + get ready$ (): Observable { return this.ready } - private ready = new AsyncSubject() + private ready = new AsyncSubject() private changed = new Subject() private _store: any private defaults: any @@ -213,7 +213,7 @@ export class ConfigService { private async init () { await this.load() - this.ready.next() + this.ready.next(true) this.ready.complete() this.hostApp.configChangeBroadcast$.subscribe(() => { diff --git a/terminus-core/src/services/electron.service.ts b/terminus-core/src/services/electron.service.ts index c562a66f..54105642 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 { App, IpcRenderer, Shell, Dialog, Clipboard, GlobalShortcut, Screen, Remote, AutoUpdater, TouchBar, BrowserWindow, Menu, MenuItem, NativeImage, MessageBoxOptions } from 'electron' +import { App, IpcRenderer, Shell, Dialog, Clipboard, GlobalShortcut, Screen, Remote, AutoUpdater, TouchBar, BrowserWindow, Menu, MenuItem, NativeImage } from 'electron' import * as remote from '@electron/remote' export interface MessageBoxResponse { @@ -44,11 +44,4 @@ export class ElectronService { this.Menu = remote.Menu this.MenuItem = remote.MenuItem } - - async showMessageBox ( - browserWindow: BrowserWindow, - options: MessageBoxOptions - ): Promise { - return this.dialog.showMessageBox(browserWindow, options) - } } diff --git a/terminus-core/src/services/hostApp.service.ts b/terminus-core/src/services/hostApp.service.ts index 1d8a05db..bb9d3248 100644 --- a/terminus-core/src/services/hostApp.service.ts +++ b/terminus-core/src/services/hostApp.service.ts @@ -33,7 +33,6 @@ export class HostAppService { * Fired once the window is visible */ shown = new EventEmitter() - isFullScreen = false isPortable = !!process.env.PORTABLE_EXECUTABLE_FILE private preferencesMenu = new Subject() @@ -92,14 +91,6 @@ export class HostAppService { this.logger.error('Unhandled exception:', err) }) - electron.ipcRenderer.on('host:window-enter-full-screen', () => this.zone.run(() => { - this.isFullScreen = true - })) - - electron.ipcRenderer.on('host:window-leave-full-screen', () => this.zone.run(() => { - this.isFullScreen = false - })) - electron.ipcRenderer.on('host:window-shown', () => { this.zone.run(() => this.shown.emit()) }) @@ -163,11 +154,6 @@ export class HostAppService { this.electron.ipcRenderer.send('app:new-window') } - toggleFullscreen (): void { - const window = this.getWindow() - window.setFullScreen(!this.isFullScreen) - } - openDevTools (): void { this.getWindow().webContents.openDevTools({ mode: 'undocked' }) } @@ -176,22 +162,6 @@ export class HostAppService { this.electron.ipcRenderer.send('window-focus') } - minimize (): void { - this.electron.ipcRenderer.send('window-minimize') - } - - maximize (): void { - this.electron.ipcRenderer.send('window-maximize') - } - - unmaximize (): void { - this.electron.ipcRenderer.send('window-unmaximize') - } - - toggleMaximize (): void { - this.electron.ipcRenderer.send('window-toggle-maximize') - } - setBounds (bounds: Bounds): void { this.electron.ipcRenderer.send('window-set-bounds', bounds) } @@ -200,10 +170,6 @@ export class HostAppService { this.electron.ipcRenderer.send('window-set-always-on-top', flag) } - setTitle (title?: string): void { - this.electron.ipcRenderer.send('window-set-title', title ?? 'Terminus') - } - setTouchBar (touchBar: TouchBar): void { this.getWindow().setTouchBar(touchBar) } @@ -223,10 +189,6 @@ export class HostAppService { this.electron.ipcRenderer.send('window-bring-to-front') } - closeWindow (): void { - this.electron.ipcRenderer.send('window-close') - } - registerGlobalHotkey (specs: string[]): void { this.electron.ipcRenderer.send('app:register-global-hotkey', specs) } diff --git a/terminus-electron/src/index.ts b/terminus-electron/src/index.ts index 8b9f58fb..0c9f6ade 100644 --- a/terminus-electron/src/index.ts +++ b/terminus-electron/src/index.ts @@ -1,18 +1,20 @@ import { NgModule } from '@angular/core' -import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, ElectronService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild } from 'terminus-core' +import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, ElectronService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild, HostWindowService } from 'terminus-core' import { TerminalColorSchemeProvider } from 'terminus-terminal' import { HyperColorSchemes } from './colorSchemes' -import { ElectronPlatformService } from './services/platform' +import { ElectronPlatformService } from './services/platform.service' import { ElectronLogService } from './services/log.service' import { ElectronUpdaterService } from './services/updater.service' import { TouchbarService } from './services/touchbar.service' import { ElectronDockingService } from './services/docking.service' +import { ElectronHostWindow } from './services/hostWindow.service' @NgModule({ providers: [ { provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true }, { provide: PlatformService, useClass: ElectronPlatformService }, + { provide: HostWindowService, useClass: ElectronHostWindow }, { provide: LogService, useClass: ElectronLogService }, { provide: UpdaterService, useClass: ElectronUpdaterService }, { provide: DockingService, useClass: ElectronDockingService }, diff --git a/terminus-electron/src/services/hostWindow.service.ts b/terminus-electron/src/services/hostWindow.service.ts new file mode 100644 index 00000000..53908fbb --- /dev/null +++ b/terminus-electron/src/services/hostWindow.service.ts @@ -0,0 +1,51 @@ +import { Injectable, NgZone } from '@angular/core' +import { Observable, Subject } from 'rxjs' +import { ElectronService, HostAppService, HostWindowService } from 'terminus-core' + +@Injectable({ providedIn: 'root' }) +export class ElectronHostWindow extends HostWindowService { + get closeRequest$ (): Observable { return this.closeRequest } + get isFullscreen (): boolean { return this._isFullScreen} + + private closeRequest = new Subject() + private _isFullScreen = false + + constructor ( + private electron: ElectronService, + private hostApp: HostAppService, + zone: NgZone, + ) { + super() + electron.ipcRenderer.on('host:window-enter-full-screen', () => zone.run(() => { + this._isFullScreen = true + })) + + electron.ipcRenderer.on('host:window-leave-full-screen', () => zone.run(() => { + this._isFullScreen = false + })) + } + + reload (): void { + this.hostApp.getWindow().reload() + } + + setTitle (title?: string): void { + this.electron.ipcRenderer.send('window-set-title', title ?? 'Terminus') + } + + toggleFullscreen (): void { + this.hostApp.getWindow().setFullScreen(!this._isFullScreen) + } + + minimize (): void { + this.electron.ipcRenderer.send('window-minimize') + } + + toggleMaximize (): void { + this.electron.ipcRenderer.send('window-toggle-maximize') + } + + close (): void { + this.electron.ipcRenderer.send('window-close') + } +} diff --git a/terminus-electron/src/services/platform.ts b/terminus-electron/src/services/platform.service.ts similarity index 89% rename from terminus-electron/src/services/platform.ts rename to terminus-electron/src/services/platform.service.ts index acbba4f9..50cadad5 100644 --- a/terminus-electron/src/services/platform.ts +++ b/terminus-electron/src/services/platform.service.ts @@ -4,7 +4,7 @@ import * as os from 'os' import promiseIpc from 'electron-promise-ipc' import { execFile } from 'mz/child_process' import { Injectable } from '@angular/core' -import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions } from 'terminus-core' +import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult } from 'terminus-core' const fontManager = require('fontmanager-redux') // eslint-disable-line /* eslint-disable block-scoped-var */ @@ -30,6 +30,10 @@ export class ElectronPlatformService extends PlatformService { this.configPath = path.join(electron.app.getPath('userData'), 'config.yaml') } + readClipboard (): string { + return this.electron.clipboard.readText() + } + setClipboard (content: ClipboardContent): void { require('@electron/remote').clipboard.write(content) } @@ -86,7 +90,7 @@ export class ElectronPlatformService extends PlatformService { async loadConfig (): Promise { if (await fs.exists(this.configPath)) { - return fs.readFileSync(this.configPath, 'utf8') + return fs.readFile(this.configPath, 'utf8') } else { return '' } @@ -141,6 +145,12 @@ export class ElectronPlatformService extends PlatformService { } popupContextMenu (menu: MenuItemOptions[], _event?: MouseEvent): void { - this.electron.Menu.buildFromTemplate(menu).popup({}) + this.electron.Menu.buildFromTemplate(menu.map(item => ({ + ...item, + }))).popup({}) + } + + async showMessageBox (options: MessageBoxOptions): Promise { + return this.electron.dialog.showMessageBox(this.hostApp.getWindow(), options) } } diff --git a/terminus-electron/src/services/shellIntegration.service.ts b/terminus-electron/src/services/shellIntegration.service.ts index 39013367..ae02b363 100644 --- a/terminus-electron/src/services/shellIntegration.service.ts +++ b/terminus-electron/src/services/shellIntegration.service.ts @@ -2,8 +2,7 @@ import * as path from 'path' import * as fs from 'mz/fs' import { exec } from 'mz/child_process' import { Injectable } from '@angular/core' -import { ElectronService } from '../../../terminus-core/src/services/electron.service' -import { HostAppService, Platform } from '../../../terminus-core/src/services/hostApp.service' +import { ElectronService, HostAppService, Platform } from 'terminus-core' /* eslint-disable block-scoped-var */ diff --git a/terminus-electron/src/services/updater.service.ts b/terminus-electron/src/services/updater.service.ts index f6d16250..e17678ea 100644 --- a/terminus-electron/src/services/updater.service.ts +++ b/terminus-electron/src/services/updater.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core' import axios from 'axios' -import { Logger, LogService, ElectronService, ConfigService, HostAppService, UpdaterService } from 'terminus-core' +import { Logger, LogService, ElectronService, ConfigService, UpdaterService, PlatformService } from 'terminus-core' const UPDATES_URL = 'https://api.github.com/repos/eugeny/terminus/releases/latest' @@ -15,8 +15,8 @@ export class ElectronUpdaterService extends UpdaterService { constructor ( log: LogService, config: ConfigService, + private platform: PlatformService, private electron: ElectronService, - private hostApp: HostAppService, ) { super() this.logger = log.create('updater') @@ -42,18 +42,21 @@ export class ElectronUpdaterService extends UpdaterService { electron.autoUpdater.once('update-downloaded', () => resolve(true)) }) - if (config.store.enableAutomaticUpdates && this.electronUpdaterAvailable && !process.env.TERMINUS_DEV) { - this.logger.debug('Checking for updates') - try { - electron.autoUpdater.setFeedURL({ - url: `https://update.electronjs.org/eugeny/terminus/${process.platform}-${process.arch}/${electron.app.getVersion()}`, - }) - electron.autoUpdater.checkForUpdates() - } catch (e) { - this.electronUpdaterAvailable = false - this.logger.info('Electron updater unavailable, falling back', e) + + config.ready$.toPromise().then(() => { + if (config.store.enableAutomaticUpdates && this.electronUpdaterAvailable && !process.env.TERMINUS_DEV) { + this.logger.debug('Checking for updates') + try { + electron.autoUpdater.setFeedURL({ + url: `https://update.electronjs.org/eugeny/terminus/${process.platform}-${process.arch}/${electron.app.getVersion()}`, + }) + electron.autoUpdater.checkForUpdates() + } catch (e) { + this.electronUpdaterAvailable = false + this.logger.info('Electron updater unavailable, falling back', e) + } } - } + }) } async check (): Promise { @@ -117,8 +120,7 @@ export class ElectronUpdaterService extends UpdaterService { if (!this.electronUpdaterAvailable) { this.electron.shell.openExternal(this.updateURL) } else { - if ((await this.electron.showMessageBox( - this.hostApp.getWindow(), + if ((await this.platform.showMessageBox( { type: 'warning', message: 'Installing the update will close all tabs and restart Terminus.', diff --git a/terminus-local/src/components/terminalTab.component.ts b/terminus-local/src/components/terminalTab.component.ts index 64a65674..1304f2fd 100644 --- a/terminus-local/src/components/terminalTab.component.ts +++ b/terminus-local/src/components/terminalTab.component.ts @@ -90,8 +90,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent { if (!children?.length) { return true } - return (await this.electron.showMessageBox( - this.hostApp.getWindow(), + return (await this.platform.showMessageBox( { type: 'warning', message: `"${children[0].command}" is still running. Close?`, diff --git a/terminus-local/src/hotkeys.ts b/terminus-local/src/hotkeys.ts index aeb964d3..10b29dac 100644 --- a/terminus-local/src/hotkeys.ts +++ b/terminus-local/src/hotkeys.ts @@ -4,7 +4,7 @@ import { TerminalService } from './services/terminal.service' /** @hidden */ @Injectable() -export class TerminalHotkeyProvider extends HotkeyProvider { +export class LocalTerminalHotkeyProvider extends HotkeyProvider { hotkeys: HotkeyDescription[] = [ { id: 'new-tab', diff --git a/terminus-local/src/index.ts b/terminus-local/src/index.ts index 72c11d4b..6c99c356 100644 --- a/terminus-local/src/index.ts +++ b/terminus-local/src/index.ts @@ -21,7 +21,7 @@ import { RecoveryProvider } from './recoveryProvider' import { ShellProvider } from './api' import { ShellSettingsTabProvider } from './settings' import { TerminalConfigProvider } from './config' -import { TerminalHotkeyProvider } from './hotkeys' +import { LocalTerminalHotkeyProvider } from './hotkeys' import { NewTabContextMenu, SaveAsProfileContextMenu } from './tabContextMenu' import { CmderShellProvider } from './shells/cmder' @@ -55,7 +55,7 @@ import { AutoOpenTabCLIHandler, OpenPathCLIHandler, TerminalCLIHandler } from '. { provide: ToolbarButtonProvider, useClass: ButtonProvider, multi: true }, { provide: TabRecoveryProvider, useClass: RecoveryProvider, multi: true }, { provide: ConfigProvider, useClass: TerminalConfigProvider, multi: true }, - { provide: HotkeyProvider, useClass: TerminalHotkeyProvider, multi: true }, + { provide: HotkeyProvider, useClass: LocalTerminalHotkeyProvider, multi: true }, { provide: ShellProvider, useClass: WindowsDefaultShellProvider, multi: true }, { provide: ShellProvider, useClass: MacOSDefaultShellProvider, multi: true }, diff --git a/terminus-serial/src/components/editConnectionModal.component.ts b/terminus-serial/src/components/editConnectionModal.component.ts index 84adc870..1c3aae1e 100644 --- a/terminus-serial/src/components/editConnectionModal.component.ts +++ b/terminus-serial/src/components/editConnectionModal.component.ts @@ -2,7 +2,7 @@ import { Component } from '@angular/core' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators' -import { ElectronService, HostAppService } from 'terminus-core' +import { PlatformService } from 'terminus-core' import { SerialConnection, LoginScript, SerialPortInfo, BAUD_RATES } from '../api' import { SerialService } from '../services/serial.service' @@ -32,8 +32,7 @@ export class EditConnectionModalComponent { constructor ( private modalInstance: NgbActiveModal, - private electron: ElectronService, - private hostApp: HostAppService, + private platform: PlatformService, private serial: SerialService, ) { } @@ -100,8 +99,7 @@ export class EditConnectionModalComponent { } async deleteScript (script: LoginScript) { - if (this.connection.scripts && (await this.electron.showMessageBox( - this.hostApp.getWindow(), + if (this.connection.scripts && (await this.platform.showMessageBox( { type: 'warning', message: 'Delete this script?', diff --git a/terminus-serial/src/components/serialSettingsTab.component.ts b/terminus-serial/src/components/serialSettingsTab.component.ts index 0651666b..75d55aca 100644 --- a/terminus-serial/src/components/serialSettingsTab.component.ts +++ b/terminus-serial/src/components/serialSettingsTab.component.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Component } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { ConfigService, ElectronService, HostAppService } from 'terminus-core' +import { ConfigService, PlatformService } from 'terminus-core' import { SerialConnection } from '../api' import { EditConnectionModalComponent } from './editConnectionModal.component' @@ -14,8 +14,7 @@ export class SerialSettingsTabComponent { constructor ( public config: ConfigService, - private electron: ElectronService, - private hostApp: HostAppService, + private platform: PlatformService, private ngbModal: NgbModal, ) { this.connections = this.config.store.serial.connections @@ -62,8 +61,7 @@ export class SerialSettingsTabComponent { } async deleteConnection (connection: SerialConnection) { - if ((await this.electron.showMessageBox( - this.hostApp.getWindow(), + if ((await this.platform.showMessageBox( { type: 'warning', message: `Delete "${connection.name}"?`, diff --git a/terminus-ssh/src/components/editConnectionModal.component.ts b/terminus-ssh/src/components/editConnectionModal.component.ts index 601b0307..a1436c14 100644 --- a/terminus-ssh/src/components/editConnectionModal.component.ts +++ b/terminus-ssh/src/components/editConnectionModal.component.ts @@ -4,7 +4,7 @@ import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { Observable } from 'rxjs' import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators' -import { ElectronService, HostAppService, ConfigService } from 'terminus-core' +import { ElectronService, HostAppService, ConfigService, PlatformService } from 'terminus-core' import { PasswordStorageService } from '../services/passwordStorage.service' import { SSHConnection, LoginScript, ForwardedPortConfig, SSHAlgorithmType, ALGORITHM_BLACKLIST } from '../api' import { PromptModalComponent } from './promptModal.component' @@ -29,6 +29,7 @@ export class EditConnectionModalComponent { public config: ConfigService, private modalInstance: NgbActiveModal, private electron: ElectronService, + private platform: PlatformService, private hostApp: HostAppService, private passwordStorage: PasswordStorageService, private ngbModal: NgbModal, @@ -153,8 +154,7 @@ export class EditConnectionModalComponent { } async deleteScript (script: LoginScript) { - if (this.connection.scripts && (await this.electron.showMessageBox( - this.hostApp.getWindow(), + if (this.connection.scripts && (await this.platform.showMessageBox( { type: 'warning', message: 'Delete this script?', diff --git a/terminus-ssh/src/components/sshSettingsTab.component.ts b/terminus-ssh/src/components/sshSettingsTab.component.ts index 1f714b41..49db496a 100644 --- a/terminus-ssh/src/components/sshSettingsTab.component.ts +++ b/terminus-ssh/src/components/sshSettingsTab.component.ts @@ -2,7 +2,7 @@ import deepClone from 'clone-deep' import { Component } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { ConfigService, ElectronService, HostAppService, Platform } from 'terminus-core' +import { ConfigService, HostAppService, Platform, PlatformService } from 'terminus-core' import { PasswordStorageService } from '../services/passwordStorage.service' import { SSHConnection } from '../api' import { EditConnectionModalComponent } from './editConnectionModal.component' @@ -28,7 +28,7 @@ export class SSHSettingsTabComponent { constructor ( public config: ConfigService, public hostApp: HostAppService, - private electron: ElectronService, + private platform: PlatformService, private ngbModal: NgbModal, private passwordStorage: PasswordStorageService, ) { @@ -81,8 +81,7 @@ export class SSHSettingsTabComponent { } async deleteConnection (connection: SSHConnection) { - if ((await this.electron.showMessageBox( - this.hostApp.getWindow(), + if ((await this.platform.showMessageBox( { type: 'warning', message: `Delete "${connection.name}"?`, @@ -115,8 +114,7 @@ export class SSHSettingsTabComponent { } async deleteGroup (group: SSHConnectionGroup) { - if ((await this.electron.showMessageBox( - this.hostApp.getWindow(), + if ((await this.platform.showMessageBox( { type: 'warning', message: `Delete "${group.name}"?`, diff --git a/terminus-ssh/src/components/sshTab.component.ts b/terminus-ssh/src/components/sshTab.component.ts index 5f016090..71d9d19e 100644 --- a/terminus-ssh/src/components/sshTab.component.ts +++ b/terminus-ssh/src/components/sshTab.component.ts @@ -210,8 +210,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent { if (!(this.connection?.warnOnClose ?? this.config.store.ssh.warnOnClose)) { return true } - return (await this.electron.showMessageBox( - this.hostApp.getWindow(), + return (await this.platform.showMessageBox( { type: 'warning', message: `Disconnect from ${this.connection?.host}?`, diff --git a/terminus-terminal/src/api/baseTerminalTab.component.ts b/terminus-terminal/src/api/baseTerminalTab.component.ts index d288d472..5298bcf6 100644 --- a/terminus-terminal/src/api/baseTerminalTab.component.ts +++ b/terminus-terminal/src/api/baseTerminalTab.component.ts @@ -3,7 +3,7 @@ import { first } from 'rxjs/operators' import colors from 'ansi-colors' import { NgZone, OnInit, OnDestroy, Injector, ViewChild, HostBinding, Input, ElementRef, InjectFlags } from '@angular/core' import { trigger, transition, style, animate, AnimationTriggerMetadata } from '@angular/animations' -import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, NotificationsService, Platform, LogService, Logger, TabContextMenuItemProvider, SplitTabComponent, SubscriptionContainer, MenuItemOptions, PlatformService } from 'terminus-core' +import { AppService, ConfigService, BaseTabComponent, HostAppService, HotkeysService, NotificationsService, Platform, LogService, Logger, TabContextMenuItemProvider, SplitTabComponent, SubscriptionContainer, MenuItemOptions, PlatformService } from 'terminus-core' import { BaseSession } from '../session' import { TerminalFrontendService } from '../services/terminalFrontend.service' @@ -82,7 +82,6 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit protected app: AppService protected hostApp: HostAppService protected hotkeys: HotkeysService - protected electron: ElectronService protected platform: PlatformService protected terminalContainersService: TerminalFrontendService protected notifications: NotificationsService @@ -135,7 +134,6 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit this.app = injector.get(AppService) this.hostApp = injector.get(HostAppService) this.hotkeys = injector.get(HotkeysService) - this.electron = injector.get(ElectronService) this.platform = injector.get(PlatformService) this.terminalContainersService = injector.get(TerminalFrontendService) this.notifications = injector.get(NotificationsService) @@ -359,7 +357,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit } async paste (): Promise { - let data = this.electron.clipboard.readText() + let data = this.platform.readClipboard() if (this.config.store.terminal.bracketedPaste) { data = `\x1b[200~${data}\x1b[201~` } @@ -374,15 +372,13 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit if (data.includes('\r') && this.config.store.terminal.warnOnMultilinePaste) { const buttons = ['Paste', 'Cancel'] - const result = (await this.electron.showMessageBox( - this.hostApp.getWindow(), + const result = (await this.platform.showMessageBox( { type: 'warning', detail: data, message: `Paste multiple lines?`, buttons, defaultId: 0, - cancelId: 1, } )).response if (result === 1) { @@ -463,7 +459,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit cwd = await this.session.getWorkingDirectory() } if (cwd) { - this.electron.clipboard.writeText(cwd) + this.platform.setClipboard({ text: cwd }) this.notifications.notice('Copied') } else { this.notifications.error('Shell does not support current path detection') diff --git a/terminus-terminal/src/components/colorSchemeSettingsTab.component.ts b/terminus-terminal/src/components/colorSchemeSettingsTab.component.ts index 7629c110..49dfbd73 100644 --- a/terminus-terminal/src/components/colorSchemeSettingsTab.component.ts +++ b/terminus-terminal/src/components/colorSchemeSettingsTab.component.ts @@ -2,7 +2,7 @@ import deepEqual from 'deep-equal' import { Component, Inject, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core' -import { ConfigService, HostAppService, ElectronService } from 'terminus-core' +import { ConfigService, PlatformService } from 'terminus-core' import { TerminalColorSchemeProvider } from '../api/colorSchemeProvider' import { TerminalColorScheme } from '../api/interfaces' @@ -26,8 +26,7 @@ export class ColorSchemeSettingsTabComponent { constructor ( @Inject(TerminalColorSchemeProvider) private colorSchemeProviders: TerminalColorSchemeProvider[], private changeDetector: ChangeDetectorRef, - private hostApp: HostAppService, - private electron: ElectronService, + private platform: PlatformService, public config: ConfigService, ) { } @@ -76,8 +75,7 @@ export class ColorSchemeSettingsTabComponent { } async deleteScheme (scheme: TerminalColorScheme) { - if ((await this.electron.showMessageBox( - this.hostApp.getWindow(), + if ((await this.platform.showMessageBox( { type: 'warning', message: `Delete "${scheme.name}"?`, diff --git a/terminus-terminal/src/features/debug.ts b/terminus-terminal/src/features/debug.ts index 1a403b60..e792d503 100644 --- a/terminus-terminal/src/features/debug.ts +++ b/terminus-terminal/src/features/debug.ts @@ -2,13 +2,14 @@ import * as fs from 'fs' import { Injectable } from '@angular/core' import { TerminalDecorator } from '../api/decorator' import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component' -import { ElectronService, HostAppService } from 'terminus-core' +import { ElectronService, HostAppService, PlatformService } from 'terminus-core' /** @hidden */ @Injectable() export class DebugDecorator extends TerminalDecorator { constructor ( private electron: ElectronService, + private platform: PlatformService, private hostApp: HostAppService, ) { super() @@ -93,7 +94,7 @@ export class DebugDecorator extends TerminalDecorator { private async doCopyState (terminal: BaseTerminalTabComponent) { const data = '```' + JSON.stringify(terminal.frontend!.saveState()) + '```' - this.electron.clipboard.writeText(data) + this.platform.setClipboard({ text: data }) } private async doLoadState (terminal: BaseTerminalTabComponent) { @@ -104,7 +105,7 @@ export class DebugDecorator extends TerminalDecorator { } private async doPasteState (terminal: BaseTerminalTabComponent) { - let data = this.electron.clipboard.readText() + let data = this.platform.readClipboard() if (data) { if (data.startsWith('`')) { data = data.substring(3, data.length - 3) @@ -119,7 +120,7 @@ export class DebugDecorator extends TerminalDecorator { private async doCopyOutput (buffer: string) { const data = '```' + JSON.stringify(buffer) + '```' - this.electron.clipboard.writeText(data) + this.platform.setClipboard({ text: data }) } private async doLoadOutput (terminal: BaseTerminalTabComponent) { @@ -130,7 +131,7 @@ export class DebugDecorator extends TerminalDecorator { } private async doPasteOutput (terminal: BaseTerminalTabComponent) { - let data = this.electron.clipboard.readText() + let data = this.platform.readClipboard() if (data) { if (data.startsWith('`')) { data = data.substring(3, data.length - 3) diff --git a/terminus-web/src/components/messageBoxModal.component.pug b/terminus-web/src/components/messageBoxModal.component.pug new file mode 100644 index 00000000..67ec43bd --- /dev/null +++ b/terminus-web/src/components/messageBoxModal.component.pug @@ -0,0 +1,13 @@ +.modal-body + div {{options.message}} + small {{options.detail}} + +.modal-footer + .ml-auto + button.btn( + *ngFor='let button of options.buttons; index as i', + [autofocus]='i === options.defaultId', + [class.btn-primary]='i === options.defaultId', + [class.btn-secondary]='i !== options.defaultId', + (click)='onButton(i)', + ) {{button}} diff --git a/terminus-web/src/components/messageBoxModal.component.ts b/terminus-web/src/components/messageBoxModal.component.ts new file mode 100644 index 00000000..6cda2c39 --- /dev/null +++ b/terminus-web/src/components/messageBoxModal.component.ts @@ -0,0 +1,34 @@ +import { Component, Input, ElementRef, } from '@angular/core' +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' +import { BaseComponent, HotkeysService, MessageBoxOptions } from 'terminus-core' + +/** @hidden */ +@Component({ + template: require('./messageBoxModal.component.pug'), +}) +export class MessageBoxModalComponent extends BaseComponent { + @Input() options: MessageBoxOptions + + constructor ( + hotkeys: HotkeysService, + private element: ElementRef, + private modalInstance: NgbActiveModal, + ) { + super() + this.subscribeUntilDestroyed(hotkeys.key, (event: KeyboardEvent) => { + if (event.type === 'keydown') { + if (event.key === 'Enter' && this.options.defaultId !== undefined) { + this.modalInstance.close(this.options.defaultId) + } + } + }) + } + + ngAfterViewInit (): void { + this.element.nativeElement.querySelector('button[autofocus]').focus() + } + + onButton (index: number): void { + this.modalInstance.close(index) + } +} diff --git a/terminus-web/src/index.ts b/terminus-web/src/index.ts index 10676b7c..f9d4abb7 100644 --- a/terminus-web/src/index.ts +++ b/terminus-web/src/index.ts @@ -1,17 +1,30 @@ import { NgModule } from '@angular/core' -import { LogService, PlatformService, UpdaterService } from 'terminus-core' +import { CommonModule } from '@angular/common' +import { HostWindowService, LogService, PlatformService, UpdaterService } from 'terminus-core' import { WebPlatformService } from './platform' import { ConsoleLogService } from './services/log.service' import { NullUpdaterService } from './services/updater.service' +import { WebHostWindow } from './services/hostWindow.service' +import { MessageBoxModalComponent } from './components/messageBoxModal.component' import './styles.scss' @NgModule({ + imports: [ + CommonModule, + ], providers: [ { provide: PlatformService, useClass: WebPlatformService }, { provide: LogService, useClass: ConsoleLogService }, { provide: UpdaterService, useClass: NullUpdaterService }, + { provide: HostWindowService, useClass: WebHostWindow }, + ], + declarations: [ + MessageBoxModalComponent, + ], + entryComponents: [ + MessageBoxModalComponent, ], }) export default class WebModule { } // eslint-disable-line @typescript-eslint/no-extraneous-class diff --git a/terminus-web/src/platform.ts b/terminus-web/src/platform.ts index aae00d84..80f8817b 100644 --- a/terminus-web/src/platform.ts +++ b/terminus-web/src/platform.ts @@ -1,11 +1,13 @@ import '@vaadin/vaadin-context-menu/vaadin-context-menu.js' import copyToClipboard from 'copy-text-to-clipboard' import { Injectable } from '@angular/core' -import { PlatformService, ClipboardContent, MenuItemOptions } from 'terminus-core' +import { NgbModal } from '@ng-bootstrap/ng-bootstrap' +import { PlatformService, ClipboardContent, MenuItemOptions, MessageBoxOptions, MessageBoxResult } from 'terminus-core' // eslint-disable-next-line no-duplicate-imports import type { ContextMenuElement, ContextMenuItem } from '@vaadin/vaadin-context-menu/vaadin-context-menu.js' +import { MessageBoxModalComponent } from './components/messageBoxModal.component' import './styles.scss' @Injectable() @@ -13,14 +15,19 @@ export class WebPlatformService extends PlatformService { private menu: ContextMenuElement private contextMenuHandlers = new Map void>() - constructor () { + constructor ( + private ngbModal: NgbModal, + ) { super() this.menu = window.document.createElement('vaadin-context-menu') this.menu.addEventListener('item-selected', e => { this.contextMenuHandlers.get(e.detail.value)?.() }) document.body.appendChild(this.menu) - console.log(require('./styles.scss')) + } + + readClipboard (): string { + return '' } setClipboard (content: ClipboardContent): void { @@ -73,4 +80,19 @@ export class WebPlatformService extends PlatformService { } return cmi } + + async showMessageBox (options: MessageBoxOptions): Promise { + console.log(options) + const modal = this.ngbModal.open(MessageBoxModalComponent, { + backdrop: 'static', + }) + const instance: MessageBoxModalComponent = modal.componentInstance + instance.options = options + try { + const response = await modal.result + return { response } + } catch { + return { response: 0 } + } + } } diff --git a/terminus-web/src/services/hostWindow.service.ts b/terminus-web/src/services/hostWindow.service.ts new file mode 100644 index 00000000..decb6ebf --- /dev/null +++ b/terminus-web/src/services/hostWindow.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core' +import { Observable, Subject } from 'rxjs' +import { HostWindowService } from 'terminus-core' + +@Injectable({ providedIn: 'root' }) +export class WebHostWindow extends HostWindowService { + get closeRequest$ (): Observable { return this.closeRequest } + get isFullscreen (): boolean { return !!document.fullscreenElement } + + private closeRequest = new Subject() + + reload (): void { + location.reload() + } + + setTitle (title?: string): void { + document.title = title ?? 'Terminus' + } + + toggleFullscreen (): void { + if (this.isFullscreen) { + document.exitFullscreen() + } else { + document.body.requestFullscreen({ navigationUI: 'hide' }) + } + } + + minimize (): void { + throw new Error('Unavailable') + } + + toggleMaximize (): void { + throw new Error('Unavailable') + } + + close (): void { + window.close() + } +}