Browse Source

started separating terminus-electron and terminus-web

Eugene Pankov 4 years ago
parent
commit
012986dc7e
94 changed files with 1899 additions and 972 deletions
  1. 12 0
      .github/dependabot.yml
  2. 2 0
      HACKING.md
  3. 0 4
      app/common.ts
  4. 1 1
      app/index.pug
  5. 17 0
      app/lib/app.ts
  6. 3 1
      app/lib/window.ts
  7. 1 0
      app/package.json
  8. 6 0
      app/src/app.module.ts
  9. 42 0
      app/src/entry-web.ts
  10. 5 7
      app/src/entry.ts
  11. 4 2
      app/webpack.config.js
  12. 1 0
      app/webpack.main.config.js
  13. 1 1
      package.json
  14. 3 0
      scripts/prepackage-plugins.js
  15. 2 0
      scripts/vars.js
  16. 1 5
      terminus-core/package.json
  17. 6 5
      terminus-core/src/api/index.ts
  18. 0 4
      terminus-core/src/api/main-process.ts
  19. 4 0
      terminus-core/src/api/mainProcess.ts
  20. 9 0
      terminus-core/src/api/menu.ts
  21. 69 0
      terminus-core/src/api/platform.ts
  22. 2 2
      terminus-core/src/api/tabContextMenuProvider.ts
  23. 3 37
      terminus-core/src/components/appRoot.component.ts
  24. 8 16
      terminus-core/src/components/tabHeader.component.ts
  25. 1 0
      terminus-core/src/config.ts
  26. 7 6
      terminus-core/src/index.ts
  27. 10 9
      terminus-core/src/services/app.service.ts
  28. 33 25
      terminus-core/src/services/config.service.ts
  29. 15 7
      terminus-core/src/services/docking.service.ts
  30. 9 10
      terminus-core/src/services/homeBase.service.ts
  31. 11 44
      terminus-core/src/services/hostApp.service.ts
  32. 5 6
      terminus-core/src/services/hotkeys.service.ts
  33. 9 53
      terminus-core/src/services/log.service.ts
  34. 9 10
      terminus-core/src/services/themes.service.ts
  35. 3 136
      terminus-core/src/services/updater.service.ts
  36. 9 10
      terminus-core/src/tabContextMenu.ts
  37. 4 226
      terminus-core/yarn.lock
  38. 1 0
      terminus-electron/.gitignore
  39. 27 0
      terminus-electron/package.json
  40. 1 2
      terminus-electron/src/colorSchemes.ts
  41. 77 0
      terminus-electron/src/index.ts
  42. 97 0
      terminus-electron/src/services/docking.service.ts
  43. 54 0
      terminus-electron/src/services/log.service.ts
  44. 146 0
      terminus-electron/src/services/platform.ts
  45. 2 2
      terminus-electron/src/services/shellIntegration.service.ts
  46. 1 3
      terminus-electron/src/services/touchbar.service.ts
  47. 134 0
      terminus-electron/src/services/updater.service.ts
  48. 7 0
      terminus-electron/tsconfig.json
  49. 14 0
      terminus-electron/tsconfig.typings.json
  50. 5 0
      terminus-electron/webpack.config.js
  51. 477 0
      terminus-electron/yarn.lock
  52. 1 1
      terminus-local/src/components/terminalTab.component.ts
  53. 5 2
      terminus-local/src/index.ts
  54. 4 2
      terminus-local/src/services/terminal.service.ts
  55. 17 10
      terminus-local/src/session.ts
  56. 5 6
      terminus-local/src/tabContextMenu.ts
  57. 0 1
      terminus-plugin-manager/package.json
  58. 4 4
      terminus-plugin-manager/src/components/pluginsSettingsTab.component.ts
  59. 4 4
      terminus-plugin-manager/src/services/pluginManager.service.ts
  60. 0 175
      terminus-plugin-manager/yarn.lock
  61. 3 1
      terminus-settings/src/components/hotkeyInputModal.component.ts
  62. 5 2
      terminus-settings/src/components/settingsTab.component.pug
  63. 7 9
      terminus-settings/src/components/settingsTab.component.ts
  64. 9 9
      terminus-settings/src/components/windowSettingsTab.component.pug
  65. 13 7
      terminus-settings/src/components/windowSettingsTab.component.ts
  66. 1 1
      terminus-ssh/src/components/sshSettingsTab.component.pug
  67. 3 2
      terminus-ssh/src/components/sshSettingsTab.component.ts
  68. 3 17
      terminus-ssh/src/services/ssh.service.ts
  69. 6 17
      terminus-ssh/src/winSCPIntegration.ts
  70. 16 11
      terminus-terminal/src/api/baseTerminalTab.component.ts
  71. 2 2
      terminus-terminal/src/api/contextMenuProvider.ts
  72. 3 19
      terminus-terminal/src/components/appearanceSettingsTab.component.ts
  73. 2 2
      terminus-terminal/src/components/terminalSettingsTab.component.pug
  74. 6 3
      terminus-terminal/src/components/terminalSettingsTab.component.ts
  75. 3 5
      terminus-terminal/src/frontends/frontend.ts
  76. 11 1
      terminus-terminal/src/frontends/htermFrontend.ts
  77. 25 8
      terminus-terminal/src/frontends/xtermFrontend.ts
  78. 0 2
      terminus-terminal/src/index.ts
  79. 4 8
      terminus-terminal/src/services/terminalFrontend.service.ts
  80. 4 5
      terminus-terminal/src/tabContextMenu.ts
  81. 1 0
      terminus-web/.gitignore
  82. 27 0
      terminus-web/package.json
  83. 17 0
      terminus-web/src/index.ts
  84. 76 0
      terminus-web/src/platform.ts
  85. 9 0
      terminus-web/src/services/log.service.ts
  86. 10 0
      terminus-web/src/services/updater.service.ts
  87. 11 0
      terminus-web/src/styles.scss
  88. 7 0
      terminus-web/tsconfig.json
  89. 14 0
      terminus-web/tsconfig.typings.json
  90. 5 0
      terminus-web/webpack.config.js
  91. 178 0
      terminus-web/yarn.lock
  92. 2 1
      tsconfig.json
  93. 2 0
      webpack.config.js
  94. 4 1
      webpack.plugin.config.js

+ 12 - 0
.github/dependabot.yml

@@ -42,6 +42,18 @@ updates:
     interval: daily
     time: "04:00"
   open-pull-requests-limit: 20
+- package-ecosystem: npm
+  directory: "/terminus-electron"
+  schedule:
+    interval: daily
+    time: "04:00"
+  open-pull-requests-limit: 20
+- package-ecosystem: npm
+  directory: "/terminus-web"
+  schedule:
+    interval: daily
+    time: "04:00"
+  open-pull-requests-limit: 20
 - package-ecosystem: npm
   directory: "/terminus-plugin-manager"
   schedule:

+ 2 - 0
HACKING.md

@@ -42,10 +42,12 @@ terminus
 ├─ scripts                              # Maintenance scripts
 ├─ terminus-community-color-schemes     # Plugin that provides color schemes
 ├─ terminus-core                        # Plugin that provides base UI and tab management
+├─ terminus-electron                    # Plugin that provides Electron-specific functions
 └─ terminus-local                       # Plugin that provides local shells and profiles
 ├─ terminus-plugin-manager              # Plugin that installs other plugins
 ├─ terminus-settings                    # Plugin that provides the settings tab
 └─ terminus-terminal                    # Plugin that provides terminal tabs
+├─ terminus-web                         # Plugin that provides web-specific functions
 ```
 
 # Plugin layout

+ 0 - 4
app/common.ts

@@ -1,4 +0,0 @@
-export interface BootstrapData {
-    config: Record<string, any>
-    executable: string
-}

+ 1 - 1
app/index.pug

@@ -1,5 +1,5 @@
 doctype html
-html
+html.terminus
     head
         meta(charset='UTF-8')
         base(href='index.html')

+ 17 - 0
app/lib/app.ts

@@ -7,6 +7,12 @@ import { Window, WindowOptions } from './window'
 import { pluginManager } from './pluginManager'
 import { PTYManager } from './pty'
 
+/* eslint-disable block-scoped-var */
+
+try {
+    var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
+} catch (_) { }
+
 export class Application {
     private tray?: Tray
     private ptyManager = new PTYManager()
@@ -14,6 +20,7 @@ export class Application {
 
     constructor () {
         remote.initialize()
+        this.useBuiltinGraphics()
         this.ptyManager.init(this)
 
         ipcMain.on('app:config-change', (_event, config) => {
@@ -161,6 +168,16 @@ export class Application {
         this.windows[this.windows.length - 1].passCliArguments(argv, cwd, true)
     }
 
+    private useBuiltinGraphics (): void {
+        if (process.platform === 'win32') {
+            const keyPath = 'SOFTWARE\\Microsoft\\DirectX\\UserGpuPreferences'
+            const valueName = app.getPath('exe')
+            if (!wnr.getRegistryValue(wnr.HK.CU, keyPath, valueName)) {
+                wnr.setRegistryValue(wnr.HK.CU, keyPath, valueName, wnr.REG.SZ, 'GpuPreference=1;')
+            }
+        }
+    }
+
     private setupMenu () {
         const template: MenuItemConstructorOptions[] = [
             {

+ 3 - 1
app/lib/window.ts

@@ -122,7 +122,7 @@ export class Window {
             }
         })
 
-        this.window.loadURL(`file://${app.getAppPath()}/dist/index.html?${this.window.id}`, { extraHeaders: 'pragma: no-cache\n' })
+        this.window.loadURL(`file://${app.getAppPath()}/dist/index.html`, { extraHeaders: 'pragma: no-cache\n' })
 
         this.window.webContents.setVisualZoomLevelLimits(1, 1)
         this.window.webContents.setZoomFactor(1)
@@ -297,6 +297,8 @@ export class Window {
             this.window.webContents.send('start', {
                 config: this.configStore,
                 executable: app.getPath('exe'),
+                windowID: this.window.id,
+                isFirstWindow: this.window.id === 1,
             })
         })
 

+ 1 - 0
app/package.json

@@ -57,6 +57,7 @@
   "peerDependencies": {
     "terminus-community-color-schemes": "*",
     "terminus-core": "*",
+    "terminus-electron": "*",
     "terminus-local": "*",
     "terminus-plugin-manager": "*",
     "terminus-serial": "*",

+ 6 - 0
app/src/app.module.ts

@@ -16,10 +16,15 @@ export function getRootModule (plugins: any[]) {
             extendedTimeOut: 1000,
         }),
     ]
+
     const bootstrap = [
         ...plugins.filter(x => x.bootstrap).map(x => x.bootstrap),
     ]
 
+    const providers = [
+        ...plugins.filter(x => x.providers).map(x => x.providers),
+    ].flat()
+
     if (bootstrap.length === 0) {
         throw new Error('Did not find any bootstrap components. Are there any plugins installed?')
     }
@@ -27,6 +32,7 @@ export function getRootModule (plugins: any[]) {
     @NgModule({
         imports,
         bootstrap,
+        providers,
     }) class RootModule { } // eslint-disable-line @typescript-eslint/no-extraneous-class
 
     return RootModule

+ 42 - 0
app/src/entry-web.ts

@@ -0,0 +1,42 @@
+import 'zone.js'
+import 'core-js/proposals/reflect-metadata'
+import 'core-js/features/array/flat'
+import 'rxjs'
+
+import './global.scss'
+import './toastr.scss'
+
+import { enableProdMode, NgModuleRef, ApplicationRef } from '@angular/core'
+import { enableDebugTools } from '@angular/platform-browser'
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
+
+import { getRootModule } from './app.module'
+import { BootstrapData, BOOTSTRAP_DATA } from '../../terminus-core/src/api/mainProcess'
+
+
+window['bootstrapTerminus'] = async function bootstrap (packageModules: any[], bootstrapData: BootstrapData, debugMode = false): Promise<NgModuleRef<any>> {
+    const pluginModules = []
+    for (const packageModule of packageModules) {
+        const pluginModule = packageModule.default.forRoot ? packageModule.default.forRoot() : packageModule.default
+        pluginModule.pluginName = packageModule.pluginName
+        pluginModule.bootstrap = packageModule.bootstrap
+        pluginModules.push(pluginModule)
+    }
+
+    if (!debugMode) {
+        enableProdMode()
+    }
+
+    const module = getRootModule(pluginModules)
+    window['rootModule'] = module
+
+    const moduleRef = await platformBrowserDynamic([
+        { provide: BOOTSTRAP_DATA, useValue: bootstrapData },
+    ]).bootstrapModule(module)
+    if (debugMode) {
+        const applicationRef = moduleRef.injector.get(ApplicationRef)
+        const componentRef = applicationRef.components[0]
+        enableDebugTools(componentRef)
+    }
+    return moduleRef
+}

+ 5 - 7
app/src/entry.ts

@@ -12,7 +12,7 @@ import { ipcRenderer } from 'electron'
 
 import { getRootModule } from './app.module'
 import { findPlugins, loadPlugins, PluginInfo } from './plugins'
-import { BootstrapData } from '../common'
+import { BootstrapData, BOOTSTRAP_DATA } from '../../terminus-core/src/api/mainProcess'
 
 // Always land on the start view
 location.hash = ''
@@ -39,11 +39,9 @@ async function bootstrap (plugins: PluginInfo[], bootstrapData: BootstrapData, s
     })
     const module = getRootModule(pluginModules)
     window['rootModule'] = module
-    const moduleRef = await platformBrowserDynamic().bootstrapModule(module, {
-        providers: [
-            { provide: 'bootstrapData', useValue: bootstrapData },
-        ],
-    })
+    const moduleRef = await platformBrowserDynamic([
+        { provide: BOOTSTRAP_DATA, useValue: bootstrapData },
+    ]).bootstrapModule(module)
     if (process.env.TERMINUS_DEV) {
         const applicationRef = moduleRef.injector.get(ApplicationRef)
         const componentRef = applicationRef.components[0]
@@ -54,12 +52,12 @@ async function bootstrap (plugins: PluginInfo[], bootstrapData: BootstrapData, s
 
 ipcRenderer.once('start', async (_$event, bootstrapData: BootstrapData) => {
     console.log('Window bootstrap data:', bootstrapData)
-    ;(window as any).bootstrapData = bootstrapData
 
     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')
     console.log('Starting with plugins:', plugins)
     try {
         await bootstrap(plugins, bootstrapData)

+ 4 - 2
app/webpack.config.js

@@ -9,6 +9,7 @@ module.exports = {
     sentry: path.resolve(__dirname, 'lib/sentry.ts'),
     preload: path.resolve(__dirname, 'src/entry.preload.ts'),
     bundle: path.resolve(__dirname, 'src/entry.ts'),
+    'bundle-web': path.resolve(__dirname, 'src/entry-web.ts'),
   },
   mode: process.env.TERMINUS_DEV ? 'development' : 'production',
   optimization:{
@@ -41,9 +42,9 @@ module.exports = {
       {
         test: /\.(png|svg)$/,
         use: {
-          loader: 'file-loader',
+          loader: 'url-loader',
           options: {
-            name: 'images/[name].[ext]',
+            limit: 999999,
           },
         },
       },
@@ -66,6 +67,7 @@ module.exports = {
     '@angular/forms': 'commonjs @angular/forms',
     '@angular/common': 'commonjs @angular/common',
     '@ng-bootstrap/ng-bootstrap': 'commonjs @ng-bootstrap/ng-bootstrap',
+    '@electron/remote': 'commonjs @electron/remote',
     child_process: 'commonjs child_process',
     electron: 'commonjs electron',
     fs: 'commonjs fs',

+ 1 - 0
app/webpack.main.config.js

@@ -51,6 +51,7 @@ module.exports = {
     util: 'commonjs util',
     'source-map-support': 'commonjs source-map-support',
     'windows-swca': 'commonjs windows-swca',
+    'windows-native-registry': 'commonjs windows-native-registry',
     'windows-blurbehind': 'commonjs windows-blurbehind',
     'yargs/yargs': 'commonjs yargs/yargs',
   },

+ 1 - 1
package.json

@@ -66,7 +66,7 @@
     "**/graceful-fs": "^4.2.4"
   },
   "scripts": {
-    "build": "npm run build:typings && webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-local/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js && webpack --color --config terminus-serial/webpack.config.js",
+    "build": "npm run build:typings && webpack --color --config app/webpack.main.config.js && webpack --color --config app/webpack.config.js && webpack --color --config terminus-core/webpack.config.js && webpack --color --config terminus-settings/webpack.config.js && webpack --color --config terminus-terminal/webpack.config.js && webpack --color --config terminus-local/webpack.config.js && webpack --color --config terminus-plugin-manager/webpack.config.js && webpack --color --config terminus-community-color-schemes/webpack.config.js && webpack --color --config terminus-ssh/webpack.config.js && webpack --color --config terminus-serial/webpack.config.js && webpack --color --config terminus-electron/webpack.config.js && webpack --color --config terminus-web/webpack.config.js",
     "build:typings": "node scripts/build-typings.js",
     "watch": "cross-env TERMINUS_DEV=1 webpack --progress --color --watch",
     "start": "cross-env TERMINUS_DEV=1 electron app --debug --inspect",

+ 3 - 0
scripts/prepackage-plugins.js

@@ -11,6 +11,9 @@ sh.mkdir('-p', target)
 fs.writeFileSync(path.join(target, 'package.json'), '{}')
 sh.cd(target)
 vars.builtinPlugins.forEach(plugin => {
+  if (plugin === 'terminus-web') {
+    continue
+  }
   log.info('install', plugin)
   sh.cp('-r', path.join('..', plugin), '.')
   sh.rm('-rf', path.join(plugin, 'node_modules'))

+ 2 - 0
scripts/vars.js

@@ -19,7 +19,9 @@ exports.builtinPlugins = [
   'terminus-settings',
   'terminus-terminal',
   'terminus-local',
+  'terminus-web',
   'terminus-community-color-schemes',
+  'terminus-electron',
   'terminus-plugin-manager',
   'terminus-ssh',
   'terminus-serial',

+ 1 - 5
terminus-core/package.json

@@ -17,10 +17,7 @@
   "author": "Eugene Pankov",
   "license": "MIT",
   "devDependencies": {
-    "@electron/remote": "1.1.0",
     "@types/js-yaml": "^4.0.0",
-    "@types/winston": "^2.3.6",
-    "axios": "^0.21.1",
     "bootstrap": "^4.1.3",
     "clone-deep": "^4.0.1",
     "core-js": "^3.1.2",
@@ -31,8 +28,7 @@
     "ng2-dnd": "^5.0.2",
     "ngx-perfect-scrollbar": "^10.1.0",
     "readable-stream": "3.6.0",
-    "uuid": "^8.0.0",
-    "winston": "^3.3.3"
+    "uuid": "^8.0.0"
   },
   "peerDependencies": {
     "@angular/animations": "^9.1.9",

+ 6 - 5
terminus-core/src/api/index.ts

@@ -10,18 +10,19 @@ export { Theme } from './theme'
 export { TabContextMenuItemProvider } from './tabContextMenuProvider'
 export { SelectorOption } from './selector'
 export { CLIHandler, CLIEvent } from './cli'
-export { BootstrapData } from './main-process'
+export { PlatformService, ClipboardContent } from './platform'
+export { MenuItemOptions } from './menu'
+export { BootstrapData, BOOTSTRAP_DATA } from './mainProcess'
 
 export { AppService } from '../services/app.service'
 export { ConfigService } from '../services/config.service'
-export { DockingService } from '../services/docking.service'
+export { DockingService, Screen } from '../services/docking.service'
 export { ElectronService } from '../services/electron.service'
-export { Logger, LogService } from '../services/log.service'
+export { Logger, ConsoleLogger, LogService } from '../services/log.service'
 export { HomeBaseService } from '../services/homeBase.service'
 export { HotkeysService } from '../services/hotkeys.service'
-export { HostAppService, Platform } from '../services/hostApp.service'
+export { HostAppService, Platform, Bounds } from '../services/hostApp.service'
 export { NotificationsService } from '../services/notifications.service'
-export { ShellIntegrationService } from '../services/shellIntegration.service'
 export { ThemesService } from '../services/themes.service'
 export { TabsService } from '../services/tabs.service'
 export { UpdaterService } from '../services/updater.service'

+ 0 - 4
terminus-core/src/api/main-process.ts

@@ -1,4 +0,0 @@
-export interface BootstrapData {
-    config: Record<string, any>
-    executable: string
-}

+ 4 - 0
terminus-core/src/api/mainProcess.ts

@@ -1,4 +1,8 @@
+export const BOOTSTRAP_DATA = 'BOOTSTRAP_DATA'
+
 export interface BootstrapData {
     config: Record<string, any>
     executable: string
+    isFirstWindow: boolean
+    windowID: number
 }

+ 9 - 0
terminus-core/src/api/menu.ts

@@ -0,0 +1,9 @@
+export interface MenuItemOptions {
+    type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio')
+    label?: string
+    sublabel?: string
+    enabled?: boolean
+    checked?: boolean
+    submenu?: MenuItemOptions[]
+    click?: () => void
+}

+ 69 - 0
terminus-core/src/api/platform.ts

@@ -0,0 +1,69 @@
+import { MenuItemOptions } from './menu'
+
+/* eslint-disable @typescript-eslint/no-unused-vars */
+export interface ClipboardContent {
+    text: string
+    html?: string
+}
+
+export abstract class PlatformService {
+    supportsWindowControls = false
+
+    abstract setClipboard (content: ClipboardContent): void
+    abstract loadConfig (): Promise<string>
+    abstract saveConfig (content: string): Promise<void>
+
+    getConfigPath (): string|null {
+        return null
+    }
+
+    showItemInFolder (path: string): void {
+        throw new Error('Not implemented')
+    }
+
+    async isProcessRunning (name: string): Promise<boolean> {
+        return false
+    }
+
+    async installPlugin (name: string, version: string): Promise<void> {
+        throw new Error('Not implemented')
+    }
+
+    async uninstallPlugin (name: string): Promise<void> {
+        throw new Error('Not implemented')
+    }
+
+    getWinSCPPath (): string|null {
+        throw new Error('Not implemented')
+    }
+
+    exec (app: string, argv: string[]): void {
+        throw new Error('Not implemented')
+    }
+
+    isShellIntegrationSupported (): boolean {
+        return false
+    }
+
+    async isShellIntegrationInstalled (): Promise<boolean> {
+        return false
+    }
+
+    async installShellIntegration (): Promise<void> {
+        throw new Error('Not implemented')
+    }
+
+    async uninstallShellIntegration (): Promise<void> {
+        throw new Error('Not implemented')
+    }
+
+    openPath (path: string): void {
+        throw new Error('Not implemented')
+    }
+
+    abstract getOSRelease (): string
+    abstract getAppVersion (): string
+    abstract openExternal (url: string): void
+    abstract listFonts (): Promise<string[]>
+    abstract popupContextMenu (menu: MenuItemOptions[], event?: MouseEvent): void
+}

+ 2 - 2
terminus-core/src/api/tabContextMenuProvider.ts

@@ -1,6 +1,6 @@
-import type { MenuItemConstructorOptions } from 'electron'
 import { BaseTabComponent } from '../components/baseTab.component'
 import { TabHeaderComponent } from '../components/tabHeader.component'
+import { MenuItemOptions } from './menu'
 
 /**
  * Extend to add items to the tab header's context menu
@@ -8,5 +8,5 @@ import { TabHeaderComponent } from '../components/tabHeader.component'
 export abstract class TabContextMenuItemProvider {
     weight = 0
 
-    abstract getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemConstructorOptions[]>
+    abstract getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]>
 }

+ 3 - 37
terminus-core/src/components/appRoot.component.ts

@@ -3,19 +3,16 @@ 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 { ElectronService } from '../services/electron.service'
 import { HostAppService, Platform } from '../services/hostApp.service'
 import { HotkeysService } from '../services/hotkeys.service'
 import { Logger, LogService } from '../services/log.service'
 import { ConfigService } from '../services/config.service'
-import { DockingService } from '../services/docking.service'
 import { ThemesService } from '../services/themes.service'
 import { UpdaterService } from '../services/updater.service'
-import { TouchbarService } from '../services/touchbar.service'
 
 import { BaseTabComponent } from './baseTab.component'
 import { SafeModeModalComponent } from './safeModeModal.component'
-import { AppService, ToolbarButton, ToolbarButtonProvider } from '../api'
+import { AppService, PlatformService, ToolbarButton, ToolbarButtonProvider } from '../api'
 
 /** @hidden */
 @Component({
@@ -67,21 +64,19 @@ export class AppRootComponent {
     private logger: Logger
 
     private constructor (
-        private docking: DockingService,
         private hotkeys: HotkeysService,
         private updater: UpdaterService,
-        private touchbar: TouchbarService,
         public hostApp: HostAppService,
         public config: ConfigService,
         public app: AppService,
         @Inject(ToolbarButtonProvider) private toolbarButtonProviders: ToolbarButtonProvider[],
-        electron: ElectronService,
+        platform: PlatformService,
         log: LogService,
         ngbModal: NgbModal,
         _themes: ThemesService,
     ) {
         this.logger = log.create('main')
-        this.logger.info('v', electron.app.getVersion())
+        this.logger.info('v', platform.getAppVersion())
 
         this.leftToolbarButtons = this.getToolbarButtons(false)
         this.rightToolbarButtons = this.getToolbarButtons(true)
@@ -123,11 +118,6 @@ export class AppRootComponent {
             }
         })
 
-        this.docking.dock()
-        this.hostApp.shown.subscribe(() => {
-            this.docking.dock()
-        })
-
         this.hostApp.windowCloseRequest$.subscribe(async () => {
             this.app.closeWindow()
         })
@@ -144,27 +134,8 @@ export class AppRootComponent {
             }
         }, 3600 * 12 * 1000)
 
-        this.touchbar.update()
-
-        this.hostApp.useBuiltinGraphics()
-
-        config.changed$.subscribe(() => this.updateVibrancy())
-        this.updateVibrancy()
-
-        let lastProgress: number|null = null
         this.app.tabOpened$.subscribe(tab => {
             this.unsortedTabs.push(tab)
-            tab.progress$.subscribe(progress => {
-                if (lastProgress === progress) {
-                    return
-                }
-                if (progress !== null) {
-                    this.hostApp.getWindow().setProgressBar(progress / 100.0, { mode: 'normal' })
-                } else {
-                    this.hostApp.getWindow().setProgressBar(-1, { mode: 'none' })
-                }
-                lastProgress = progress
-            })
             this.noTabs = false
         })
 
@@ -224,9 +195,4 @@ export class AppRootComponent {
             .filter(button => (button.weight ?? 0) > 0 === aboveZero)
             .sort((a: ToolbarButton, b: ToolbarButton) => (a.weight ?? 0) - (b.weight ?? 0))
     }
-
-    private updateVibrancy () {
-        this.hostApp.setVibrancy(this.config.store.appearance.vibrancy, this.config.store.appearance.vibrancyType)
-        this.hostApp.getWindow().setOpacity(this.config.store.appearance.opacity)
-    }
 }

+ 8 - 16
terminus-core/src/components/tabHeader.component.ts

@@ -1,5 +1,4 @@
 /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
-import type { MenuItemConstructorOptions } from 'electron'
 import { Component, Input, Optional, Inject, HostBinding, HostListener, ViewChild, ElementRef, NgZone } from '@angular/core'
 import { SortableComponent } from 'ng2-dnd'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@@ -7,11 +6,12 @@ import { TabContextMenuItemProvider } from '../api/tabContextMenuProvider'
 import { BaseTabComponent } from './baseTab.component'
 import { RenameTabModalComponent } from './renameTabModal.component'
 import { HotkeysService } from '../services/hotkeys.service'
-import { ElectronService } from '../services/electron.service'
 import { AppService } from '../services/app.service'
 import { HostAppService, Platform } from '../services/hostApp.service'
 import { ConfigService } from '../services/config.service'
 import { BaseComponent } from './base.component'
+import { MenuItemOptions } from '../api/menu'
+import { PlatformService } from '../api/platform'
 
 /** @hidden */
 export interface SortableComponentProxy {
@@ -34,10 +34,10 @@ export class TabHeaderComponent extends BaseComponent {
     private constructor (
         public app: AppService,
         public config: ConfigService,
-        private electron: ElectronService,
         private hostApp: HostAppService,
         private ngbModal: NgbModal,
         private hotkeys: HotkeysService,
+        private platform: PlatformService,
         private zone: NgZone,
         @Inject(SortableComponent) private parentDraggable: SortableComponentProxy,
         @Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
@@ -76,8 +76,8 @@ export class TabHeaderComponent extends BaseComponent {
         }).catch(() => null)
     }
 
-    async buildContextMenu (): Promise<MenuItemConstructorOptions[]> {
-        let items: MenuItemConstructorOptions[] = []
+    async buildContextMenu (): Promise<MenuItemOptions[]> {
+        let items: MenuItemOptions[] = []
         for (const section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this.tab, this)))) {
             items.push({ type: 'separator' })
             items = items.concat(section)
@@ -105,16 +105,8 @@ export class TabHeaderComponent extends BaseComponent {
         }
     }
 
-    @HostListener('auxclick', ['$event']) async onAuxClick ($event: MouseEvent) {
-        if ($event.which === 3) {
-            $event.preventDefault()
-
-            const contextMenu = this.electron.Menu.buildFromTemplate(await this.buildContextMenu())
-
-            contextMenu.popup({
-                x: $event.pageX,
-                y: $event.pageY,
-            })
-        }
+    @HostListener('contextmenu', ['$event']) async onContextMenu ($event: MouseEvent) {
+        $event.preventDefault()
+        this.platform.popupContextMenu(await this.buildContextMenu(), $event)
     }
 }

+ 1 - 0
terminus-core/src/config.ts

@@ -7,6 +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'),
     }
     defaults = require('./configDefaults.yaml')
 }

+ 7 - 6
terminus-core/src/index.ts

@@ -1,4 +1,4 @@
-import { NgModule, ModuleWithProviders } from '@angular/core'
+import { NgModule, ModuleWithProviders, APP_INITIALIZER } from '@angular/core'
 import { BrowserModule } from '@angular/platform-browser'
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
 import { FormsModule } from '@angular/forms'
@@ -24,7 +24,7 @@ import { WelcomeTabComponent } from './components/welcomeTab.component'
 import { AutofocusDirective } from './directives/autofocus.directive'
 import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
 
-import { Theme, CLIHandler, BootstrapData, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider } from './api'
+import { Theme, CLIHandler, TabContextMenuItemProvider, TabRecoveryProvider, HotkeyProvider, ConfigProvider } from './api'
 
 import { AppService } from './services/app.service'
 import { ConfigService } from './services/config.service'
@@ -38,6 +38,10 @@ 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 },
@@ -50,6 +54,7 @@ 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 */
@@ -110,10 +115,6 @@ export default class AppModule { // eslint-disable-line @typescript-eslint/no-ex
     }
 }
 
-export function getBootstrapData (): BootstrapData {
-    return (window as any).bootstrapData
-}
-
 export { AppRootComponent as bootstrap }
 export * from './api'
 

+ 10 - 9
terminus-core/src/services/app.service.ts

@@ -1,7 +1,7 @@
 
 import { Observable, Subject, AsyncSubject } from 'rxjs'
 import { takeUntil } from 'rxjs/operators'
-import { Injectable } from '@angular/core'
+import { Injectable, Inject } from '@angular/core'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 
 import { BaseTabComponent } from '../components/baseTab.component'
@@ -9,6 +9,7 @@ import { SplitTabComponent } from '../components/splitTab.component'
 import { SelectorModalComponent } from '../components/selectorModal.component'
 import { SelectorOption } from '../api/selector'
 import { RecoveryToken } from '../api/tabRecovery'
+import { BootstrapData, BOOTSTRAP_DATA } from '../api/mainProcess'
 
 import { ConfigService } from './config.service'
 import { HostAppService } from './hostApp.service'
@@ -75,6 +76,7 @@ export class AppService {
         private tabRecovery: TabRecoveryService,
         private tabsService: TabsService,
         private ngbModal: NgbModal,
+        @Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData,
     ) {
         this.tabsChanged$.subscribe(() => {
             this.tabRecovery.saveTabs(this.tabs)
@@ -83,19 +85,18 @@ export class AppService {
             this.tabRecovery.saveTabs(this.tabs)
         }, 30000)
 
-        if (hostApp.getWindow().id === 1) {
-            if (config.store.terminal.recoverTabs) {
-                this.tabRecovery.recoverTabs().then(tabs => {
+        config.ready$.toPromise().then(async () => {
+            if (this.bootstrapData.isFirstWindow) {
+                if (config.store.terminal.recoverTabs) {
+                    const tabs = await this.tabRecovery.recoverTabs()
                     for (const tab of tabs) {
                         this.openNewTabRaw(tab.type, tab.options)
                     }
-                    this.tabRecovery.enabled = true
-                })
-            } else {
+                }
                 /** Continue to store the tabs even if the setting is currently off */
                 this.tabRecovery.enabled = true
             }
-        }
+        })
 
         hostApp.windowFocused$.subscribe(() => this._activeTab?.emitFocused())
 
@@ -118,7 +119,7 @@ export class AppService {
         this.tabsChanged.next()
         this.tabOpened.next(tab)
 
-        if (this.hostApp.getWindow().id === 1) {
+        if (this.bootstrapData.isFirstWindow) {
             tab.recoveryStateChangedHint$.subscribe(() => {
                 this.tabRecovery.saveTabs(this.tabs)
             })

+ 33 - 25
terminus-core/src/services/config.service.ts

@@ -1,13 +1,12 @@
-import { Observable, Subject } from 'rxjs'
+import { Observable, Subject, AsyncSubject } from 'rxjs'
 import * as yaml from 'js-yaml'
-import * as path from 'path'
-import * as fs from 'fs'
 import { Injectable, Inject } from '@angular/core'
 import { ConfigProvider } from '../api/configProvider'
-import { ElectronService } from './electron.service'
+import { PlatformService } from '../api/platform'
 import { HostAppService } from './hostApp.service'
+const deepmerge = require('deepmerge')
 
-const configMerge = (a, b) => require('deepmerge')(a, b, { arrayMerge: (_d, s) => s }) // eslint-disable-line @typescript-eslint/no-var-requires
+const configMerge = (a, b) => deepmerge(a, b, { arrayMerge: (_d, s) => s }) // eslint-disable-line @typescript-eslint/no-var-requires
 
 function isStructuralMember (v) {
     return v instanceof Object && !(v instanceof Array) &&
@@ -89,11 +88,10 @@ export class ConfigService {
      */
     restartRequested: boolean
 
-    /**
-     * Full config file path
-     */
-    path: string
+    /** Fires once when the config is loaded */
+    get ready$ (): Observable<void> { return this.ready }
 
+    private ready = new AsyncSubject<void>()
     private changed = new Subject<void>()
     private _store: any
     private defaults: any
@@ -103,26 +101,24 @@ export class ConfigService {
 
     /** @hidden */
     private constructor (
-        electron: ElectronService,
         private hostApp: HostAppService,
+        private platform: PlatformService,
         @Inject(ConfigProvider) private configProviders: ConfigProvider[],
     ) {
-        this.path = path.join(electron.app.getPath('userData'), 'config.yaml')
         this.defaults = this.mergeDefaults()
-        this.load()
-
-        hostApp.configChangeBroadcast$.subscribe(() => {
-            this.load()
-            this.emitChange()
-        })
+        this.init()
     }
 
     mergeDefaults (): unknown {
         const providers = this.configProviders
         return providers.map(provider => {
-            let defaults = provider.platformDefaults[this.hostApp.platform] || {}
+            let defaults = provider.platformDefaults[this.hostApp.configPlatform] ?? {}
+            defaults = configMerge(
+                defaults,
+                provider.platformDefaults[this.hostApp.platform] ?? {},
+            )
             if (provider.defaults) {
-                defaults = configMerge(defaults, provider.defaults)
+                defaults = configMerge(provider.defaults, defaults)
             }
             return defaults
         }).reduce(configMerge)
@@ -147,19 +143,20 @@ export class ConfigService {
         return cleanup(this.defaults)
     }
 
-    load (): void {
-        if (fs.existsSync(this.path)) {
-            this._store = yaml.load(fs.readFileSync(this.path, 'utf8'))
+    async load (): Promise<void> {
+        const content = await this.platform.loadConfig()
+        if (content) {
+            this._store = yaml.load(content)
         } else {
             this._store = {}
         }
         this.store = new ConfigProxy(this._store, this.defaults)
     }
 
-    save (): void {
+    async save (): Promise<void> {
         // Scrub undefined values
-        this._store = JSON.parse(JSON.stringify(this._store))
-        fs.writeFileSync(this.path, yaml.dump(this._store), 'utf8')
+        const cleanStore = JSON.parse(JSON.stringify(this._store))
+        await this.platform.saveConfig(yaml.dump(cleanStore))
         this.emitChange()
         this.hostApp.broadcastConfigChange(JSON.parse(JSON.stringify(this.store)))
     }
@@ -214,6 +211,17 @@ export class ConfigService {
         })
     }
 
+    private async init () {
+        await this.load()
+        this.ready.next()
+        this.ready.complete()
+
+        this.hostApp.configChangeBroadcast$.subscribe(() => {
+            this.load()
+            this.emitChange()
+        })
+    }
+
     private emitChange (): void {
         this.changed.next()
     }

+ 15 - 7
terminus-core/src/services/docking.service.ts

@@ -1,11 +1,19 @@
 import type { Display } from 'electron'
-import { Injectable } from '@angular/core'
 import { ConfigService } from '../services/config.service'
 import { ElectronService } from '../services/electron.service'
 import { HostAppService, Bounds } from '../services/hostApp.service'
 
-@Injectable({ providedIn: 'root' })
-export class DockingService {
+export abstract class Screen {
+    id: number
+    name?: string
+}
+
+export abstract class DockingService {
+    abstract dock (): void
+    abstract getScreens (): Screen[]
+}
+
+export class ElectronDockingService {
     /** @hidden */
     private constructor (
         private electron: ElectronService,
@@ -68,10 +76,6 @@ export class DockingService {
         })
     }
 
-    getCurrentScreen (): Display {
-        return this.electron.screen.getDisplayNearestPoint(this.electron.screen.getCursorScreenPoint())
-    }
-
     getScreens (): Display[] {
         const primaryDisplayID = this.electron.screen.getPrimaryDisplay().id
         return this.electron.screen.getAllDisplays().sort((a, b) =>
@@ -85,6 +89,10 @@ export class DockingService {
         })
     }
 
+    private getCurrentScreen (): Display {
+        return this.electron.screen.getDisplayNearestPoint(this.electron.screen.getCursorScreenPoint())
+    }
+
     private repositionWindow () {
         const [x, y] = this.hostApp.getWindow().getPosition()
         for (const screen of this.electron.screen.getAllDisplays()) {

+ 9 - 10
terminus-core/src/services/homeBase.service.ts

@@ -1,9 +1,8 @@
-import * as os from 'os'
 import { Injectable } from '@angular/core'
-import { ElectronService } from './electron.service'
-import { ConfigService } from './config.service'
 import * as mixpanel from 'mixpanel'
 import { v4 as uuidv4 } from 'uuid'
+import { ConfigService } from './config.service'
+import { PlatformService } from '../api'
 
 @Injectable({ providedIn: 'root' })
 export class HomeBaseService {
@@ -12,10 +11,10 @@ export class HomeBaseService {
 
     /** @hidden */
     private constructor (
-        private electron: ElectronService,
         private config: ConfigService,
+        private platform: PlatformService,
     ) {
-        this.appVersion = electron.app.getVersion()
+        this.appVersion = platform.getAppVersion()
 
         if (this.config.store.enableAnalytics && !this.config.store.enableWelcomeTab) {
             this.enableAnalytics()
@@ -23,12 +22,12 @@ export class HomeBaseService {
     }
 
     openGitHub (): void {
-        this.electron.shell.openExternal('https://github.com/eugeny/terminus')
+        this.platform.openExternal('https://github.com/eugeny/terminus')
     }
 
     reportBug (): void {
         let body = `Version: ${this.appVersion}\n`
-        body += `Platform: ${os.platform()} ${os.release()}\n`
+        body += `Platform: ${process.platform} ${this.platform.getOSRelease()}\n`
         const label = {
             aix: 'OS: IBM AIX',
             android: 'OS: Android',
@@ -38,10 +37,10 @@ export class HomeBaseService {
             openbsd: 'OS: OpenBSD',
             sunos: 'OS: Solaris',
             win32: 'OS: Windows',
-        }[os.platform()]
+        }[process.platform]
         const plugins = (window as any).installedPlugins.filter(x => !x.isBuiltin).map(x => x.name)
         body += `Plugins: ${plugins.join(', ') || 'none'}\n\n`
-        this.electron.shell.openExternal(`https://github.com/eugeny/terminus/issues/new?body=${encodeURIComponent(body)}&labels=${label}`)
+        this.platform.openExternal(`https://github.com/eugeny/terminus/issues/new?body=${encodeURIComponent(body)}&labels=${label}`)
     }
 
     enableAnalytics (): void {
@@ -60,7 +59,7 @@ export class HomeBaseService {
         return {
             distinct_id: window.localStorage.analyticsUserID,
             platform: process.platform,
-            os: os.release(),
+            os: this.platform.getOSRelease(),
             version: this.appVersion,
         }
     }

+ 11 - 44
terminus-core/src/services/hostApp.service.ts

@@ -1,21 +1,17 @@
-import type { BrowserWindow, TouchBar, MenuItemConstructorOptions } from 'electron'
+import type { BrowserWindow, TouchBar } from 'electron'
 import { Observable, Subject } from 'rxjs'
-import { Injectable, NgZone, EventEmitter, Injector } from '@angular/core'
+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'
 
-/* eslint-disable block-scoped-var */
-
-try {
-    var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
-} catch (_) { }
-
 export enum Platform {
     Linux = 'Linux',
     macOS = 'macOS',
     Windows = 'Windows',
+    Web = 'Web',
 }
 
 export interface Bounds {
@@ -31,6 +27,7 @@ export interface Bounds {
 @Injectable({ providedIn: 'root' })
 export class HostAppService {
     platform: Platform
+    configPlatform: Platform
 
     /**
      * Fired once the window is visible
@@ -47,7 +44,6 @@ export class HostAppService {
     private displayMetricsChanged = new Subject<void>()
     private displaysChanged = new Subject<void>()
     private logger: Logger
-    private windowId: number
 
     /**
      * Fired when Preferences is selected in the macOS menu
@@ -75,18 +71,20 @@ export class HostAppService {
     private constructor (
         private zone: NgZone,
         private electron: ElectronService,
+        @Inject(BOOTSTRAP_DATA) private bootstrapData: BootstrapData,
         injector: Injector,
         log: LogService,
     ) {
         this.logger = log.create('hostApp')
-        this.platform = {
+        this.configPlatform = this.platform = {
             win32: Platform.Windows,
             darwin: Platform.macOS,
             linux: Platform.Linux,
         }[process.platform]
 
-        this.windowId = parseInt(location.search.substring(1))
-        this.logger.info('Window ID:', this.windowId)
+        if (process.env.XWEB) {
+            this.platform = Platform.Web
+        }
 
         electron.ipcRenderer.on('host:preferences-menu', () => this.zone.run(() => this.preferencesMenu.next()))
 
@@ -158,7 +156,7 @@ export class HostAppService {
      * Returns the current remote [[BrowserWindow]]
      */
     getWindow (): BrowserWindow {
-        return this.electron.BrowserWindow.fromId(this.windowId)!
+        return this.electron.BrowserWindow.fromId(this.bootstrapData.windowID)!
     }
 
     newWindow (): void {
@@ -202,19 +200,6 @@ export class HostAppService {
         this.electron.ipcRenderer.send('window-set-always-on-top', flag)
     }
 
-    /**
-     * Sets window vibrancy mode (Windows, macOS)
-     *
-     * @param type `null`, or `fluent` when supported (Windowd only)
-     */
-    setVibrancy (enable: boolean, type: string|null): void {
-        if (this.platform === Platform.Windows && !isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)) {
-            type = null
-        }
-        document.body.classList.toggle('vibrant', enable)
-        this.electron.ipcRenderer.send('window-set-vibrancy', enable, type)
-    }
-
     setTitle (title?: string): void {
         this.electron.ipcRenderer.send('window-set-title', title ?? 'Terminus')
     }
@@ -223,10 +208,6 @@ export class HostAppService {
         this.getWindow().setTouchBar(touchBar)
     }
 
-    popupContextMenu (menuDefinition: MenuItemConstructorOptions[]): void {
-        this.electron.Menu.buildFromTemplate(menuDefinition).popup({})
-    }
-
     /**
      * Notifies other windows of config file changes
      */
@@ -250,20 +231,6 @@ export class HostAppService {
         this.electron.ipcRenderer.send('app:register-global-hotkey', specs)
     }
 
-    useBuiltinGraphics (): void {
-        const keyPath = 'SOFTWARE\\Microsoft\\DirectX\\UserGpuPreferences'
-        const valueName = this.electron.app.getPath('exe')
-        if (this.platform === Platform.Windows) {
-            if (!wnr.getRegistryValue(wnr.HK.CU, keyPath, valueName)) {
-                wnr.setRegistryValue(wnr.HK.CU, keyPath, valueName, wnr.REG.SZ, 'GpuPreference=1;')
-            }
-        }
-    }
-
-    setTrafficLightInset (x: number, y: number): void {
-        this.getWindow().setTrafficLightPosition({ x, y })
-    }
-
     relaunch (): void {
         if (this.isPortable) {
             this.electron.app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE })

+ 5 - 6
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 { ElectronService } from './electron.service'
 import { HostAppService } from './hostApp.service'
 
 export interface PartialHotkeyMatch {
@@ -35,7 +34,6 @@ export class HotkeysService {
     private constructor (
         private zone: NgZone,
         private hostApp: HostAppService,
-        private electron: ElectronService,
         private config: ConfigService,
         @Inject(HotkeyProvider) private hotkeyProviders: HotkeyProvider[],
     ) {
@@ -52,9 +50,11 @@ export class HotkeysService {
         this.config.changed$.subscribe(() => {
             this.registerGlobalHotkey()
         })
-        this.registerGlobalHotkey()
-        this.getHotkeyDescriptions().then(hotkeys => {
-            this.hotkeyDescriptions = hotkeys
+        this.config.ready$.toPromise().then(() => {
+            this.registerGlobalHotkey()
+            this.getHotkeyDescriptions().then(hotkeys => {
+                this.hotkeyDescriptions = hotkeys
+            })
         })
 
         // deprecated
@@ -183,7 +183,6 @@ export class HotkeysService {
     }
 
     private registerGlobalHotkey () {
-        this.electron.globalShortcut.unregisterAll()
         let value = this.config.store.hotkeys['toggle-window'] || []
         if (typeof value === 'string') {
             value = [value]

+ 9 - 53
terminus-core/src/services/log.service.ts

@@ -1,38 +1,5 @@
-import { Injectable } from '@angular/core'
-import { ElectronService } from './electron.service'
-import type * as winston from 'winston'
-import * as fs from 'fs'
-import * as path from 'path'
-
-const initializeWinston = (electron: ElectronService) => {
-    const logDirectory = electron.app.getPath('userData')
-    // eslint-disable-next-line
-    const winston = require('winston')
-
-    if (!fs.existsSync(logDirectory)) {
-        fs.mkdirSync(logDirectory)
-    }
-
-    return winston.createLogger({
-        transports: [
-            new winston.transports.File({
-                level: 'debug',
-                filename: path.join(logDirectory, 'log.txt'),
-                format: winston.format.simple(),
-                handleExceptions: false,
-                maxsize: 5242880,
-                maxFiles: 5,
-            }),
-        ],
-        exitOnError: false,
-    })
-}
-
-export class Logger {
-    constructor (
-        private winstonLogger: winston.Logger,
-        private name: string,
-    ) {}
+export abstract class Logger {
+    constructor (protected name: string) { }
 
     debug (...args: any[]): void {
         this.doLog('debug', ...args)
@@ -54,26 +21,15 @@ export class Logger {
         this.doLog('log', ...args)
     }
 
-    private doLog (level: string, ...args: any[]): void {
-        console[level](`%c[${this.name}]`, 'color: #aaa', ...args)
-        this.winstonLogger[level](...args)
-    }
+    protected abstract doLog (level: string, ...args: any[]): void
 }
 
-@Injectable({ providedIn: 'root' })
-export class LogService {
-    private log: winston.Logger
-
-    /** @hidden */
-    private constructor (electron: ElectronService) {
-        if (!process.env.XWEB) {
-            this.log = initializeWinston(electron)
-        } else {
-            this.log = console as any
-        }
+export class ConsoleLogger extends Logger {
+    protected doLog (level: string, ...args: any[]): void {
+        console[level](`%c[${this.name}]`, 'color: #aaa', ...args)
     }
+}
 
-    create (name: string): Logger {
-        return new Logger(this.log, name)
-    }
+export abstract class LogService {
+    abstract create (name: string): Logger
 }

+ 9 - 10
terminus-core/src/services/themes.service.ts

@@ -1,21 +1,25 @@
 import { Inject, Injectable } from '@angular/core'
+import { Subject, Observable } from 'rxjs'
 import { ConfigService } from '../services/config.service'
 import { Theme } from '../api/theme'
-import { HostAppService, Platform } from './hostApp.service'
 
 @Injectable({ providedIn: 'root' })
 export class ThemesService {
+    get themeChanged$ (): Observable<Theme> { return this.themeChanged }
+    private themeChanged = new Subject<Theme>()
+
     private styleElement: HTMLElement|null = null
 
     /** @hidden */
     private constructor (
         private config: ConfigService,
-        private hostApp: HostAppService,
         @Inject(Theme) private themes: Theme[],
     ) {
-        this.applyCurrentTheme()
-        config.changed$.subscribe(() => {
+        config.ready$.toPromise().then(() => {
             this.applyCurrentTheme()
+            config.changed$.subscribe(() => {
+                this.applyCurrentTheme()
+            })
         })
     }
 
@@ -35,12 +39,7 @@ export class ThemesService {
         }
         this.styleElement.textContent = theme.css
         document.querySelector('style#custom-css')!.innerHTML = this.config.store.appearance.css
-        if (this.hostApp.platform === Platform.macOS) {
-            this.hostApp.setTrafficLightInset(
-                theme.macOSWindowButtonsInsetX ?? 14,
-                theme.macOSWindowButtonsInsetY ?? 22,
-            )
-        }
+        this.themeChanged.next(theme)
     }
 
     private applyCurrentTheme (): void {

+ 3 - 136
terminus-core/src/services/updater.service.ts

@@ -1,137 +1,4 @@
-import axios from 'axios'
-
-import { Injectable } from '@angular/core'
-import { Logger, LogService } from './log.service'
-import { ElectronService } from './electron.service'
-import { ConfigService } from './config.service'
-import { HostAppService } from './hostApp.service'
-
-const UPDATES_URL = 'https://api.github.com/repos/eugeny/terminus/releases/latest'
-
-/** @hidden */
-@Injectable({ providedIn: 'root' })
-export class UpdaterService {
-    private logger: Logger
-    private downloaded: Promise<boolean>
-    private electronUpdaterAvailable = true
-    private updateURL: string
-
-    private constructor (
-        log: LogService,
-        config: ConfigService,
-        private electron: ElectronService,
-        private hostApp: HostAppService,
-    ) {
-        this.logger = log.create('updater')
-
-        if (process.platform === 'linux') {
-            this.electronUpdaterAvailable = false
-            return
-        }
-
-        electron.autoUpdater.on('update-available', () => {
-            this.logger.info('Update available')
-        })
-
-        electron.autoUpdater.once('update-not-available', () => {
-            this.logger.info('No updates')
-        })
-
-        electron.autoUpdater.once('error', err => {
-            this.logger.error(err)
-        })
-
-        this.downloaded = new Promise<boolean>(resolve => {
-            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)
-            }
-        }
-    }
-
-    async check (): Promise<boolean> {
-        if (this.electronUpdaterAvailable) {
-            return new Promise((resolve, reject) => {
-                // eslint-disable-next-line @typescript-eslint/init-declarations, prefer-const
-                let cancel
-                const onNoUpdate = () => {
-                    cancel()
-                    resolve(false)
-                }
-                const onUpdate = () => {
-                    cancel()
-                    resolve(this.downloaded)
-                }
-                const onError = (err) => {
-                    cancel()
-                    reject(err)
-                }
-                cancel = () => {
-                    this.electron.autoUpdater.off('error', onError)
-                    this.electron.autoUpdater.off('update-not-available', onNoUpdate)
-                    this.electron.autoUpdater.off('update-available', onUpdate)
-                }
-                this.electron.autoUpdater.on('error', onError)
-                this.electron.autoUpdater.on('update-not-available', onNoUpdate)
-                this.electron.autoUpdater.on('update-available', onUpdate)
-                try {
-                    this.electron.autoUpdater.checkForUpdates()
-                } catch (e) {
-                    this.electronUpdaterAvailable = false
-                    this.logger.info('Electron updater unavailable, falling back', e)
-                }
-            })
-
-            this.electron.autoUpdater.on('update-available', () => {
-                this.logger.info('Update available')
-            })
-
-            this.electron.autoUpdater.once('update-not-available', () => {
-                this.logger.info('No updates')
-            })
-
-        } else {
-            this.logger.debug('Checking for updates through fallback method.')
-            const response = await axios.get(UPDATES_URL)
-            const data = response.data
-            const version = data.tag_name.substring(1)
-            if (this.electron.app.getVersion() !== version) {
-                this.logger.info('Update available')
-                this.updateURL = data.html_url
-                return true
-            }
-            this.logger.info('No updates')
-            return false
-        }
-        return this.downloaded
-    }
-
-    async update (): Promise<void> {
-        if (!this.electronUpdaterAvailable) {
-            this.electron.shell.openExternal(this.updateURL)
-        } else {
-            if ((await this.electron.showMessageBox(
-                this.hostApp.getWindow(),
-                {
-                    type: 'warning',
-                    message: 'Installing the update will close all tabs and restart Terminus.',
-                    buttons: ['Cancel', 'Update'],
-                    defaultId: 1,
-                }
-            )).response === 1) {
-                await this.downloaded
-                this.electron.autoUpdater.quitAndInstall()
-            }
-        }
-    }
+export abstract class UpdaterService {
+    abstract check (): Promise<boolean>
+    abstract update (): Promise<void>
 }

+ 9 - 10
terminus-core/src/tabContextMenu.ts

@@ -1,5 +1,4 @@
 /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
-import type { MenuItemConstructorOptions } from 'electron'
 import { Injectable, NgZone } from '@angular/core'
 import { Subscription } from 'rxjs'
 import { AppService } from './services/app.service'
@@ -7,6 +6,7 @@ import { BaseTabComponent } from './components/baseTab.component'
 import { TabHeaderComponent } from './components/tabHeader.component'
 import { SplitTabComponent, SplitDirection } from './components/splitTab.component'
 import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
+import { MenuItemOptions } from './api/menu'
 
 /** @hidden */
 @Injectable()
@@ -20,8 +20,8 @@ export class TabManagementContextMenu extends TabContextMenuItemProvider {
         super()
     }
 
-    async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemConstructorOptions[]> {
-        let items: MenuItemConstructorOptions[] = [
+    async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
+        let items: MenuItemOptions[] = [
             {
                 label: 'Close',
                 click: () => this.zone.run(() => {
@@ -76,7 +76,7 @@ export class TabManagementContextMenu extends TabContextMenuItemProvider {
                         click: () => this.zone.run(() => {
                             (tab.parent as SplitTabComponent).splitTab(tab, dir)
                         }),
-                    })) as MenuItemConstructorOptions[],
+                    })) as MenuItemOptions[],
                 })
             }
         }
@@ -106,8 +106,8 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
         super()
     }
 
-    async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemConstructorOptions[]> {
-        let items: MenuItemConstructorOptions[] = []
+    async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
+        let items: MenuItemOptions[] = []
         if (tabHeader) {
             items = [
                 ...items,
@@ -129,7 +129,7 @@ export class CommonOptionsContextMenu extends TabContextMenuItemProvider {
                         click: () => this.zone.run(() => {
                             tab.color = color.value
                         }),
-                    })) as MenuItemConstructorOptions[],
+                    })) as MenuItemOptions[],
                 },
             ]
         }
@@ -147,15 +147,14 @@ export class TaskCompletionContextMenu extends TabContextMenuItemProvider {
         super()
     }
 
-    async getItems (tab: BaseTabComponent): Promise<MenuItemConstructorOptions[]> {
+    async getItems (tab: BaseTabComponent): Promise<MenuItemOptions[]> {
         const process = await tab.getCurrentProcess()
-        const items: MenuItemConstructorOptions[] = []
+        const items: MenuItemOptions[] = []
 
         const extTab: (BaseTabComponent & { __completionNotificationEnabled?: boolean, __outputNotificationSubscription?: Subscription|null }) = tab
 
         if (process) {
             items.push({
-                id: 'process-name',
                 enabled: false,
                 label: 'Current process: ' + process.name,
             })

+ 4 - 226
terminus-core/yarn.lock

@@ -2,20 +2,6 @@
 # yarn lockfile v1
 
 
-"@dabh/diagnostics@^2.0.2":
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31"
-  integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==
-  dependencies:
-    colorspace "1.1.x"
-    enabled "2.0.x"
-    kuler "^2.0.0"
-
-"@electron/[email protected]":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-1.1.0.tgz#167d119c7c03c7778b556fdc4f1f38a44b23f1c2"
-  integrity sha512-yr8gZTkIgJYKbFqExI4QZqMSjn1kL/us9Dl46+TH1EZdhgRtsJ6HDfdsIxu0QEc6Hv+DMAXs69rgquH+8FDk4w==
-
 "@types/js-yaml@^4.0.0":
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.1.tgz#5544730b65a480b18ace6b6ce914e519cec2d43b"
@@ -26,13 +12,6 @@
   resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.5.tgz#74deebbbcb1e86634dbf10a5b5e8798626f5a597"
   integrity sha512-iotVxtCCsPLRAvxMFFgxL8HD2l4mAZ2Oin7/VJ2ooWO0VOK4EGOGmZWZn1uCq7RofR3I/1IOSjCHlFT71eVK0Q==
 
-"@types/winston@^2.3.6":
-  version "2.4.4"
-  resolved "https://registry.yarnpkg.com/@types/winston/-/winston-2.4.4.tgz#48cc744b7b42fad74b9a2e8490e0112bd9a3d08d"
-  integrity sha512-BVGCztsypW8EYwJ+Hq+QNYiT/MUyCif0ouBH+flrY66O5W+KIXAMML6E/0fJpm7VjIzgangahl5S03bJJQGrZw==
-  dependencies:
-    winston "*"
-
 agent-base@6:
   version "6.0.2"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -45,18 +24,6 @@ argparse@^2.0.1:
   resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
   integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
 
-async@^3.1.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
-  integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
-
-axios@^0.21.1:
-  version "0.21.1"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
-  integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
-  dependencies:
-    follow-redirects "^1.10.0"
-
 bootstrap@^4.1.3:
   version "4.5.3"
   resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.3.tgz#c6a72b355aaf323920be800246a6e4ef30997fe6"
@@ -79,62 +46,11 @@ clone-deep@^4.0.1:
     kind-of "^6.0.2"
     shallow-clone "^3.0.0"
 
-color-convert@^1.9.1:
-  version "1.9.3"
-  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
-  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
-  dependencies:
-    color-name "1.1.3"
-
[email protected]:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
-  integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
-
-color-name@^1.0.0:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
-  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-
-color-string@^1.5.2:
-  version "1.5.4"
-  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6"
-  integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==
-  dependencies:
-    color-name "^1.0.0"
-    simple-swizzle "^0.2.2"
-
[email protected]:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a"
-  integrity sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==
-  dependencies:
-    color-convert "^1.9.1"
-    color-string "^1.5.2"
-
-colors@^1.2.1:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
-  integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
-
[email protected]:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.2.tgz#e0128950d082b86a2168580796a0aa5d6c68d8c5"
-  integrity sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==
-  dependencies:
-    color "3.0.x"
-    text-hex "1.0.x"
-
 core-js@^3.1.2:
   version "3.12.1"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.12.1.tgz#6b5af4ff55616c08a44d386f1f510917ff204112"
   integrity sha512-Ne9DKPHTObRuB09Dru5AjwKjY4cJHVGu+y5f7coGn1E9Grkc3p2iBwE9AI/nJzsE29mQF7oq+mhYYRqOMFN1Bw==
 
-core-util-is@~1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
-  integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
-
 debug@4:
   version "4.3.1"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
@@ -168,31 +84,6 @@ electron-updater@^4.0.6:
     lodash.isequal "^4.5.0"
     semver "^7.3.5"
 
[email protected]:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
-  integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
-
-fast-safe-stringify@^2.0.4:
-  version "2.0.7"
-  resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
-  integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
-
-fecha@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.0.tgz#3ffb6395453e3f3efff850404f0a59b6747f5f41"
-  integrity sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==
-
[email protected]:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
-  integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
-
-follow-redirects@^1.10.0:
-  version "1.13.1"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
-  integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
-
 fs-extra@^10.0.0:
   version "10.0.0"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1"
@@ -215,16 +106,11 @@ [email protected]:
     agent-base "6"
     debug "4"
 
-inherits@^2.0.3, inherits@~2.0.3:
+inherits@^2.0.3:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
 
-is-arrayish@^0.3.1:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
-  integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
-
 is-plain-object@^2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@@ -232,16 +118,6 @@ is-plain-object@^2.0.4:
   dependencies:
     isobject "^3.0.1"
 
-is-stream@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
-  integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
-
-isarray@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
-  integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
-
 isobject@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
@@ -268,11 +144,6 @@ kind-of@^6.0.2:
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
   integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
 
-kuler@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
-  integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
-
 lazy-val@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.4.tgz#882636a7245c2cfe6e0a4e3ba6c5d68a137e5c65"
@@ -288,17 +159,6 @@ lodash.isequal@^4.5.0:
   resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
   integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
 
-logform@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2"
-  integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==
-  dependencies:
-    colors "^1.2.1"
-    fast-safe-stringify "^2.0.4"
-    fecha "^4.2.0"
-    ms "^2.1.1"
-    triple-beam "^1.3.0"
-
 lru-cache@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@@ -313,7 +173,7 @@ mixpanel@^0.13.0:
   dependencies:
     https-proxy-agent "5.0.0"
 
[email protected], ms@^2.1.1:
[email protected]:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
@@ -332,24 +192,12 @@ ngx-perfect-scrollbar@^10.1.0:
     resize-observer-polyfill "^1.5.0"
     tslib "^2.0.0"
 
-one-time@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
-  integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==
-  dependencies:
-    fn.name "1.x.x"
-
 [email protected]:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/perfect-scrollbar/-/perfect-scrollbar-1.5.0.tgz#821d224ed8ff61990c23f26db63048cdc75b6b83"
   integrity sha512-NrNHJn5mUGupSiheBTy6x+6SXCFbLlm8fVZh9moIzw/LgqElN5q4ncR4pbCBCYuCJ8Kcl9mYM0NgDxvW+b4LxA==
 
-process-nextick-args@~2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
-  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-
[email protected], readable-stream@^3.4.0:
[email protected]:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
   integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
@@ -358,29 +206,11 @@ [email protected], readable-stream@^3.4.0:
     string_decoder "^1.1.1"
     util-deprecate "^1.0.1"
 
-readable-stream@^2.3.7:
-  version "2.3.7"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
-  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.3"
-    isarray "~1.0.0"
-    process-nextick-args "~2.0.0"
-    safe-buffer "~5.1.1"
-    string_decoder "~1.1.1"
-    util-deprecate "~1.0.1"
-
 resize-observer-polyfill@^1.5.0:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
   integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
 
-safe-buffer@~5.1.0, safe-buffer@~5.1.1:
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
-  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-
 safe-buffer@~5.2.0:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
@@ -405,18 +235,6 @@ shallow-clone@^3.0.0:
   dependencies:
     kind-of "^6.0.2"
 
-simple-swizzle@^0.2.2:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
-  integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
-  dependencies:
-    is-arrayish "^0.3.1"
-
[email protected]:
-  version "0.0.10"
-  resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
-  integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
-
 string_decoder@^1.1.1:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@@ -424,23 +242,6 @@ string_decoder@^1.1.1:
   dependencies:
     safe-buffer "~5.2.0"
 
-string_decoder@~1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
-  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
-  dependencies:
-    safe-buffer "~5.1.0"
-
[email protected]:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
-  integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
-
-triple-beam@^1.2.0, triple-beam@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
-  integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
-
 tslib@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
@@ -451,7 +252,7 @@ universalify@^2.0.0:
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
   integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
 
-util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+util-deprecate@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
@@ -461,29 +262,6 @@ uuid@^8.0.0:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
   integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
 
-winston-transport@^4.4.0:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59"
-  integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==
-  dependencies:
-    readable-stream "^2.3.7"
-    triple-beam "^1.2.0"
-
-winston@*, winston@^3.3.3:
-  version "3.3.3"
-  resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170"
-  integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==
-  dependencies:
-    "@dabh/diagnostics" "^2.0.2"
-    async "^3.1.0"
-    is-stream "^2.0.0"
-    logform "^2.2.0"
-    one-time "^1.0.0"
-    readable-stream "^3.4.0"
-    stack-trace "0.0.x"
-    triple-beam "^1.3.0"
-    winston-transport "^4.4.0"
-
 yallist@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"

+ 1 - 0
terminus-electron/.gitignore

@@ -0,0 +1 @@
+dist

+ 27 - 0
terminus-electron/package.json

@@ -0,0 +1,27 @@
+{
+  "name": "terminus-electron",
+  "version": "1.0.135-nightly.0",
+  "description": "Electron-specific bindings",
+  "keywords": [
+    "terminus-builtin-plugin"
+  ],
+  "main": "dist/index.js",
+  "typings": "typings/index.d.ts",
+  "scripts": {
+    "build": "webpack --progress --color",
+    "watch": "webpack --progress --color --watch"
+  },
+  "files": [
+    "dist"
+  ],
+  "author": "Eugene Pankov",
+  "license": "MIT",
+  "peerDependencies": {
+    "@angular/core": "^9.1.9"
+  },
+  "devDependencies": {
+    "axios": "^0.21.1",
+    "winston": "^3.3.3",
+    "electron-promise-ipc": "^2.2.4"
+  }
+}

+ 1 - 2
terminus-terminal/src/colorSchemes.ts → terminus-electron/src/colorSchemes.ts

@@ -1,8 +1,7 @@
 import * as fs from 'mz/fs'
 import * as path from 'path'
 import { Injectable } from '@angular/core'
-import { TerminalColorSchemeProvider } from './api/colorSchemeProvider'
-import { TerminalColorScheme } from './api/interfaces'
+import { TerminalColorSchemeProvider, TerminalColorScheme } from 'terminus-terminal'
 
 /** @hidden */
 @Injectable()

+ 77 - 0
terminus-electron/src/index.ts

@@ -0,0 +1,77 @@
+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 { TerminalColorSchemeProvider } from 'terminus-terminal'
+
+import { HyperColorSchemes } from './colorSchemes'
+import { ElectronPlatformService } from './services/platform'
+import { ElectronLogService } from './services/log.service'
+import { ElectronUpdaterService } from './services/updater.service'
+import { TouchbarService } from './services/touchbar.service'
+import { ElectronDockingService } from './services/docking.service'
+
+@NgModule({
+    providers: [
+        { provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
+        { provide: PlatformService, useClass: ElectronPlatformService },
+        { provide: LogService, useClass: ElectronLogService },
+        { provide: UpdaterService, useClass: ElectronUpdaterService },
+        { provide: DockingService, useClass: ElectronDockingService },
+    ],
+})
+export default class ElectronModule {
+    constructor (
+        private config: ConfigService,
+        private hostApp: HostAppService,
+        private electron: ElectronService,
+        touchbar: TouchbarService,
+        docking: DockingService,
+        themeService: ThemesService,
+        app: AppService
+    ) {
+        config.ready$.toPromise().then(() => {
+            touchbar.update()
+            docking.dock()
+            hostApp.shown.subscribe(() => {
+                docking.dock()
+            })
+            this.updateVibrancy()
+        })
+
+        themeService.themeChanged$.subscribe(theme => {
+            if (hostApp.platform === Platform.macOS) {
+                hostApp.getWindow().setTrafficLightPosition({
+                    x: theme.macOSWindowButtonsInsetX ?? 14,
+                    y: theme.macOSWindowButtonsInsetY ?? 22,
+                })
+            }
+        })
+
+        let lastProgress: number|null = null
+        app.tabOpened$.subscribe(tab => {
+            tab.progress$.subscribe(progress => {
+                if (lastProgress === progress) {
+                    return
+                }
+                if (progress !== null) {
+                    hostApp.getWindow().setProgressBar(progress / 100.0, { mode: 'normal' })
+                } else {
+                    hostApp.getWindow().setProgressBar(-1, { mode: 'none' })
+                }
+                lastProgress = progress
+            })
+        })
+
+        config.changed$.subscribe(() => this.updateVibrancy())
+    }
+
+    private updateVibrancy () {
+        let vibrancyType = this.config.store.appearance.vibrancyType
+        if (this.hostApp.platform === Platform.Windows && !isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)) {
+            vibrancyType = null
+        }
+        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)
+    }
+}

+ 97 - 0
terminus-electron/src/services/docking.service.ts

@@ -0,0 +1,97 @@
+import { Injectable } from '@angular/core'
+import type { Display } from 'electron'
+import { ConfigService, ElectronService, HostAppService, Bounds, DockingService, Screen } from 'terminus-core'
+
+@Injectable()
+export class ElectronDockingService extends DockingService {
+    constructor (
+        private electron: ElectronService,
+        private config: ConfigService,
+        private hostApp: HostAppService,
+    ) {
+        super()
+        hostApp.displaysChanged$.subscribe(() => this.repositionWindow())
+        hostApp.displayMetricsChanged$.subscribe(() => this.repositionWindow())
+    }
+
+    dock (): void {
+        const dockSide = this.config.store.appearance.dock
+
+        if (dockSide === 'off') {
+            this.hostApp.setAlwaysOnTop(false)
+            return
+        }
+
+        let display = this.electron.screen.getAllDisplays()
+            .filter(x => x.id === this.config.store.appearance.dockScreen)[0]
+        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+        if (!display) {
+            display = this.getCurrentScreen()
+        }
+
+        const newBounds: Bounds = { x: 0, y: 0, width: 0, height: 0 }
+
+        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()
+
+        if (dockSide === 'left' || dockSide === 'right') {
+            newBounds.width = Math.max(minWidth, Math.round(fill * display.bounds.width))
+            newBounds.height = Math.round(display.bounds.height * space)
+        }
+        if (dockSide === 'top' || dockSide === 'bottom') {
+            newBounds.width = Math.round(display.bounds.width * space)
+            newBounds.height = Math.max(minHeight, Math.round(fill * display.bounds.height))
+        }
+        if (dockSide === 'right') {
+            newBounds.x = display.bounds.x + display.bounds.width - newBounds.width
+        } else if (dockSide === 'left') {
+            newBounds.x = display.bounds.x
+        } else {
+            newBounds.x = display.bounds.x + Math.round(display.bounds.width / 2 * (1 - space))
+        }
+        if (dockSide === 'bottom') {
+            newBounds.y = display.bounds.y + display.bounds.height - newBounds.height
+        } else if (dockSide === 'top') {
+            newBounds.y = display.bounds.y
+        } else {
+            newBounds.y = display.bounds.y + Math.round(display.bounds.height / 2 * (1 - space))
+        }
+
+        const alwaysOnTop = this.config.store.appearance.dockAlwaysOnTop
+
+        this.hostApp.setAlwaysOnTop(alwaysOnTop)
+        setImmediate(() => {
+            this.hostApp.setBounds(newBounds)
+        })
+    }
+
+    getScreens (): Screen[] {
+        const primaryDisplayID = this.electron.screen.getPrimaryDisplay().id
+        return this.electron.screen.getAllDisplays().sort((a, b) =>
+            a.bounds.x === b.bounds.x ? a.bounds.y - b.bounds.y : a.bounds.x - b.bounds.x
+        ).map((display, index) => {
+            return {
+                ...display,
+                id: display.id,
+                name: display.id === primaryDisplayID ? 'Primary Display' : `Display ${index + 1}`,
+            }
+        })
+    }
+
+    private getCurrentScreen (): Display {
+        return this.electron.screen.getDisplayNearestPoint(this.electron.screen.getCursorScreenPoint())
+    }
+
+    private repositionWindow () {
+        const [x, y] = this.hostApp.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) {
+                return
+            }
+        }
+        const screen = this.electron.screen.getPrimaryDisplay()
+        this.hostApp.getWindow().setPosition(screen.bounds.x, screen.bounds.y)
+    }
+}

+ 54 - 0
terminus-electron/src/services/log.service.ts

@@ -0,0 +1,54 @@
+import * as fs from 'fs'
+import * as path from 'path'
+import * as winston from 'winston'
+import { Injectable } from '@angular/core'
+import { ConsoleLogger, Logger, ElectronService } from 'terminus-core'
+
+const initializeWinston = (electron: ElectronService) => {
+    const logDirectory = electron.app.getPath('userData')
+    // eslint-disable-next-line
+    const winston = require('winston')
+
+    if (!fs.existsSync(logDirectory)) {
+        fs.mkdirSync(logDirectory)
+    }
+
+    return winston.createLogger({
+        transports: [
+            new winston.transports.File({
+                level: 'debug',
+                filename: path.join(logDirectory, 'log.txt'),
+                format: winston.format.simple(),
+                handleExceptions: false,
+                maxsize: 5242880,
+                maxFiles: 5,
+            }),
+        ],
+        exitOnError: false,
+    })
+}
+
+export class WinstonAndConsoleLogger extends ConsoleLogger {
+    constructor (private winstonLogger: winston.Logger, name: string) {
+        super(name)
+    }
+
+    protected doLog (level: string, ...args: any[]): void {
+        super.doLog(level, ...args)
+        this.winstonLogger[level](...args)
+    }
+}
+
+@Injectable({ providedIn: 'root' })
+export class ElectronLogService {
+    private log: winston.Logger
+
+    /** @hidden */
+    constructor (electron: ElectronService) {
+        this.log = initializeWinston(electron)
+    }
+
+    create (name: string): Logger {
+        return new WinstonAndConsoleLogger(this.log, name)
+    }
+}

+ 146 - 0
terminus-electron/src/services/platform.ts

@@ -0,0 +1,146 @@
+import * as path from 'path'
+import * as fs from 'mz/fs'
+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'
+const fontManager = require('fontmanager-redux') // eslint-disable-line
+
+/* eslint-disable block-scoped-var */
+
+try {
+    // eslint-disable-next-line no-var
+    var windowsProcessTreeNative = require('windows-process-tree/build/Release/windows_process_tree.node')
+    // eslint-disable-next-line no-var
+    var wnr = require('windows-native-registry')
+} catch { }
+
+@Injectable()
+export class ElectronPlatformService extends PlatformService {
+    supportsWindowControls = true
+    private userPluginsPath: string = (window as any).userPluginsPath
+    private configPath: string
+
+    constructor (
+        private hostApp: HostAppService,
+        private electron: ElectronService,
+    ) {
+        super()
+        this.configPath = path.join(electron.app.getPath('userData'), 'config.yaml')
+    }
+
+    setClipboard (content: ClipboardContent): void {
+        require('@electron/remote').clipboard.write(content)
+    }
+
+    async installPlugin (name: string, version: string): Promise<void> {
+        await (promiseIpc as any).send('plugin-manager:install', this.userPluginsPath, name, version)
+    }
+
+    async uninstallPlugin (name: string): Promise<void> {
+        await (promiseIpc as any).send('plugin-manager:uninstall', this.userPluginsPath, name)
+    }
+
+    async isProcessRunning (name: string): Promise<boolean> {
+        if (this.hostApp.platform === Platform.Windows) {
+            return new Promise<boolean>(resolve => {
+                windowsProcessTreeNative.getProcessList(list => { // eslint-disable-line block-scoped-var
+                    resolve(list.some(x => x.name === name))
+                }, 0)
+            })
+        } else {
+            throw new Error('Not supported')
+        }
+    }
+
+    getWinSCPPath (): string|null {
+        const key = wnr.getRegistryKey(wnr.HK.CR, 'WinSCP.Url\\DefaultIcon')
+        if (key?.['']) {
+            let detectedPath = key[''].value?.split(',')[0]
+            detectedPath = detectedPath?.substring(1, detectedPath.length - 1)
+            return detectedPath
+        }
+        return null
+    }
+
+    exec (app: string, argv: string[]): void {
+        execFile(app, argv)
+    }
+
+    isShellIntegrationSupported (): boolean {
+        return this.hostApp.platform !== Platform.Linux
+    }
+
+    async isShellIntegrationInstalled (): Promise<boolean> {
+        return false
+    }
+
+    async installShellIntegration (): Promise<void> {
+        throw new Error('Not implemented')
+    }
+
+    async uninstallShellIntegration (): Promise<void> {
+        throw new Error('Not implemented')
+    }
+
+    async loadConfig (): Promise<string> {
+        if (await fs.exists(this.configPath)) {
+            return fs.readFileSync(this.configPath, 'utf8')
+        } else {
+            return ''
+        }
+    }
+
+    async saveConfig (content: string): Promise<void> {
+        await fs.writeFile(this.configPath, content, 'utf8')
+    }
+
+    getConfigPath (): string|null {
+        return this.configPath
+    }
+
+    showItemInFolder (p: string): void {
+        this.electron.shell.showItemInFolder(p)
+    }
+
+    openExternal (url: string): void {
+        this.electron.shell.openExternal(url)
+    }
+
+    openPath (p: string): void {
+        this.electron.shell.openPath(p)
+    }
+
+    getOSRelease (): string {
+        return os.release()
+    }
+
+    getAppVersion (): string {
+        return this.electron.app.getVersion()
+    }
+
+    async listFonts (): Promise<string[]> {
+        if (this.hostApp.platform === Platform.Windows || this.hostApp.platform === Platform.macOS) {
+            let fonts = await new Promise<any[]>((resolve) => fontManager.findFonts({ monospace: true }, resolve))
+            fonts = fonts.map(x => x.family.trim())
+            return fonts
+        }
+        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+        if (this.hostApp.platform === Platform.Linux) {
+            const stdout = (await execFile('fc-list', [':spacing=mono']))[0]
+            const fonts = stdout.toString()
+                .split('\n')
+                .filter(x => !!x)
+                .map(x => x.split(':')[1].trim())
+                .map(x => x.split(',')[0].trim())
+            fonts.sort()
+            return fonts
+        }
+        return []
+    }
+
+    popupContextMenu (menu: MenuItemOptions[], _event?: MouseEvent): void {
+        this.electron.Menu.buildFromTemplate(menu).popup({})
+    }
+}

+ 2 - 2
terminus-core/src/services/shellIntegration.service.ts → terminus-electron/src/services/shellIntegration.service.ts

@@ -2,8 +2,8 @@ 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 './electron.service'
-import { HostAppService, Platform } from './hostApp.service'
+import { ElectronService } from '../../../terminus-core/src/services/electron.service'
+import { HostAppService, Platform } from '../../../terminus-core/src/services/hostApp.service'
 
 /* eslint-disable block-scoped-var */
 

+ 1 - 3
terminus-core/src/services/touchbar.service.ts → terminus-electron/src/services/touchbar.service.ts

@@ -1,8 +1,6 @@
 import { SegmentedControlSegment, TouchBarSegmentedControl } from 'electron'
 import { Injectable, NgZone } from '@angular/core'
-import { AppService } from './app.service'
-import { ElectronService } from './electron.service'
-import { HostAppService, Platform } from './hostApp.service'
+import { AppService, HostAppService, Platform, ElectronService } from 'terminus-core'
 
 /** @hidden */
 @Injectable({ providedIn: 'root' })

+ 134 - 0
terminus-electron/src/services/updater.service.ts

@@ -0,0 +1,134 @@
+import { Injectable } from '@angular/core'
+import axios from 'axios'
+
+import { Logger, LogService, ElectronService, ConfigService, HostAppService, UpdaterService } from 'terminus-core'
+
+const UPDATES_URL = 'https://api.github.com/repos/eugeny/terminus/releases/latest'
+
+@Injectable()
+export class ElectronUpdaterService extends UpdaterService {
+    private logger: Logger
+    private downloaded: Promise<boolean>
+    private electronUpdaterAvailable = true
+    private updateURL: string
+
+    constructor (
+        log: LogService,
+        config: ConfigService,
+        private electron: ElectronService,
+        private hostApp: HostAppService,
+    ) {
+        super()
+        this.logger = log.create('updater')
+
+        if (process.platform === 'linux') {
+            this.electronUpdaterAvailable = false
+            return
+        }
+
+        electron.autoUpdater.on('update-available', () => {
+            this.logger.info('Update available')
+        })
+
+        electron.autoUpdater.once('update-not-available', () => {
+            this.logger.info('No updates')
+        })
+
+        electron.autoUpdater.once('error', err => {
+            this.logger.error(err)
+        })
+
+        this.downloaded = new Promise<boolean>(resolve => {
+            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)
+            }
+        }
+    }
+
+    async check (): Promise<boolean> {
+        if (this.electronUpdaterAvailable) {
+            return new Promise((resolve, reject) => {
+                // eslint-disable-next-line @typescript-eslint/init-declarations, prefer-const
+                let cancel
+                const onNoUpdate = () => {
+                    cancel()
+                    resolve(false)
+                }
+                const onUpdate = () => {
+                    cancel()
+                    resolve(this.downloaded)
+                }
+                const onError = (err) => {
+                    cancel()
+                    reject(err)
+                }
+                cancel = () => {
+                    this.electron.autoUpdater.off('error', onError)
+                    this.electron.autoUpdater.off('update-not-available', onNoUpdate)
+                    this.electron.autoUpdater.off('update-available', onUpdate)
+                }
+                this.electron.autoUpdater.on('error', onError)
+                this.electron.autoUpdater.on('update-not-available', onNoUpdate)
+                this.electron.autoUpdater.on('update-available', onUpdate)
+                try {
+                    this.electron.autoUpdater.checkForUpdates()
+                } catch (e) {
+                    this.electronUpdaterAvailable = false
+                    this.logger.info('Electron updater unavailable, falling back', e)
+                }
+            })
+
+            this.electron.autoUpdater.on('update-available', () => {
+                this.logger.info('Update available')
+            })
+
+            this.electron.autoUpdater.once('update-not-available', () => {
+                this.logger.info('No updates')
+            })
+
+        } else {
+            this.logger.debug('Checking for updates through fallback method.')
+            const response = await axios.get(UPDATES_URL)
+            const data = response.data
+            const version = data.tag_name.substring(1)
+            if (this.electron.app.getVersion() !== version) {
+                this.logger.info('Update available')
+                this.updateURL = data.html_url
+                return true
+            }
+            this.logger.info('No updates')
+            return false
+        }
+        return this.downloaded
+    }
+
+    async update (): Promise<void> {
+        if (!this.electronUpdaterAvailable) {
+            this.electron.shell.openExternal(this.updateURL)
+        } else {
+            if ((await this.electron.showMessageBox(
+                this.hostApp.getWindow(),
+                {
+                    type: 'warning',
+                    message: 'Installing the update will close all tabs and restart Terminus.',
+                    buttons: ['Cancel', 'Update'],
+                    defaultId: 1,
+                }
+            )).response === 1) {
+                await this.downloaded
+                this.electron.autoUpdater.quitAndInstall()
+            }
+        }
+    }
+}

+ 7 - 0
terminus-electron/tsconfig.json

@@ -0,0 +1,7 @@
+{
+  "extends": "../tsconfig.json",
+  "exclude": ["node_modules", "dist"],
+  "compilerOptions": {
+    "baseUrl": "src"
+  }
+}

+ 14 - 0
terminus-electron/tsconfig.typings.json

@@ -0,0 +1,14 @@
+{
+  "extends": "../tsconfig.json",
+  "exclude": ["node_modules", "dist", "typings"],
+  "compilerOptions": {
+    "baseUrl": "src",
+    "emitDeclarationOnly": true,
+    "declaration": true,
+    "declarationDir": "./typings",
+    "paths": {
+      "terminus-*": ["../../terminus-*"],
+      "*": ["../../app/node_modules/*"]
+    }
+  }
+}

+ 5 - 0
terminus-electron/webpack.config.js

@@ -0,0 +1,5 @@
+const config = require('../webpack.plugin.config')
+module.exports = config({
+    name: 'electron',
+    dirname: __dirname,
+})

+ 477 - 0
terminus-electron/yarn.lock

@@ -0,0 +1,477 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@dabh/diagnostics@^2.0.2":
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31"
+  integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==
+  dependencies:
+    colorspace "1.1.x"
+    enabled "2.0.x"
+    kuler "^2.0.0"
+
+async@^3.1.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
+  integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
+
+axios@^0.21.1:
+  version "0.21.1"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
+  integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
+  dependencies:
+    follow-redirects "^1.10.0"
+
+call-bind@^1.0.0, call-bind@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
+  integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
+  dependencies:
+    function-bind "^1.1.1"
+    get-intrinsic "^1.0.2"
+
+color-convert@^1.9.1:
+  version "1.9.3"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+  dependencies:
+    color-name "1.1.3"
+
[email protected]:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+  integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+color-name@^1.0.0:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+color-string@^1.5.2:
+  version "1.5.5"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014"
+  integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==
+  dependencies:
+    color-name "^1.0.0"
+    simple-swizzle "^0.2.2"
+
[email protected]:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a"
+  integrity sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==
+  dependencies:
+    color-convert "^1.9.1"
+    color-string "^1.5.2"
+
+colors@^1.2.1:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
+  integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
+
[email protected]:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.2.tgz#e0128950d082b86a2168580796a0aa5d6c68d8c5"
+  integrity sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==
+  dependencies:
+    color "3.0.x"
+    text-hex "1.0.x"
+
+core-util-is@~1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+  integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+define-properties@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+  integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+  dependencies:
+    object-keys "^1.0.12"
+
+electron-promise-ipc@^2.2.4:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/electron-promise-ipc/-/electron-promise-ipc-2.2.4.tgz#b82daf86ca6d0f0b8655937fdbe9a554590deeea"
+  integrity sha512-xCkFEeuru9l7H/+m1gpK4F1utexvTT7+n1PTquP2MVTpmBmpgFBlLqSXC7TqwpROkHRm9wGpaCJEx0djonnSEg==
+  dependencies:
+    is-electron-renderer "^2.0.1"
+    object.entries "^1.1.3"
+    serialize-error "^5.0.0"
+    uuid "^3.0.1"
+
[email protected]:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
+  integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
+
+es-abstract@^1.18.0-next.1:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4"
+  integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==
+  dependencies:
+    call-bind "^1.0.2"
+    es-to-primitive "^1.2.1"
+    function-bind "^1.1.1"
+    get-intrinsic "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.2"
+    is-callable "^1.2.3"
+    is-negative-zero "^2.0.1"
+    is-regex "^1.1.2"
+    is-string "^1.0.5"
+    object-inspect "^1.9.0"
+    object-keys "^1.1.1"
+    object.assign "^4.1.2"
+    string.prototype.trimend "^1.0.4"
+    string.prototype.trimstart "^1.0.4"
+    unbox-primitive "^1.0.0"
+
+es-to-primitive@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+  integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
+  dependencies:
+    is-callable "^1.1.4"
+    is-date-object "^1.0.1"
+    is-symbol "^1.0.2"
+
+fast-safe-stringify@^2.0.4:
+  version "2.0.7"
+  resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
+  integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
+
+fecha@^4.2.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce"
+  integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==
+
[email protected]:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
+  integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
+
+follow-redirects@^1.10.0:
+  version "1.14.1"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
+  integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
+  integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
+  dependencies:
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
+
+has-bigints@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
+  integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
+
+has-symbols@^1.0.1, has-symbols@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
+  integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
+
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+inherits@^2.0.3, inherits@~2.0.3:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+is-arrayish@^0.3.1:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
+  integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
+
+is-bigint@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a"
+  integrity sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==
+
+is-boolean-object@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.1.tgz#3c0878f035cb821228d350d2e1e36719716a3de8"
+  integrity sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==
+  dependencies:
+    call-bind "^1.0.2"
+
+is-callable@^1.1.4, is-callable@^1.2.3:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e"
+  integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==
+
+is-date-object@^1.0.1:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.4.tgz#550cfcc03afada05eea3dd30981c7b09551f73e5"
+  integrity sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==
+
+is-electron-renderer@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-electron-renderer/-/is-electron-renderer-2.0.1.tgz#a469d056f975697c58c98c6023eb0aa79af895a2"
+  integrity sha1-pGnQVvl1aXxYyYxgI+sKp5r4laI=
+
+is-negative-zero@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
+  integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
+
+is-number-object@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.5.tgz#6edfaeed7950cff19afedce9fbfca9ee6dd289eb"
+  integrity sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==
+
+is-regex@^1.1.2:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
+  integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==
+  dependencies:
+    call-bind "^1.0.2"
+    has-symbols "^1.0.2"
+
+is-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
+  integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
+
+is-string@^1.0.5:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f"
+  integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==
+
+is-symbol@^1.0.2, is-symbol@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
+  integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
+  dependencies:
+    has-symbols "^1.0.2"
+
+isarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+  integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+kuler@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
+  integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
+
+logform@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2"
+  integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==
+  dependencies:
+    colors "^1.2.1"
+    fast-safe-stringify "^2.0.4"
+    fecha "^4.2.0"
+    ms "^2.1.1"
+    triple-beam "^1.3.0"
+
+ms@^2.1.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+object-inspect@^1.9.0:
+  version "1.10.3"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369"
+  integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==
+
+object-keys@^1.0.12, object-keys@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+  integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+object.assign@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
+  integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
+  dependencies:
+    call-bind "^1.0.0"
+    define-properties "^1.1.3"
+    has-symbols "^1.0.1"
+    object-keys "^1.1.1"
+
+object.entries@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.3.tgz#c601c7f168b62374541a07ddbd3e2d5e4f7711a6"
+  integrity sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==
+  dependencies:
+    call-bind "^1.0.0"
+    define-properties "^1.1.3"
+    es-abstract "^1.18.0-next.1"
+    has "^1.0.3"
+
+one-time@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
+  integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==
+  dependencies:
+    fn.name "1.x.x"
+
+process-nextick-args@~2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
+readable-stream@^2.3.7:
+  version "2.3.7"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
+  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.3"
+    isarray "~1.0.0"
+    process-nextick-args "~2.0.0"
+    safe-buffer "~5.1.1"
+    string_decoder "~1.1.1"
+    util-deprecate "~1.0.1"
+
+readable-stream@^3.4.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-buffer@~5.2.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+serialize-error@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-5.0.0.tgz#a7ebbcdb03a5d71a6ed8461ffe0fc1a1afed62ac"
+  integrity sha512-/VtpuyzYf82mHYTtI4QKtwHa79vAdU5OQpNPAmE/0UDdlGT0ZxHwC+J6gXkw29wwoVI8fMPsfcVHOwXtUQYYQA==
+  dependencies:
+    type-fest "^0.8.0"
+
+simple-swizzle@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
+  integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
+  dependencies:
+    is-arrayish "^0.3.1"
+
[email protected]:
+  version "0.0.10"
+  resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
+  integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
+
+string.prototype.trimend@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
+  integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+
+string.prototype.trimstart@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
+  integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+
+string_decoder@^1.1.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+  dependencies:
+    safe-buffer "~5.2.0"
+
+string_decoder@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+  dependencies:
+    safe-buffer "~5.1.0"
+
[email protected]:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
+  integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
+
+triple-beam@^1.2.0, triple-beam@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
+  integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
+
+type-fest@^0.8.0:
+  version "0.8.1"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
+  integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
+
+unbox-primitive@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
+  integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==
+  dependencies:
+    function-bind "^1.1.1"
+    has-bigints "^1.0.1"
+    has-symbols "^1.0.2"
+    which-boxed-primitive "^1.0.2"
+
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+uuid@^3.0.1:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
+  integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
+
+which-boxed-primitive@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
+  integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
+  dependencies:
+    is-bigint "^1.0.1"
+    is-boolean-object "^1.1.0"
+    is-number-object "^1.0.4"
+    is-string "^1.0.5"
+    is-symbol "^1.0.3"
+
+winston-transport@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59"
+  integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==
+  dependencies:
+    readable-stream "^2.3.7"
+    triple-beam "^1.2.0"
+
+winston@^3.3.3:
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170"
+  integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==
+  dependencies:
+    "@dabh/diagnostics" "^2.0.2"
+    async "^3.1.0"
+    is-stream "^2.0.0"
+    logform "^2.2.0"
+    one-time "^1.0.0"
+    readable-stream "^3.4.0"
+    stack-trace "0.0.x"
+    triple-beam "^1.3.0"
+    winston-transport "^4.4.0"

+ 1 - 1
terminus-local/src/components/terminalTab.component.ts

@@ -24,7 +24,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
 
     ngOnInit (): void {
         this.logger = this.log.create('terminalTab')
-        this.session = new Session(this.config)
+        this.session = new Session(this.injector)
 
         const isConPTY = isWindowsBuild(WIN_BUILD_CONPTY_SUPPORTED) && this.config.store.terminal.useConPTY
 

+ 5 - 2
terminus-local/src/index.ts

@@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms'
 import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
 import { ToastrModule } from 'ngx-toastr'
 
-import TerminusCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, TabContextMenuItemProvider, CLIHandler } from 'terminus-core'
+import TerminusCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, TabContextMenuItemProvider, CLIHandler, ConfigService } from 'terminus-core'
 import TerminusTerminalModule from 'terminus-terminal'
 import { SettingsTabProvider } from 'terminus-settings'
 
@@ -104,6 +104,7 @@ export default class LocalTerminalModule { // eslint-disable-line @typescript-es
         terminal: TerminalService,
         hostApp: HostAppService,
         dockMenu: DockMenuService,
+        config: ConfigService,
     ) {
         hotkeys.matchedHotkey.subscribe(async (hotkey) => {
             if (hotkey === 'new-tab') {
@@ -120,7 +121,9 @@ export default class LocalTerminalModule { // eslint-disable-line @typescript-es
             }
         })
 
-        dockMenu.update()
+        config.ready$.toPromise().then(() => {
+            dockMenu.update()
+        })
     }
 }
 

+ 4 - 2
terminus-local/src/services/terminal.service.ts

@@ -26,10 +26,12 @@ export class TerminalService {
         log: LogService,
     ) {
         this.logger = log.create('terminal')
-        this.reloadShells()
 
-        config.changed$.subscribe(() => {
+        config.ready$.toPromise().then(() => {
             this.reloadShells()
+            config.changed$.subscribe(() => {
+                this.reloadShells()
+            })
         })
     }
 

+ 17 - 10
terminus-local/src/session.ts

@@ -1,7 +1,8 @@
 import * as psNode from 'ps-node'
 import * as fs from 'mz/fs'
 import * as os from 'os'
-import { ConfigService, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild, getBootstrapData } from 'terminus-core'
+import { Injector } from '@angular/core'
+import { HostAppService, ConfigService, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild, Platform, BootstrapData, BOOTSTRAP_DATA } from 'terminus-core'
 import { BaseSession } from 'terminus-terminal'
 import { ipcRenderer } from 'electron'
 import { getWorkingDirectoryFromPID } from 'native-process-working-directory'
@@ -91,9 +92,15 @@ export class Session extends BaseSession {
     private guessedCWD: string|null = null
     private reportedCWD: string
     private initialCWD: string|null = null
+    private config: ConfigService
+    private hostApp: HostAppService
+    private bootstrapData: BootstrapData
 
-    constructor (private config: ConfigService) {
+    constructor (injector: Injector) {
         super()
+        this.config = injector.get(ConfigService)
+        this.hostApp = injector.get(HostAppService)
+        this.bootstrapData = injector.get(BOOTSTRAP_DATA) as BootstrapData
     }
 
     start (options: SessionOptions): void {
@@ -115,13 +122,13 @@ export class Session extends BaseSession {
                 ...this.config.store.terminal.environment || {},
             }
 
-            if (process.platform === 'win32') {
-                env.COMSPEC = getBootstrapData().executable
+            if (this.hostApp.platform === Platform.Windows) {
+                env.COMSPEC = this.bootstrapData.executable
             }
 
             delete env['']
 
-            if (process.platform === 'darwin' && !process.env.LC_ALL) {
+            if (this.hostApp.platform === Platform.macOS && !process.env.LC_ALL) {
                 const locale = process.env.LC_CTYPE ?? 'en_US.UTF-8'
                 Object.assign(env, {
                     LANG: locale,
@@ -177,7 +184,7 @@ export class Session extends BaseSession {
             let data = Buffer.from(array)
             data = this.processOSC1337(data)
             this.emitOutput(data)
-            if (process.platform === 'win32') {
+            if (this.hostApp.platform === Platform.Windows) {
                 this.guessWindowsCWD(data.toString())
             }
         })
@@ -229,7 +236,7 @@ export class Session extends BaseSession {
         if (!this.truePID) {
             return []
         }
-        if (process.platform === 'darwin') {
+        if (this.hostApp.platform === Platform.macOS) {
             const processes = await macOSNativeProcessList.getProcessList()
             return processes.filter(x => x.ppid === this.truePID).map(p => ({
                 pid: p.pid,
@@ -237,7 +244,7 @@ export class Session extends BaseSession {
                 command: p.name,
             }))
         }
-        if (process.platform === 'win32') {
+        if (this.hostApp.platform === Platform.Windows) {
             return new Promise<ChildProcess[]>(resolve => {
                 windowsProcessTree.getProcessTree(this.truePID, tree => {
                     resolve(tree ? tree.children.map(child => ({
@@ -259,7 +266,7 @@ export class Session extends BaseSession {
     }
 
     async gracefullyKillProcess (): Promise<void> {
-        if (process.platform === 'win32') {
+        if (this.hostApp.platform === Platform.Windows) {
             this.kill()
         } else {
             await new Promise<void>((resolve) => {
@@ -302,7 +309,7 @@ export class Session extends BaseSession {
             cwd = await fs.realpath(cwd)
         } catch {}
 
-        if (process.platform === 'win32' && (cwd === this.initialCWD || cwd === process.env.WINDIR)) {
+        if (this.hostApp.platform === Platform.Windows && (cwd === this.initialCWD || cwd === process.env.WINDIR)) {
             // shell doesn't truly change its process' CWD
             cwd = null
         }

+ 5 - 6
terminus-local/src/tabContextMenu.ts

@@ -1,6 +1,5 @@
-import { MenuItemConstructorOptions } from 'electron'
 import { Injectable, NgZone } from '@angular/core'
-import { ConfigService, BaseTabComponent, TabContextMenuItemProvider, TabHeaderComponent, SplitTabComponent, NotificationsService } from 'terminus-core'
+import { ConfigService, BaseTabComponent, TabContextMenuItemProvider, TabHeaderComponent, SplitTabComponent, NotificationsService, MenuItemOptions } from 'terminus-core'
 import { TerminalTabComponent } from './components/terminalTab.component'
 import { UACService } from './services/uac.service'
 import { TerminalService } from './services/terminal.service'
@@ -16,11 +15,11 @@ export class SaveAsProfileContextMenu extends TabContextMenuItemProvider {
         super()
     }
 
-    async getItems (tab: BaseTabComponent, _tabHeader?: TabHeaderComponent): Promise<MenuItemConstructorOptions[]> {
+    async getItems (tab: BaseTabComponent, _tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
         if (!(tab instanceof TerminalTabComponent)) {
             return []
         }
-        const items: MenuItemConstructorOptions[] = [
+        const items: MenuItemOptions[] = [
             {
                 label: 'Save as profile',
                 click: () => this.zone.run(async () => {
@@ -59,10 +58,10 @@ export class NewTabContextMenu extends TabContextMenuItemProvider {
         super()
     }
 
-    async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemConstructorOptions[]> {
+    async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
         const profiles = await this.terminalService.getProfiles()
 
-        const items: MenuItemConstructorOptions[] = [
+        const items: MenuItemOptions[] = [
             {
                 label: 'New terminal',
                 click: () => this.zone.run(() => {

+ 0 - 1
terminus-plugin-manager/package.json

@@ -19,7 +19,6 @@
   "devDependencies": {
     "@types/semver": "^7.1.0",
     "axios": "^0.21.1",
-    "electron-promise-ipc": "^2.2.4",
     "mz": "^2.6.0",
     "semver": "^7.1.1"
   },

+ 4 - 4
terminus-plugin-manager/src/components/pluginsSettingsTab.component.ts

@@ -4,7 +4,7 @@ import { debounceTime, distinctUntilChanged, first, tap, flatMap, map } from 'rx
 import semverGt from 'semver/functions/gt'
 
 import { Component, Input } from '@angular/core'
-import { ConfigService, ElectronService } from 'terminus-core'
+import { ConfigService, PlatformService } from 'terminus-core'
 import { PluginInfo, PluginManagerService } from '../services/pluginManager.service'
 
 enum BusyState { Installing = 'Installing', Uninstalling = 'Uninstalling' }
@@ -27,8 +27,8 @@ export class PluginsSettingsTabComponent {
     @Input() errorMessage: string
 
     constructor (
-        private electron: ElectronService,
         private config: ConfigService,
+        private platform: PlatformService,
         public pluginManager: PluginManagerService
     ) {
     }
@@ -57,7 +57,7 @@ export class PluginsSettingsTabComponent {
     }
 
     openPluginsFolder (): void {
-        this.electron.shell.openPath(this.pluginManager.userPluginsPath)
+        this.platform.openPath(this.pluginManager.userPluginsPath)
     }
 
     searchAvailable (query: string) {
@@ -101,7 +101,7 @@ export class PluginsSettingsTabComponent {
     }
 
     showPluginInfo (plugin: PluginInfo) {
-        this.electron.shell.openExternal('https://www.npmjs.com/package/' + plugin.packageName)
+        this.platform.openExternal('https://www.npmjs.com/package/' + plugin.packageName)
     }
 
     isPluginEnabled (plugin: PluginInfo) {

+ 4 - 4
terminus-plugin-manager/src/services/pluginManager.service.ts

@@ -1,9 +1,8 @@
 import axios from 'axios'
-import promiseIpc from 'electron-promise-ipc'
 import { Observable, from } from 'rxjs'
 import { map } from 'rxjs/operators'
 import { Injectable } from '@angular/core'
-import { Logger, LogService } from 'terminus-core'
+import { Logger, LogService, PlatformService } from 'terminus-core'
 
 const NAME_PREFIX = 'terminus-'
 const KEYWORD = 'terminus-plugin'
@@ -34,6 +33,7 @@ export class PluginManagerService {
 
     private constructor (
         log: LogService,
+        private platform: PlatformService,
     ) {
         this.logger = log.create('pluginManager')
     }
@@ -63,7 +63,7 @@ export class PluginManagerService {
 
     async installPlugin (plugin: PluginInfo): Promise<void> {
         try {
-            await (promiseIpc as any).send('plugin-manager:install', this.userPluginsPath, plugin.packageName, plugin.version)
+            await this.platform.installPlugin(plugin.packageName, plugin.version)
             this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
             this.installedPlugins.push(plugin)
         } catch (err) {
@@ -74,7 +74,7 @@ export class PluginManagerService {
 
     async uninstallPlugin (plugin: PluginInfo): Promise<void> {
         try {
-            await (promiseIpc as any).send('plugin-manager:uninstall', this.userPluginsPath, plugin.packageName)
+            await this.platform.uninstallPlugin(plugin.packageName)
             this.installedPlugins = this.installedPlugins.filter(x => x.packageName !== plugin.packageName)
         } catch (err) {
             this.logger.error(err)

+ 0 - 175
terminus-plugin-manager/yarn.lock

@@ -19,123 +19,11 @@ axios@^0.21.1:
   dependencies:
     follow-redirects "^1.10.0"
 
-call-bind@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce"
-  integrity sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==
-  dependencies:
-    function-bind "^1.1.1"
-    get-intrinsic "^1.0.0"
-
-define-properties@^1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
-  integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
-  dependencies:
-    object-keys "^1.0.12"
-
-electron-promise-ipc@^2.2.4:
-  version "2.2.4"
-  resolved "https://registry.yarnpkg.com/electron-promise-ipc/-/electron-promise-ipc-2.2.4.tgz#b82daf86ca6d0f0b8655937fdbe9a554590deeea"
-  integrity sha512-xCkFEeuru9l7H/+m1gpK4F1utexvTT7+n1PTquP2MVTpmBmpgFBlLqSXC7TqwpROkHRm9wGpaCJEx0djonnSEg==
-  dependencies:
-    is-electron-renderer "^2.0.1"
-    object.entries "^1.1.3"
-    serialize-error "^5.0.0"
-    uuid "^3.0.1"
-
-es-abstract@^1.18.0-next.1:
-  version "1.18.0-next.1"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
-  integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
-  dependencies:
-    es-to-primitive "^1.2.1"
-    function-bind "^1.1.1"
-    has "^1.0.3"
-    has-symbols "^1.0.1"
-    is-callable "^1.2.2"
-    is-negative-zero "^2.0.0"
-    is-regex "^1.1.1"
-    object-inspect "^1.8.0"
-    object-keys "^1.1.1"
-    object.assign "^4.1.1"
-    string.prototype.trimend "^1.0.1"
-    string.prototype.trimstart "^1.0.1"
-
-es-to-primitive@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
-  integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
-  dependencies:
-    is-callable "^1.1.4"
-    is-date-object "^1.0.1"
-    is-symbol "^1.0.2"
-
 follow-redirects@^1.10.0:
   version "1.13.1"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
   integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
 
-function-bind@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
-  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
-
-get-intrinsic@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.2.tgz#6820da226e50b24894e08859469dc68361545d49"
-  integrity sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==
-  dependencies:
-    function-bind "^1.1.1"
-    has "^1.0.3"
-    has-symbols "^1.0.1"
-
-has-symbols@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
-  integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
-
-has@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
-  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
-  dependencies:
-    function-bind "^1.1.1"
-
-is-callable@^1.1.4, is-callable@^1.2.2:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
-  integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
-
-is-date-object@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
-  integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
-
-is-electron-renderer@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/is-electron-renderer/-/is-electron-renderer-2.0.1.tgz#a469d056f975697c58c98c6023eb0aa79af895a2"
-  integrity sha1-pGnQVvl1aXxYyYxgI+sKp5r4laI=
-
-is-negative-zero@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
-  integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
-
-is-regex@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
-  integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
-  dependencies:
-    has-symbols "^1.0.1"
-
-is-symbol@^1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
-  integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
-  dependencies:
-    has-symbols "^1.0.1"
-
 lru-cache@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@@ -157,36 +45,6 @@ object-assign@^4.0.1:
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
   integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
 
-object-inspect@^1.8.0:
-  version "1.9.0"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
-  integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
-
-object-keys@^1.0.12, object-keys@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
-  integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
-
-object.assign@^4.1.1:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
-  integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
-  dependencies:
-    call-bind "^1.0.0"
-    define-properties "^1.1.3"
-    has-symbols "^1.0.1"
-    object-keys "^1.1.1"
-
-object.entries@^1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.3.tgz#c601c7f168b62374541a07ddbd3e2d5e4f7711a6"
-  integrity sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==
-  dependencies:
-    call-bind "^1.0.0"
-    define-properties "^1.1.3"
-    es-abstract "^1.18.0-next.1"
-    has "^1.0.3"
-
 semver@^7.1.1:
   version "7.3.5"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
@@ -194,29 +52,6 @@ semver@^7.1.1:
   dependencies:
     lru-cache "^6.0.0"
 
-serialize-error@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-5.0.0.tgz#a7ebbcdb03a5d71a6ed8461ffe0fc1a1afed62ac"
-  integrity sha512-/VtpuyzYf82mHYTtI4QKtwHa79vAdU5OQpNPAmE/0UDdlGT0ZxHwC+J6gXkw29wwoVI8fMPsfcVHOwXtUQYYQA==
-  dependencies:
-    type-fest "^0.8.0"
-
-string.prototype.trimend@^1.0.1:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b"
-  integrity sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==
-  dependencies:
-    call-bind "^1.0.0"
-    define-properties "^1.1.3"
-
-string.prototype.trimstart@^1.0.1:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa"
-  integrity sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==
-  dependencies:
-    call-bind "^1.0.0"
-    define-properties "^1.1.3"
-
 thenify-all@^1.0.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
@@ -231,16 +66,6 @@ thenify-all@^1.0.0:
   dependencies:
     any-promise "^1.0.0"
 
-type-fest@^0.8.0:
-  version "0.8.1"
-  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
-  integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
-
-uuid@^3.0.1:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
-  integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
-
 yallist@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"

+ 3 - 1
terminus-settings/src/components/hotkeyInputModal.component.ts

@@ -67,6 +67,7 @@ export class HotkeyInputModalComponent extends BaseComponent {
             }
             this.timeoutProgress = Math.min(100, (performance.now() - this.lastKeyEvent) * 100 / INPUT_TIMEOUT)
             if (this.timeoutProgress === 100) {
+                clearInterval(this.keyTimeoutInterval!)
                 this.modalInstance.close(this.value)
             }
         }, 25)
@@ -74,13 +75,14 @@ export class HotkeyInputModalComponent extends BaseComponent {
     }
 
     ngOnDestroy (): void {
+        clearInterval(this.keyTimeoutInterval!)
         this.hotkeys.clearCurrentKeystrokes()
         this.hotkeys.enable()
-        clearInterval(this.keyTimeoutInterval!)
         super.ngOnDestroy()
     }
 
     close (): void {
+        clearInterval(this.keyTimeoutInterval!)
         this.modalInstance.dismiss()
     }
 }

+ 5 - 2
terminus-settings/src/components/settingsTab.component.pug

@@ -40,7 +40,7 @@ button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(clic
                             i.fas.fa-sync
                             span Update
 
-                .form-line(*ngIf='hostApp.platform !== Platform.Linux')
+                .form-line(*ngIf='platform.isShellIntegrationSupported()')
                     .header
                         .title Shell integration
                         .description Allows quickly opening a terminal in the selected folder
@@ -101,7 +101,10 @@ button.btn.btn-outline-warning.btn-block(*ngIf='config.restartRequested', '(clic
                         button.btn.btn-primary(disabled, *ngIf='!isConfigFileValid()')
                             i.fas.fa-exclamation-triangle.mr-2
                             | Invalid syntax
-                        button.btn.btn-secondary.ml-auto((click)='showConfigFile()')
+                        button.btn.btn-secondary.ml-auto(
+                            *ngIf='platform.getConfigPath()',
+                            (click)='showConfigFile()'
+                        )
                             i.fas.fa-external-link-square-alt.mr-2
                             | Show config file
 

+ 7 - 9
terminus-settings/src/components/settingsTab.component.ts

@@ -3,14 +3,13 @@ import * as yaml from 'js-yaml'
 import { debounce } from 'utils-decorators/dist/cjs'
 import { Component, Inject, Input, HostBinding, NgZone } from '@angular/core'
 import {
-    ElectronService,
     ConfigService,
     BaseTabComponent,
     HostAppService,
     Platform,
     HomeBaseService,
-    ShellIntegrationService,
     UpdaterService,
+    PlatformService,
 } from 'terminus-core'
 
 import { SettingsTabProvider } from '../api'
@@ -35,10 +34,9 @@ export class SettingsTabComponent extends BaseTabComponent {
 
     constructor (
         public config: ConfigService,
-        private electron: ElectronService,
         public hostApp: HostAppService,
         public homeBase: HomeBaseService,
-        public shellIntegration: ShellIntegrationService,
+        public platform: PlatformService,
         public zone: NgZone,
         private updater: UpdaterService,
         @Inject(SettingsTabProvider) public settingsProviders: SettingsTabProvider[],
@@ -61,16 +59,16 @@ export class SettingsTabComponent extends BaseTabComponent {
     }
 
     async ngOnInit () {
-        this.isShellIntegrationInstalled = await this.shellIntegration.isInstalled()
+        this.isShellIntegrationInstalled = await this.platform.isShellIntegrationInstalled()
     }
 
     async toggleShellIntegration () {
         if (!this.isShellIntegrationInstalled) {
-            await this.shellIntegration.install()
+            await this.platform.installShellIntegration()
         } else {
-            await this.shellIntegration.remove()
+            await this.platform.uninstallShellIntegration()
         }
-        this.isShellIntegrationInstalled = await this.shellIntegration.isInstalled()
+        this.isShellIntegrationInstalled = await this.platform.isShellIntegrationInstalled()
     }
 
     ngOnDestroy () {
@@ -96,7 +94,7 @@ export class SettingsTabComponent extends BaseTabComponent {
     }
 
     showConfigFile () {
-        this.electron.shell.showItemInFolder(this.config.path)
+        this.platform.showItemInFolder(this.platform.getConfigPath()!)
     }
 
     isConfigFileValid () {

+ 9 - 9
terminus-settings/src/components/windowSettingsTab.component.pug

@@ -9,7 +9,7 @@ h3.mb-3 Window
     )
         option(*ngFor='let theme of themes', [ngValue]='theme.name') {{theme.name}}
 
-.form-line
+.form-line(*ngIf='platform.supportsWindowControls')
     .header
         .title(*ngIf='hostApp.platform !== Platform.macOS') Acrylic background
         .title(*ngIf='hostApp.platform === Platform.macOS') Vibrancy
@@ -43,7 +43,7 @@ h3.mb-3 Window
             )
             | Fluent
 
-.form-line
+.form-line(*ngIf='platform.supportsWindowControls')
     .header
         .title Opacity
     input(
@@ -55,7 +55,7 @@ h3.mb-3 Window
         step='0.01'
     )
 
-.form-line
+.form-line(*ngIf='platform.supportsWindowControls')
     .header
         .title Window frame
         .description Whether a custom window or an OS native window should be used
@@ -87,7 +87,7 @@ h3.mb-3 Window
             )
             | Full
 
-.form-line
+.form-line(*ngIf='docking')
     .header
         .title Dock the terminal
         .description Snaps the window to a side of the screen
@@ -133,7 +133,7 @@ h3.mb-3 Window
             )
             | Bottom
 
-.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
+.ml-5.form-line(*ngIf='docking && config.store.appearance.dock != "off"')
     .header
         .title Display on
         .description Snaps the window to a side of the screen
@@ -158,7 +158,7 @@ h3.mb-3 Window
             )
             | {{screen.name}}
 
-.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
+.ml-5.form-line(*ngIf='docking && config.store.appearance.dock != "off"')
     .header
         .title Dock always on top
         .description Keep docked terminal always on top
@@ -167,7 +167,7 @@ h3.mb-3 Window
         (ngModelChange)='saveConfiguration(); docking.dock()',
     )
 
-.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
+.ml-5.form-line(*ngIf='docking && config.store.appearance.dock != "off"')
     .header
         .title Docked terminal size
     input(
@@ -179,7 +179,7 @@ h3.mb-3 Window
         step='0.01'
     )
 
-.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
+.ml-5.form-line(*ngIf='docking && config.store.appearance.dock != "off"')
     .header
         .title Docked terminal space
     input(
@@ -191,7 +191,7 @@ h3.mb-3 Window
         step='0.01'
     )
 
-.ml-5.form-line(*ngIf='config.store.appearance.dock != "off"')
+.ml-5.form-line(*ngIf='docking && config.store.appearance.dock != "off"')
     .header
         .title Hide dock on blur
         .description Hides the docked terminal when you click away.

+ 13 - 7
terminus-settings/src/components/windowSettingsTab.component.ts

@@ -1,6 +1,6 @@
 /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
 import { debounce } from 'utils-decorators/dist/cjs'
-import { Component, Inject, NgZone } from '@angular/core'
+import { Component, Inject, NgZone, Optional } from '@angular/core'
 import {
     DockingService,
     ConfigService,
@@ -10,6 +10,8 @@ import {
     isWindowsBuild,
     WIN_BUILD_FLUENT_BG_SUPPORTED,
     BaseComponent,
+    Screen,
+    PlatformService,
 } from 'terminus-core'
 
 
@@ -19,24 +21,28 @@ import {
     template: require('./windowSettingsTab.component.pug'),
 })
 export class WindowSettingsTabComponent extends BaseComponent {
-    screens: any[]
+    screens: Screen[]
     Platform = Platform
     isFluentVibrancySupported = false
 
     constructor (
         public config: ConfigService,
-        public docking: DockingService,
         public hostApp: HostAppService,
+        public platform: PlatformService,
         public zone: NgZone,
         @Inject(Theme) public themes: Theme[],
+        @Optional() public docking?: DockingService,
     ) {
         super()
-        this.screens = this.docking.getScreens()
+
         this.themes = config.enabledServices(this.themes)
 
-        this.subscribeUntilDestroyed(hostApp.displaysChanged$, () => {
-            this.zone.run(() => this.screens = this.docking.getScreens())
-        })
+        if (this.docking) {
+            this.subscribeUntilDestroyed(hostApp.displaysChanged$, () => {
+                this.zone.run(() => this.screens = this.docking!.getScreens())
+            })
+            this.screens = this.docking.getScreens()
+        }
 
         this.isFluentVibrancySupported = isWindowsBuild(WIN_BUILD_FLUENT_BG_SUPPORTED)
     }

+ 1 - 1
terminus-ssh/src/components/sshSettingsTab.component.pug

@@ -61,7 +61,7 @@ h3.mt-5 Options
         (ngModelChange)='config.save()',
     )
 
-.form-line
+.form-line(*ngIf='hostApp.platform === Platform.Windows')
     .header
         .title WinSCP path
         .descriptions When WinSCP is detected, you can launch an SCP session from the context menu.

+ 3 - 2
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 } from 'terminus-core'
+import { ConfigService, ElectronService, HostAppService, Platform } from 'terminus-core'
 import { PasswordStorageService } from '../services/passwordStorage.service'
 import { SSHConnection } from '../api'
 import { EditConnectionModalComponent } from './editConnectionModal.component'
@@ -23,11 +23,12 @@ export class SSHSettingsTabComponent {
     childGroups: SSHConnectionGroup[]
     groupCollapsed: Record<string, boolean> = {}
     filter = ''
+    Platform = Platform
 
     constructor (
         public config: ConfigService,
+        public hostApp: HostAppService,
         private electron: ElectronService,
-        private hostApp: HostAppService,
         private ngbModal: NgbModal,
         private passwordStorage: PasswordStorageService,
     ) {

+ 3 - 17
terminus-ssh/src/services/ssh.service.ts

@@ -10,7 +10,7 @@ import { exec } from 'child_process'
 import * as path from 'path'
 import * as sshpk from 'sshpk'
 import { Subject, Observable } from 'rxjs'
-import { HostAppService, Platform, Logger, LogService, AppService, SelectorOption, ConfigService, NotificationsService } from 'terminus-core'
+import { HostAppService, Platform, Logger, LogService, AppService, SelectorOption, ConfigService, NotificationsService, PlatformService } from 'terminus-core'
 import { SettingsTabComponent } from 'terminus-settings'
 import { ALGORITHM_BLACKLIST, ForwardedPort, SSHConnection, SSHSession } from '../api'
 import { PromptModalComponent } from '../components/promptModal.component'
@@ -20,15 +20,6 @@ import { ChildProcess } from 'node:child_process'
 
 const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent'
 
-try {
-    var windowsProcessTreeNative = require('windows-process-tree/build/Release/windows_process_tree.node') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
-} catch { }
-
-
-// eslint-disable-next-line @typescript-eslint/no-type-alias
-export type SSHLogCallback = (message: string) => void
-
-
 @Injectable({ providedIn: 'root' })
 export class SSHService {
     private logger: Logger
@@ -42,6 +33,7 @@ export class SSHService {
         private notifications: NotificationsService,
         private app: AppService,
         private config: ConfigService,
+        private platform: PlatformService,
     ) {
         this.logger = log.create('ssh')
     }
@@ -197,13 +189,7 @@ export class SSHService {
             if (await fs.exists(WINDOWS_OPENSSH_AGENT_PIPE)) {
                 agent = WINDOWS_OPENSSH_AGENT_PIPE
             } else {
-                // eslint-disable-next-line @typescript-eslint/no-shadow
-                const pageantRunning = await new Promise<boolean>(resolve => {
-                    windowsProcessTreeNative.getProcessList(list => { // eslint-disable-line block-scoped-var
-                        resolve(list.some(x => x.name === 'pageant.exe'))
-                    }, 0)
-                })
-                if (pageantRunning) {
+                if (await this.platform.isProcessRunning('pageant.exe')) {
                     agent = 'pageant'
                 }
             }

+ 6 - 17
terminus-ssh/src/winSCPIntegration.ts

@@ -1,27 +1,20 @@
-import type { MenuItemConstructorOptions } from 'electron'
-import { execFile } from 'child_process'
 import { Injectable } from '@angular/core'
-import { ConfigService, BaseTabComponent, TabContextMenuItemProvider, TabHeaderComponent, HostAppService, Platform } from 'terminus-core'
+import { ConfigService, BaseTabComponent, TabContextMenuItemProvider, TabHeaderComponent, HostAppService, Platform, PlatformService, MenuItemOptions } from 'terminus-core'
 import { SSHTabComponent } from './components/sshTab.component'
 import { PasswordStorageService } from './services/passwordStorage.service'
 import { SSHConnection } from './api'
 
 
-/* eslint-disable block-scoped-var */
-try {
-    var wnr = require('windows-native-registry') // eslint-disable-line @typescript-eslint/no-var-requires, no-var
-} catch { }
-
-
 /** @hidden */
 @Injectable()
 export class WinSCPContextMenu extends TabContextMenuItemProvider {
     weight = 10
-    private detectedPath?: string
+    private detectedPath: string | null
 
     constructor (
         private hostApp: HostAppService,
         private config: ConfigService,
+        private platform: PlatformService,
         private passwordStorage: PasswordStorageService,
     ) {
         super()
@@ -30,14 +23,10 @@ export class WinSCPContextMenu extends TabContextMenuItemProvider {
             return
         }
 
-        const key = wnr.getRegistryKey(wnr.HK.CR, 'WinSCP.Url\\DefaultIcon')
-        if (key?.['']) {
-            this.detectedPath = key[''].value?.split(',')[0]
-            this.detectedPath = this.detectedPath?.substring(1, this.detectedPath.length - 1)
-        }
+        this.detectedPath = platform.getWinSCPPath()
     }
 
-    async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemConstructorOptions[]> {
+    async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
         if (this.hostApp.platform !== Platform.Windows || tabHeader) {
             return []
         }
@@ -81,6 +70,6 @@ export class WinSCPContextMenu extends TabContextMenuItemProvider {
             args.push('/privatekey')
             args.push(connection.privateKey)
         }
-        execFile(path, args)
+        this.platform.exec(path, args)
     }
 }

+ 16 - 11
terminus-terminal/src/api/baseTerminalTab.component.ts

@@ -1,10 +1,9 @@
-import type { MenuItemConstructorOptions } from 'electron'
 import { Observable, Subject, Subscription } from 'rxjs'
 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 } from 'terminus-core'
+import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, NotificationsService, Platform, LogService, Logger, TabContextMenuItemProvider, SplitTabComponent, SubscriptionContainer, MenuItemOptions, PlatformService } from 'terminus-core'
 
 import { BaseSession } from '../session'
 import { TerminalFrontendService } from '../services/terminalFrontend.service'
@@ -84,6 +83,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
     protected hostApp: HostAppService
     protected hotkeys: HotkeysService
     protected electron: ElectronService
+    protected platform: PlatformService
     protected terminalContainersService: TerminalFrontendService
     protected notifications: NotificationsService
     protected log: LogService
@@ -136,6 +136,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
         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)
         this.log = injector.get(LogService)
@@ -312,8 +313,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
         }
     }
 
-    async buildContextMenu (): Promise<MenuItemConstructorOptions[]> {
-        let items: MenuItemConstructorOptions[] = []
+    async buildContextMenu (): Promise<MenuItemOptions[]> {
+        let items: MenuItemOptions[] = []
         for (const section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(this)))) {
             items = items.concat(section)
             items.push({ type: 'separator' })
@@ -498,6 +499,16 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
         this.termContainerSubscriptions.cancelAll()
     }
 
+    protected async handleRightClick (event: MouseEvent): Promise<void> {
+        event.preventDefault()
+        event.stopPropagation()
+        if (this.config.store.terminal.rightClick === 'menu') {
+            this.platform.popupContextMenu(await this.buildContextMenu(), event)
+        } else if (this.config.store.terminal.rightClick === 'paste') {
+            this.paste()
+        }
+    }
+
     protected attachTermContainerHandlers (): void {
         this.detachTermContainerHandlers()
 
@@ -531,13 +542,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
                     return
                 }
                 if (event.which === 3 || event.which === 1 && event.ctrlKey) {
-                    if (this.config.store.terminal.rightClick === 'menu') {
-                        this.hostApp.popupContextMenu(await this.buildContextMenu())
-                    } else if (this.config.store.terminal.rightClick === 'paste') {
-                        this.paste()
-                    }
-                    event.preventDefault()
-                    event.stopPropagation()
+                    this.handleRightClick(event)
                     return
                 }
             }

+ 2 - 2
terminus-terminal/src/api/contextMenuProvider.ts

@@ -1,4 +1,4 @@
-import type { MenuItemConstructorOptions } from 'electron'
+import type { MenuItemOptions } from 'terminus-core'
 import { BaseTerminalTabComponent } from './baseTerminalTab.component'
 
 /**
@@ -8,5 +8,5 @@ import { BaseTerminalTabComponent } from './baseTerminalTab.component'
 export abstract class TerminalContextMenuItemProvider {
     weight: number
 
-    abstract getItems (tab: BaseTerminalTabComponent): Promise<MenuItemConstructorOptions[]>
+    abstract getItems (tab: BaseTerminalTabComponent): Promise<MenuItemOptions[]>
 }

+ 3 - 19
terminus-terminal/src/components/appearanceSettingsTab.component.ts

@@ -2,11 +2,9 @@
 import { Observable } from 'rxjs'
 import { debounce } from 'utils-decorators/dist/cjs'
 import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'
-import { exec } from 'mz/child_process'
-const fontManager = require('fontmanager-redux') // eslint-disable-line
 
 import { Component } from '@angular/core'
-import { ConfigService, HostAppService, Platform, getCSSFontFamily } from 'terminus-core'
+import { ConfigService, getCSSFontFamily, PlatformService } from 'terminus-core'
 
 /** @hidden */
 @Component({
@@ -17,26 +15,12 @@ export class AppearanceSettingsTabComponent {
     fonts: string[] = []
 
     constructor (
-        private hostApp: HostAppService,
         public config: ConfigService,
+        private platform: PlatformService,
     ) { }
 
     async ngOnInit () {
-        if (this.hostApp.platform === Platform.Windows || this.hostApp.platform === Platform.macOS) {
-            const fonts = await new Promise<any[]>((resolve) => fontManager.findFonts({ monospace: true }, resolve))
-            this.fonts = fonts.map(x => x.family.trim())
-            this.fonts.sort()
-        }
-        if (this.hostApp.platform === Platform.Linux) {
-            exec('fc-list :spacing=mono').then(([stdout, _]) => {
-                this.fonts = stdout.toString()
-                    .split('\n')
-                    .filter(x => !!x)
-                    .map(x => x.split(':')[1].trim())
-                    .map(x => x.split(',')[0].trim())
-                this.fonts.sort()
-            })
-        }
+        this.fonts = await this.platform.listFonts()
     }
 
     fontAutocomplete = (text$: Observable<string>) => {

+ 2 - 2
terminus-terminal/src/components/terminalSettingsTab.component.pug

@@ -1,6 +1,6 @@
 h3.mb-3 Terminal
 
-.form-line
+.form-line(*ngIf='hostApp.platform !== Platform.Web')
     .header
         .title Frontend
         .description Switches terminal frontend implementation (experimental)
@@ -86,7 +86,7 @@ h3.mb-3 Terminal
         (ngModelChange)='config.save()',
     )
 
-.form-line
+.form-line(*ngIf='hostApp.platform !== Platform.Web')
     .header
         .title Auto-open a terminal on app start
 

+ 6 - 3
terminus-terminal/src/components/terminalSettingsTab.component.ts

@@ -1,19 +1,22 @@
 import { execFile } from 'mz/child_process'
 import { Component } from '@angular/core'
-import { ConfigService, ElectronService } from 'terminus-core'
+import { ConfigService, HostAppService, Platform, PlatformService } from 'terminus-core'
 
 /** @hidden */
 @Component({
     template: require('./terminalSettingsTab.component.pug'),
 })
 export class TerminalSettingsTabComponent {
+    Platform = Platform
+
     constructor (
         public config: ConfigService,
-        private electron: ElectronService,
+        public hostApp: HostAppService,
+        private platform: PlatformService,
     ) { }
 
     openWSLVolumeMixer (): void {
-        this.electron.shell.openPath('sndvol.exe')
+        this.platform.openPath('sndvol.exe')
         execFile('wsl.exe', ['tput', 'bel'])
     }
 }

+ 3 - 5
terminus-terminal/src/frontends/frontend.ts

@@ -1,6 +1,6 @@
+import { Injector } from '@angular/core'
 import { Observable, Subject, AsyncSubject, ReplaySubject, BehaviorSubject } from 'rxjs'
 import { ResizeEvent } from '../api/interfaces'
-import { ConfigService, ThemesService, HotkeysService } from 'terminus-core'
 
 export interface SearchOptions {
     regex?: boolean
@@ -13,10 +13,6 @@ export interface SearchOptions {
  * Extend to add support for a different VT frontend implementation
  */
 export abstract class Frontend {
-    configService: ConfigService
-    themesService: ThemesService
-    hotkeysService: HotkeysService
-
     enableResizing = true
     protected ready = new AsyncSubject<void>()
     protected title = new ReplaySubject<string>(1)
@@ -40,6 +36,8 @@ export abstract class Frontend {
     get dragOver$ (): Observable<DragEvent> { return this.dragOver }
     get drop$ (): Observable<DragEvent> { return this.drop }
 
+    constructor (protected injector: Injector) { }
+
     destroy (): void {
         for (const o of [
             this.ready,

+ 11 - 1
terminus-terminal/src/frontends/htermFrontend.ts

@@ -1,6 +1,7 @@
+import { Injector } from '@angular/core'
+import { ConfigService, getCSSFontFamily, ThemesService } from 'terminus-core'
 import { Frontend, SearchOptions } from './frontend'
 import { hterm, preferenceManager } from './hterm'
-import { getCSSFontFamily } from 'terminus-core'
 
 /** @hidden */
 export class HTermFrontend extends Frontend {
@@ -13,6 +14,15 @@ export class HTermFrontend extends Frontend {
     private configuredBackgroundColor = 'transparent'
     private zoom = 0
 
+    private configService: ConfigService
+    private themesService: ThemesService
+
+    constructor (injector: Injector) {
+        super(injector)
+        this.configService = injector.get(ConfigService)
+        this.themesService = injector.get(ThemesService)
+    }
+
     async attach (host: HTMLElement): Promise<void> {
         if (!this.initialized) {
             this.init()

+ 25 - 8
terminus-terminal/src/frontends/xtermFrontend.ts

@@ -1,4 +1,5 @@
-import { getCSSFontFamily } from 'terminus-core'
+import { Injector } from '@angular/core'
+import { ConfigService, getCSSFontFamily, HostAppService, HotkeysService, Platform, PlatformService } from 'terminus-core'
 import { Frontend, SearchOptions } from './frontend'
 import { Terminal, ITheme } from 'xterm'
 import { FitAddon } from 'xterm-addon-fit'
@@ -39,8 +40,18 @@ export class XTermFrontend extends Frontend {
     private opened = false
     private resizeObserver?: any
 
-    constructor () {
-        super()
+    private configService: ConfigService
+    private hotkeysService: HotkeysService
+    private platformService: PlatformService
+    private hostApp: HostAppService
+
+    constructor (injector: Injector) {
+        super(injector)
+        this.configService = injector.get(ConfigService)
+        this.hotkeysService = injector.get(HotkeysService)
+        this.platformService = injector.get(PlatformService)
+        this.hostApp = injector.get(HostAppService)
+
         this.xterm = new Terminal({
             allowTransparency: true,
             windowsMode: process.platform === 'win32',
@@ -88,9 +99,11 @@ export class XTermFrontend extends Frontend {
         }
 
         this.xterm.attachCustomKeyEventHandler((event: KeyboardEvent) => {
-            if (event.getModifierState('Meta') && event.key.toLowerCase() === 'v') {
-                event.preventDefault()
-                return false
+            if (this.hostApp.platform !== Platform.Web) {
+                if (event.getModifierState('Meta') && event.key.toLowerCase() === 'v') {
+                    event.preventDefault()
+                    return false
+                }
             }
             if (event.getModifierState('Meta') && event.key.startsWith('Arrow')) {
                 return false
@@ -167,6 +180,10 @@ export class XTermFrontend extends Frontend {
         host.addEventListener('mousedown', event => this.mouseEvent.next(event))
         host.addEventListener('mouseup', event => this.mouseEvent.next(event))
         host.addEventListener('mousewheel', event => this.mouseEvent.next(event as MouseEvent))
+        host.addEventListener('contextmenu', event => {
+            event.preventDefault()
+            event.stopPropagation()
+        })
 
         this.resizeObserver = new window['ResizeObserver'](() => setTimeout(() => this.resizeHandler()))
         this.resizeObserver.observe(host)
@@ -190,12 +207,12 @@ export class XTermFrontend extends Frontend {
     copySelection (): void {
         const text = this.getSelection()
         if (text.length < 1024 * 32) {
-            require('@electron/remote').clipboard.write({
+            this.platformService.setClipboard({
                 text: this.getSelection(),
                 html: this.getSelectionAsHTML(),
             })
         } else {
-            require('@electron/remote').clipboard.write({
+            this.platformService.setClipboard({
                 text: this.getSelection(),
             })
         }

+ 0 - 2
terminus-terminal/src/index.ts

@@ -25,7 +25,6 @@ import { PathDropDecorator } from './features/pathDrop'
 import { ZModemDecorator } from './features/zmodem'
 import { TerminalConfigProvider } from './config'
 import { TerminalHotkeyProvider } from './hotkeys'
-import { HyperColorSchemes } from './colorSchemes'
 import { CopyPasteContextMenu, LegacyContextMenu } from './tabContextMenu'
 
 import { hterm } from './frontends/hterm'
@@ -50,7 +49,6 @@ import { TerminalCLIHandler } from './cli'
 
         { provide: ConfigProvider, useClass: TerminalConfigProvider, multi: true },
         { provide: HotkeyProvider, useClass: TerminalHotkeyProvider, multi: true },
-        { provide: TerminalColorSchemeProvider, useClass: HyperColorSchemes, multi: true },
         { provide: TerminalDecorator, useClass: PathDropDecorator, multi: true },
         { provide: TerminalDecorator, useClass: ZModemDecorator, multi: true },
         { provide: TerminalDecorator, useClass: DebugDecorator, multi: true },

+ 4 - 8
terminus-terminal/src/services/terminalFrontend.service.ts

@@ -1,5 +1,5 @@
-import { Injectable } from '@angular/core'
-import { ConfigService, ThemesService, HotkeysService } from 'terminus-core'
+import { Injectable, Injector } from '@angular/core'
+import { ConfigService } from 'terminus-core'
 import { Frontend } from '../frontends/frontend'
 import { HTermFrontend } from '../frontends/htermFrontend'
 import { XTermFrontend, XTermWebGLFrontend } from '../frontends/xtermFrontend'
@@ -12,8 +12,7 @@ export class TerminalFrontendService {
     /** @hidden */
     private constructor (
         private config: ConfigService,
-        private themes: ThemesService,
-        private hotkeys: HotkeysService,
+        private injector: Injector,
     ) { }
 
     getFrontend (session?: BaseSession|null): Frontend {
@@ -22,10 +21,7 @@ export class TerminalFrontendService {
                 xterm: XTermFrontend,
                 'xterm-webgl': XTermWebGLFrontend,
                 hterm: HTermFrontend,
-            }[this.config.store.terminal.frontend]()
-            frontend.configService = this.config
-            frontend.themesService = this.themes
-            frontend.hotkeysService = this.hotkeys
+            }[this.config.store.terminal.frontend](this.injector)
             return frontend
         }
         if (!this.containers.has(session)) {

+ 4 - 5
terminus-terminal/src/tabContextMenu.ts

@@ -1,6 +1,5 @@
-import { MenuItemConstructorOptions } from 'electron'
 import { Injectable, NgZone, Optional, Inject } from '@angular/core'
-import { BaseTabComponent, TabContextMenuItemProvider, TabHeaderComponent, NotificationsService } from 'terminus-core'
+import { BaseTabComponent, TabContextMenuItemProvider, TabHeaderComponent, NotificationsService, MenuItemOptions } from 'terminus-core'
 import { BaseTerminalTabComponent } from './api/baseTerminalTab.component'
 import { TerminalContextMenuItemProvider } from './api/contextMenuProvider'
 
@@ -16,7 +15,7 @@ export class CopyPasteContextMenu extends TabContextMenuItemProvider {
         super()
     }
 
-    async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemConstructorOptions[]> {
+    async getItems (tab: BaseTabComponent, tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
         if (tabHeader) {
             return []
         }
@@ -56,12 +55,12 @@ export class LegacyContextMenu extends TabContextMenuItemProvider {
         super()
     }
 
-    async getItems (tab: BaseTabComponent, _tabHeader?: TabHeaderComponent): Promise<MenuItemConstructorOptions[]> {
+    async getItems (tab: BaseTabComponent, _tabHeader?: TabHeaderComponent): Promise<MenuItemOptions[]> {
         if (!this.contextMenuProviders) {
             return []
         }
         if (tab instanceof BaseTerminalTabComponent) {
-            let items: MenuItemConstructorOptions[] = []
+            let items: MenuItemOptions[] = []
             for (const p of this.contextMenuProviders) {
                 items = items.concat(await p.getItems(tab))
             }

+ 1 - 0
terminus-web/.gitignore

@@ -0,0 +1 @@
+dist

+ 27 - 0
terminus-web/package.json

@@ -0,0 +1,27 @@
+{
+  "name": "terminus-web",
+  "version": "1.0.135-nightly.0",
+  "description": "Web-specific bindings",
+  "keywords": [
+    "terminus-builtin-plugin"
+  ],
+  "main": "dist/index.js",
+  "typings": "typings/index.d.ts",
+  "scripts": {
+    "build": "webpack --progress --color",
+    "watch": "webpack --progress --color --watch"
+  },
+  "files": [
+    "dist"
+  ],
+  "author": "Eugene Pankov",
+  "license": "MIT",
+  "peerDependencies": {
+    "@angular/core": "^9.1.9"
+  },
+  "devDependencies": {
+    "@vaadin/vaadin-context-menu": "^5.0.0",
+    "bootstrap": "^4.1.3",
+    "copy-text-to-clipboard": "^3.0.1"
+  }
+}

+ 17 - 0
terminus-web/src/index.ts

@@ -0,0 +1,17 @@
+import { NgModule } from '@angular/core'
+import { LogService, PlatformService, UpdaterService } from 'terminus-core'
+
+import { WebPlatformService } from './platform'
+import { ConsoleLogService } from './services/log.service'
+import { NullUpdaterService } from './services/updater.service'
+
+import './styles.scss'
+
+@NgModule({
+    providers: [
+        { provide: PlatformService, useClass: WebPlatformService },
+        { provide: LogService, useClass: ConsoleLogService },
+        { provide: UpdaterService, useClass: NullUpdaterService },
+    ],
+})
+export default class WebModule { } // eslint-disable-line @typescript-eslint/no-extraneous-class

+ 76 - 0
terminus-web/src/platform.ts

@@ -0,0 +1,76 @@
+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'
+
+// eslint-disable-next-line no-duplicate-imports
+import type { ContextMenuElement, ContextMenuItem } from '@vaadin/vaadin-context-menu/vaadin-context-menu.js'
+
+import './styles.scss'
+
+@Injectable()
+export class WebPlatformService extends PlatformService {
+    private menu: ContextMenuElement
+    private contextMenuHandlers = new Map<ContextMenuItem, () => void>()
+
+    constructor () {
+        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'))
+    }
+
+    setClipboard (content: ClipboardContent): void {
+        copyToClipboard(content.text)
+    }
+
+    async loadConfig (): Promise<string> {
+        return window['terminusConfig']
+    }
+
+    async saveConfig (content: string): Promise<void> {
+        console.log('config save', content)
+    }
+
+    getOSRelease (): string {
+        return '1.0'
+    }
+
+    openExternal (url: string): void {
+        window.open(url)
+    }
+
+    getAppVersion (): string {
+        return '1.0-web'
+    }
+
+    async listFonts (): Promise<string[]> {
+        return []
+    }
+
+    popupContextMenu (menu: MenuItemOptions[], event?: MouseEvent): void {
+        this.contextMenuHandlers.clear()
+        this.menu.items = menu
+            .filter(x => x.type !== 'separator')
+            .map(x => this.remapMenuItem(x))
+        setTimeout(() => {
+            this.menu.open(event)
+        }, 10)
+    }
+
+    private remapMenuItem (item: MenuItemOptions): ContextMenuItem {
+        const cmi = {
+            text: item.label,
+            disabled: !(item.enabled ?? true),
+            checked: item.checked,
+            children: item.submenu?.map(i => this.remapMenuItem(i)),
+        }
+        if (item.click) {
+            this.contextMenuHandlers.set(cmi, item.click)
+        }
+        return cmi
+    }
+}

+ 9 - 0
terminus-web/src/services/log.service.ts

@@ -0,0 +1,9 @@
+import { Injectable } from '@angular/core'
+import { ConsoleLogger, Logger } from 'terminus-core'
+
+@Injectable({ providedIn: 'root' })
+export class ConsoleLogService {
+    create (name: string): Logger {
+        return new ConsoleLogger(name)
+    }
+}

+ 10 - 0
terminus-web/src/services/updater.service.ts

@@ -0,0 +1,10 @@
+import { UpdaterService } from 'terminus-core'
+
+export class NullUpdaterService extends UpdaterService {
+    async check (): Promise<boolean> {
+        return false
+    }
+
+    // eslint-disable-next-line @typescript-eslint/no-empty-function
+    async update (): Promise<void> { }
+}

+ 11 - 0
terminus-web/src/styles.scss

@@ -0,0 +1,11 @@
+@import "../../terminus-core/src/theme.vars.scss";
+
+html.terminus {
+    --lumo-primary-text-color: #{$body-color};
+    --lumo-base-color: #{$body-bg};
+    --lumo-body-text-color: #{$body-color};
+    --lumo-tint-5pct: #{$body-bg};
+    --lumo-font-family: #{$font-family-sans-serif};
+    --lumo-font-size-m: #{$font-size-base};
+    --lumo-box-shadow-m: #{$dropdown-box-shadow};
+}

+ 7 - 0
terminus-web/tsconfig.json

@@ -0,0 +1,7 @@
+{
+  "extends": "../tsconfig.json",
+  "exclude": ["node_modules", "dist"],
+  "compilerOptions": {
+    "baseUrl": "src"
+  }
+}

+ 14 - 0
terminus-web/tsconfig.typings.json

@@ -0,0 +1,14 @@
+{
+  "extends": "../tsconfig.json",
+  "exclude": ["node_modules", "dist", "typings"],
+  "compilerOptions": {
+    "baseUrl": "src",
+    "emitDeclarationOnly": true,
+    "declaration": true,
+    "declarationDir": "./typings",
+    "paths": {
+      "terminus-*": ["../../terminus-*"],
+      "*": ["../../app/node_modules/*"]
+    }
+  }
+}

+ 5 - 0
terminus-web/webpack.config.js

@@ -0,0 +1,5 @@
+const config = require('../webpack.plugin.config')
+module.exports = config({
+    name: 'web',
+    dirname: __dirname,
+})

+ 178 - 0
terminus-web/yarn.lock

@@ -0,0 +1,178 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@polymer/iron-flex-layout@^3.0.0-pre.26":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@polymer/iron-flex-layout/-/iron-flex-layout-3.0.1.tgz#36f9e1a8eb792d279b2bc75d362628721ad37f0c"
+  integrity sha512-7gB869czArF+HZcPTVSgvA7tXYFze9EKckvM95NB7SqYF+NnsQyhoXgKnpFwGyo95lUjUW9TFDLUwDXnCYFtkw==
+  dependencies:
+    "@polymer/polymer" "^3.0.0"
+
+"@polymer/iron-icon@^3.0.0":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@polymer/iron-icon/-/iron-icon-3.0.1.tgz#93211c39d8825fe4965a68419566036c1df291eb"
+  integrity sha512-QLPwirk+UPZNaLnMew9VludXA4CWUCenRewgEcGYwdzVgDPCDbXxy6vRJjmweZobMQv/oVLppT2JZtJFnPxX6g==
+  dependencies:
+    "@polymer/iron-flex-layout" "^3.0.0-pre.26"
+    "@polymer/iron-meta" "^3.0.0-pre.26"
+    "@polymer/polymer" "^3.0.0"
+
+"@polymer/iron-iconset-svg@^3.0.0":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@polymer/iron-iconset-svg/-/iron-iconset-svg-3.0.1.tgz#568d6e7dbc120299dae63be3600aeba0d30ddbea"
+  integrity sha512-XNwURbNHRw6u2fJe05O5fMYye6GSgDlDqCO+q6K1zAnKIrpgZwf2vTkBd5uCcZwsN0FyCB3mvNZx4jkh85dRDw==
+  dependencies:
+    "@polymer/iron-meta" "^3.0.0-pre.26"
+    "@polymer/polymer" "^3.0.0"
+
+"@polymer/iron-media-query@^3.0.0":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@polymer/iron-media-query/-/iron-media-query-3.0.1.tgz#5cd8a1c1e8c9b8bafd3dd5da14e0f8d2cfa76d83"
+  integrity sha512-czUX1pm1zfmfcZtq5J57XFkcobBv08Y50exp0/3v8Bos5VL/jv2tU0RwiTfDBxUMhjicGbgwEBFQPY2V5DMzyw==
+  dependencies:
+    "@polymer/polymer" "^3.0.0"
+
+"@polymer/iron-meta@^3.0.0-pre.26":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@polymer/iron-meta/-/iron-meta-3.0.1.tgz#7f140628d127b0a284f882f1bb323a261bc125f5"
+  integrity sha512-pWguPugiLYmWFV9UWxLWzZ6gm4wBwQdDy4VULKwdHCqR7OP7u98h+XDdGZsSlDPv6qoryV/e3tGHlTIT0mbzJA==
+  dependencies:
+    "@polymer/polymer" "^3.0.0"
+
+"@polymer/polymer@^3.0.0":
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/@polymer/polymer/-/polymer-3.4.1.tgz#333bef25711f8411bb5624fb3eba8212ef8bee96"
+  integrity sha512-KPWnhDZibtqKrUz7enIPOiO4ZQoJNOuLwqrhV2MXzIt3VVnUVJVG5ORz4Z2sgO+UZ+/UZnPD0jqY+jmw/+a9mQ==
+  dependencies:
+    "@webcomponents/shadycss" "^1.9.1"
+
+"@vaadin/vaadin-context-menu@^5.0.0":
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/@vaadin/vaadin-context-menu/-/vaadin-context-menu-5.0.0.tgz#c8ef7a78f107c9824ef90c9331159d5f2818fdac"
+  integrity sha512-+OIFseHPRy1QraQFLUT/jxCKlvsOVg/NaaHhfonTZdwrO31CTpKGZFCDB0Gvos2W9WdXa6WI12DRJLZF7Wcr0g==
+  dependencies:
+    "@polymer/iron-media-query" "^3.0.0"
+    "@polymer/polymer" "^3.0.0"
+    "@vaadin/vaadin-element-mixin" "^2.4.1"
+    "@vaadin/vaadin-item" "^3.0.0"
+    "@vaadin/vaadin-list-box" "^2.0.0"
+    "@vaadin/vaadin-lumo-styles" "^1.6.1"
+    "@vaadin/vaadin-material-styles" "^1.3.2"
+    "@vaadin/vaadin-overlay" "^3.5.0"
+    "@vaadin/vaadin-themable-mixin" "^1.6.2"
+
+"@vaadin/vaadin-development-mode-detector@^2.0.0":
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/@vaadin/vaadin-development-mode-detector/-/vaadin-development-mode-detector-2.0.4.tgz#f49c8009856bead92d248377c36b295b5aae78e5"
+  integrity sha512-S+PaFrZpK8uBIOnIHxjntTrgumd5ztuCnZww96ydGKXgo9whXfZsbMwDuD/102a/IuPUMyF+dh/n3PbWzJ6igA==
+
+"@vaadin/vaadin-element-mixin@^2.4.0", "@vaadin/vaadin-element-mixin@^2.4.1":
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/@vaadin/vaadin-element-mixin/-/vaadin-element-mixin-2.4.2.tgz#3c8040a8e756bc274b7777723b1fba2b9895cd41"
+  integrity sha512-VSDVK0XUsFe/RohpwSzQwgqb2Pwpok6sDNhIDS4CARr3HPhq2voMzT/FowFbkEy0J1hFtN/ZfC7tkv3kdEKKIQ==
+  dependencies:
+    "@polymer/polymer" "^3.0.0"
+    "@vaadin/vaadin-development-mode-detector" "^2.0.0"
+    "@vaadin/vaadin-usage-statistics" "^2.1.0"
+
+"@vaadin/vaadin-item@^3.0.0":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@vaadin/vaadin-item/-/vaadin-item-3.0.0.tgz#abbeadd752dd46351217b94351c05bf93d6fad1c"
+  integrity sha512-AcSqaOd2LJr51JWT3j7GcdbU54oBHAE8xlfeN0O5OdCcsAQJLekkNJ3uxt8Kr3ZP99nnEFTZ1WKcQtEufSAVhA==
+  dependencies:
+    "@polymer/polymer" "^3.0.0"
+    "@vaadin/vaadin-element-mixin" "^2.4.1"
+    "@vaadin/vaadin-lumo-styles" "^1.6.1"
+    "@vaadin/vaadin-material-styles" "^1.3.2"
+    "@vaadin/vaadin-themable-mixin" "^1.6.2"
+
+"@vaadin/vaadin-list-box@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@vaadin/vaadin-list-box/-/vaadin-list-box-2.0.0.tgz#783e1abf1dd50609a7a00a6de2acd2394a1d808e"
+  integrity sha512-3WU7oU3cgrp7jPet1aAjAIJSQqdVbKAqIPxOH3LsLX7QQAYnWvUwQY+UApPHiJIjpnKF0PfYiIZe1o6adqKivg==
+  dependencies:
+    "@polymer/polymer" "^3.0.0"
+    "@vaadin/vaadin-element-mixin" "^2.4.1"
+    "@vaadin/vaadin-item" "^3.0.0"
+    "@vaadin/vaadin-list-mixin" "^2.5.0"
+    "@vaadin/vaadin-lumo-styles" "^1.6.1"
+    "@vaadin/vaadin-material-styles" "^1.3.2"
+    "@vaadin/vaadin-themable-mixin" "^1.6.1"
+
+"@vaadin/vaadin-list-mixin@^2.5.0":
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/@vaadin/vaadin-list-mixin/-/vaadin-list-mixin-2.5.1.tgz#f6ab60cc658900d3eb7bfff18cf42d769374b659"
+  integrity sha512-XcMzQ0hJnK/AAiV+bW95nwJgmMIrXUBiSDwM+uvfurcBKqPyM4pm3sj8imh8zXSTfpN4HSjMnrLWU1ZfR330vg==
+  dependencies:
+    "@polymer/polymer" "^3.0.0"
+    "@vaadin/vaadin-element-mixin" "^2.4.1"
+
+"@vaadin/vaadin-lumo-styles@^1.3.0", "@vaadin/vaadin-lumo-styles@^1.6.1":
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/@vaadin/vaadin-lumo-styles/-/vaadin-lumo-styles-1.6.1.tgz#2099227b0f646ead16f7289e704b6a793594bf5c"
+  integrity sha512-Yh9ZcekpY7byXP1QJnfx94rVvK71xHBEspsVV7LL7YMvqXU4EAYuzQGYsljryV4PGS9PFPD6sqbGqhEkIhHPnQ==
+  dependencies:
+    "@polymer/iron-icon" "^3.0.0"
+    "@polymer/iron-iconset-svg" "^3.0.0"
+    "@polymer/polymer" "^3.0.0"
+
+"@vaadin/vaadin-material-styles@^1.2.0", "@vaadin/vaadin-material-styles@^1.3.2":
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/@vaadin/vaadin-material-styles/-/vaadin-material-styles-1.3.2.tgz#d2c1bd290db16721152ae672dbe052c381686696"
+  integrity sha512-EFrvGScoxhLNrPnWtT2Ia77whjF2TD4jrcyeh1jv9joCA2n5SUba+4XJciVSGmopqqQato6lwRnZSvMLJX7cyw==
+  dependencies:
+    "@polymer/polymer" "^3.0.0"
+
+"@vaadin/vaadin-overlay@^3.5.0":
+  version "3.5.1"
+  resolved "https://registry.yarnpkg.com/@vaadin/vaadin-overlay/-/vaadin-overlay-3.5.1.tgz#c4391b3c6c1f7a512b0a6f0dd96f11480feed402"
+  integrity sha512-0g+poK/BXF92L2lSKrHMY5rcKzUxCBZNzP/NDwgi4a86nbjL7CAKKZdno7Yl+j8UsTR76nOEw4fAYTFi86B0qg==
+  dependencies:
+    "@polymer/polymer" "^3.0.0"
+    "@vaadin/vaadin-element-mixin" "^2.4.0"
+    "@vaadin/vaadin-lumo-styles" "^1.3.0"
+    "@vaadin/vaadin-material-styles" "^1.2.0"
+    "@vaadin/vaadin-themable-mixin" "^1.6.1"
+
+"@vaadin/vaadin-themable-mixin@^1.6.1", "@vaadin/vaadin-themable-mixin@^1.6.2":
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/@vaadin/vaadin-themable-mixin/-/vaadin-themable-mixin-1.6.2.tgz#8d619722819ba850af777579a550ff8b1d2b960f"
+  integrity sha512-PZZOZnke3KUlZsDrRVbWxAGEeFBPRyRayNRCvip0XnQK+Zs3cLuRgdgbdro3Ir9LZ3Izsw6HqA6XNMKffEP67A==
+  dependencies:
+    "@polymer/polymer" "^3.0.0"
+    lit-element "^2.0.0"
+
+"@vaadin/vaadin-usage-statistics@^2.1.0":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@vaadin/vaadin-usage-statistics/-/vaadin-usage-statistics-2.1.0.tgz#9c0fd71dded80f401bcdfbcb3f45b5640fc4256d"
+  integrity sha512-e81nbqY5zsaYhLJuOVkJkB/Um1pGK5POIqIlTNhUfjeoyGaJ63tiX8+D5n6F+GgVxUTLUarsKa6SKRcQel0AzA==
+  dependencies:
+    "@vaadin/vaadin-development-mode-detector" "^2.0.0"
+
+"@webcomponents/shadycss@^1.9.1":
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.10.2.tgz#40e03cab6dc5e12f199949ba2b79e02f183d1e7b"
+  integrity sha512-9Iseu8bRtecb0klvv+WXZOVZatsRkbaH7M97Z+f+Pt909R4lDfgUODAnra23DOZTpeMTAkVpf4m/FZztN7Ox1A==
+
+bootstrap@^4.1.3:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.0.tgz#97b9f29ac98f98dfa43bf7468262d84392552fd7"
+  integrity sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==
+
+copy-text-to-clipboard@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.0.1.tgz#8cbf8f90e0a47f12e4a24743736265d157bce69c"
+  integrity sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q==
+
+lit-element@^2.0.0:
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-2.5.1.tgz#3fa74b121a6cd22902409ae3859b7847d01aa6b6"
+  integrity sha512-ogu7PiJTA33bEK0xGu1dmaX5vhcRjBXCFexPja0e7P7jqLhTpNKYRPmE+GmiCaRVAbiQKGkUgkh/i6+bh++dPQ==
+  dependencies:
+    lit-html "^1.1.1"
+
+lit-html@^1.1.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-1.4.1.tgz#0c6f3ee4ad4eb610a49831787f0478ad8e9ae5e0"
+  integrity sha512-B9btcSgPYb1q4oSOb/PrOT6Z/H+r6xuNzfH4lFli/AWhYwdtrgQkQWBbIc6mdnf6E2IL3gDXdkkqNktpU0OZQA==

+ 2 - 1
tsconfig.json

@@ -23,7 +23,8 @@
       "es6",
       "es7",
       "es2015",
-      "es2017"
+      "es2017",
+      "es2019"
     ],
     "paths": {
       "terminus-*": ["../../terminus-*/src"]

+ 2 - 0
webpack.config.js

@@ -2,6 +2,8 @@ module.exports = [
     require('./app/webpack.config.js'),
     require('./app/webpack.main.config.js'),
     require('./terminus-core/webpack.config.js'),
+    require('./terminus-electron/webpack.config.js'),
+    require('./terminus-web/webpack.config.js'),
     require('./terminus-settings/webpack.config.js'),
     require('./terminus-terminal/webpack.config.js'),
     require('./terminus-local/webpack.config.js'),

+ 4 - 1
webpack.plugin.config.js

@@ -43,6 +43,7 @@ module.exports = options => {
         },
         module: {
             rules: [
+                ...options.rules ?? [],
                 {
                     test: /\.ts$/,
                     use: {
@@ -64,7 +65,8 @@ module.exports = options => {
                     },
                 },
                 { test: /\.pug$/, use: ['apply-loader', 'pug-loader'] },
-                { test: /\.scss$/, use: ['@terminus-term/to-string-loader', 'css-loader', 'sass-loader'] },
+                { test: /\.scss$/, use: ['@terminus-term/to-string-loader', 'css-loader', 'sass-loader'], include: /(theme.*|component)\.scss/ },
+                { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'], exclude: /(theme.*|component)\.scss/ },
                 { test: /\.css$/, use: ['@terminus-term/to-string-loader', 'css-loader'], include: /component\.css/ },
                 { test: /\.css$/, use: ['style-loader', 'css-loader'], exclude: /component\.css/ },
                 { test: /\.yaml$/, use: ['json-loader', 'yaml-loader'] },
@@ -81,6 +83,7 @@ module.exports = options => {
             ],
         },
         externals: [
+            '@electron/remote',
             'any-promise',
             'child_process',
             'electron-promise-ipc',