diff --git a/app/lib/app.ts b/app/lib/app.ts index 98b455b2..c0a5b316 100644 --- a/app/lib/app.ts +++ b/app/lib/app.ts @@ -1,6 +1,8 @@ import { app, ipcMain, Menu, Tray, shell, screen, globalShortcut, MenuItemConstructorOptions } from 'electron' import * as promiseIpc from 'electron-promise-ipc' import * as remote from '@electron/remote/main' +import * as path from 'path' +import * as fs from 'fs' import { loadConfig } from './config' import { Window, WindowOptions } from './window' @@ -17,6 +19,7 @@ export class Application { private tray?: Tray private ptyManager = new PTYManager() private windows: Window[] = [] + userPluginsPath: string constructor () { remote.initialize() @@ -36,12 +39,12 @@ export class Application { } }) - ;(promiseIpc as any).on('plugin-manager:install', (path, name, version) => { - return pluginManager.install(path, name, version) + ;(promiseIpc as any).on('plugin-manager:install', (name, version) => { + return pluginManager.install(this.userPluginsPath, name, version) }) - ;(promiseIpc as any).on('plugin-manager:uninstall', (path, name) => { - return pluginManager.uninstall(path, name) + ;(promiseIpc as any).on('plugin-manager:uninstall', (name) => { + return pluginManager.uninstall(this.userPluginsPath, name) }) const configData = loadConfig() @@ -53,6 +56,15 @@ export class Application { } } + this.userPluginsPath = path.join( + app.getPath('userData'), + 'plugins', + ) + + if (!fs.existsSync(this.userPluginsPath)) { + fs.mkdirSync(this.userPluginsPath) + } + app.commandLine.appendSwitch('disable-http-cache') app.commandLine.appendSwitch('max-active-webgl-contexts', '9000') app.commandLine.appendSwitch('lang', 'EN') @@ -70,7 +82,7 @@ export class Application { } async newWindow (options?: WindowOptions): Promise { - const window = new Window(options) + const window = new Window(this, options) this.windows.push(window) window.visible$.subscribe(visible => { if (visible) { diff --git a/app/lib/window.ts b/app/lib/window.ts index 1d413265..e265c6f1 100644 --- a/app/lib/window.ts +++ b/app/lib/window.ts @@ -9,6 +9,7 @@ import * as path from 'path' import macOSRelease from 'macos-release' import * as compareVersions from 'compare-versions' +import type { Application } from './app' import { parseArgs } from './cli' import { loadConfig } from './config' @@ -43,7 +44,7 @@ export class Window { get visible$ (): Observable { return this.visible } get closed$ (): Observable { return this.closed } - constructor (options?: WindowOptions) { + constructor (private application: Application, options?: WindowOptions) { this.configStore = loadConfig() options = options ?? {} @@ -299,16 +300,10 @@ export class Window { executable: app.getPath('exe'), windowID: this.window.id, isFirstWindow: this.window.id === 1, + userPluginsPath: this.application.userPluginsPath, }) }) - ipcMain.on('window-focus', event => { - if (!this.window || event.sender !== this.window.webContents) { - return - } - this.window.focus() - }) - ipcMain.on('window-toggle-maximize', event => { if (!this.window || event.sender !== this.window.webContents) { return diff --git a/app/src/entry.ts b/app/src/entry.ts index 745af861..d48f949e 100644 --- a/app/src/entry.ts +++ b/app/src/entry.ts @@ -11,7 +11,7 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' import { ipcRenderer } from 'electron' import { getRootModule } from './app.module' -import { findPlugins, loadPlugins, PluginInfo } from './plugins' +import { findPlugins, initModuleLookup, loadPlugins } from './plugins' import { BootstrapData, BOOTSTRAP_DATA } from '../../terminus-core/src/api/mainProcess' // Always land on the start view @@ -29,12 +29,12 @@ if (process.env.TERMINUS_DEV && !process.env.TERMINUS_FORCE_ANGULAR_PROD) { enableProdMode() } -async function bootstrap (plugins: PluginInfo[], bootstrapData: BootstrapData, safeMode = false): Promise> { +async function bootstrap (bootstrapData: BootstrapData, safeMode = false): Promise> { if (safeMode) { - plugins = plugins.filter(x => x.isBuiltin) + bootstrapData.installedPlugins = bootstrapData.installedPlugins.filter(x => x.isBuiltin) } - const pluginModules = await loadPlugins(plugins, (current, total) => { + const pluginModules = await loadPlugins(bootstrapData.installedPlugins, (current, total) => { (document.querySelector('.progress .bar') as HTMLElement).style.width = `${100 * current / total}%` // eslint-disable-line }) const module = getRootModule(pluginModules) @@ -53,20 +53,24 @@ async function bootstrap (plugins: PluginInfo[], bootstrapData: BootstrapData, s ipcRenderer.once('start', async (_$event, bootstrapData: BootstrapData) => { console.log('Window bootstrap data:', bootstrapData) + initModuleLookup(bootstrapData.userPluginsPath) + let plugins = await findPlugins() if (bootstrapData.config.pluginBlacklist) { plugins = plugins.filter(x => !bootstrapData.config.pluginBlacklist.includes(x.name)) } plugins = plugins.filter(x => x.name !== 'web') + bootstrapData.installedPlugins = plugins + console.log('Starting with plugins:', plugins) try { - await bootstrap(plugins, bootstrapData) + await bootstrap(bootstrapData) } catch (error) { console.error('Angular bootstrapping error:', error) console.warn('Trying safe mode') window['safeModeReason'] = error try { - await bootstrap(plugins, bootstrapData, true) + await bootstrap(bootstrapData, true) } catch (error2) { console.error('Bootstrap failed:', error2) } diff --git a/app/src/plugins.ts b/app/src/plugins.ts index 4666e9f9..671ea147 100644 --- a/app/src/plugins.ts +++ b/app/src/plugins.ts @@ -1,8 +1,11 @@ import * as fs from 'mz/fs' import * as path from 'path' import * as remote from '@electron/remote' +import { PluginInfo } from '../../terminus-core/src/api/mainProcess' + const nodeModule = require('module') // eslint-disable-line @typescript-eslint/no-var-requires -const nodeRequire = (global as any).require + +const nodeRequire = global['require'] function normalizePath (p: string): string { const cygwinPrefix = '/cygdrive/' @@ -13,45 +16,8 @@ function normalizePath (p: string): string { return p } -global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x))) - -if (process.env.TERMINUS_DEV) { - nodeModule.globalPaths.unshift(path.dirname(remote.app.getAppPath())) -} - const builtinPluginsPath = process.env.TERMINUS_DEV ? path.dirname(remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins') -const userPluginsPath = path.join( - remote.app.getPath('userData'), - 'plugins', -) - -if (!fs.existsSync(userPluginsPath)) { - fs.mkdir(userPluginsPath) -} - -Object.assign(window, { builtinPluginsPath, userPluginsPath }) -nodeModule.globalPaths.unshift(builtinPluginsPath) -nodeModule.globalPaths.unshift(path.join(userPluginsPath, 'node_modules')) -// nodeModule.globalPaths.unshift(path.join((process as any).resourcesPath, 'app.asar', 'node_modules')) -if (process.env.TERMINUS_PLUGINS) { - process.env.TERMINUS_PLUGINS.split(':').map(x => nodeModule.globalPaths.push(normalizePath(x))) -} - -export type ProgressCallback = (current: number, total: number) => void // eslint-disable-line @typescript-eslint/no-type-alias - -export interface PluginInfo { - name: string - description: string - packageName: string - isBuiltin: boolean - version: string - author: string - homepage?: string - path?: string - info?: any -} - const builtinModules = [ '@angular/animations', '@angular/common', @@ -71,25 +37,42 @@ const builtinModules = [ 'zone.js/dist/zone.js', ] -const cachedBuiltinModules = {} -builtinModules.forEach(m => { - cachedBuiltinModules[m] = nodeRequire(m) -}) +export type ProgressCallback = (current: number, total: number) => void // eslint-disable-line @typescript-eslint/no-type-alias -const originalRequire = (global as any).require -;(global as any).require = function (query: string) { - if (cachedBuiltinModules[query]) { - return cachedBuiltinModules[query] - } - return originalRequire.apply(this, [query]) -} +export function initModuleLookup (userPluginsPath: string): void { + global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x))) -const originalModuleRequire = nodeModule.prototype.require -nodeModule.prototype.require = function (query: string) { - if (cachedBuiltinModules[query]) { - return cachedBuiltinModules[query] + if (process.env.TERMINUS_DEV) { + nodeModule.globalPaths.unshift(path.dirname(remote.app.getAppPath())) + } + + nodeModule.globalPaths.unshift(builtinPluginsPath) + nodeModule.globalPaths.unshift(path.join(userPluginsPath, 'node_modules')) + // nodeModule.globalPaths.unshift(path.join((process as any).resourcesPath, 'app.asar', 'node_modules')) + if (process.env.TERMINUS_PLUGINS) { + process.env.TERMINUS_PLUGINS.split(':').map(x => nodeModule.globalPaths.push(normalizePath(x))) + } + + const cachedBuiltinModules = {} + builtinModules.forEach(m => { + cachedBuiltinModules[m] = nodeRequire(m) + }) + + const originalRequire = (global as any).require + ;(global as any).require = function (query: string) { + if (cachedBuiltinModules[query]) { + return cachedBuiltinModules[query] + } + return originalRequire.apply(this, [query]) + } + + const originalModuleRequire = nodeModule.prototype.require + nodeModule.prototype.require = function (query: string) { + if (cachedBuiltinModules[query]) { + return cachedBuiltinModules[query] + } + return originalModuleRequire.call(this, query) } - return originalModuleRequire.call(this, query) } export async function findPlugins (): Promise { @@ -167,8 +150,6 @@ export async function findPlugins (): Promise { } foundPlugins.sort((a, b) => a.name > b.name ? 1 : -1) - - ;(window as any).installedPlugins = foundPlugins return foundPlugins } diff --git a/scripts/vars.js b/scripts/vars.js index c02093ab..c37c0fe5 100755 --- a/scripts/vars.js +++ b/scripts/vars.js @@ -3,7 +3,6 @@ const fs = require('fs') const semver = require('semver') const childProcess = require('child_process') -const appInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../app/package.json'))) const electronInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../node_modules/electron/package.json'))) exports.version = childProcess.execSync('git describe --tags', {encoding:'utf-8'}) @@ -18,10 +17,10 @@ exports.builtinPlugins = [ 'terminus-core', 'terminus-settings', 'terminus-terminal', + 'terminus-electron', 'terminus-local', 'terminus-web', 'terminus-community-color-schemes', - 'terminus-electron', 'terminus-plugin-manager', 'terminus-ssh', 'terminus-serial', diff --git a/terminus-core/src/api/hostApp.ts b/terminus-core/src/api/hostApp.ts new file mode 100644 index 00000000..ab37a45b --- /dev/null +++ b/terminus-core/src/api/hostApp.ts @@ -0,0 +1,53 @@ +import { Observable, Subject } from 'rxjs' +import { Injector } from '@angular/core' +import { Logger, LogService } from '../services/log.service' + +export enum Platform { + Linux = 'Linux', + macOS = 'macOS', + Windows = 'Windows', + Web = 'Web', +} + +/** + * Provides interaction with the main process + */ +export abstract class HostAppService { + abstract get platform (): Platform + abstract get configPlatform (): Platform + + protected settingsUIRequest = new Subject() + protected configChangeBroadcast = new Subject() + protected logger: Logger + + /** + * Fired when Preferences is selected in the macOS menu + */ + get settingsUIRequest$ (): Observable { return this.settingsUIRequest } + + /** + * Fired when another window modified the config file + */ + get configChangeBroadcast$ (): Observable { return this.configChangeBroadcast } + + constructor ( + injector: Injector, + ) { + this.logger = injector.get(LogService).create('hostApp') + } + + abstract newWindow (): void + + /** + * Notifies other windows of config file changes + */ + // eslint-disable-next-line @typescript-eslint/no-empty-function + broadcastConfigChange (_configStore: Record): void { } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + emitReady (): void { } + + abstract relaunch (): void + + abstract quit (): void +} diff --git a/terminus-core/src/api/hostWindow.ts b/terminus-core/src/api/hostWindow.ts index e9579080..f00b489f 100644 --- a/terminus-core/src/api/hostWindow.ts +++ b/terminus-core/src/api/hostWindow.ts @@ -1,7 +1,24 @@ -import { Observable } from 'rxjs' +import { Observable, Subject } from 'rxjs' export abstract class HostWindowService { - abstract readonly closeRequest$: Observable + + /** + * Fired once the window is visible + */ + get windowShown$ (): Observable { return this.windowShown } + + /** + * Fired when the window close button is pressed + */ + get windowCloseRequest$ (): Observable { return this.windowCloseRequest } + get windowMoved$ (): Observable { return this.windowMoved } + get windowFocused$ (): Observable { return this.windowFocused } + + protected windowShown = new Subject() + protected windowCloseRequest = new Subject() + protected windowMoved = new Subject() + protected windowFocused = new Subject() + abstract readonly isFullscreen: boolean abstract reload (): void abstract setTitle (title?: string): void @@ -9,4 +26,10 @@ export abstract class HostWindowService { abstract minimize (): void abstract toggleMaximize (): void abstract close (): void + + // eslint-disable-next-line @typescript-eslint/no-empty-function + openDevTools (): void { } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + bringToFront (): void { } } diff --git a/terminus-core/src/api/index.ts b/terminus-core/src/api/index.ts index 27d0fad7..af63b952 100644 --- a/terminus-core/src/api/index.ts +++ b/terminus-core/src/api/index.ts @@ -12,8 +12,9 @@ export { SelectorOption } from './selector' export { CLIHandler, CLIEvent } from './cli' export { PlatformService, ClipboardContent, MessageBoxResult, MessageBoxOptions, FileDownload, FileUpload, FileTransfer, HTMLFileUpload, FileUploadOptions } from './platform' export { MenuItemOptions } from './menu' -export { BootstrapData, BOOTSTRAP_DATA } from './mainProcess' +export { BootstrapData, PluginInfo, BOOTSTRAP_DATA } from './mainProcess' export { HostWindowService } from './hostWindow' +export { HostAppService, Platform } from './hostApp' export { AppService } from '../services/app.service' export { ConfigService } from '../services/config.service' @@ -22,7 +23,6 @@ export { ElectronService } from '../services/electron.service' export { Logger, ConsoleLogger, LogService } from '../services/log.service' export { HomeBaseService } from '../services/homeBase.service' export { HotkeysService } from '../services/hotkeys.service' -export { HostAppService, Platform, Bounds } from '../services/hostApp.service' export { NotificationsService } from '../services/notifications.service' export { ThemesService } from '../services/themes.service' export { TabsService } from '../services/tabs.service' diff --git a/terminus-core/src/api/mainProcess.ts b/terminus-core/src/api/mainProcess.ts index 1fbdd017..578b0c75 100644 --- a/terminus-core/src/api/mainProcess.ts +++ b/terminus-core/src/api/mainProcess.ts @@ -1,8 +1,22 @@ export const BOOTSTRAP_DATA = 'BOOTSTRAP_DATA' +export interface PluginInfo { + name: string + description: string + packageName: string + isBuiltin: boolean + version: string + author: string + homepage?: string + path?: string + info?: any +} + export interface BootstrapData { config: Record executable: string isFirstWindow: boolean windowID: number + installedPlugins: PluginInfo[] + userPluginsPath: string } diff --git a/terminus-core/src/api/platform.ts b/terminus-core/src/api/platform.ts index e4daf242..5309cbb2 100644 --- a/terminus-core/src/api/platform.ts +++ b/terminus-core/src/api/platform.ts @@ -77,8 +77,10 @@ export abstract class PlatformService { supportsWindowControls = false get fileTransferStarted$ (): Observable { return this.fileTransferStarted } + get displayMetricsChanged$ (): Observable { return this.displayMetricsChanged } protected fileTransferStarted = new Subject() + protected displayMetricsChanged = new Subject() abstract readClipboard (): string abstract setClipboard (content: ClipboardContent): void @@ -158,6 +160,7 @@ export abstract class PlatformService { abstract getAppVersion (): string abstract openExternal (url: string): void abstract listFonts (): Promise + abstract setErrorHandler (handler: (_: any) => void): void abstract popupContextMenu (menu: MenuItemOptions[], event?: MouseEvent): void abstract showMessageBox (options: MessageBoxOptions): Promise abstract quit (): void @@ -191,6 +194,9 @@ export class HTMLFileUpload extends FileUpload { return chunk } + // eslint-disable-next-line @typescript-eslint/no-empty-function + bringToFront (): void { } + // eslint-disable-next-line @typescript-eslint/no-empty-function close (): void { } } diff --git a/terminus-core/src/cli.ts b/terminus-core/src/cli.ts index 75090ca3..395b50e5 100644 --- a/terminus-core/src/cli.ts +++ b/terminus-core/src/cli.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { HostAppService } from './services/hostApp.service' +import { HostAppService } from './api/hostApp' import { CLIHandler, CLIEvent } from './api/cli' @Injectable() diff --git a/terminus-core/src/components/appRoot.component.ts b/terminus-core/src/components/appRoot.component.ts index bdc2a7fd..3c0bbe83 100644 --- a/terminus-core/src/components/appRoot.component.ts +++ b/terminus-core/src/components/appRoot.component.ts @@ -3,7 +3,7 @@ import { Component, Inject, Input, HostListener, HostBinding } from '@angular/co import { trigger, style, animate, transition, state } from '@angular/animations' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { HostAppService, Platform } from '../services/hostApp.service' +import { HostAppService, Platform } from '../api/hostApp' import { HotkeysService } from '../services/hotkeys.service' import { Logger, LogService } from '../services/log.service' import { ConfigService } from '../services/config.service' @@ -115,7 +115,7 @@ export class AppRootComponent { } }) - this.hostApp.windowCloseRequest$.subscribe(async () => { + this.hostWindow.windowCloseRequest$.subscribe(async () => { this.app.closeWindow() }) diff --git a/terminus-core/src/components/tabHeader.component.ts b/terminus-core/src/components/tabHeader.component.ts index bf1fdb41..c1414a0e 100644 --- a/terminus-core/src/components/tabHeader.component.ts +++ b/terminus-core/src/components/tabHeader.component.ts @@ -7,7 +7,7 @@ import { BaseTabComponent } from './baseTab.component' import { RenameTabModalComponent } from './renameTabModal.component' import { HotkeysService } from '../services/hotkeys.service' import { AppService } from '../services/app.service' -import { HostAppService, Platform } from '../services/hostApp.service' +import { HostAppService, Platform } from '../api/hostApp' import { ConfigService } from '../services/config.service' import { BaseComponent } from './base.component' import { MenuItemOptions } from '../api/menu' diff --git a/terminus-core/src/config.ts b/terminus-core/src/config.ts index 07b38859..5ff96a08 100644 --- a/terminus-core/src/config.ts +++ b/terminus-core/src/config.ts @@ -1,5 +1,5 @@ import { ConfigProvider } from './api/configProvider' -import { Platform } from './services/hostApp.service' +import { Platform } from './api/hostApp' /** @hidden */ export class CoreConfigProvider extends ConfigProvider { @@ -7,7 +7,7 @@ export class CoreConfigProvider extends ConfigProvider { [Platform.macOS]: require('./configDefaults.macos.yaml'), [Platform.Windows]: require('./configDefaults.windows.yaml'), [Platform.Linux]: require('./configDefaults.linux.yaml'), - [Platform.Web]: require('./configDefaults.windows.yaml'), + [Platform.Web]: require('./configDefaults.web.yaml'), } defaults = require('./configDefaults.yaml') } diff --git a/terminus-core/src/configDefaults.linux.yaml b/terminus-core/src/configDefaults.linux.yaml index 3340d7db..e8939c30 100644 --- a/terminus-core/src/configDefaults.linux.yaml +++ b/terminus-core/src/configDefaults.linux.yaml @@ -1,8 +1,4 @@ hotkeys: - new-window: - - 'Ctrl-Shift-N' - toggle-window: - - 'Ctrl+Space' toggle-fullscreen: - 'F11' close-tab: diff --git a/terminus-core/src/configDefaults.macos.yaml b/terminus-core/src/configDefaults.macos.yaml index 446f9c17..970d6fbb 100644 --- a/terminus-core/src/configDefaults.macos.yaml +++ b/terminus-core/src/configDefaults.macos.yaml @@ -1,8 +1,4 @@ hotkeys: - new-window: - - '⌘-N' - toggle-window: - - 'Ctrl+Space' toggle-fullscreen: - 'Ctrl+⌘+F' close-tab: diff --git a/terminus-core/src/configDefaults.web.yaml b/terminus-core/src/configDefaults.web.yaml new file mode 100644 index 00000000..e3efd96c --- /dev/null +++ b/terminus-core/src/configDefaults.web.yaml @@ -0,0 +1,6 @@ +pluginBlacklist: ['local'] +terminal: + recoverTabs: false +enableAnalytics: false +enableWelcomeTab: false +enableAutomaticUpdates: false diff --git a/terminus-core/src/configDefaults.windows.yaml b/terminus-core/src/configDefaults.windows.yaml index f2ab2a49..e03c6776 100644 --- a/terminus-core/src/configDefaults.windows.yaml +++ b/terminus-core/src/configDefaults.windows.yaml @@ -1,8 +1,4 @@ hotkeys: - new-window: - - 'Ctrl-Shift-N' - toggle-window: - - 'Ctrl+Space' toggle-fullscreen: - 'F11' - 'Alt-Enter' diff --git a/terminus-core/src/hotkeys.ts b/terminus-core/src/hotkeys.ts index e9510e8b..aaec3cb4 100644 --- a/terminus-core/src/hotkeys.ts +++ b/terminus-core/src/hotkeys.ts @@ -5,14 +5,6 @@ import { HotkeyDescription, HotkeyProvider } from './api/hotkeyProvider' @Injectable() export class AppHotkeyProvider extends HotkeyProvider { hotkeys: HotkeyDescription[] = [ - { - id: 'new-window', - name: 'New window', - }, - { - id: 'toggle-window', - name: 'Toggle terminal window', - }, { id: 'toggle-fullscreen', name: 'Toggle fullscreen mode', diff --git a/terminus-core/src/index.ts b/terminus-core/src/index.ts index d79ee031..459c1280 100644 --- a/terminus-core/src/index.ts +++ b/terminus-core/src/index.ts @@ -27,7 +27,7 @@ import { AutofocusDirective } from './directives/autofocus.directive' import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive' import { DropZoneDirective } from './directives/dropZone.directive' -import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider } from './api' +import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider, PlatformService } from './api' import { AppService } from './services/app.service' import { ConfigService } from './services/config.service' @@ -102,12 +102,16 @@ const PROVIDERS = [ ], }) export default class AppModule { // eslint-disable-line @typescript-eslint/no-extraneous-class - constructor (app: AppService, config: ConfigService) { + constructor (app: AppService, config: ConfigService, platform: PlatformService) { app.ready$.subscribe(() => { if (config.store.enableWelcomeTab) { app.openNewTabRaw(WelcomeTabComponent) } }) + + platform.setErrorHandler(err => { + console.error('Unhandled exception:', err) + }) } static forRoot (): ModuleWithProviders { diff --git a/terminus-core/src/services/app.service.ts b/terminus-core/src/services/app.service.ts index fc07e483..8f2d2b00 100644 --- a/terminus-core/src/services/app.service.ts +++ b/terminus-core/src/services/app.service.ts @@ -11,9 +11,9 @@ import { SelectorOption } from '../api/selector' import { RecoveryToken } from '../api/tabRecovery' import { BootstrapData, BOOTSTRAP_DATA } from '../api/mainProcess' import { HostWindowService } from '../api/hostWindow' +import { HostAppService } from '../api/hostApp' import { ConfigService } from './config.service' -import { HostAppService } from './hostApp.service' import { TabRecoveryService } from './tabRecovery.service' import { TabsService, TabComponentType } from './tabs.service' @@ -100,7 +100,7 @@ export class AppService { } }) - hostApp.windowFocused$.subscribe(() => this._activeTab?.emitFocused()) + hostWindow.windowFocused$.subscribe(() => this._activeTab?.emitFocused()) this.tabClosed$.subscribe(async tab => { const token = await tabRecovery.getFullRecoveryToken(tab) diff --git a/terminus-core/src/services/config.service.ts b/terminus-core/src/services/config.service.ts index c9135687..0750dbcb 100644 --- a/terminus-core/src/services/config.service.ts +++ b/terminus-core/src/services/config.service.ts @@ -3,7 +3,7 @@ import * as yaml from 'js-yaml' import { Injectable, Inject } from '@angular/core' import { ConfigProvider } from '../api/configProvider' import { PlatformService } from '../api/platform' -import { HostAppService } from './hostApp.service' +import { HostAppService } from '../api/hostApp' import { Vault, VaultService } from './vault.service' const deepmerge = require('deepmerge') diff --git a/terminus-core/src/services/docking.service.ts b/terminus-core/src/services/docking.service.ts index cf845fb0..55766cdd 100644 --- a/terminus-core/src/services/docking.service.ts +++ b/terminus-core/src/services/docking.service.ts @@ -1,9 +1,14 @@ +import { Observable, Subject } from 'rxjs' + export abstract class Screen { id: number name?: string } export abstract class DockingService { + get screensChanged$ (): Observable { return this.screensChanged } + protected screensChanged = new Subject() + abstract dock (): void abstract getScreens (): Screen[] } diff --git a/terminus-core/src/services/homeBase.service.ts b/terminus-core/src/services/homeBase.service.ts index 35ecf28e..b683cefb 100644 --- a/terminus-core/src/services/homeBase.service.ts +++ b/terminus-core/src/services/homeBase.service.ts @@ -1,8 +1,8 @@ -import { Injectable } from '@angular/core' +import { Injectable, Inject } from '@angular/core' import * as mixpanel from 'mixpanel' import { v4 as uuidv4 } from 'uuid' import { ConfigService } from './config.service' -import { PlatformService } from '../api' +import { PlatformService, BOOTSTRAP_DATA, BootstrapData } from '../api' @Injectable({ providedIn: 'root' }) export class HomeBaseService { @@ -13,6 +13,7 @@ export class HomeBaseService { private constructor ( private config: ConfigService, private platform: PlatformService, + @Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData, ) { this.appVersion = platform.getAppVersion() @@ -38,7 +39,7 @@ export class HomeBaseService { sunos: 'OS: Solaris', win32: 'OS: Windows', }[process.platform] - const plugins = (window as any).installedPlugins.filter(x => !x.isBuiltin).map(x => x.name) + const plugins = this.bootstrapData.installedPlugins.filter(x => !x.isBuiltin).map(x => x.name) body += `Plugins: ${plugins.join(', ') || 'none'}\n\n` this.platform.openExternal(`https://github.com/eugeny/terminus/issues/new?body=${encodeURIComponent(body)}&labels=${label}`) } diff --git a/terminus-core/src/services/hostApp.service.ts b/terminus-core/src/services/hostApp.service.ts deleted file mode 100644 index bb9d3248..00000000 --- a/terminus-core/src/services/hostApp.service.ts +++ /dev/null @@ -1,209 +0,0 @@ -import type { BrowserWindow, TouchBar } from 'electron' -import { Observable, Subject } from 'rxjs' -import { Injectable, NgZone, EventEmitter, Injector, Inject } from '@angular/core' -import { ElectronService } from './electron.service' -import { Logger, LogService } from './log.service' -import { CLIHandler } from '../api/cli' -import { BootstrapData, BOOTSTRAP_DATA } from '../api/mainProcess' -import { isWindowsBuild, WIN_BUILD_FLUENT_BG_SUPPORTED } from '../utils' - -export enum Platform { - Linux = 'Linux', - macOS = 'macOS', - Windows = 'Windows', - Web = 'Web', -} - -export interface Bounds { - x: number - y: number - width: number - height: number -} - -/** - * Provides interaction with the main process - */ -@Injectable({ providedIn: 'root' }) -export class HostAppService { - platform: Platform - configPlatform: Platform - - /** - * Fired once the window is visible - */ - shown = new EventEmitter() - isPortable = !!process.env.PORTABLE_EXECUTABLE_FILE - - private preferencesMenu = new Subject() - private configChangeBroadcast = new Subject() - private windowCloseRequest = new Subject() - private windowMoved = new Subject() - private windowFocused = new Subject() - private displayMetricsChanged = new Subject() - private displaysChanged = new Subject() - private logger: Logger - - /** - * Fired when Preferences is selected in the macOS menu - */ - get preferencesMenu$ (): Observable { return this.preferencesMenu } - - /** - * Fired when another window modified the config file - */ - get configChangeBroadcast$ (): Observable { return this.configChangeBroadcast } - - /** - * Fired when the window close button is pressed - */ - get windowCloseRequest$ (): Observable { return this.windowCloseRequest } - - get windowMoved$ (): Observable { return this.windowMoved } - - get windowFocused$ (): Observable { return this.windowFocused } - - get displayMetricsChanged$ (): Observable { return this.displayMetricsChanged } - - get displaysChanged$ (): Observable { return this.displaysChanged } - - private constructor ( - private zone: NgZone, - private electron: ElectronService, - @Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData, - injector: Injector, - log: LogService, - ) { - this.logger = log.create('hostApp') - this.configPlatform = this.platform = { - win32: Platform.Windows, - darwin: Platform.macOS, - linux: Platform.Linux, - }[process.platform] - - if (process.env.XWEB) { - this.platform = Platform.Web - } - - electron.ipcRenderer.on('host:preferences-menu', () => this.zone.run(() => this.preferencesMenu.next())) - - electron.ipcRenderer.on('uncaughtException', (_$event, err) => { - this.logger.error('Unhandled exception:', err) - }) - - electron.ipcRenderer.on('host:window-shown', () => { - this.zone.run(() => this.shown.emit()) - }) - - electron.ipcRenderer.on('host:window-close-request', () => { - this.zone.run(() => this.windowCloseRequest.next()) - }) - - electron.ipcRenderer.on('host:window-moved', () => { - this.zone.run(() => this.windowMoved.next()) - }) - - electron.ipcRenderer.on('host:window-focused', () => { - this.zone.run(() => this.windowFocused.next()) - }) - - electron.ipcRenderer.on('host:display-metrics-changed', () => { - this.zone.run(() => this.displayMetricsChanged.next()) - }) - - electron.ipcRenderer.on('host:displays-changed', () => { - this.zone.run(() => this.displaysChanged.next()) - }) - - electron.ipcRenderer.on('cli', (_$event, argv: any, cwd: string, secondInstance: boolean) => this.zone.run(async () => { - const event = { argv, cwd, secondInstance } - this.logger.info('CLI arguments received:', event) - - const cliHandlers = injector.get(CLIHandler) as unknown as CLIHandler[] - cliHandlers.sort((a, b) => b.priority - a.priority) - - let handled = false - for (const handler of cliHandlers) { - if (handled && handler.firstMatchOnly) { - continue - } - if (await handler.handle(event)) { - this.logger.info('CLI handler matched:', handler.constructor.name) - handled = true - } - } - })) - - electron.ipcRenderer.on('host:config-change', () => this.zone.run(() => { - this.configChangeBroadcast.next() - })) - - if (isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)) { - electron.ipcRenderer.send('window-set-disable-vibrancy-while-dragging', true) - } - } - - /** - * Returns the current remote [[BrowserWindow]] - */ - getWindow (): BrowserWindow { - return this.electron.BrowserWindow.fromId(this.bootstrapData.windowID)! - } - - newWindow (): void { - this.electron.ipcRenderer.send('app:new-window') - } - - openDevTools (): void { - this.getWindow().webContents.openDevTools({ mode: 'undocked' }) - } - - focusWindow (): void { - this.electron.ipcRenderer.send('window-focus') - } - - setBounds (bounds: Bounds): void { - this.electron.ipcRenderer.send('window-set-bounds', bounds) - } - - setAlwaysOnTop (flag: boolean): void { - this.electron.ipcRenderer.send('window-set-always-on-top', flag) - } - - setTouchBar (touchBar: TouchBar): void { - this.getWindow().setTouchBar(touchBar) - } - - /** - * Notifies other windows of config file changes - */ - broadcastConfigChange (configStore: Record): void { - this.electron.ipcRenderer.send('app:config-change', configStore) - } - - emitReady (): void { - this.electron.ipcRenderer.send('app:ready') - } - - bringToFront (): void { - this.electron.ipcRenderer.send('window-bring-to-front') - } - - registerGlobalHotkey (specs: string[]): void { - this.electron.ipcRenderer.send('app:register-global-hotkey', specs) - } - - relaunch (): void { - if (this.isPortable) { - this.electron.app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE }) - } else { - this.electron.app.relaunch() - } - this.electron.app.exit() - } - - quit (): void { - this.logger.info('Quitting') - this.electron.app.quit() - } -} diff --git a/terminus-core/src/services/hotkeys.service.ts b/terminus-core/src/services/hotkeys.service.ts index 0b7b59b2..dd1af5a4 100644 --- a/terminus-core/src/services/hotkeys.service.ts +++ b/terminus-core/src/services/hotkeys.service.ts @@ -3,7 +3,6 @@ import { Observable, Subject } from 'rxjs' import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider' import { stringifyKeySequence, EventData } from './hotkeys.util' import { ConfigService } from './config.service' -import { HostAppService } from './hostApp.service' export interface PartialHotkeyMatch { id: string @@ -33,7 +32,6 @@ export class HotkeysService { private constructor ( private zone: NgZone, - private hostApp: HostAppService, private config: ConfigService, @Inject(HotkeyProvider) private hotkeyProviders: HotkeyProvider[], ) { @@ -47,11 +45,7 @@ export class HotkeysService { } }) }) - this.config.changed$.subscribe(() => { - this.registerGlobalHotkey() - }) this.config.ready$.toPromise().then(() => { - this.registerGlobalHotkey() this.getHotkeyDescriptions().then(hotkeys => { this.hotkeyDescriptions = hotkeys }) @@ -182,30 +176,6 @@ export class HotkeysService { ).reduce((a, b) => a.concat(b)) } - private registerGlobalHotkey () { - let value = this.config.store.hotkeys['toggle-window'] || [] - if (typeof value === 'string') { - value = [value] - } - const specs: string[] = [] - value.forEach((item: string | string[]) => { - item = typeof item === 'string' ? [item] : item - - try { - let electronKeySpec = item[0] - electronKeySpec = electronKeySpec.replace('Meta', 'Super') - electronKeySpec = electronKeySpec.replace('⌘', 'Command') - electronKeySpec = electronKeySpec.replace('⌥', 'Alt') - electronKeySpec = electronKeySpec.replace(/-/g, '+') - specs.push(electronKeySpec) - } catch (err) { - console.error('Could not register the global hotkey:', err) - } - }) - - this.hostApp.registerGlobalHotkey(specs) - } - private getHotkeysConfig () { return this.getHotkeysConfigRecursive(this.config.store.hotkeys) } diff --git a/terminus-electron/src/config.ts b/terminus-electron/src/config.ts new file mode 100644 index 00000000..2c9b89f6 --- /dev/null +++ b/terminus-electron/src/config.ts @@ -0,0 +1,26 @@ +import { ConfigProvider, Platform } from 'terminus-core' + +/** @hidden */ +export class ElectronConfigProvider extends ConfigProvider { + platformDefaults = { + [Platform.macOS]: { + hotkeys: { + 'toggle-window': ['Ctrl-Space'], + 'new-window': ['⌘-N'], + }, + }, + [Platform.Windows]: { + hotkeys: { + 'toggle-window': ['Ctrl-Space'], + 'new-window': ['Ctrl-Shift-N'], + }, + }, + [Platform.Linux]: { + hotkeys: { + 'toggle-window': ['Ctrl-Space'], + 'new-window': ['Ctrl-Shift-N'], + }, + }, + } + defaults = {} +} diff --git a/terminus-electron/src/hotkeys.ts b/terminus-electron/src/hotkeys.ts new file mode 100644 index 00000000..5432987c --- /dev/null +++ b/terminus-electron/src/hotkeys.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core' +import { HotkeyDescription, HotkeyProvider } from 'terminus-core' + +/** @hidden */ +@Injectable() +export class ElectronHotkeyProvider extends HotkeyProvider { + hotkeys: HotkeyDescription[] = [ + { + id: 'new-window', + name: 'New window', + }, + { + id: 'toggle-window', + name: 'Toggle terminal window', + }, + ] + + async provide (): Promise { + return this.hotkeys + } +} diff --git a/terminus-electron/src/index.ts b/terminus-electron/src/index.ts index 0c9f6ade..bddf5faa 100644 --- a/terminus-electron/src/index.ts +++ b/terminus-electron/src/index.ts @@ -1,5 +1,5 @@ import { NgModule } from '@angular/core' -import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, ElectronService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild, HostWindowService } from 'terminus-core' +import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, ElectronService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild, HostWindowService, HotkeyProvider, ConfigProvider } from 'terminus-core' import { TerminalColorSchemeProvider } from 'terminus-terminal' import { HyperColorSchemes } from './colorSchemes' @@ -9,39 +9,51 @@ import { ElectronUpdaterService } from './services/updater.service' import { TouchbarService } from './services/touchbar.service' import { ElectronDockingService } from './services/docking.service' import { ElectronHostWindow } from './services/hostWindow.service' +import { ElectronHostAppService } from './services/hostApp.service' +import { ElectronHotkeyProvider } from './hotkeys' +import { ElectronConfigProvider } from './config' @NgModule({ providers: [ { provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true }, { provide: PlatformService, useClass: ElectronPlatformService }, { provide: HostWindowService, useClass: ElectronHostWindow }, + { provide: HostAppService, useClass: ElectronHostAppService }, { provide: LogService, useClass: ElectronLogService }, { provide: UpdaterService, useClass: ElectronUpdaterService }, { provide: DockingService, useClass: ElectronDockingService }, + { provide: HotkeyProvider, useClass: ElectronHotkeyProvider, multi: true }, + { provide: ConfigProvider, useClass: ElectronConfigProvider, multi: true }, ], }) export default class ElectronModule { constructor ( private config: ConfigService, - private hostApp: HostAppService, + private hostApp: ElectronHostAppService, private electron: ElectronService, + private hostWindow: ElectronHostWindow, touchbar: TouchbarService, docking: DockingService, themeService: ThemesService, - app: AppService + app: AppService, ) { config.ready$.toPromise().then(() => { touchbar.update() docking.dock() - hostApp.shown.subscribe(() => { + hostWindow.windowShown$.subscribe(() => { docking.dock() }) + this.registerGlobalHotkey() this.updateVibrancy() }) + config.changed$.subscribe(() => { + this.registerGlobalHotkey() + }) + themeService.themeChanged$.subscribe(theme => { if (hostApp.platform === Platform.macOS) { - hostApp.getWindow().setTrafficLightPosition({ + hostWindow.getWindow().setTrafficLightPosition({ x: theme.macOSWindowButtonsInsetX ?? 14, y: theme.macOSWindowButtonsInsetY ?? 11, }) @@ -55,9 +67,9 @@ export default class ElectronModule { return } if (progress !== null) { - hostApp.getWindow().setProgressBar(progress / 100.0, { mode: 'normal' }) + hostWindow.getWindow().setProgressBar(progress / 100.0, { mode: 'normal' }) } else { - hostApp.getWindow().setProgressBar(-1, { mode: 'none' }) + hostWindow.getWindow().setProgressBar(-1, { mode: 'none' }) } lastProgress = progress }) @@ -66,6 +78,30 @@ export default class ElectronModule { config.changed$.subscribe(() => this.updateVibrancy()) } + private registerGlobalHotkey () { + let value = this.config.store.hotkeys['toggle-window'] || [] + if (typeof value === 'string') { + value = [value] + } + const specs: string[] = [] + value.forEach((item: string | string[]) => { + item = typeof item === 'string' ? [item] : item + + try { + let electronKeySpec = item[0] + electronKeySpec = electronKeySpec.replace('Meta', 'Super') + electronKeySpec = electronKeySpec.replace('⌘', 'Command') + electronKeySpec = electronKeySpec.replace('⌥', 'Alt') + electronKeySpec = electronKeySpec.replace(/-/g, '+') + specs.push(electronKeySpec) + } catch (err) { + console.error('Could not register the global hotkey:', err) + } + }) + + this.electron.ipcRenderer.send('app:register-global-hotkey', specs) + } + private updateVibrancy () { let vibrancyType = this.config.store.appearance.vibrancyType if (this.hostApp.platform === Platform.Windows && !isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)) { @@ -74,6 +110,8 @@ export default class ElectronModule { document.body.classList.toggle('vibrant', this.config.store.appearance.vibrancy) this.electron.ipcRenderer.send('window-set-vibrancy', this.config.store.appearance.vibrancy, vibrancyType) - this.hostApp.getWindow().setOpacity(this.config.store.appearance.opacity) + this.hostWindow.getWindow().setOpacity(this.config.store.appearance.opacity) } } + +export { ElectronHostWindow, ElectronHostAppService } diff --git a/terminus-electron/src/services/docking.service.ts b/terminus-electron/src/services/docking.service.ts index 0aefe8b5..b422c1e3 100644 --- a/terminus-electron/src/services/docking.service.ts +++ b/terminus-electron/src/services/docking.service.ts @@ -1,24 +1,31 @@ -import { Injectable } from '@angular/core' +import { Injectable, NgZone } from '@angular/core' import type { Display } from 'electron' -import { ConfigService, ElectronService, HostAppService, Bounds, DockingService, Screen } from 'terminus-core' +import { ConfigService, ElectronService, DockingService, Screen, PlatformService } from 'terminus-core' +import { ElectronHostWindow, Bounds } from './hostWindow.service' @Injectable() export class ElectronDockingService extends DockingService { constructor ( private electron: ElectronService, private config: ConfigService, - private hostApp: HostAppService, + private zone: NgZone, + private hostWindow: ElectronHostWindow, + platform: PlatformService, ) { super() - hostApp.displaysChanged$.subscribe(() => this.repositionWindow()) - hostApp.displayMetricsChanged$.subscribe(() => this.repositionWindow()) + this.screensChanged$.subscribe(() => this.repositionWindow()) + platform.displayMetricsChanged$.subscribe(() => this.repositionWindow()) + + electron.ipcRenderer.on('host:displays-changed', () => { + this.zone.run(() => this.screensChanged.next()) + }) } dock (): void { const dockSide = this.config.store.appearance.dock if (dockSide === 'off') { - this.hostApp.setAlwaysOnTop(false) + this.hostWindow.setAlwaysOnTop(false) return } @@ -33,7 +40,7 @@ export class ElectronDockingService extends DockingService { const fill = this.config.store.appearance.dockFill <= 1 ? this.config.store.appearance.dockFill : 1 const space = this.config.store.appearance.dockSpace <= 1 ? this.config.store.appearance.dockSpace : 1 - const [minWidth, minHeight] = this.hostApp.getWindow().getMinimumSize() + const [minWidth, minHeight] = this.hostWindow.getWindow().getMinimumSize() if (dockSide === 'left' || dockSide === 'right') { newBounds.width = Math.max(minWidth, Math.round(fill * display.bounds.width)) @@ -60,9 +67,9 @@ export class ElectronDockingService extends DockingService { const alwaysOnTop = this.config.store.appearance.dockAlwaysOnTop - this.hostApp.setAlwaysOnTop(alwaysOnTop) + this.hostWindow.setAlwaysOnTop(alwaysOnTop) setImmediate(() => { - this.hostApp.setBounds(newBounds) + this.hostWindow.setBounds(newBounds) }) } @@ -84,7 +91,7 @@ export class ElectronDockingService extends DockingService { } private repositionWindow () { - const [x, y] = this.hostApp.getWindow().getPosition() + const [x, y] = this.hostWindow.getWindow().getPosition() for (const screen of this.electron.screen.getAllDisplays()) { const bounds = screen.bounds if (x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height) { @@ -92,6 +99,6 @@ export class ElectronDockingService extends DockingService { } } const screen = this.electron.screen.getPrimaryDisplay() - this.hostApp.getWindow().setPosition(screen.bounds.x, screen.bounds.y) + this.hostWindow.getWindow().setPosition(screen.bounds.x, screen.bounds.y) } } diff --git a/terminus-electron/src/services/hostApp.service.ts b/terminus-electron/src/services/hostApp.service.ts new file mode 100644 index 00000000..b3ddd26a --- /dev/null +++ b/terminus-electron/src/services/hostApp.service.ts @@ -0,0 +1,85 @@ +import { Injectable, NgZone, Injector } from '@angular/core' +import { ElectronService, isWindowsBuild, WIN_BUILD_FLUENT_BG_SUPPORTED, HostAppService, Platform, CLIHandler } from 'terminus-core' + + +@Injectable({ providedIn: 'root' }) +export class ElectronHostAppService extends HostAppService { + get platform (): Platform { + return this.configPlatform + } + + get configPlatform (): Platform { + return { + win32: Platform.Windows, + darwin: Platform.macOS, + linux: Platform.Linux, + }[process.platform] + } + + constructor ( + private zone: NgZone, + private electron: ElectronService, + injector: Injector, + ) { + super(injector) + + electron.ipcRenderer.on('host:preferences-menu', () => this.zone.run(() => this.settingsUIRequest.next())) + + electron.ipcRenderer.on('cli', (_$event, argv: any, cwd: string, secondInstance: boolean) => this.zone.run(async () => { + const event = { argv, cwd, secondInstance } + this.logger.info('CLI arguments received:', event) + + const cliHandlers = injector.get(CLIHandler) as unknown as CLIHandler[] + cliHandlers.sort((a, b) => b.priority - a.priority) + + let handled = false + for (const handler of cliHandlers) { + if (handled && handler.firstMatchOnly) { + continue + } + if (await handler.handle(event)) { + this.logger.info('CLI handler matched:', handler.constructor.name) + handled = true + } + } + })) + + electron.ipcRenderer.on('host:config-change', () => this.zone.run(() => { + this.configChangeBroadcast.next() + })) + + if (isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)) { + electron.ipcRenderer.send('window-set-disable-vibrancy-while-dragging', true) + } + } + + newWindow (): void { + this.electron.ipcRenderer.send('app:new-window') + } + + /** + * Notifies other windows of config file changes + */ + broadcastConfigChange (configStore: Record): void { + this.electron.ipcRenderer.send('app:config-change', configStore) + } + + emitReady (): void { + this.electron.ipcRenderer.send('app:ready') + } + + relaunch (): void { + const isPortable = !!process.env.PORTABLE_EXECUTABLE_FILE + if (isPortable) { + this.electron.app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE }) + } else { + this.electron.app.relaunch() + } + this.electron.app.exit() + } + + quit (): void { + this.logger.info('Quitting') + this.electron.app.quit() + } +} diff --git a/terminus-electron/src/services/hostWindow.service.ts b/terminus-electron/src/services/hostWindow.service.ts index 53908fbb..b44170df 100644 --- a/terminus-electron/src/services/hostWindow.service.ts +++ b/terminus-electron/src/services/hostWindow.service.ts @@ -1,19 +1,24 @@ -import { Injectable, NgZone } from '@angular/core' -import { Observable, Subject } from 'rxjs' -import { ElectronService, HostAppService, HostWindowService } from 'terminus-core' +import type { BrowserWindow, TouchBar } from 'electron' +import { Injectable, Inject, NgZone } from '@angular/core' +import { BootstrapData, BOOTSTRAP_DATA, ElectronService, HostWindowService } from 'terminus-core' + +export interface Bounds { + x: number + y: number + width: number + height: number +} @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, + private electron: ElectronService, + @Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData, ) { super() electron.ipcRenderer.on('host:window-enter-full-screen', () => zone.run(() => { @@ -23,10 +28,34 @@ export class ElectronHostWindow extends HostWindowService { electron.ipcRenderer.on('host:window-leave-full-screen', () => zone.run(() => { this._isFullScreen = false })) + + electron.ipcRenderer.on('host:window-shown', () => { + zone.run(() => this.windowShown.next()) + }) + + electron.ipcRenderer.on('host:window-close-request', () => { + zone.run(() => this.windowCloseRequest.next()) + }) + + electron.ipcRenderer.on('host:window-moved', () => { + zone.run(() => this.windowMoved.next()) + }) + + electron.ipcRenderer.on('host:window-focused', () => { + zone.run(() => this.windowFocused.next()) + }) + } + + getWindow (): BrowserWindow { + return this.electron.BrowserWindow.fromId(this.bootstrapData.windowID)! + } + + openDevTools (): void { + this.getWindow().webContents.openDevTools({ mode: 'undocked' }) } reload (): void { - this.hostApp.getWindow().reload() + this.getWindow().reload() } setTitle (title?: string): void { @@ -34,7 +63,7 @@ export class ElectronHostWindow extends HostWindowService { } toggleFullscreen (): void { - this.hostApp.getWindow().setFullScreen(!this._isFullScreen) + this.getWindow().setFullScreen(!this._isFullScreen) } minimize (): void { @@ -48,4 +77,20 @@ export class ElectronHostWindow extends HostWindowService { close (): void { this.electron.ipcRenderer.send('window-close') } + + setBounds (bounds: Bounds): void { + this.electron.ipcRenderer.send('window-set-bounds', bounds) + } + + setAlwaysOnTop (flag: boolean): void { + this.electron.ipcRenderer.send('window-set-always-on-top', flag) + } + + setTouchBar (touchBar: TouchBar): void { + this.getWindow().setTouchBar(touchBar) + } + + bringToFront (): void { + this.electron.ipcRenderer.send('window-bring-to-front') + } } diff --git a/terminus-electron/src/services/platform.service.ts b/terminus-electron/src/services/platform.service.ts index 2572d7ec..989aea99 100644 --- a/terminus-electron/src/services/platform.service.ts +++ b/terminus-electron/src/services/platform.service.ts @@ -6,6 +6,7 @@ import promiseIpc from 'electron-promise-ipc' import { execFile } from 'mz/child_process' import { Injectable, NgZone } from '@angular/core' import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileDownload, FileUploadOptions, wrapPromise } from 'terminus-core' +import { ElectronHostWindow } from './hostWindow.service' const fontManager = require('fontmanager-redux') // eslint-disable-line /* eslint-disable block-scoped-var */ @@ -20,16 +21,20 @@ try { @Injectable() export class ElectronPlatformService extends PlatformService { supportsWindowControls = true - private userPluginsPath: string = (window as any).userPluginsPath private configPath: string constructor ( private hostApp: HostAppService, + private hostWindow: ElectronHostWindow, private electron: ElectronService, private zone: NgZone, ) { super() this.configPath = path.join(electron.app.getPath('userData'), 'config.yaml') + + electron.ipcRenderer.on('host:display-metrics-changed', () => { + this.zone.run(() => this.displayMetricsChanged.next()) + }) } readClipboard (): string { @@ -41,11 +46,11 @@ export class ElectronPlatformService extends PlatformService { } async installPlugin (name: string, version: string): Promise { - await (promiseIpc as any).send('plugin-manager:install', this.userPluginsPath, name, version) + await (promiseIpc as any).send('plugin-manager:install', name, version) } async uninstallPlugin (name: string): Promise { - await (promiseIpc as any).send('plugin-manager:uninstall', this.userPluginsPath, name) + await (promiseIpc as any).send('plugin-manager:uninstall', name) } async isProcessRunning (name: string): Promise { @@ -163,7 +168,7 @@ export class ElectronPlatformService extends PlatformService { } async showMessageBox (options: MessageBoxOptions): Promise { - return this.electron.dialog.showMessageBox(this.hostApp.getWindow(), options) + return this.electron.dialog.showMessageBox(this.hostWindow.getWindow(), options) } quit (): void { @@ -179,7 +184,7 @@ export class ElectronPlatformService extends PlatformService { } const result = await this.electron.dialog.showOpenDialog( - this.hostApp.getWindow(), + this.hostWindow.getWindow(), { buttonLabel: 'Select', properties, @@ -199,7 +204,7 @@ export class ElectronPlatformService extends PlatformService { async startDownload (name: string, size: number): Promise { const result = await this.electron.dialog.showSaveDialog( - this.hostApp.getWindow(), + this.hostWindow.getWindow(), { defaultPath: name, }, @@ -212,6 +217,12 @@ export class ElectronPlatformService extends PlatformService { this.fileTransferStarted.next(transfer) return transfer } + + setErrorHandler (handler: (_: any) => void): void { + this.electron.ipcRenderer.on('uncaughtException', (_$event, err) => { + handler(err) + }) + } } class ElectronFileUpload extends FileUpload { diff --git a/terminus-electron/src/services/touchbar.service.ts b/terminus-electron/src/services/touchbar.service.ts index 7cb7209b..ab819f13 100644 --- a/terminus-electron/src/services/touchbar.service.ts +++ b/terminus-electron/src/services/touchbar.service.ts @@ -1,6 +1,7 @@ import { SegmentedControlSegment, TouchBarSegmentedControl } from 'electron' import { Injectable, NgZone } from '@angular/core' import { AppService, HostAppService, Platform, ElectronService } from 'terminus-core' +import { ElectronHostWindow } from './hostWindow.service' /** @hidden */ @Injectable({ providedIn: 'root' }) @@ -11,6 +12,7 @@ export class TouchbarService { private constructor ( private app: AppService, private hostApp: HostAppService, + private hostWindow: ElectronHostWindow, private electron: ElectronService, private zone: NgZone, ) { @@ -68,7 +70,7 @@ export class TouchbarService { this.tabsSegmentedControl, ], }) - this.hostApp.setTouchBar(touchBar) + this.hostWindow.setTouchBar(touchBar) } private shortenTitle (title: string): string { diff --git a/terminus-local/src/cli.ts b/terminus-local/src/cli.ts index 4f620888..b30fe3f1 100644 --- a/terminus-local/src/cli.ts +++ b/terminus-local/src/cli.ts @@ -1,7 +1,7 @@ import * as path from 'path' import * as fs from 'mz/fs' import { Injectable } from '@angular/core' -import { CLIHandler, CLIEvent, HostAppService, AppService, ConfigService } from 'terminus-core' +import { CLIHandler, CLIEvent, AppService, ConfigService, HostWindowService } from 'terminus-core' import { TerminalService } from './services/terminal.service' @Injectable() @@ -11,7 +11,7 @@ export class TerminalCLIHandler extends CLIHandler { constructor ( private config: ConfigService, - private hostApp: HostAppService, + private hostWindow: HostWindowService, private terminal: TerminalService, ) { super() @@ -40,7 +40,7 @@ export class TerminalCLIHandler extends CLIHandler { if (await fs.exists(directory)) { if ((await fs.stat(directory)).isDirectory()) { this.terminal.openTab(undefined, directory) - this.hostApp.bringToFront() + this.hostWindow.bringToFront() } } } @@ -53,7 +53,7 @@ export class TerminalCLIHandler extends CLIHandler { args: command.slice(1), }, }, null, true) - this.hostApp.bringToFront() + this.hostWindow.bringToFront() } private handleOpenProfile (profileName: string) { @@ -63,7 +63,7 @@ export class TerminalCLIHandler extends CLIHandler { return } this.terminal.openTabWithOptions(profile.sessionOptions) - this.hostApp.bringToFront() + this.hostWindow.bringToFront() } } @@ -75,7 +75,7 @@ export class OpenPathCLIHandler extends CLIHandler { constructor ( private terminal: TerminalService, - private hostApp: HostAppService, + private hostWindow: HostWindowService, ) { super() } @@ -86,7 +86,7 @@ export class OpenPathCLIHandler extends CLIHandler { if (opAsPath && (await fs.lstat(opAsPath)).isDirectory()) { this.terminal.openTab(undefined, opAsPath) - this.hostApp.bringToFront() + this.hostWindow.bringToFront() return true } diff --git a/terminus-local/src/components/shellSettingsTab.component.ts b/terminus-local/src/components/shellSettingsTab.component.ts index e2abbb52..1f1e5732 100644 --- a/terminus-local/src/components/shellSettingsTab.component.ts +++ b/terminus-local/src/components/shellSettingsTab.component.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { Subscription } from 'rxjs' import { ConfigService, ElectronService, HostAppService, Platform, WIN_BUILD_CONPTY_SUPPORTED, WIN_BUILD_CONPTY_STABLE, isWindowsBuild } from 'terminus-core' +import { ElectronHostWindow } from 'terminus-electron' import { EditProfileModalComponent } from './editProfileModal.component' import { Shell, Profile } from '../api' import { TerminalService } from '../services/terminal.service' @@ -21,6 +22,7 @@ export class ShellSettingsTabComponent { constructor ( public config: ConfigService, public hostApp: HostAppService, + public hostWindow: ElectronHostWindow, public terminal: TerminalService, private electron: ElectronService, private ngbModal: NgbModal, @@ -54,7 +56,7 @@ export class ShellSettingsTabComponent { return } const paths = (await this.electron.dialog.showOpenDialog( - this.hostApp.getWindow(), + this.hostWindow.getWindow(), { defaultPath: shell.fsBase, properties: ['openDirectory', 'showHiddenFiles'], diff --git a/terminus-plugin-manager/src/components/pluginsSettingsTab.component.ts b/terminus-plugin-manager/src/components/pluginsSettingsTab.component.ts index caab9b38..849fe937 100644 --- a/terminus-plugin-manager/src/components/pluginsSettingsTab.component.ts +++ b/terminus-plugin-manager/src/components/pluginsSettingsTab.component.ts @@ -4,8 +4,8 @@ import { debounceTime, distinctUntilChanged, first, tap, flatMap, map } from 'rx import semverGt from 'semver/functions/gt' import { Component, Input } from '@angular/core' -import { ConfigService, PlatformService } from 'terminus-core' -import { PluginInfo, PluginManagerService } from '../services/pluginManager.service' +import { ConfigService, PlatformService, PluginInfo } from 'terminus-core' +import { PluginManagerService } from '../services/pluginManager.service' enum BusyState { Installing = 'Installing', Uninstalling = 'Uninstalling' } diff --git a/terminus-plugin-manager/src/services/pluginManager.service.ts b/terminus-plugin-manager/src/services/pluginManager.service.ts index 3717b5b7..3c7d69a3 100644 --- a/terminus-plugin-manager/src/services/pluginManager.service.ts +++ b/terminus-plugin-manager/src/services/pluginManager.service.ts @@ -1,8 +1,8 @@ import axios from 'axios' import { Observable, from } from 'rxjs' import { map } from 'rxjs/operators' -import { Injectable } from '@angular/core' -import { Logger, LogService, PlatformService } from 'terminus-core' +import { Injectable, Inject } from '@angular/core' +import { Logger, LogService, PlatformService, BOOTSTRAP_DATA, BootstrapData, PluginInfo } from 'terminus-core' const NAME_PREFIX = 'terminus-' const KEYWORD = 'terminus-plugin' @@ -12,30 +12,20 @@ const BLACKLIST = [ 'terminus-shell-selector', // superseded by profiles ] -export interface PluginInfo { - name: string - description: string - packageName: string - isBuiltin: boolean - isOfficial: boolean - version: string - homepage?: string - author: string - path?: string -} - @Injectable({ providedIn: 'root' }) export class PluginManagerService { logger: Logger - builtinPluginsPath: string = (window as any).builtinPluginsPath - userPluginsPath: string = (window as any).userPluginsPath - installedPlugins: PluginInfo[] = (window as any).installedPlugins + userPluginsPath: string + installedPlugins: PluginInfo[] private constructor ( log: LogService, private platform: PlatformService, + @Inject(BOOTSTRAP_DATA) bootstrapData: BootstrapData, ) { this.logger = log.create('pluginManager') + this.installedPlugins = bootstrapData.installedPlugins + this.userPluginsPath = bootstrapData.userPluginsPath } listAvailable (query?: string): Observable { diff --git a/terminus-settings/src/buttonProvider.ts b/terminus-settings/src/buttonProvider.ts index c3e00a1f..375efd59 100644 --- a/terminus-settings/src/buttonProvider.ts +++ b/terminus-settings/src/buttonProvider.ts @@ -12,7 +12,7 @@ export class ButtonProvider extends ToolbarButtonProvider { private app: AppService, ) { super() - hostApp.preferencesMenu$.subscribe(() => this.open()) + hostApp.settingsUIRequest$.subscribe(() => this.open()) hotkeys.matchedHotkey.subscribe(async (hotkey) => { if (hotkey === 'settings') { diff --git a/terminus-settings/src/components/settingsTab.component.pug b/terminus-settings/src/components/settingsTab.component.pug index 48a9340a..7264987b 100644 --- a/terminus-settings/src/components/settingsTab.component.pug +++ b/terminus-settings/src/components/settingsTab.component.pug @@ -24,7 +24,7 @@ button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(clic span Report a problem button.btn.btn-secondary( - *ngIf='!updateAvailable', + *ngIf='!updateAvailable && hostApp.platform !== Platform.Web', (click)='checkForUpdates()', [disabled]='checkingForUpdate' ) @@ -46,7 +46,7 @@ button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(clic .description Allows quickly opening a terminal in the selected folder toggle([ngModel]='isShellIntegrationInstalled', (ngModelChange)='toggleShellIntegration()') - .form-line + .form-line(*ngIf='hostApp.platform !== Platform.Web') .header .title Enable analytics .description We're only tracking your Terminus and OS versions. @@ -55,17 +55,17 @@ button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(clic (ngModelChange)='saveConfiguration(true)', ) - .form-line + .form-line(*ngIf='hostApp.platform !== Platform.Web') .header .title Automatic Updates .description Enable automatic installation of updates when they become available. toggle([(ngModel)]='config.store.enableAutomaticUpdates', (ngModelChange)='saveConfiguration()') - .form-line + .form-line(*ngIf='hostApp.platform !== Platform.Web') .header .title Debugging - button.btn.btn-secondary((click)='hostApp.openDevTools()') + button.btn.btn-secondary((click)='hostWindow.openDevTools()') i.fas.fa-bug span Open DevTools diff --git a/terminus-settings/src/components/settingsTab.component.ts b/terminus-settings/src/components/settingsTab.component.ts index 0ae9f17d..38dc1403 100644 --- a/terminus-settings/src/components/settingsTab.component.ts +++ b/terminus-settings/src/components/settingsTab.component.ts @@ -10,6 +10,7 @@ import { HomeBaseService, UpdaterService, PlatformService, + HostWindowService, } from 'terminus-core' import { SettingsTabProvider } from '../api' @@ -36,6 +37,7 @@ export class SettingsTabComponent extends BaseTabComponent { constructor ( public config: ConfigService, public hostApp: HostAppService, + public hostWindow: HostWindowService, public homeBase: HomeBaseService, public platform: PlatformService, public zone: NgZone, diff --git a/terminus-settings/src/components/windowSettingsTab.component.ts b/terminus-settings/src/components/windowSettingsTab.component.ts index c6a8d319..646f10c3 100644 --- a/terminus-settings/src/components/windowSettingsTab.component.ts +++ b/terminus-settings/src/components/windowSettingsTab.component.ts @@ -39,7 +39,7 @@ export class WindowSettingsTabComponent extends BaseComponent { const dockingService = docking if (dockingService) { - this.subscribeUntilDestroyed(hostApp.displaysChanged$, () => { + this.subscribeUntilDestroyed(dockingService.screensChanged$, () => { this.zone.run(() => this.screens = dockingService.getScreens()) }) this.screens = dockingService.getScreens() diff --git a/terminus-ssh/src/buttonProvider.ts b/terminus-ssh/src/buttonProvider.ts index ee186ba6..592b1ce0 100644 --- a/terminus-ssh/src/buttonProvider.ts +++ b/terminus-ssh/src/buttonProvider.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Injectable } from '@angular/core' -import { HotkeysService, ToolbarButtonProvider, ToolbarButton } from 'terminus-core' +import { HotkeysService, ToolbarButtonProvider, ToolbarButton, HostAppService, Platform } from 'terminus-core' import { SSHService } from './services/ssh.service' /** @hidden */ @@ -8,6 +8,7 @@ import { SSHService } from './services/ssh.service' export class ButtonProvider extends ToolbarButtonProvider { constructor ( hotkeys: HotkeysService, + private hostApp: HostAppService, private ssh: SSHService, ) { super() @@ -23,14 +24,20 @@ export class ButtonProvider extends ToolbarButtonProvider { } provide (): ToolbarButton[] { - return [{ - icon: require('./icons/globe.svg'), - weight: 5, - title: 'SSH connections', - touchBarNSImage: 'NSTouchBarOpenInBrowserTemplate', - click: () => { - this.activate() - }, - }] + if (this.hostApp.platform === Platform.Web) { + return [{ + icon: require('../../terminus-local/src/icons/plus.svg'), + title: 'SSH connections', + click: () => this.activate(), + }] + } else { + return [{ + icon: require('./icons/globe.svg'), + weight: 5, + title: 'SSH connections', + touchBarNSImage: 'NSTouchBarOpenInBrowserTemplate', + click: () => this.activate(), + }] + } } } diff --git a/terminus-ssh/src/components/editConnectionModal.component.ts b/terminus-ssh/src/components/editConnectionModal.component.ts index 13783415..4acd7cf2 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, PlatformService } from 'terminus-core' +import { ElectronService, 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' @@ -30,7 +30,6 @@ export class EditConnectionModalComponent { private modalInstance: NgbActiveModal, private electron: ElectronService, private platform: PlatformService, - private hostApp: HostAppService, private passwordStorage: PasswordStorageService, private ngbModal: NgbModal, ) { @@ -104,7 +103,6 @@ export class EditConnectionModalComponent { addPrivateKey () { this.electron.dialog.showOpenDialog( - this.hostApp.getWindow(), { defaultPath: this.connection.privateKeys![0], title: 'Select private key', diff --git a/terminus-terminal/src/api/baseTerminalTab.component.ts b/terminus-terminal/src/api/baseTerminalTab.component.ts index aeb7070a..421d7390 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, 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, HostWindowService } from 'terminus-core' import { BaseSession } from '../session' import { TerminalFrontendService } from '../services/terminalFrontend.service' @@ -108,6 +108,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit protected log: LogService protected decorators: TerminalDecorator[] = [] protected contextMenuProviders: TabContextMenuItemProvider[] + protected hostWindow: HostWindowService // Deps end protected logger: Logger @@ -160,6 +161,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit this.log = injector.get(LogService) this.decorators = injector.get(TerminalDecorator, null, InjectFlags.Optional) as TerminalDecorator[] this.contextMenuProviders = injector.get(TabContextMenuItemProvider, null, InjectFlags.Optional) as TabContextMenuItemProvider[] + this.hostWindow = injector.get(HostWindowService) this.logger = this.log.create('baseTerminalTab') this.setTitle('Terminal') @@ -596,8 +598,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit }) }) - this.termContainerSubscriptions.subscribe(this.hostApp.displayMetricsChanged$, maybeConfigure) - this.termContainerSubscriptions.subscribe(this.hostApp.windowMoved$, maybeConfigure) + this.termContainerSubscriptions.subscribe(this.platform.displayMetricsChanged$, maybeConfigure) + this.termContainerSubscriptions.subscribe(this.hostWindow.windowMoved$, maybeConfigure) } setSession (session: BaseSession|null, destroyOnSessionClose = false): void { diff --git a/terminus-terminal/src/cli.ts b/terminus-terminal/src/cli.ts index a3b1775e..d3c0de86 100644 --- a/terminus-terminal/src/cli.ts +++ b/terminus-terminal/src/cli.ts @@ -1,6 +1,6 @@ import shellEscape from 'shell-escape' import { Injectable } from '@angular/core' -import { CLIHandler, CLIEvent, HostAppService, AppService } from 'terminus-core' +import { CLIHandler, CLIEvent, AppService, HostWindowService } from 'terminus-core' import { BaseTerminalTabComponent } from './api/baseTerminalTab.component' @Injectable() @@ -10,7 +10,7 @@ export class TerminalCLIHandler extends CLIHandler { constructor ( private app: AppService, - private hostApp: HostAppService, + private hostWindow: HostWindowService, ) { super() } @@ -30,11 +30,10 @@ export class TerminalCLIHandler extends CLIHandler { return false } - private handlePaste (text: string) { if (this.app.activeTab instanceof BaseTerminalTabComponent && this.app.activeTab.session) { this.app.activeTab.sendInput(text) - this.hostApp.bringToFront() + this.hostWindow.bringToFront() } } } diff --git a/terminus-web/src/index.ts b/terminus-web/src/index.ts index f9d4abb7..33b8e405 100644 --- a/terminus-web/src/index.ts +++ b/terminus-web/src/index.ts @@ -1,11 +1,12 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' -import { HostWindowService, LogService, PlatformService, UpdaterService } from 'terminus-core' +import { HostAppService, 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 { WebHostApp } from './services/hostApp.service' import { MessageBoxModalComponent } from './components/messageBoxModal.component' import './styles.scss' @@ -19,6 +20,7 @@ import './styles.scss' { provide: LogService, useClass: ConsoleLogService }, { provide: UpdaterService, useClass: NullUpdaterService }, { provide: HostWindowService, useClass: WebHostWindow }, + { provide: HostAppService, useClass: WebHostApp }, ], declarations: [ MessageBoxModalComponent, diff --git a/terminus-web/src/platform.ts b/terminus-web/src/platform.ts index 3d2d415e..f4625075 100644 --- a/terminus-web/src/platform.ts +++ b/terminus-web/src/platform.ts @@ -61,7 +61,7 @@ export class WebPlatformService extends PlatformService { } getAppVersion (): string { - return '1.0-web' + return this.connector.getAppVersion() } async listFonts (): Promise { @@ -136,6 +136,10 @@ export class WebPlatformService extends PlatformService { this.fileSelector.click() }) } + + setErrorHandler (handler: (_: any) => void): void { + window.addEventListener('error', handler) + } } class HTMLFileDownload extends FileDownload { diff --git a/terminus-web/src/services/hostApp.service.ts b/terminus-web/src/services/hostApp.service.ts new file mode 100644 index 00000000..963a78a8 --- /dev/null +++ b/terminus-web/src/services/hostApp.service.ts @@ -0,0 +1,33 @@ +import { Injectable, Injector } from '@angular/core' +import { HostAppService, Platform } from 'terminus-core' + +@Injectable() +export class WebHostApp extends HostAppService { + get platform (): Platform { + return Platform.Web + } + + get configPlatform (): Platform { + return Platform.Windows // TODO + } + + // Needed for injector metadata + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor ( + injector: Injector, + ) { + super(injector) + } + + newWindow (): void { + throw new Error('Not implemented') + } + + relaunch (): void { + location.reload() + } + + quit (): void { + window.close() + } +} diff --git a/terminus-web/src/services/hostWindow.service.ts b/terminus-web/src/services/hostWindow.service.ts index decb6ebf..3bda71c2 100644 --- a/terminus-web/src/services/hostWindow.service.ts +++ b/terminus-web/src/services/hostWindow.service.ts @@ -1,13 +1,15 @@ 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() + constructor () { + super() + this.windowShown.next() + this.windowFocused.next() + } reload (): void { location.reload()